@greese/ui 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +19 -14
  2. package/dist/_maps.scss +1 -1
  3. package/dist/_tokens-base.scss +30 -6
  4. package/dist/_tokens.scss +31 -2
  5. package/dist/_utilities-table.scss +1615 -0
  6. package/dist/clipboard.d.ts +18 -0
  7. package/dist/clipboard.js +62 -0
  8. package/dist/css/Button.css +2 -2
  9. package/dist/css/Carousel.css +145 -47
  10. package/dist/css/Checkbox.css +2 -2
  11. package/dist/css/Chip.css +38 -8
  12. package/dist/css/ChoiceGroup.css +42 -1
  13. package/dist/css/Collapsible.css +1 -0
  14. package/dist/css/ColorPicker.css +84 -0
  15. package/dist/css/Combobox.css +2 -2
  16. package/dist/css/Dialog.css +24 -22
  17. package/dist/css/EditableField.css +219 -0
  18. package/dist/css/Input.css +11 -2
  19. package/dist/css/Menu.css +9 -0
  20. package/dist/css/NumberField.css +53 -8
  21. package/dist/css/NumberStepper.css +9 -0
  22. package/dist/css/OtpField.css +1 -1
  23. package/dist/css/PageTransition.css +12 -0
  24. package/dist/css/Radio.css +2 -2
  25. package/dist/css/ScrollCarousel.css +225 -0
  26. package/dist/css/SegmentedControl.css +2 -2
  27. package/dist/css/Select.css +3 -2
  28. package/dist/css/Slider.css +45 -5
  29. package/dist/css/Switch.css +6 -6
  30. package/dist/css/Text.css +14 -14
  31. package/dist/css/Textarea.css +1 -1
  32. package/dist/css/core.css +36 -6
  33. package/dist/css/resets.css +162 -0
  34. package/dist/css.d.ts +2 -0
  35. package/dist/css.js +2 -0
  36. package/dist/greese.css +990 -138
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.js +2 -1
  39. package/dist/scss/components/_Accordion.scss +2 -0
  40. package/dist/scss/components/_ActionSelect.scss +2 -0
  41. package/dist/scss/components/_Alert.scss +2 -0
  42. package/dist/scss/components/_AppBar.scss +2 -0
  43. package/dist/scss/components/_AspectRatio.scss +2 -0
  44. package/dist/scss/components/_AutoLayout.scss +2 -0
  45. package/dist/scss/components/_Avatar.scss +2 -0
  46. package/dist/scss/components/_Badge.scss +2 -0
  47. package/dist/scss/components/_Box.scss +2 -0
  48. package/dist/scss/components/_Breadcrumbs.scss +2 -0
  49. package/dist/scss/components/_Button.scss +2 -0
  50. package/dist/scss/components/_ButtonGroup.scss +2 -0
  51. package/dist/scss/components/_Card.scss +2 -0
  52. package/dist/scss/components/_Carousel.scss +2 -0
  53. package/dist/scss/components/_Center.scss +2 -0
  54. package/dist/scss/components/_ChatBubble.scss +2 -0
  55. package/dist/scss/components/_Checkbox.scss +2 -0
  56. package/dist/scss/components/_Chip.scss +2 -0
  57. package/dist/scss/components/_ChoiceGroup.scss +2 -0
  58. package/dist/scss/components/_Code.scss +2 -0
  59. package/dist/scss/components/_Collapsible.scss +2 -0
  60. package/dist/scss/components/_ColorPicker.scss +2 -0
  61. package/dist/scss/components/_Combobox.scss +2 -0
  62. package/dist/scss/components/_Container.scss +2 -0
  63. package/dist/scss/components/_Countdown.scss +2 -0
  64. package/dist/scss/components/_DateField.scss +2 -0
  65. package/dist/scss/components/_Dialog.scss +2 -0
  66. package/dist/scss/components/_Diff.scss +2 -0
  67. package/dist/scss/components/_Divider.scss +2 -0
  68. package/dist/scss/components/_Dock.scss +2 -0
  69. package/dist/scss/components/_EditableField.scss +2 -0
  70. package/dist/scss/components/_Field.scss +2 -0
  71. package/dist/scss/components/_Fieldset.scss +2 -0
  72. package/dist/scss/components/_FileInput.scss +2 -0
  73. package/dist/scss/components/_Footer.scss +2 -0
  74. package/dist/scss/components/_Grid.scss +2 -0
  75. package/dist/scss/components/_Header.scss +2 -0
  76. package/dist/scss/components/_Heading.scss +2 -0
  77. package/dist/scss/components/_Icon.scss +2 -0
  78. package/dist/scss/components/_IconButton.scss +2 -0
  79. package/dist/scss/components/_Image.scss +2 -0
  80. package/dist/scss/components/_Indicator.scss +2 -0
  81. package/dist/scss/components/_Input.scss +2 -0
  82. package/dist/scss/components/_Key.scss +2 -0
  83. package/dist/scss/components/_Link.scss +2 -0
  84. package/dist/scss/components/_List.scss +2 -0
  85. package/dist/scss/components/_Media.scss +2 -0
  86. package/dist/scss/components/_Menu.scss +2 -0
  87. package/dist/scss/components/_Motion.scss +2 -0
  88. package/dist/scss/components/_NumberField.scss +2 -0
  89. package/dist/scss/components/_NumberStepper.scss +2 -0
  90. package/dist/scss/components/_OtpField.scss +2 -0
  91. package/dist/scss/components/_PageTransition.scss +2 -0
  92. package/dist/scss/components/_Pagination.scss +2 -0
  93. package/dist/scss/components/_Popover.scss +2 -0
  94. package/dist/scss/components/_Progress.scss +2 -0
  95. package/dist/scss/components/_RadialProgress.scss +2 -0
  96. package/dist/scss/components/_Radio.scss +2 -0
  97. package/dist/scss/components/_Rating.scss +2 -0
  98. package/dist/scss/components/_ScrollCarousel.scss +2 -0
  99. package/dist/scss/components/_ScrollSpy.scss +2 -0
  100. package/dist/scss/components/_SegmentedControl.scss +2 -0
  101. package/dist/scss/components/_Select.scss +2 -0
  102. package/dist/scss/components/_Sidebar.scss +2 -0
  103. package/dist/scss/components/_Skeleton.scss +2 -0
  104. package/dist/scss/components/_Slider.scss +2 -0
  105. package/dist/scss/components/_Spacer.scss +2 -0
  106. package/dist/scss/components/_SpeedDial.scss +2 -0
  107. package/dist/scss/components/_Spinner.scss +2 -0
  108. package/dist/scss/components/_Stack.scss +2 -0
  109. package/dist/scss/components/_Stat.scss +2 -0
  110. package/dist/scss/components/_Status.scss +2 -0
  111. package/dist/scss/components/_Steps.scss +2 -0
  112. package/dist/scss/components/_Swap.scss +2 -0
  113. package/dist/scss/components/_Switch.scss +2 -0
  114. package/dist/scss/components/_Table.scss +2 -0
  115. package/dist/scss/components/_Tabs.scss +2 -0
  116. package/dist/scss/components/_Text.scss +2 -0
  117. package/dist/scss/components/_TextRotate.scss +2 -0
  118. package/dist/scss/components/_Textarea.scss +2 -0
  119. package/dist/scss/components/_ThemeProvider.scss +2 -0
  120. package/dist/scss/components/_Tile.scss +2 -0
  121. package/dist/scss/components/_Toast.scss +2 -0
  122. package/dist/scss/components/_ToggleButton.scss +2 -0
  123. package/dist/scss/components/_Tooltip.scss +2 -0
  124. package/dist/scss/components/_Type.scss +2 -0
  125. package/dist/scss/components/_Visibility.scss +2 -0
  126. package/dist/scss/components/_VisuallyHidden.scss +2 -0
  127. package/dist/shapes.d.ts +12 -1
  128. package/dist/shapes.js +11 -10
  129. package/dist/style-manifest.json +18837 -132
  130. package/dist/theme.js +6 -1
  131. package/dist/themeFiles/index.d.ts +9 -0
  132. package/dist/themeFiles/index.js +9 -0
  133. package/dist/themeFiles/scssModule.d.ts +100 -0
  134. package/dist/themeFiles/scssModule.js +384 -0
  135. package/dist/themeFiles/spec.d.ts +198 -0
  136. package/dist/themeFiles/spec.js +877 -0
  137. package/dist/tokens.css +30 -1
  138. package/dist/typography.js +1 -1
  139. package/dist/utilities.css +435 -0
  140. package/dist/utilities.d.ts +8 -0
  141. package/dist/utilities.js +75 -0
  142. package/package.json +20 -1
  143. package/src/components/AutoLayout/AutoLayout.scss +2 -26
  144. package/src/components/Carousel/Carousel.scss +122 -126
  145. package/src/components/Checkbox/Checkbox.scss +2 -2
  146. package/src/components/Chip/Chip.scss +63 -8
  147. package/src/components/ChoiceGroup/ChoiceGroup.scss +19 -1
  148. package/src/components/Collapsible/Collapsible.scss +3 -0
  149. package/src/components/ColorPicker/ColorPicker.scss +95 -0
  150. package/src/components/Combobox/Combobox.scss +4 -2
  151. package/src/components/Dialog/Dialog.scss +24 -7
  152. package/src/components/EditableField/EditableField.scss +275 -0
  153. package/src/components/Grid/Grid.scss +2 -70
  154. package/src/components/Input/Input.scss +29 -0
  155. package/src/components/Menu/Menu.scss +18 -0
  156. package/src/components/NumberField/NumberField.scss +56 -9
  157. package/src/components/NumberStepper/NumberStepper.scss +13 -0
  158. package/src/components/PageTransition/PageTransition.scss +29 -0
  159. package/src/components/Radio/Radio.scss +2 -2
  160. package/src/components/ScrollCarousel/ScrollCarousel.scss +142 -0
  161. package/src/components/Select/Select.scss +4 -0
  162. package/src/components/Slider/Slider.scss +51 -6
  163. package/src/components/Stack/Stack.scss +2 -48
  164. package/src/components/Switch/Switch.scss +9 -6
  165. package/src/components/Text/Text.scss +5 -2
  166. package/src/styles/_breakpoint-vars.scss +17 -11
  167. package/src/styles/_controls.scss +2 -2
  168. package/src/styles/_core.scss +1 -0
  169. package/src/styles/_kit.scss +483 -56
  170. package/src/styles/_layout-props.scss +197 -0
  171. package/src/styles/_manifest.scss +55 -25
  172. package/src/styles/_mixins.scss +53 -15
  173. package/src/styles/_slides.scss +112 -0
  174. package/src/styles/_utilities.scss +104 -0
  175. package/src/styles/breakpoints.test.ts +21 -1
  176. package/src/styles/entryPoints.test.ts +25 -2
  177. package/src/styles/index.scss +3 -1
  178. package/src/styles/kit.test.ts +171 -1
  179. package/src/styles/public/_breakpoints.scss +43 -0
  180. package/src/styles/public/_slides.scss +3 -0
  181. package/src/styles/public/_utilities.scss +3 -0
  182. package/src/styles/resets.scss +7 -0
  183. package/src/styles/utilities.test.ts +39 -0
  184. package/themes/bevel.css +187 -187
  185. package/themes/bevel.scss +960 -958
  186. package/themes/candy.css +87 -0
  187. package/themes/candy.scss +32 -0
  188. package/themes/extrude.css +42 -42
  189. package/themes/folio.css +200 -200
  190. package/themes/folio.scss +880 -878
  191. package/themes/forest.css +87 -0
  192. package/themes/forest.scss +32 -0
  193. package/themes/grape.css +87 -0
  194. package/themes/grape.scss +32 -0
  195. package/themes/greese.css +36 -0
  196. package/themes/greese.scss +21 -0
  197. package/themes/mono.css +87 -0
  198. package/themes/mono.scss +32 -0
  199. package/themes/ocean.css +87 -0
  200. package/themes/ocean.scss +32 -0
  201. package/themes/outline.css +18 -18
  202. package/themes/pebble.css +8 -8
  203. package/themes/ridge.css +188 -188
  204. package/themes/ridge.scss +820 -818
  205. package/themes/soft.css +11 -11
  206. package/themes/sunset.css +87 -0
  207. package/themes/sunset.scss +32 -0
  208. package/themes/ticket.css +180 -180
  209. package/themes/ticket.scss +881 -879
  210. package/themes/tilt.css +25 -25
@@ -0,0 +1,104 @@
1
+ // `pkg:@greese/ui/scss/utilities`: the utility classes (ADR 0019), written by
2
+ // Sass from the same table as `utilities.css`, the Vite plugin and the CLI
3
+ // (src/utilities.ts, as dist/_utilities-table.scss). Built here, they take the
4
+ // breakpoints the stylesheet is built with, so `md-…` agrees with the
5
+ // components' `md` without passing the breakpoints twice:
6
+ // @use 'pkg:@greese/ui/scss/config' with ($breakpoints: (…));
7
+ // @use 'pkg:@greese/ui/scss/utilities';
8
+ // @include utilities.classes($groups: (spacing, layout), $prefix: g);
9
+ // Sass can't read your source to keep only the classes it uses: name them in
10
+ // `$only`, or use the Vite plugin or `greese-utilities --content`, which can.
11
+ @use 'sass:list';
12
+ @use 'sass:map';
13
+ @use 'sass:meta';
14
+ @use 'sass:string';
15
+ @use 'config';
16
+ @use '../../dist/maps' as defaults;
17
+ @use '../../dist/utilities-table' as table;
18
+
19
+ /// The groups, in the order they're written.
20
+ $groups: table.$groups;
21
+
22
+ @function _name($parts...) {
23
+ $out: '';
24
+
25
+ @each $part in $parts {
26
+ @if $part and $part != '' {
27
+ @if $out == '' {
28
+ $out: '#{$part}';
29
+ } @else {
30
+ $out: '#{$out}-#{$part}';
31
+ }
32
+ }
33
+ }
34
+
35
+ @return $out;
36
+ }
37
+
38
+ @mixin _variant($variant, $groups, $only, $prefix) {
39
+ @each $d in table.$definitions {
40
+ $group: map.get($d, group);
41
+ $wanted: not $groups or list.index($groups, $group);
42
+
43
+ @if $variant == hover {
44
+ $wanted: $wanted and map.get($d, hover);
45
+ } @else if $variant {
46
+ $wanted: $wanted and map.get($d, responsive);
47
+ }
48
+
49
+ @if $wanted {
50
+ @each $value, $declarations in map.get($d, values) {
51
+ $name: _name($prefix, $variant, map.get($d, stem), $value);
52
+
53
+ @if not $only or list.index($only, $name) {
54
+ $selector: '.#{$name}';
55
+
56
+ @if $variant == hover {
57
+ $selector: '#{$selector}:hover';
58
+ }
59
+
60
+ #{$selector} {
61
+ @each $property, $v in $declarations {
62
+ #{$property}: string.unquote($v);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /// The utility classes, in `@layer greese.utilities`: plain, then `hover-…`
72
+ /// where hover exists, then each breakpoint's (`md-…`), smallest first, so a
73
+ /// larger screen's class wins (mobile first).
74
+ ///
75
+ /// $groups only these groups (`spacing`, `sizing`, `layout`, `flex`,
76
+ /// `grid`, `typography`, `border`, `color`, `effects`); all by default
77
+ /// $only only these class names, prefix included: your purge list
78
+ /// $prefix a prefix for every class (`g` gives `g-p-4`)
79
+ @mixin classes($groups: null, $only: null, $prefix: '') {
80
+ @each $group in $groups or () {
81
+ @if not list.index(table.$groups, $group) {
82
+ @error 'greese ui utilities: no group `#{$group}`. Groups: #{meta.inspect(table.$groups)}.';
83
+ }
84
+ }
85
+
86
+ // stylelint-disable-next-line layer-name-pattern -- the whole list, interpolated
87
+ @layer #{defaults.$layer-order};
88
+
89
+ @layer greese.utilities {
90
+ @include _variant(null, $groups, $only, $prefix);
91
+
92
+ @media (hover: hover) {
93
+ @include _variant(hover, $groups, $only, $prefix);
94
+ }
95
+
96
+ @each $key, $min in config.$breakpoints {
97
+ @if $min > 0 {
98
+ @media (width >= #{$min}) {
99
+ @include _variant($key, $groups, $only, $prefix);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
@@ -8,7 +8,10 @@ const build = (breakpoints: string): string =>
8
8
 
9
9
  const DEFAULTS = 'xs: 0px, sm: 600px, md: 900px, lg: 1200px, xl: 1536px'
10
10
  const vars = (css: string) =>
11
- [...css.matchAll(/--greese-breakpoint-([a-z0-9]+): ([^;]+);/g)].map((m) => [m[1], m[2]])
11
+ [...css.matchAll(/--greese-breakpoint-(?!keys)([a-z0-9]+): ([^;]+);/g)].map((m) => [
12
+ m[1],
13
+ m[2],
14
+ ])
12
15
 
13
16
  describe('breakpoints, fixed when the stylesheet is built', () => {
14
17
  it('declares the defaults as custom properties, in the tokens', () => {
@@ -20,12 +23,29 @@ describe('breakpoints, fixed when the stylesheet is built', () => {
20
23
  ['lg', '1200px'],
21
24
  ['xl', '1536px'],
22
25
  ])
26
+ // And their keys, in order, for configureBreakpoints() to read.
27
+ expect(tokens).toContain('--greese-breakpoint-keys: xs sm md lg xl;')
23
28
  })
24
29
 
25
30
  it('does not repeat them when built with the defaults', () => {
26
31
  expect(vars(build(`(${DEFAULTS})`)).length).toBe(5)
27
32
  })
28
33
 
34
+ it('declares only the configured ones in a build of its own, keys included', () => {
35
+ const css = build(
36
+ '(xs: 0px, sm: 600px, tablet: 700px, md: 777px, lg: 1200px, xl: 1536px)'
37
+ )
38
+ expect(vars(css)).toEqual([
39
+ ['xs', '0px'],
40
+ ['sm', '600px'],
41
+ ['tablet', '700px'],
42
+ ['md', '777px'],
43
+ ['lg', '1200px'],
44
+ ['xl', '1536px'],
45
+ ])
46
+ expect(css).toContain('--greese-breakpoint-keys: xs sm tablet md lg xl;')
47
+ })
48
+
29
49
  it('adds a key: classes, media queries and a custom property', () => {
30
50
  const css = build(
31
51
  '(xs: 0px, sm: 600px, tablet: 700px, md: 900px, lg: 1200px, xl: 1536px)'
@@ -5,7 +5,15 @@ import { compileAsProject, rules } from '../test/sass.ts'
5
5
 
6
6
  const compile = compileAsProject
7
7
 
8
- const AUTHORING = ['mixins', 'tones', 'floating', 'controls'] as const
8
+ const AUTHORING = [
9
+ 'mixins',
10
+ 'breakpoints',
11
+ 'tones',
12
+ 'floating',
13
+ 'controls',
14
+ 'slides',
15
+ 'utilities',
16
+ ] as const
9
17
 
10
18
  describe('the SCSS entry points for theme authors (ADR 0021)', () => {
11
19
  it('are exported, and each compiles alone without writing CSS', () => {
@@ -14,7 +22,7 @@ describe('the SCSS entry points for theme authors (ADR 0021)', () => {
14
22
  expect(exported, name).toContain(`./scss/${name}`)
15
23
  expect(compile(`@use 'pkg:@greese/ui/scss/${name}';`), name).toBe('')
16
24
  }
17
- })
25
+ }, 30_000)
18
26
 
19
27
  it('give a component’s own members, under the name its source uses', () => {
20
28
  const css = compile(`
@@ -66,3 +74,18 @@ describe('the SCSS entry points for theme authors (ADR 0021)', () => {
66
74
  ).toThrow(/Undefined mixin/)
67
75
  })
68
76
  })
77
+
78
+ describe('a build that picks its components (ADR 0034)', () => {
79
+ it('compiles core and chosen components from their sources, with its own breakpoints', () => {
80
+ const css = compile(`
81
+ @use 'pkg:@greese/ui/scss/config' with ($breakpoints: (xs: 0px, sm: 480px, md: 700px, lg: 1024px, xl: 1440px));
82
+ @use 'pkg:@greese/ui/scss/core';
83
+ @use 'pkg:@greese/ui/scss/components/Stack';
84
+ @use 'pkg:@greese/ui/scss/components/Button';`)
85
+ expect(css).toContain('--greese-color-primary:')
86
+ expect(rules(css, '.greese-Button').length).toBeGreaterThan(0)
87
+ expect(css).toContain('@media (width >= 700px)')
88
+ expect(css).not.toContain('greese-Dialog')
89
+ expect(css.match(/@layer greese\.reset, /g)).toHaveLength(1)
90
+ })
91
+ })
@@ -5,7 +5,6 @@
5
5
  // lets `@use … with ($breakpoints: …)` reconfigure every component.
6
6
  @forward 'config';
7
7
  @use 'core';
8
- @use 'breakpoint-vars';
9
8
  @use '../components/ThemeProvider/ThemeProvider';
10
9
  @use '../components/VisuallyHidden/VisuallyHidden';
11
10
  @use '../components/Box/Box';
@@ -69,13 +68,16 @@
69
68
  @use '../components/SegmentedControl/SegmentedControl';
70
69
  @use '../components/DateField/DateField';
71
70
  @use '../components/NumberField/NumberField';
71
+ @use '../components/NumberStepper/NumberStepper';
72
72
  @use '../components/Table/Table';
73
73
  @use '../components/Combobox/Combobox';
74
+ @use '../components/ScrollCarousel/ScrollCarousel';
74
75
  @use '../components/Carousel/Carousel';
75
76
  @use '../components/ChatBubble/ChatBubble';
76
77
  @use '../components/Countdown/Countdown';
77
78
  @use '../components/Diff/Diff';
78
79
  @use '../components/Dock/Dock';
80
+ @use '../components/EditableField/EditableField';
79
81
  @use '../components/Fieldset/Fieldset';
80
82
  @use '../components/Indicator/Indicator';
81
83
  @use '../components/Key/Key';
@@ -13,8 +13,15 @@ import {
13
13
  } from '../index.ts'
14
14
  import { blocks, compileScss, rules } from '../test/sass.ts'
15
15
 
16
- const compile = (scss: string, use = `@use 'kit' as greese;`): string =>
16
+ /** The guard theme rules carry for `layer` (see reset-guard() in the kit). */
17
+ const guard = (layer: string) =>
18
+ `:where(:not(.greese-reset-${layer}, .greese-reset-${layer} *, .greese-reset, .greese-reset *))`
19
+
20
+ /** Compiled kit output with or without the reset guards, which most tests aren't about. */
21
+ const compileGuarded = (scss: string, use = `@use 'kit' as greese;`): string =>
17
22
  compileScss(`${use}\n${scss}`)
23
+ const compile = (scss: string, use?: string): string =>
24
+ compileGuarded(scss, use).replace(/:where\(:not\(\.greese-reset-[^)]*\)\)/g, '')
18
25
 
19
26
  /** The custom properties the first rule for `selector` declares, in order. */
20
27
  const declared = (css: string, selector: string): Array<[string, string]> => {
@@ -231,6 +238,23 @@ describe('SCSS kit: motion()', () => {
231
238
  ])
232
239
  })
233
240
 
241
+ it('assigns the carousel roles, forward and back', () => {
242
+ const css = compile(`@include greese.motion-defaults((
243
+ carousel: fade-in,
244
+ carousel-back: pocket-right,
245
+ carousel-exit: fade-out,
246
+ carousel-back-exit: pocket-out-right,
247
+ carousel-duration: slow,
248
+ ));`)
249
+ expect(Object.fromEntries(declared(css, ':root'))).toMatchObject({
250
+ '--greese-motion-carousel': 'greese-fade-in',
251
+ '--greese-motion-carousel-back': 'greese-pocket-right',
252
+ '--greese-motion-carousel-exit-opacity': '0',
253
+ '--greese-motion-carousel-back-exit-translate': '100% 0px',
254
+ '--greese-motion-carousel-duration': 'var(--greese-duration-slow)',
255
+ })
256
+ })
257
+
234
258
  it('assigns motion roles with motion-defaults(), pointing at effects and tokens', () => {
235
259
  const css = compile(`@include greese.motion-defaults((
236
260
  content: wipe-open-x,
@@ -611,6 +635,55 @@ describe('SCSS kit: tokens() and theme()', () => {
611
635
  ])
612
636
  })
613
637
 
638
+ /** What each selector ends up declaring, by where it sits: later blocks win. */
639
+ const effective = (css: string) => {
640
+ const out = new Map<string, Map<string, string>>()
641
+ for (const b of blocks(css)) {
642
+ const key = [...b.within, b.prelude].join(' > ')
643
+ const seen = out.get(key) ?? new Map<string, string>()
644
+ for (const [p, v] of b.declarations) seen.set(p, v)
645
+ if (b.declarations.length) out.set(key, seen)
646
+ }
647
+ return out
648
+ }
649
+
650
+ it.each([
651
+ ['in a named theme', (body: string) => `@include greese.theme(brand) { ${body} }`],
652
+ ['on the page', (body: string) => body],
653
+ ])('adds up repeated calls %s, as one merged call', (_, wrap) => {
654
+ const color = `@include greese.tokens((color: (primary: (light: #0f766e, dark: #5eead4), accentContent: #000)));`
655
+ const shape = `@include greese.tokens((radius: (md: 2px), color: (accent: "#fde047")), $extend: (space: (9: 5rem)));`
656
+ const merged = `@include greese.tokens((color: (primary: (light: #0f766e, dark: #5eead4), accentContent: #000, accent: "#fde047"), radius: (md: 2px)), $extend: (space: (9: 5rem)));`
657
+ const split = compile(wrap(`${color}\n${shape}`))
658
+ expect(effective(split)).toEqual(effective(compile(wrap(merged))))
659
+ // The second call writes only what it changes, not the theme again.
660
+ expect(split.match(/--greese-space-4:/g)?.length ?? 0).toBeLessThanOrEqual(1)
661
+ })
662
+
663
+ it('keeps an earlier call’s explicit value over what a later one derives', () => {
664
+ const css = compile(`@include greese.theme(brand) {
665
+ @include greese.tokens((color: (primaryContent: #123456)));
666
+ @include greese.tokens((color: (primary: "#fde047")));
667
+ }`)
668
+ const values = effective(css).get('@layer greese.theme > [data-greese-theme=brand]')
669
+ expect(values?.get('--greese-color-primary-content')).toBe('#123456')
670
+ expect(values?.get('--greese-color-primary')).toBe('#fde047')
671
+ })
672
+
673
+ it('scopes to a theme with scope(name), and to the page with null, false or empty', () => {
674
+ const card = `@include greese.component(Card) { padding: 0; }`
675
+ expect(
676
+ rules(
677
+ compile(`@include greese.scope(brand) { ${card} }`),
678
+ '[data-greese-theme=brand] .greese-Card'
679
+ )
680
+ ).toHaveLength(1)
681
+ for (const none of ['null', 'false', "''"])
682
+ expect(
683
+ rules(compile(`@include greese.scope(${none}) { ${card} }`), '.greese-Card')
684
+ ).toHaveLength(1)
685
+ })
686
+
614
687
  it('rejects unknown token groups and keys', () => {
615
688
  expect(() => compile('@include greese.tokens((colr: (accent: red)));')).toThrow(
616
689
  /no token group `colr`/
@@ -752,3 +825,100 @@ describe('SCSS kit: keyframes()', () => {
752
825
  ).toThrow(/built-in effect keeps its keyframes/)
753
826
  })
754
827
  })
828
+
829
+ describe('SCSS kit: resets', () => {
830
+ it('guards each theme rule for its layer: component() as shape, motion() as motion', () => {
831
+ const css = compileGuarded(`@include greese.component(Dialog) {
832
+ padding: 0;
833
+ @include greese.part(body) { margin: 0; }
834
+ @include greese.motion((enter: fade-up));
835
+ }
836
+ @include greese.component(Card, $layer: color) { color: red; }`)
837
+ expect(rules(css, `.greese-Dialog${guard('shape')}`)[0]?.declarations).toEqual([
838
+ ['padding', '0'],
839
+ ])
840
+ expect(
841
+ rules(css, `.greese-Dialog${guard('shape')} .greese-Dialog-body`)
842
+ ).toHaveLength(1)
843
+ expect(
844
+ ruleIn(css, `.greese-Dialog${guard('motion')}`, '@layer greese.overrides', motionOk)
845
+ ).toContainEqual(expect.arrayContaining(['animation']))
846
+ expect(rules(css, `.greese-Card${guard('color')}`)).toHaveLength(1)
847
+ })
848
+
849
+ it('writes reset-guard() for rules of your own', () => {
850
+ const css = compileGuarded(`.x#{greese.reset-guard(motion)} { a: b; }`)
851
+ expect(rules(css, `.x${guard('motion')}`)).toHaveLength(1)
852
+ expect(() => compileGuarded(`.x#{greese.reset-guard(sound)} { a: b; }`)).toThrow(
853
+ /no theme layer `sound`. Layers: color, shape, motion/
854
+ )
855
+ })
856
+
857
+ it('declares each layer’s defaults in the reset classes, and all three in .greese-reset', () => {
858
+ const css = compileGuarded('@include greese.reset-classes;')
859
+ const color = declared(css, '.greese-reset-color')
860
+ expect(color).toContainEqual([
861
+ '--greese-color-primary',
862
+ 'light-dark(#2771e0, #81b2ff)',
863
+ ])
864
+ expect(declared(css, '.greese-reset-shape')).toContainEqual([
865
+ '--greese-corner-control',
866
+ expect.any(String),
867
+ ])
868
+ expect(declared(css, '.greese-reset-motion')).toEqual(
869
+ expect.arrayContaining([
870
+ ['--greese-motion-overlay', 'initial'],
871
+ ['--greese-motion-dialog-exit-opacity', 'initial'],
872
+ ])
873
+ )
874
+ for (const layer of ['color', 'shape', 'motion'])
875
+ expect(rules(css, '.greese-reset').map((r) => r.declarations)).toContainEqual(
876
+ declared(css, `.greese-reset-${layer}`)
877
+ )
878
+ })
879
+
880
+ it('declares the defaults in any rule with reset()', () => {
881
+ const css = compileGuarded('.notice { @include greese.reset(color); }')
882
+ expect(declared(css, '.notice')).toContainEqual([
883
+ '--greese-color-primary',
884
+ 'light-dark(#2771e0, #81b2ff)',
885
+ ])
886
+ expect(declared(css, '.notice').some(([p]) => p.startsWith('--greese-corner'))).toBe(
887
+ false
888
+ )
889
+ })
890
+
891
+ it('leaves a component out of the layers reset() takes from it, for the rest of the build', () => {
892
+ const css = compileGuarded(`
893
+ @include greese.component(Card) { @include greese.reset(shape); }
894
+ @include greese.component(Card) { border-radius: 0; @include greese.motion((enter: fade-up)); }
895
+ @include greese.component(Card, $layer: color) { color: red; }
896
+ .a { clip-path: none; }
897
+ #{greese.themed('.greese-Card', Card)}, #{greese.themed('.greese-Tile', Tile)} { clip-path: inset(0); }`)
898
+ // The reset layer's rule matches nothing; motion() inside it still plays.
899
+ expect(
900
+ rules(css, `.greese-Card${guard('shape')}:not(*)`).map((r) => r.declarations)
901
+ ).toContainEqual([['border-radius', '0']])
902
+ expect(
903
+ ruleIn(css, `.greese-Card${guard('motion')}`, '@layer greese.overrides', motionOk)
904
+ ).toHaveLength(1)
905
+ expect(css).toContain('color: red')
906
+ expect(
907
+ rules(css, `.greese-Card${guard('shape')}:not(*)`).map((r) => r.declarations)
908
+ ).toContainEqual([['clip-path', 'inset(0)']])
909
+ expect(rules(css, `.greese-Tile${guard('shape')}`)).toHaveLength(1)
910
+ })
911
+
912
+ it('warns when reset() comes after the rules it would take back', () => {
913
+ const warnings: string[] = []
914
+ compileScss(
915
+ `@use 'kit' as greese;
916
+ @include greese.component(Card) { border-radius: 0; }
917
+ @include greese.component(Card) { @include greese.reset(shape); }`,
918
+ warnings
919
+ )
920
+ expect(warnings.join('\n')).toMatch(
921
+ /reset\(shape\) for Card comes after Card's shape rules/
922
+ )
923
+ })
924
+ })
@@ -0,0 +1,43 @@
1
+ // `pkg:@greese/ui/scss/breakpoints`: more breakpoints for the layout
2
+ // components, measured against a container instead of the viewport. A library
3
+ // whose components live in boxes of their own size (@greese/authoring's
4
+ // panels) names its container and its keys, and gets Stack's, Grid's and
5
+ // AutoLayout's responsive classes for them:
6
+ // @use 'pkg:@greese/ui/scss/breakpoints';
7
+ // @layer greese.components {
8
+ // @include breakpoints.container-set((panelmd: 360px), greese-panel);
9
+ // }
10
+ // Keys follow `$breakpoints`' rules (one word, no dashes, increasing), and
11
+ // mustn't be one of its own. JavaScript learns them through
12
+ // `addContainerBreakpoints()` from @greese/react, so `resolveResponsive()`
13
+ // writes their classes. Writes no CSS until included.
14
+ @use 'sass:map';
15
+ @use 'sass:string';
16
+ @use '../config';
17
+ @use '../layout-props';
18
+
19
+ /// Stack's, Grid's and AutoLayout's responsive props for `$set`, each key
20
+ /// applying where the nearest `$container` is at least that wide.
21
+ @mixin container-set($set, $container) {
22
+ $previous: -1px;
23
+
24
+ @each $key, $min in $set {
25
+ @if map.has-key(config.$breakpoints, $key) {
26
+ @error 'container-set: `#{$key}` is already a viewport breakpoint; give the container\'s its own name.';
27
+ }
28
+
29
+ @if string.index('#{$key}', '-') {
30
+ @error 'container-set: key `#{$key}` must be one word, without dashes.';
31
+ }
32
+
33
+ @if $min <= $previous {
34
+ @error 'container-set: breakpoints must increase, but `#{$key}` (#{$min}) does not exceed the one before it.';
35
+ }
36
+
37
+ $previous: $min;
38
+ }
39
+
40
+ @include layout-props.stack($set, $container);
41
+ @include layout-props.grid($set, $container);
42
+ @include layout-props.autolayout($set, $container);
43
+ }
@@ -0,0 +1,3 @@
1
+ // `pkg:@greese/ui/scss/slides`: the previous/next buttons and dots that
2
+ // ScrollCarousel and Carousel share, for theme authors (ADR 0021).
3
+ @forward '../slides';
@@ -0,0 +1,3 @@
1
+ // `pkg:@greese/ui/scss/utilities`: the utility classes, built with this
2
+ // build's breakpoints (ADR 0034). The library's own module, as the others.
3
+ @forward '../utilities';
@@ -0,0 +1,7 @@
1
+ // Compiled to dist/css/resets.css: the classes that take a theme's layers back
2
+ // from part of a page (`.greese-reset-color`, `-shape`, `-motion`, and
3
+ // `.greese-reset` for all three) with their default tokens. Opt-in, since most
4
+ // pages never reset. Theme rules stop at the classes either way; this file
5
+ // adds the defaults the tokens return to.
6
+ @use 'kit';
7
+ @include kit.reset-classes;
@@ -0,0 +1,39 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it } from 'vitest'
3
+ import { generateUtilities } from '../utilities.ts'
4
+ import { blocks, compileScss } from '../test/sass.ts'
5
+
6
+ describe('utilities in Sass (pkg:@greese/ui/scss/utilities)', () => {
7
+ /** Each block's place, selector and declarations: the CSS, formatting aside. */
8
+ const shape = (css: string) =>
9
+ blocks(css)
10
+ .filter((b) => b.declarations.length)
11
+ .map((b) => [...b.within, b.prelude, ...b.declarations.flat()].join(' | '))
12
+
13
+ const sass = (call: string, use = '') =>
14
+ compileScss(`${use}\n@use 'utilities';\n@include utilities.classes${call};`)
15
+
16
+ it('writes what generateUtilities() writes', () => {
17
+ expect(shape(sass(''))).toEqual(shape(generateUtilities()))
18
+ })
19
+
20
+ it('takes the build’s breakpoints, groups, a prefix and a purge list', () => {
21
+ const breakpoints = { xs: 0, sm: 480, tablet: 700, md: 768, lg: 1024, xl: 1440 }
22
+ const custom = `@use 'config' with ($breakpoints: (${Object.entries(breakpoints)
23
+ .map(([k, v]) => `${k}: ${v}px`)
24
+ .join(', ')}));`
25
+ expect(shape(sass('($groups: (spacing, layout), $prefix: g)', custom))).toEqual(
26
+ shape(
27
+ generateUtilities({ groups: ['spacing', 'layout'], prefix: 'g', breakpoints })
28
+ )
29
+ )
30
+ const used = new Set(['p-4', 'md-display-flex', 'hover-opacity-75'])
31
+ expect(shape(sass(`($only: (${[...used].join(', ')}))`))).toEqual(
32
+ shape(generateUtilities({ used }))
33
+ )
34
+ })
35
+
36
+ it('stops the build on an unknown group', () => {
37
+ expect(() => sass('($groups: (spaces))')).toThrow(/no group `spaces`/)
38
+ })
39
+ })