From d0feae60e7847a888abcd901afb945dceb302cf0 Mon Sep 17 00:00:00 2001 From: Kevin Segaud Date: Mon, 2 Mar 2026 13:34:29 +0100 Subject: [PATCH] feat: add CssScrollPadding, CssOverscrollBehavior, and CssAppearance property support --- packages/spark_css/CHANGELOG.md | 3 + .../lib/src/css_types/css_appearance.dart | 54 +++++++++++ .../css_types/css_overscroll_behavior.dart | 76 ++++++++++++++++ .../lib/src/css_types/css_scroll_padding.dart | 90 +++++++++++++++++++ .../lib/src/css_types/css_types.dart | 3 + packages/spark_css/lib/src/style.dart | 12 +++ .../spark_css/test/css_appearance_test.dart | 43 +++++++++ .../test/css_overscroll_behavior_test.dart | 70 +++++++++++++++ .../test/css_scroll_padding_test.dart | 71 +++++++++++++++ 9 files changed, 422 insertions(+) create mode 100644 packages/spark_css/lib/src/css_types/css_appearance.dart create mode 100644 packages/spark_css/lib/src/css_types/css_overscroll_behavior.dart create mode 100644 packages/spark_css/lib/src/css_types/css_scroll_padding.dart create mode 100644 packages/spark_css/test/css_appearance_test.dart create mode 100644 packages/spark_css/test/css_overscroll_behavior_test.dart create mode 100644 packages/spark_css/test/css_scroll_padding_test.dart diff --git a/packages/spark_css/CHANGELOG.md b/packages/spark_css/CHANGELOG.md index d9b8a4d..8ec0035 100644 --- a/packages/spark_css/CHANGELOG.md +++ b/packages/spark_css/CHANGELOG.md @@ -36,6 +36,9 @@ - **Feat**: Added `CssTouchAction` sealed class for `touch-action` property with `.auto`, `.none`, `.manipulation` standalone keywords, `.panX`, `.panLeft`, `.panRight`, `.panY`, `.panUp`, `.panDown`, `.pinchZoom` combinable keywords, and `.combine()` factory, plus `.variable()`, `.raw()`, and `.global()` escape hatches. - **Feat**: Added `CssUserSelect` sealed class for `user-select` property with `.none`, `.auto`, `.text`, `.all`, `.contain` keywords, plus `.variable()`, `.raw()`, and `.global()` escape hatches. - **Feat**: Added `CssHyphens` sealed class for `hyphens` property with `.none`, `.manual`, `.auto` keywords, plus `.variable()`, `.raw()`, and `.global()` escape hatches. +- **Feat**: Added `CssScrollPadding` sealed class for `scroll-padding` property with `.all()`, `.symmetric()`, `.only()` factories, plus `.variable()`, `.raw()`, and `.global()` escape hatches. +- **Feat**: Added `CssOverscrollBehavior` sealed class for `overscroll-behavior` property with `.auto`, `.contain`, `.none` keywords, `.xy()` for two-value syntax, plus `.variable()`, `.raw()`, and `.global()` escape hatches. +- **Feat**: Added `CssAppearance` sealed class for `appearance` property with `.none`, `.auto`, `.menulistButton`, `.textfield` keywords, plus `.variable()`, `.raw()`, and `.global()` escape hatches. - **Feat**: Added `CssTabSize` sealed class for `tab-size` property with `.number()`, `.length()` factories, plus `.variable()`, `.raw()`, and `.global()` escape hatches. - **Feat**: Added `CssCaretColor` sealed class for `caret-color` property with `.auto` keyword, `.color()` factory, plus `.variable()`, `.raw()`, and `.global()` escape hatches. - **Feat**: Added `CssScrollSnapType` sealed class for `scroll-snap-type` property with `.none` keyword, `.axis()` factory with optional strictness, plus `.variable()`, `.raw()`, and `.global()` escape hatches. diff --git a/packages/spark_css/lib/src/css_types/css_appearance.dart b/packages/spark_css/lib/src/css_types/css_appearance.dart new file mode 100644 index 0000000..1e1897a --- /dev/null +++ b/packages/spark_css/lib/src/css_types/css_appearance.dart @@ -0,0 +1,54 @@ +import 'css_value.dart'; + +/// CSS appearance property values. +sealed class CssAppearance implements CssValue { + const CssAppearance._(); + + static const CssAppearance none = _CssAppearanceKeyword('none'); + static const CssAppearance auto = _CssAppearanceKeyword('auto'); + static const CssAppearance menulistButton = _CssAppearanceKeyword( + 'menulist-button', + ); + static const CssAppearance textfield = _CssAppearanceKeyword('textfield'); + + /// CSS variable reference. + factory CssAppearance.variable(String varName) = _CssAppearanceVariable; + + /// Raw CSS value escape hatch. + factory CssAppearance.raw(String value) = _CssAppearanceRaw; + + /// Global keyword (inherit, initial, unset, revert). + factory CssAppearance.global(CssGlobal global) = _CssAppearanceGlobal; +} + +final class _CssAppearanceKeyword extends CssAppearance { + final String keyword; + const _CssAppearanceKeyword(this.keyword) : super._(); + + @override + String toCss() => keyword; +} + +final class _CssAppearanceVariable extends CssAppearance { + final String varName; + const _CssAppearanceVariable(this.varName) : super._(); + + @override + String toCss() => 'var(--$varName)'; +} + +final class _CssAppearanceRaw extends CssAppearance { + final String value; + const _CssAppearanceRaw(this.value) : super._(); + + @override + String toCss() => value; +} + +final class _CssAppearanceGlobal extends CssAppearance { + final CssGlobal global; + const _CssAppearanceGlobal(this.global) : super._(); + + @override + String toCss() => global.toCss(); +} diff --git a/packages/spark_css/lib/src/css_types/css_overscroll_behavior.dart b/packages/spark_css/lib/src/css_types/css_overscroll_behavior.dart new file mode 100644 index 0000000..a986a8c --- /dev/null +++ b/packages/spark_css/lib/src/css_types/css_overscroll_behavior.dart @@ -0,0 +1,76 @@ +import 'css_value.dart'; + +/// CSS overscroll-behavior property values. +sealed class CssOverscrollBehavior implements CssValue { + const CssOverscrollBehavior._(); + + static const CssOverscrollBehavior auto = _CssOverscrollBehaviorKeyword( + 'auto', + ); + static const CssOverscrollBehavior contain = _CssOverscrollBehaviorKeyword( + 'contain', + ); + static const CssOverscrollBehavior none = _CssOverscrollBehaviorKeyword( + 'none', + ); + + /// Two-value syntax for x and y axes. + /// + /// Example: `CssOverscrollBehavior.xy(CssOverscrollBehavior.contain, CssOverscrollBehavior.none)` → `contain none` + factory CssOverscrollBehavior.xy( + CssOverscrollBehavior x, + CssOverscrollBehavior y, + ) = _CssOverscrollBehaviorXY; + + /// CSS variable reference. + factory CssOverscrollBehavior.variable(String varName) = + _CssOverscrollBehaviorVariable; + + /// Raw CSS value escape hatch. + factory CssOverscrollBehavior.raw(String value) = _CssOverscrollBehaviorRaw; + + /// Global keyword (inherit, initial, unset, revert). + factory CssOverscrollBehavior.global(CssGlobal global) = + _CssOverscrollBehaviorGlobal; +} + +final class _CssOverscrollBehaviorKeyword extends CssOverscrollBehavior { + final String keyword; + const _CssOverscrollBehaviorKeyword(this.keyword) : super._(); + + @override + String toCss() => keyword; +} + +final class _CssOverscrollBehaviorXY extends CssOverscrollBehavior { + final CssOverscrollBehavior x; + final CssOverscrollBehavior y; + const _CssOverscrollBehaviorXY(this.x, this.y) : super._(); + + @override + String toCss() => '${x.toCss()} ${y.toCss()}'; +} + +final class _CssOverscrollBehaviorVariable extends CssOverscrollBehavior { + final String varName; + const _CssOverscrollBehaviorVariable(this.varName) : super._(); + + @override + String toCss() => 'var(--$varName)'; +} + +final class _CssOverscrollBehaviorRaw extends CssOverscrollBehavior { + final String value; + const _CssOverscrollBehaviorRaw(this.value) : super._(); + + @override + String toCss() => value; +} + +final class _CssOverscrollBehaviorGlobal extends CssOverscrollBehavior { + final CssGlobal global; + const _CssOverscrollBehaviorGlobal(this.global) : super._(); + + @override + String toCss() => global.toCss(); +} diff --git a/packages/spark_css/lib/src/css_types/css_scroll_padding.dart b/packages/spark_css/lib/src/css_types/css_scroll_padding.dart new file mode 100644 index 0000000..b2d8a57 --- /dev/null +++ b/packages/spark_css/lib/src/css_types/css_scroll_padding.dart @@ -0,0 +1,90 @@ +import 'css_length.dart'; +import 'css_value.dart'; + +/// CSS scroll-padding property values. +sealed class CssScrollPadding implements CssValue { + const CssScrollPadding._(); + + /// Same value for all four sides. + factory CssScrollPadding.all(CssLength value) = _CssScrollPaddingAll; + + /// Symmetric scroll padding (vertical and horizontal). + factory CssScrollPadding.symmetric(CssLength vertical, CssLength horizontal) = + _CssScrollPaddingSymmetric; + + /// Individual sides. + factory CssScrollPadding.only({ + CssLength? top, + CssLength? right, + CssLength? bottom, + CssLength? left, + }) = _CssScrollPaddingOnly; + + /// CSS variable reference. + factory CssScrollPadding.variable(String varName) = _CssScrollPaddingVariable; + + /// Raw CSS value escape hatch. + factory CssScrollPadding.raw(String value) = _CssScrollPaddingRaw; + + /// Global keyword (inherit, initial, unset, revert). + factory CssScrollPadding.global(CssGlobal global) = _CssScrollPaddingGlobal; +} + +final class _CssScrollPaddingAll extends CssScrollPadding { + final CssLength value; + const _CssScrollPaddingAll(this.value) : super._(); + + @override + String toCss() => value.toCss(); +} + +final class _CssScrollPaddingSymmetric extends CssScrollPadding { + final CssLength vertical; + final CssLength horizontal; + const _CssScrollPaddingSymmetric(this.vertical, this.horizontal) : super._(); + + @override + String toCss() => '${vertical.toCss()} ${horizontal.toCss()}'; +} + +final class _CssScrollPaddingOnly extends CssScrollPadding { + final CssLength? top; + final CssLength? right; + final CssLength? bottom; + final CssLength? left; + const _CssScrollPaddingOnly({this.top, this.right, this.bottom, this.left}) + : super._(); + + @override + String toCss() { + final t = top ?? CssLength.zero; + final r = right ?? CssLength.zero; + final b = bottom ?? CssLength.zero; + final l = left ?? CssLength.zero; + return '${t.toCss()} ${r.toCss()} ${b.toCss()} ${l.toCss()}'; + } +} + +final class _CssScrollPaddingVariable extends CssScrollPadding { + final String varName; + const _CssScrollPaddingVariable(this.varName) : super._(); + + @override + String toCss() => 'var(--$varName)'; +} + +final class _CssScrollPaddingRaw extends CssScrollPadding { + final String value; + const _CssScrollPaddingRaw(this.value) : super._(); + + @override + String toCss() => value; +} + +final class _CssScrollPaddingGlobal extends CssScrollPadding { + final CssGlobal global; + const _CssScrollPaddingGlobal(this.global) : super._(); + + @override + String toCss() => global.toCss(); +} diff --git a/packages/spark_css/lib/src/css_types/css_types.dart b/packages/spark_css/lib/src/css_types/css_types.dart index 092bc5c..2879580 100644 --- a/packages/spark_css/lib/src/css_types/css_types.dart +++ b/packages/spark_css/lib/src/css_types/css_types.dart @@ -3,6 +3,7 @@ library; export 'css_animation.dart'; export 'css_angle.dart'; +export 'css_appearance.dart'; export 'css_aspect_ratio.dart'; export 'css_background.dart'; export 'css_background_attachment.dart'; @@ -41,6 +42,7 @@ export 'css_number.dart'; export 'css_object_fit.dart'; export 'css_outline.dart'; export 'css_overflow.dart'; +export 'css_overscroll_behavior.dart'; export 'css_place_content.dart'; export 'css_place_items.dart'; export 'css_place_self.dart'; @@ -51,6 +53,7 @@ export 'css_radial_size.dart'; export 'css_resize.dart'; export 'css_scroll_behavior.dart'; export 'css_scroll_margin.dart'; +export 'css_scroll_padding.dart'; export 'css_scroll_snap_align.dart'; export 'css_scroll_snap_type.dart'; export 'css_spacing.dart'; diff --git a/packages/spark_css/lib/src/style.dart b/packages/spark_css/lib/src/style.dart index 96c95ea..ac5a3d9 100644 --- a/packages/spark_css/lib/src/style.dart +++ b/packages/spark_css/lib/src/style.dart @@ -330,6 +330,9 @@ class Style implements CssStyle { CssScrollSnapType? scrollSnapType, CssScrollSnapAlign? scrollSnapAlign, CssScrollMargin? scrollMargin, + CssScrollPadding? scrollPadding, + CssOverscrollBehavior? overscrollBehavior, + CssAppearance? appearance, // Backgrounds CssBackgroundImage? backgroundImage, CssBackgroundSize? backgroundSize, @@ -530,6 +533,15 @@ class Style implements CssStyle { if (scrollMargin != null) { _properties['scroll-margin'] = scrollMargin.toCss(); } + if (scrollPadding != null) { + _properties['scroll-padding'] = scrollPadding.toCss(); + } + if (overscrollBehavior != null) { + _properties['overscroll-behavior'] = overscrollBehavior.toCss(); + } + if (appearance != null) { + _properties['appearance'] = appearance.toCss(); + } // Backgrounds if (backgroundImage != null) { diff --git a/packages/spark_css/test/css_appearance_test.dart b/packages/spark_css/test/css_appearance_test.dart new file mode 100644 index 0000000..831c96e --- /dev/null +++ b/packages/spark_css/test/css_appearance_test.dart @@ -0,0 +1,43 @@ +import 'package:spark_css/spark_css.dart'; +import 'package:test/test.dart'; + +void main() { + group('CssAppearance', () { + test('keywords output correct CSS', () { + expect(CssAppearance.none.toCss(), equals('none')); + expect(CssAppearance.auto.toCss(), equals('auto')); + expect(CssAppearance.menulistButton.toCss(), equals('menulist-button')); + expect(CssAppearance.textfield.toCss(), equals('textfield')); + }); + + test('variable outputs correct CSS', () { + expect(CssAppearance.variable('ap').toCss(), equals('var(--ap)')); + }); + + test('raw outputs value as-is', () { + expect(CssAppearance.raw('none').toCss(), equals('none')); + }); + + test('global outputs correct CSS', () { + expect( + CssAppearance.global(CssGlobal.inherit).toCss(), + equals('inherit'), + ); + expect( + CssAppearance.global(CssGlobal.initial).toCss(), + equals('initial'), + ); + expect(CssAppearance.global(CssGlobal.unset).toCss(), equals('unset')); + expect(CssAppearance.global(CssGlobal.revert).toCss(), equals('revert')); + expect( + CssAppearance.global(CssGlobal.revertLayer).toCss(), + equals('revert-layer'), + ); + }); + + test('Style.typed integration', () { + final style = Style.typed(appearance: CssAppearance.none); + expect(style.toCss(), contains('appearance: none;')); + }); + }); +} diff --git a/packages/spark_css/test/css_overscroll_behavior_test.dart b/packages/spark_css/test/css_overscroll_behavior_test.dart new file mode 100644 index 0000000..9921fd9 --- /dev/null +++ b/packages/spark_css/test/css_overscroll_behavior_test.dart @@ -0,0 +1,70 @@ +import 'package:spark_css/spark_css.dart'; +import 'package:test/test.dart'; + +void main() { + group('CssOverscrollBehavior', () { + test('keywords output correct CSS', () { + expect(CssOverscrollBehavior.auto.toCss(), equals('auto')); + expect(CssOverscrollBehavior.contain.toCss(), equals('contain')); + expect(CssOverscrollBehavior.none.toCss(), equals('none')); + }); + + test('xy outputs correct CSS', () { + expect( + CssOverscrollBehavior.xy( + CssOverscrollBehavior.contain, + CssOverscrollBehavior.none, + ).toCss(), + equals('contain none'), + ); + expect( + CssOverscrollBehavior.xy( + CssOverscrollBehavior.auto, + CssOverscrollBehavior.contain, + ).toCss(), + equals('auto contain'), + ); + }); + + test('variable outputs correct CSS', () { + expect(CssOverscrollBehavior.variable('ob').toCss(), equals('var(--ob)')); + }); + + test('raw outputs value as-is', () { + expect( + CssOverscrollBehavior.raw('contain none').toCss(), + equals('contain none'), + ); + }); + + test('global outputs correct CSS', () { + expect( + CssOverscrollBehavior.global(CssGlobal.inherit).toCss(), + equals('inherit'), + ); + expect( + CssOverscrollBehavior.global(CssGlobal.initial).toCss(), + equals('initial'), + ); + expect( + CssOverscrollBehavior.global(CssGlobal.unset).toCss(), + equals('unset'), + ); + expect( + CssOverscrollBehavior.global(CssGlobal.revert).toCss(), + equals('revert'), + ); + expect( + CssOverscrollBehavior.global(CssGlobal.revertLayer).toCss(), + equals('revert-layer'), + ); + }); + + test('Style.typed integration', () { + final style = Style.typed( + overscrollBehavior: CssOverscrollBehavior.contain, + ); + expect(style.toCss(), contains('overscroll-behavior: contain;')); + }); + }); +} diff --git a/packages/spark_css/test/css_scroll_padding_test.dart b/packages/spark_css/test/css_scroll_padding_test.dart new file mode 100644 index 0000000..8026f5f --- /dev/null +++ b/packages/spark_css/test/css_scroll_padding_test.dart @@ -0,0 +1,71 @@ +import 'package:spark_css/spark_css.dart'; +import 'package:test/test.dart'; + +void main() { + group('CssScrollPadding', () { + test('all outputs correct CSS', () { + expect(CssScrollPadding.all(CssLength.px(10)).toCss(), equals('10px')); + }); + + test('symmetric outputs correct CSS', () { + expect( + CssScrollPadding.symmetric(CssLength.px(10), CssLength.px(20)).toCss(), + equals('10px 20px'), + ); + }); + + test('only with all sides outputs correct CSS', () { + expect( + CssScrollPadding.only( + top: CssLength.px(10), + right: CssLength.px(20), + bottom: CssLength.px(30), + left: CssLength.px(40), + ).toCss(), + equals('10px 20px 30px 40px'), + ); + }); + + test('only with partial values defaults to zero', () { + expect( + CssScrollPadding.only(top: CssLength.px(10)).toCss(), + equals('10px 0 0 0'), + ); + }); + + test('variable outputs correct CSS', () { + expect(CssScrollPadding.variable('sp').toCss(), equals('var(--sp)')); + }); + + test('raw outputs value as-is', () { + expect(CssScrollPadding.raw('10px 20px').toCss(), equals('10px 20px')); + }); + + test('global outputs correct CSS', () { + expect( + CssScrollPadding.global(CssGlobal.inherit).toCss(), + equals('inherit'), + ); + expect( + CssScrollPadding.global(CssGlobal.initial).toCss(), + equals('initial'), + ); + expect(CssScrollPadding.global(CssGlobal.unset).toCss(), equals('unset')); + expect( + CssScrollPadding.global(CssGlobal.revert).toCss(), + equals('revert'), + ); + expect( + CssScrollPadding.global(CssGlobal.revertLayer).toCss(), + equals('revert-layer'), + ); + }); + + test('Style.typed integration', () { + final style = Style.typed( + scrollPadding: CssScrollPadding.all(CssLength.px(10)), + ); + expect(style.toCss(), contains('scroll-padding: 10px;')); + }); + }); +}