@greese/ui 0.0.0-stage → 1.0.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 (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -2
  3. package/bin/greese-utilities.mjs +56 -0
  4. package/dist/_layers.scss +2 -0
  5. package/dist/_maps.scss +1884 -0
  6. package/dist/_tokens-base.scss +174 -0
  7. package/dist/_tokens.scss +2140 -0
  8. package/dist/base.css +67 -0
  9. package/dist/colorScheme.d.ts +16 -0
  10. package/dist/colorScheme.js +13 -0
  11. package/dist/contrast.d.ts +15 -0
  12. package/dist/contrast.js +43 -0
  13. package/dist/css/Accordion.css +9 -0
  14. package/dist/css/ActionSelect.css +6 -0
  15. package/dist/css/Alert.css +84 -0
  16. package/dist/css/AppBar.css +35 -0
  17. package/dist/css/AspectRatio.css +40 -0
  18. package/dist/css/AutoLayout.css +508 -0
  19. package/dist/css/Avatar.css +88 -0
  20. package/dist/css/Badge.css +66 -0
  21. package/dist/css/Box.css +640 -0
  22. package/dist/css/Breadcrumbs.css +57 -0
  23. package/dist/css/Button.css +275 -0
  24. package/dist/css/ButtonGroup.css +40 -0
  25. package/dist/css/Card.css +101 -0
  26. package/dist/css/Carousel.css +170 -0
  27. package/dist/css/Center.css +11 -0
  28. package/dist/css/ChatBubble.css +128 -0
  29. package/dist/css/Checkbox.css +65 -0
  30. package/dist/css/Chip.css +124 -0
  31. package/dist/css/ChoiceGroup.css +57 -0
  32. package/dist/css/Code.css +29 -0
  33. package/dist/css/Collapsible.css +126 -0
  34. package/dist/css/ColorPicker.css +191 -0
  35. package/dist/css/Combobox.css +90 -0
  36. package/dist/css/Container.css +30 -0
  37. package/dist/css/Countdown.css +25 -0
  38. package/dist/css/DateField.css +15 -0
  39. package/dist/css/Dialog.css +640 -0
  40. package/dist/css/Diff.css +103 -0
  41. package/dist/css/Divider.css +19 -0
  42. package/dist/css/Dock.css +91 -0
  43. package/dist/css/Field.css +33 -0
  44. package/dist/css/Fieldset.css +54 -0
  45. package/dist/css/FileInput.css +65 -0
  46. package/dist/css/Footer.css +72 -0
  47. package/dist/css/Grid.css +998 -0
  48. package/dist/css/Header.css +89 -0
  49. package/dist/css/Heading.css +6 -0
  50. package/dist/css/Icon.css +12 -0
  51. package/dist/css/IconButton.css +7 -0
  52. package/dist/css/Image.css +69 -0
  53. package/dist/css/Indicator.css +41 -0
  54. package/dist/css/Input.css +115 -0
  55. package/dist/css/Key.css +42 -0
  56. package/dist/css/Link.css +72 -0
  57. package/dist/css/List.css +52 -0
  58. package/dist/css/Media.css +8 -0
  59. package/dist/css/Menu.css +161 -0
  60. package/dist/css/Motion.css +1356 -0
  61. package/dist/css/NumberField.css +49 -0
  62. package/dist/css/OtpField.css +90 -0
  63. package/dist/css/PageTransition.css +979 -0
  64. package/dist/css/Pagination.css +43 -0
  65. package/dist/css/Popover.css +93 -0
  66. package/dist/css/Progress.css +101 -0
  67. package/dist/css/RadialProgress.css +44 -0
  68. package/dist/css/Radio.css +41 -0
  69. package/dist/css/Rating.css +100 -0
  70. package/dist/css/ScrollSpy.css +6 -0
  71. package/dist/css/SegmentedControl.css +101 -0
  72. package/dist/css/Select.css +115 -0
  73. package/dist/css/Sidebar.css +207 -0
  74. package/dist/css/Skeleton.css +48 -0
  75. package/dist/css/Slider.css +75 -0
  76. package/dist/css/Spacer.css +34 -0
  77. package/dist/css/SpeedDial.css +165 -0
  78. package/dist/css/Spinner.css +44 -0
  79. package/dist/css/Stack.css +528 -0
  80. package/dist/css/Stat.css +107 -0
  81. package/dist/css/Status.css +86 -0
  82. package/dist/css/Steps.css +201 -0
  83. package/dist/css/Swap.css +49 -0
  84. package/dist/css/Switch.css +66 -0
  85. package/dist/css/Table.css +269 -0
  86. package/dist/css/Tabs.css +119 -0
  87. package/dist/css/Text.css +60 -0
  88. package/dist/css/TextRotate.css +147 -0
  89. package/dist/css/Textarea.css +107 -0
  90. package/dist/css/ThemeProvider.css +6 -0
  91. package/dist/css/Tile.css +18 -0
  92. package/dist/css/Toast.css +110 -0
  93. package/dist/css/ToggleButton.css +20 -0
  94. package/dist/css/Tooltip.css +94 -0
  95. package/dist/css/Type.css +211 -0
  96. package/dist/css/Visibility.css +78 -0
  97. package/dist/css/VisuallyHidden.css +15 -0
  98. package/dist/css/core.css +251 -0
  99. package/dist/css.d.ts +29 -0
  100. package/dist/css.js +28 -0
  101. package/dist/effect-classes.d.ts +8 -0
  102. package/dist/effect-classes.js +5 -0
  103. package/dist/effects/css.d.ts +71 -0
  104. package/dist/effects/css.js +184 -0
  105. package/dist/effects/definitions.d.ts +555 -0
  106. package/dist/effects/definitions.js +379 -0
  107. package/dist/effects/frames.d.ts +18 -0
  108. package/dist/effects/frames.js +68 -0
  109. package/dist/effects/sequence.d.ts +33 -0
  110. package/dist/effects/sequence.js +106 -0
  111. package/dist/effects.d.ts +30 -0
  112. package/dist/effects.js +30 -0
  113. package/dist/greese.css +11907 -0
  114. package/dist/index.d.ts +10 -0
  115. package/dist/index.js +10 -0
  116. package/dist/kebab.d.ts +2 -0
  117. package/dist/kebab.js +2 -0
  118. package/dist/palette.css +249 -0
  119. package/dist/palette.d.ts +599 -0
  120. package/dist/palette.js +334 -0
  121. package/dist/shapes.d.ts +193 -0
  122. package/dist/shapes.js +852 -0
  123. package/dist/storage.d.ts +47 -0
  124. package/dist/storage.js +118 -0
  125. package/dist/style-manifest.json +3010 -0
  126. package/dist/theme.d.ts +71 -0
  127. package/dist/theme.js +180 -0
  128. package/dist/tokens.css +253 -0
  129. package/dist/tokens.d.ts +343 -0
  130. package/dist/tokens.js +179 -0
  131. package/dist/typography.d.ts +55 -0
  132. package/dist/typography.js +144 -0
  133. package/dist/utilities.css +6975 -0
  134. package/dist/utilities.d.ts +82 -0
  135. package/dist/utilities.js +636 -0
  136. package/dist/utilitiesFiles.d.ts +18 -0
  137. package/dist/utilitiesFiles.js +26 -0
  138. package/dist/vite.d.ts +40 -0
  139. package/dist/vite.js +43 -0
  140. package/package.json +131 -4
  141. package/src/components/Accordion/Accordion.scss +9 -0
  142. package/src/components/ActionSelect/ActionSelect.scss +6 -0
  143. package/src/components/Alert/Alert.scss +71 -0
  144. package/src/components/AppBar/AppBar.scss +43 -0
  145. package/src/components/AspectRatio/AspectRatio.scss +22 -0
  146. package/src/components/AutoLayout/AutoLayout.scss +90 -0
  147. package/src/components/Avatar/Avatar.scss +91 -0
  148. package/src/components/Badge/Badge.scss +52 -0
  149. package/src/components/Box/Box.scss +63 -0
  150. package/src/components/Breadcrumbs/Breadcrumbs.scss +59 -0
  151. package/src/components/Button/Button.scss +231 -0
  152. package/src/components/ButtonGroup/ButtonGroup.scss +56 -0
  153. package/src/components/Card/Card.scss +99 -0
  154. package/src/components/Carousel/Carousel.scss +161 -0
  155. package/src/components/Center/Center.scss +11 -0
  156. package/src/components/ChatBubble/ChatBubble.scss +119 -0
  157. package/src/components/Checkbox/Checkbox.scss +66 -0
  158. package/src/components/Chip/Chip.scss +119 -0
  159. package/src/components/ChoiceGroup/ChoiceGroup.scss +69 -0
  160. package/src/components/Code/Code.scss +28 -0
  161. package/src/components/Collapsible/Collapsible.scss +136 -0
  162. package/src/components/ColorPicker/ColorPicker.scss +235 -0
  163. package/src/components/Combobox/Combobox.scss +83 -0
  164. package/src/components/Container/Container.scss +28 -0
  165. package/src/components/Countdown/Countdown.scss +28 -0
  166. package/src/components/DateField/DateField.scss +19 -0
  167. package/src/components/Dialog/Dialog.scss +268 -0
  168. package/src/components/Diff/Diff.scss +125 -0
  169. package/src/components/Divider/Divider.scss +22 -0
  170. package/src/components/Dock/Dock.scss +86 -0
  171. package/src/components/Field/Field.scss +38 -0
  172. package/src/components/Fieldset/Fieldset.scss +63 -0
  173. package/src/components/FileInput/FileInput.scss +66 -0
  174. package/src/components/Footer/Footer.scss +79 -0
  175. package/src/components/Grid/Grid.scss +86 -0
  176. package/src/components/Header/Header.scss +111 -0
  177. package/src/components/Heading/Heading.scss +7 -0
  178. package/src/components/Icon/Icon.scss +12 -0
  179. package/src/components/IconButton/IconButton.scss +6 -0
  180. package/src/components/Image/Image.scss +46 -0
  181. package/src/components/Indicator/Indicator.scss +48 -0
  182. package/src/components/Input/Input.scss +58 -0
  183. package/src/components/Key/Key.scss +48 -0
  184. package/src/components/Link/Link.scss +50 -0
  185. package/src/components/List/List.scss +62 -0
  186. package/src/components/Media/Media.scss +7 -0
  187. package/src/components/Menu/Menu.scss +91 -0
  188. package/src/components/Motion/Motion.scss +106 -0
  189. package/src/components/NumberField/NumberField.scss +44 -0
  190. package/src/components/OtpField/OtpField.scss +91 -0
  191. package/src/components/PageTransition/PageTransition.scss +168 -0
  192. package/src/components/Pagination/Pagination.scss +55 -0
  193. package/src/components/Popover/Popover.scss +13 -0
  194. package/src/components/Progress/Progress.scss +126 -0
  195. package/src/components/RadialProgress/RadialProgress.scss +65 -0
  196. package/src/components/Radio/Radio.scss +36 -0
  197. package/src/components/Rating/Rating.scss +121 -0
  198. package/src/components/ScrollSpy/ScrollSpy.scss +6 -0
  199. package/src/components/SegmentedControl/SegmentedControl.scss +107 -0
  200. package/src/components/Select/Select.scss +59 -0
  201. package/src/components/Sidebar/Sidebar.scss +88 -0
  202. package/src/components/Skeleton/Skeleton.scss +60 -0
  203. package/src/components/Slider/Slider.scss +96 -0
  204. package/src/components/Spacer/Spacer.scss +15 -0
  205. package/src/components/SpeedDial/SpeedDial.scss +109 -0
  206. package/src/components/Spinner/Spinner.scss +43 -0
  207. package/src/components/Stack/Stack.scss +57 -0
  208. package/src/components/Stat/Stat.scss +96 -0
  209. package/src/components/Status/Status.scss +60 -0
  210. package/src/components/Steps/Steps.scss +216 -0
  211. package/src/components/Swap/Swap.scss +62 -0
  212. package/src/components/Switch/Switch.scss +63 -0
  213. package/src/components/Table/Table.scss +183 -0
  214. package/src/components/Tabs/Tabs.scss +108 -0
  215. package/src/components/Text/Text.scss +34 -0
  216. package/src/components/TextRotate/TextRotate.scss +51 -0
  217. package/src/components/Textarea/Textarea.scss +56 -0
  218. package/src/components/ThemeProvider/ThemeProvider.scss +7 -0
  219. package/src/components/Tile/Tile.scss +23 -0
  220. package/src/components/Toast/Toast.scss +93 -0
  221. package/src/components/ToggleButton/ToggleButton.scss +23 -0
  222. package/src/components/Tooltip/Tooltip.scss +21 -0
  223. package/src/components/Type/Type.scss +35 -0
  224. package/src/components/Visibility/Visibility.scss +39 -0
  225. package/src/components/VisuallyHidden/VisuallyHidden.scss +17 -0
  226. package/src/styles/_breakpoint-vars.scss +16 -0
  227. package/src/styles/_config.scss +48 -0
  228. package/src/styles/_controls.scss +88 -0
  229. package/src/styles/_core.scss +4 -0
  230. package/src/styles/_effect.scss +51 -0
  231. package/src/styles/_floating.scss +141 -0
  232. package/src/styles/_kit.scss +1548 -0
  233. package/src/styles/_manifest.scss +670 -0
  234. package/src/styles/_mixins.scss +261 -0
  235. package/src/styles/_tones.scss +63 -0
  236. package/src/styles/_type-emit.scss +76 -0
  237. package/src/styles/_typography-vars.scss +8 -0
  238. package/src/styles/base.scss +80 -0
  239. package/src/styles/breakpoints.test.ts +73 -0
  240. package/src/styles/entryPoints.test.ts +68 -0
  241. package/src/styles/index.scss +93 -0
  242. package/src/styles/kit.test.ts +754 -0
  243. package/src/styles/public/_controls.scss +3 -0
  244. package/src/styles/public/_floating.scss +3 -0
  245. package/src/styles/public/_mixins.scss +5 -0
  246. package/src/styles/public/_tones.scss +4 -0
  247. package/themes/bevel.css +1077 -0
  248. package/themes/bevel.scss +1166 -0
  249. package/themes/depth.css +123 -0
  250. package/themes/depth.scss +46 -0
  251. package/themes/extrude.css +219 -0
  252. package/themes/extrude.scss +255 -0
  253. package/themes/folio.css +986 -0
  254. package/themes/folio.scss +1047 -0
  255. package/themes/gentle.css +66 -0
  256. package/themes/gentle.scss +39 -0
  257. package/themes/outline.css +127 -0
  258. package/themes/outline.scss +126 -0
  259. package/themes/pebble.css +65 -0
  260. package/themes/pebble.scss +69 -0
  261. package/themes/pocket.css +137 -0
  262. package/themes/pocket.scss +45 -0
  263. package/themes/ridge.css +927 -0
  264. package/themes/ridge.scss +998 -0
  265. package/themes/snappy.css +82 -0
  266. package/themes/snappy.scss +40 -0
  267. package/themes/soft.css +79 -0
  268. package/themes/soft.scss +84 -0
  269. package/themes/ticket.css +960 -0
  270. package/themes/ticket.scss +1031 -0
  271. package/themes/tilt.css +148 -0
  272. package/themes/tilt.scss +169 -0
  273. package/themes/wipe.css +186 -0
  274. package/themes/wipe.scss +46 -0
@@ -0,0 +1,754 @@
1
+ // @vitest-environment node
2
+ import { readFileSync } from 'node:fs'
3
+ import { describe, expect, it } from 'vitest'
4
+ import {
5
+ createTheme,
6
+ cssPlan,
7
+ effectClass,
8
+ effectCss,
9
+ effectNames,
10
+ effectPlan,
11
+ entranceNames,
12
+ LAYER_ORDER,
13
+ } from '../index.ts'
14
+ import { blocks, compileScss, rules } from '../test/sass.ts'
15
+
16
+ const compile = (scss: string, use = `@use 'kit' as greese;`): string =>
17
+ compileScss(`${use}\n${scss}`)
18
+
19
+ /** The custom properties the first rule for `selector` declares, in order. */
20
+ const declared = (css: string, selector: string): Array<[string, string]> => {
21
+ const [rule] = rules(css, selector)
22
+ expect(rule, `${selector} in\n${css}`).toBeDefined()
23
+ return rule!.declarations.filter(([name]) => name.startsWith('--'))
24
+ }
25
+
26
+ /** The rule for `selector` inside `within` (outermost first); fails if there is none. */
27
+ const ruleIn = (css: string, selector: string, ...within: string[]) => {
28
+ const rule = rules(css, selector).find((r) => r.within.join() === within.join())
29
+ expect(rule, `${selector} in ${within.join(' > ')}:\n${css}`).toBeDefined()
30
+ return rule!.declarations
31
+ }
32
+
33
+ const motionOk = '@media (prefers-reduced-motion: no-preference)'
34
+
35
+ describe('SCSS kit: names', () => {
36
+ it('emits no CSS when used', () => {
37
+ expect(compile('')).toBe('')
38
+ })
39
+
40
+ it('adds the prefix and separators to classes', () => {
41
+ const css = compile(`
42
+ a { --block: #{greese.class(Button)}; --part: #{greese.class(Button, icon)};
43
+ --mod: #{greese.class(Button, $modifier: solid)};
44
+ --part-mod: #{greese.class(Pagination, step, next)}; }`)
45
+ expect(css).toContain('--block: .greese-Button;')
46
+ expect(css).toContain('--part: .greese-Button-icon;')
47
+ expect(css).toContain('--mod: .greese-Button--solid;')
48
+ expect(css).toContain('--part-mod: .greese-Pagination-step--next;')
49
+ })
50
+
51
+ it('stops the build on unknown names, listing the real ones', () => {
52
+ expect(() => compile('a { b: greese.class(Buton); }')).toThrow(
53
+ /no component `Buton`.*Button/
54
+ )
55
+ expect(() => compile('a { b: greese.class(Button, icn); }')).toThrow(
56
+ /`Button` has no part `icn`. Parts: icon/
57
+ )
58
+ expect(() => compile('a { b: greese.class(Button, $modifier: big); }')).toThrow(
59
+ /no modifier `big`.*solid/
60
+ )
61
+ expect(() => compile('a { b: greese.token(color, acent); }')).toThrow(
62
+ /no color token `acent`/
63
+ )
64
+ })
65
+
66
+ it('lets unknown names through with $strict: false', () => {
67
+ const css = compile(
68
+ 'a { b: greese.class(Future, $part: thing); }',
69
+ `@use 'kit' as greese with ($strict: false);`
70
+ )
71
+ expect(css).toContain('b: .greese-Future-thing;')
72
+ })
73
+
74
+ it('reads tokens and component variables as var()', () => {
75
+ const css = compile(
76
+ 'a { b: greese.token(lineHeight, tight); c: greese.variable(button-tone); }'
77
+ )
78
+ expect(css).toContain('b: var(--greese-line-height-tight);')
79
+ expect(css).toContain('c: var(--greese-button-tone);')
80
+ })
81
+ })
82
+
83
+ describe('SCSS kit: component()', () => {
84
+ const css = compile(`
85
+ @include greese.component(Button) {
86
+ font-weight: 700;
87
+ @include greese.modifier(outline) { border-width: 2px; }
88
+ @include greese.part(icon) { opacity: 0.8; }
89
+ @include greese.hover { translate: 0 -1px; }
90
+ }
91
+ @include greese.component(Pagination) {
92
+ @include greese.part(step) { @include greese.modifier(next) { color: red; } }
93
+ }
94
+ `)
95
+
96
+ it('puts overrides in greese.overrides, above components and below utilities', () => {
97
+ expect(ruleIn(css, '.greese-Button', '@layer greese.overrides')).toContainEqual([
98
+ 'font-weight',
99
+ '700',
100
+ ])
101
+ })
102
+
103
+ it('starts with the layer order, once, so it holds whichever sheet loads first', () => {
104
+ const order = LAYER_ORDER.slice(0, -1)
105
+ expect(blocks(css)[0]?.prelude).toBe(order)
106
+ expect(css.split('@layer greese.reset')).toHaveLength(2)
107
+ const tokens = blocks(compile('@include greese.tokens((radius: (md: 1px)));'))
108
+ expect(tokens.slice(0, 2).map((b) => b.prelude)).toEqual([
109
+ order,
110
+ '@layer greese.theme',
111
+ ])
112
+ })
113
+
114
+ it('builds part and modifier selectors from the enclosing component', () => {
115
+ const layer = '@layer greese.overrides'
116
+ expect(ruleIn(css, '.greese-Button.greese-Button--outline', layer)).toEqual([
117
+ ['border-width', '2px'],
118
+ ])
119
+ expect(ruleIn(css, '.greese-Button .greese-Button-icon', layer)).toEqual([
120
+ ['opacity', '0.8'],
121
+ ])
122
+ expect(
123
+ ruleIn(
124
+ css,
125
+ '.greese-Pagination .greese-Pagination-step.greese-Pagination-step--next',
126
+ layer
127
+ )
128
+ ).toEqual([['color', 'red']])
129
+ })
130
+
131
+ it('forwards the library mixins', () => {
132
+ expect(
133
+ ruleIn(
134
+ css,
135
+ '.greese-Button:hover',
136
+ '@layer greese.overrides',
137
+ '@media (hover: hover)'
138
+ )
139
+ ).toEqual([['translate', '0 -1px']])
140
+ })
141
+
142
+ it('needs a component around part() and modifier()', () => {
143
+ expect(() => compile('a { @include greese.part(icon); }')).toThrow(
144
+ /part\(\) goes inside component\(\)/
145
+ )
146
+ })
147
+ })
148
+
149
+ describe('SCSS kit: vars()', () => {
150
+ const css = compile(`
151
+ @include greese.component(Button) {
152
+ @include greese.vars((
153
+ tone: greese.token(color, positive),
154
+ radius-md: 2px,
155
+ on-tone: (light: white, dark: black),
156
+ duration-fast: 60ms,
157
+ ));
158
+ }
159
+ @include greese.component(FileInput) { @include greese.vars((file-font: 1rem)); }
160
+ `)
161
+
162
+ it('expands short names, and takes full names and tokens', () => {
163
+ expect(declared(css, '.greese-Button')).toEqual([
164
+ ['--greese-button-tone', 'var(--greese-color-positive)'],
165
+ ['--greese-radius-md', '2px'],
166
+ ['--greese-button-on-tone', 'light-dark(white, black)'],
167
+ ])
168
+ expect(css).toContain('--greese-file-font: 1rem;')
169
+ })
170
+
171
+ it('keeps duration tokens out of reduced motion', () => {
172
+ expect(rules(css, '.greese-Button')).toContainEqual({
173
+ within: ['@layer greese.overrides', motionOk],
174
+ declarations: [['--greese-duration-fast', '60ms']],
175
+ })
176
+ })
177
+
178
+ it('rejects variables the component does not have', () => {
179
+ expect(() =>
180
+ compile('@include greese.component(Card) { @include greese.vars((tone: red)); }')
181
+ ).toThrow(/`Card` has no variable `tone`. Its variables: --greese-card-padding/)
182
+ })
183
+ })
184
+
185
+ describe('SCSS kit: motion()', () => {
186
+ it('plays a preset on every instance, behind reduced motion, tunable per instance', () => {
187
+ const css = compile(`@include greese.component(Card) {
188
+ @include greese.motion((enter: fadeUp, duration: 0.4s, distance: 2rem, ease: in-out));
189
+ }`)
190
+ expect(css).toContain('--greese-motion-distance: 2rem;')
191
+ expect(
192
+ ruleIn(css, '.greese-Card', '@layer greese.overrides', motionOk)
193
+ ).toContainEqual([
194
+ 'animation',
195
+ 'greese-fade-up var(--greese-motion-duration, 0.4s) var(--greese-motion-ease, var(--greese-ease-in-out)) var(--greese-motion-delay, 0s) backwards',
196
+ ])
197
+ })
198
+
199
+ it('sets everything an overlay exit sets, as the exit classes do', () => {
200
+ const css =
201
+ compile(`@include greese.component(Dialog) { @include greese.motion((exit: scale-out)); }
202
+ @include greese.component(Popover) { @include greese.motion((exit: fade-out-up)); }`)
203
+ expect(declared(css, '.greese-Dialog')).toEqual([
204
+ ['--greese-motion-exit-translate', '0px 0px'],
205
+ ['--greese-motion-exit-scale', '0'],
206
+ ['--greese-motion-exit-opacity', '0'],
207
+ ['--greese-motion-exit-mask-image', 'none'],
208
+ ['--greese-motion-exit-transform', 'none'],
209
+ ])
210
+ expect(Object.fromEntries(declared(css, '.greese-Popover'))).toMatchObject({
211
+ '--greese-motion-exit-translate':
212
+ '0px calc(-1 * var(--greese-motion-distance, 1rem))',
213
+ })
214
+ })
215
+
216
+ it('retunes the component’s transitions through its duration and easing tokens', () => {
217
+ const css = compile(`@include greese.component(Switch) {
218
+ @include greese.motion((durations: (fast: 80ms), easings: (standard: linear)));
219
+ }`)
220
+ expect(css).toContain('--greese-easing-standard: linear;')
221
+ expect(
222
+ ruleIn(css, '.greese-Switch', '@layer greese.overrides', motionOk)
223
+ ).toContainEqual(['--greese-duration-fast', '80ms'])
224
+ })
225
+
226
+ it('tunes every entrance with motion-defaults()', () => {
227
+ const css = compile('@include greese.motion-defaults((duration: 0.3s, ease: out));')
228
+ expect(declared(css, ':root')).toEqual([
229
+ ['--greese-motion-duration', '0.3s'],
230
+ ['--greese-motion-ease', 'var(--greese-ease-out)'],
231
+ ])
232
+ })
233
+
234
+ it('assigns motion roles with motion-defaults(), pointing at effects and tokens', () => {
235
+ const css = compile(`@include greese.motion-defaults((
236
+ content: wipe-open-x,
237
+ rows: none,
238
+ dialog: iris-in,
239
+ dialog-exit: iris-out,
240
+ content-duration: slow,
241
+ exit-duration: 200ms,
242
+ enter-ease: enter,
243
+ ));`)
244
+ const root = Object.fromEntries(declared(css, ':root'))
245
+ expect(root).toMatchObject({
246
+ '--greese-motion-content': 'greese-wipe-open-x',
247
+ '--greese-motion-rows': 'none',
248
+ '--greese-motion-dialog': 'greese-iris-in',
249
+ '--greese-motion-dialog-exit-mask-size': '0% 0%',
250
+ '--greese-motion-dialog-exit-mask-from': '142% 142%',
251
+ '--greese-motion-content-duration': 'var(--greese-duration-slow)',
252
+ '--greese-motion-exit-duration': '200ms',
253
+ '--greese-motion-enter-ease': 'var(--greese-easing-enter)',
254
+ })
255
+ // The tokens and the effects keep their own values.
256
+ expect(css).not.toMatch(/--greese-duration-slow:/)
257
+ expect(css).toMatch(/@keyframes greese-wipe-open-x/)
258
+ expect(() =>
259
+ compile('@include greese.motion-defaults((content: fade-out));')
260
+ ).toThrow(/no entrance `fade-out`/)
261
+ expect(() =>
262
+ compile('@include greese.motion-defaults((dialog-exit: fade-up));')
263
+ ).toThrow(/no exit `fade-up`/)
264
+ })
265
+
266
+ it('assigns page transition roles with motion-defaults() (ADR 0029)', () => {
267
+ const css = compile(`@include greese.motion-defaults((
268
+ page: pocket-up,
269
+ page-exit: pocket-out-down,
270
+ page-back: none,
271
+ page-duration: slow,
272
+ page-ease: enter,
273
+ share-duration: 300ms,
274
+ page-stagger: 0.04s,
275
+ page-exit-order: together,
276
+ page-mode: live,
277
+ ));`)
278
+ expect(Object.fromEntries(declared(css, ':root'))).toMatchObject({
279
+ '--greese-motion-page': 'greese-pocket-up',
280
+ '--greese-motion-page-exit': 'greese-pocket-out-down',
281
+ '--greese-motion-page-back': 'none',
282
+ '--greese-motion-page-duration': 'var(--greese-duration-slow)',
283
+ '--greese-motion-page-ease': 'var(--greese-easing-enter)',
284
+ '--greese-motion-share-duration': '300ms',
285
+ '--greese-motion-page-stagger': '0.04s',
286
+ '--greese-motion-page-exit-order': 'together',
287
+ '--greese-motion-page-mode': 'live',
288
+ })
289
+ // The keyframes its snapshots play come with it, the exit's included.
290
+ expect(css).toMatch(/@keyframes greese-pocket-up/)
291
+ expect(css).toMatch(/@keyframes greese-pocket-out-down/)
292
+ expect(() =>
293
+ compile('@include greese.motion-defaults((page-exit: fade-up));')
294
+ ).toThrow(/no exit `fade-up` for `page-exit`/)
295
+ expect(() => compile('@include greese.motion-defaults((page: fade-out));')).toThrow(
296
+ /no entrance `fade-out` for `page`/
297
+ )
298
+ expect(() =>
299
+ compile('@include greese.motion-defaults((page-mode: sometimes));')
300
+ ).toThrow(/`page-mode` is one of auto, snapshot, live/)
301
+ })
302
+
303
+ it('puts a named theme’s page roles on the root too, where snapshots inherit from', () => {
304
+ const css = compile(`@include greese.theme(ocean) {
305
+ @include greese.motion-defaults((theme: fade-up, page: pocket-up, page-exit: pocket-out-down, share-duration: slow));
306
+ }`)
307
+ const scoped = Object.fromEntries(declared(css, '[data-greese-theme=ocean]'))
308
+ expect(scoped).toMatchObject({
309
+ '--greese-motion-theme': 'greese-fade-up',
310
+ '--greese-motion-page': 'greese-pocket-up',
311
+ })
312
+ // Only the page roles reach the root, and only while the theme is on the page.
313
+ expect(declared(css, ':root:has([data-greese-theme=ocean])')).toEqual([
314
+ ['--greese-motion-page', 'greese-pocket-up'],
315
+ ['--greese-motion-page-exit', 'greese-pocket-out-down'],
316
+ ['--greese-motion-share-duration', 'var(--greese-duration-slow)'],
317
+ ])
318
+ })
319
+
320
+ it('names selector rules only during a greese page change, with page-transitions()', () => {
321
+ const css = compile(`@include greese.page-transitions((
322
+ 'header, footer': still,
323
+ '.greese-Card': (exit: scale-out, enter: pop-in, duration: fast, delay: 0.05s),
324
+ ));`)
325
+ const during = ':root:active-view-transition-type(greese-page)'
326
+ expect(declared(css, `${during} :is(header, footer)`)).toEqual([])
327
+ expect(css).toMatch(
328
+ /:is\(header, footer\) \{\s*view-transition-name: match-element;\s*view-transition-class: greese-PageTransition greese-PageTransition--still;/
329
+ )
330
+ expect(css).toMatch(
331
+ /:is\(\.greese-Card\) \{\s*view-transition-name: match-element;\s*view-transition-class: greese-PageTransition greese-PageTransition--theme greese-page-base-2;/
332
+ )
333
+ expect(
334
+ ruleIn(
335
+ css,
336
+ 'html::view-transition-old(*.greese-page-base-2)',
337
+ '@layer greese.overrides',
338
+ motionOk
339
+ )
340
+ ).toEqual([
341
+ ['animation-name', 'greese-scale-out'],
342
+ ['animation-duration', 'var(--greese-duration-fast)'],
343
+ ['animation-delay', '0.05s'],
344
+ ])
345
+ expect(
346
+ ruleIn(
347
+ css,
348
+ 'html::view-transition-new(*.greese-page-base-2)',
349
+ '@layer greese.overrides',
350
+ motionOk
351
+ )
352
+ ).toEqual([
353
+ ['animation-name', 'greese-pop-in'],
354
+ ['animation-duration', 'var(--greese-duration-fast)'],
355
+ ['animation-delay', '0.05s'],
356
+ ])
357
+ expect(css).toMatch(/@keyframes greese-scale-out/)
358
+ expect(() =>
359
+ compile(`@include greese.page-transitions(('main': (exit: fade-up)));`)
360
+ ).toThrow(/`main` has no exit `fade-up`/)
361
+ expect(() =>
362
+ compile(`@include greese.page-transitions(('main': (speed: 2)));`)
363
+ ).toThrow(/`main` has no option `speed`/)
364
+ })
365
+
366
+ it('scopes selector rules to the theme they belong to', () => {
367
+ const css = compile(`@include greese.theme(ocean) {
368
+ @include greese.page-transitions(('main': (exit: fade-out)));
369
+ }`)
370
+ expect(css).toContain(
371
+ ':root:active-view-transition-type(greese-page) [data-greese-theme=ocean] :is(main)'
372
+ )
373
+ expect(css).toContain('greese-page-ocean-1')
374
+ })
375
+
376
+ it('keeps a built-in effect’s plan: effect() refuses its name', () => {
377
+ expect(() =>
378
+ compile('@include greese.effect(fade-up, (enter: (from: (opacity: 0))));')
379
+ ).toThrow(/built-in effect/)
380
+ })
381
+
382
+ it('gives every effect a page transition form: entrances arrive, exits leave', () => {
383
+ const built = readFileSync(
384
+ new URL('../../dist/css/PageTransition.css', import.meta.url),
385
+ 'utf8'
386
+ ).replace(/\s+/g, ' ')
387
+ const entrances = new Set(entranceNames())
388
+ for (const effect of effectNames) {
389
+ const name = effectClass(effect)
390
+ if (entrances.has(effect)) {
391
+ // The new snapshot plays the entrance's own keyframes (Motion.css).
392
+ expect(built, name).toContain(
393
+ `html::view-transition-new(*.greese-PageTransition--enter-${name}) { animation-name: greese-${name};`
394
+ )
395
+ } else {
396
+ // The old snapshot plays the exit, from rest to where it ends.
397
+ expect(effectPlan(effect).leave?.[0]?.[0], name).toBe('from')
398
+ expect(built, name).toContain(
399
+ `html::view-transition-old(*.greese-PageTransition--exit-${name}) { animation-name: greese-${name};`
400
+ )
401
+ expect(built, name).toContain(`@keyframes greese-${name} {`)
402
+ }
403
+ }
404
+ // Reduced motion: greese snapshots don't animate at all.
405
+ expect(built).toContain(
406
+ '@media (prefers-reduced-motion: reduce) { html::view-transition-group(*.greese-PageTransition), html::view-transition-old(*.greese-PageTransition), html::view-transition-new(*.greese-PageTransition) { animation: none;'
407
+ )
408
+ })
409
+
410
+ it('accepts every entrance src/effects.ts defines, and the built CSS has its keyframes', () => {
411
+ const built = readFileSync(
412
+ new URL('../../dist/css/Motion.css', import.meta.url),
413
+ 'utf8'
414
+ )
415
+ expect(entranceNames()).toHaveLength(54)
416
+ // One compile for all of them: one each is seconds of Sass.
417
+ const names = entranceNames().map(effectClass)
418
+ const css = compile(
419
+ names
420
+ .map(
421
+ (name) =>
422
+ `@include greese.component(Card) { @include greese.motion((enter: ${name})); }`
423
+ )
424
+ .join('\n')
425
+ )
426
+ for (const name of names) {
427
+ expect(built, name).toContain(`@keyframes greese-${name} {`)
428
+ expect(css, name).toContain(`animation: greese-${name} `)
429
+ }
430
+ })
431
+
432
+ it('rejects unknown presets and options', () => {
433
+ const inCard = (options: string) =>
434
+ compile(`@include greese.component(Card) { @include greese.motion(${options}); }`)
435
+ expect(() => inCard('(enter: wobble)')).toThrow(
436
+ /no entrance `wobble`. Effects that end at rest: fade-in/
437
+ )
438
+ expect(() => inCard('(speed: 2)')).toThrow(/motion\(\) has no option `speed`/)
439
+ })
440
+ })
441
+
442
+ describe('SCSS kit: effect()', () => {
443
+ const wobble = cssPlan({ rotate: [0, -6, 6, 0] })
444
+ const scss = (plan: string) =>
445
+ `@include greese.effect(wobble, ${plan});
446
+ @include greese.component(Card) { @include greese.motion((enter: wobble)); }`
447
+
448
+ it('makes an effect of your own work in motion(enter:) like a built-in', () => {
449
+ const css = compile(
450
+ scss(
451
+ `(enter: (from: (rotate: '0deg'), '33.33%': (rotate: '-6deg'), '66.67%': (rotate: '6deg')))`
452
+ )
453
+ )
454
+ expect(
455
+ rules(css, '.greese-Motion--enter-wobble, .greese-Motion--view-wobble')[0]
456
+ ?.declarations
457
+ ).toContainEqual(['--greese-motion-name', 'greese-wobble'])
458
+ const wobbleFrom = blocks(css).find(
459
+ (b) => b.prelude === 'from' && b.within.at(-1) === '@keyframes greese-wobble'
460
+ )
461
+ expect(wobbleFrom?.declarations).toEqual([['rotate', '0deg']])
462
+ expect(css).toContain('animation: greese-wobble ')
463
+ expect(wobble.enter).toHaveLength(3)
464
+ })
465
+
466
+ it('accepts an exit of the theme’s own in motion(exit:)', () => {
467
+ const css = compile(
468
+ `@include greese.effect(sink, (exit: (translate: '0 2rem')));
469
+ @include greese.component(Dialog) { @include greese.motion((exit: sink)); }`
470
+ )
471
+ expect(declared(css, '.greese-Dialog')).toEqual([
472
+ ['--greese-motion-exit-translate', '0 2rem'],
473
+ ])
474
+ })
475
+
476
+ it('writes the CSS that effectCss() writes for a page without the compiled theme', () => {
477
+ const squash = (css: string) => css.replace(/\s+/g, ' ').trim()
478
+ const toScss = (plan: ReturnType<typeof cssPlan>) => {
479
+ const decl = (values: Record<string, string>) =>
480
+ `(${Object.entries(values)
481
+ .map(([p, v]) => `'${p}': '${v}'`)
482
+ .join(', ')})`
483
+ const steps = (list: NonNullable<typeof plan.enter>) =>
484
+ `(${list.map(([at, d]) => `'${at}': ${decl(d)}`).join(', ')})`
485
+ return plan.enter
486
+ ? `(enter: ${steps(plan.enter)})`
487
+ : `(exit: ${Object.keys(plan.exit).length ? decl(plan.exit) : '()'}, leave: ${steps(plan.leave!)})`
488
+ }
489
+ // One compile for every effect (one each took seconds with 66 of them).
490
+ const css = squash(
491
+ compile(
492
+ effectNames
493
+ .map(
494
+ (effect) =>
495
+ `@include greese.effect(x-${effectClass(effect)}, ${toScss(effectPlan(effect))});`
496
+ )
497
+ .join('\n')
498
+ )
499
+ )
500
+ // The kit's rules sit in the utilities layer; the text is the rules alone.
501
+ for (const effect of effectNames)
502
+ expect(css, effect).toContain(
503
+ squash(effectCss(`x-${effectClass(effect)}`, effectPlan(effect)))
504
+ )
505
+ })
506
+
507
+ it('still stops the build on a name nothing defines', () => {
508
+ expect(() =>
509
+ compile(
510
+ `@include greese.component(Card) { @include greese.motion((enter: wobble)); }`
511
+ )
512
+ ).toThrow(/no entrance `wobble`/)
513
+ expect(() => compile(`@include greese.effect(wobble, ());`)).toThrow(/needs a plan/)
514
+ })
515
+ })
516
+
517
+ describe('SCSS kit: tokens() and theme()', () => {
518
+ it('overrides only what you pass on the page, mirroring the default selectors', () => {
519
+ const css = compile(`@include greese.tokens((
520
+ color: (accent: (light: #7c3aed, dark: #c4b5fd)),
521
+ radius: (md: 12px),
522
+ font: (body: ('Inter', system-ui)),
523
+ duration: (fast: 90ms),
524
+ ));`)
525
+ expect(blocks(css)[1]?.prelude).toBe('@layer greese.theme')
526
+ expect(declared(css, ':root')).toEqual([
527
+ ['--greese-radius-md', '12px'],
528
+ ['--greese-font-body', '"Inter", system-ui'],
529
+ ['--greese-duration-fast', '90ms'],
530
+ ])
531
+ expect(declared(css, '[data-greese-scheme=system]')).toContainEqual([
532
+ '--greese-color-accent',
533
+ '#7c3aed',
534
+ ])
535
+ expect(declared(css, '[data-greese-scheme=dark]')).toContainEqual([
536
+ '--greese-color-accent',
537
+ '#c4b5fd',
538
+ ])
539
+ expect(
540
+ ruleIn(
541
+ css,
542
+ ':root',
543
+ '@layer greese.theme',
544
+ '@media (prefers-reduced-motion: reduce)'
545
+ )
546
+ ).toEqual([['--greese-duration-fast', '0.01ms']])
547
+ })
548
+
549
+ it('declares the same complete theme as createTheme()', () => {
550
+ const css = compile(`@include greese.theme(brand) {
551
+ @include greese.tokens((color: (accent: (light: #7c3aed, dark: #c4b5fd), accentContent: #fff, primary: (light: #fde047, dark: #1e3a8a), secondary: lavender-600), radius: (md: 12px)));
552
+ }`)
553
+ // Derived colors included: primary's and secondary's content and strong.
554
+ const theme = createTheme({
555
+ name: 'brand',
556
+ color: {
557
+ accent: { light: '#7c3aed', dark: '#c4b5fd' },
558
+ accentContent: '#fff',
559
+ primary: { light: '#fde047', dark: '#1e3a8a' },
560
+ secondary: '#7e5bd0',
561
+ },
562
+ radius: { md: '12px' },
563
+ })
564
+ const T = "[data-greese-theme='brand']"
565
+ const sass = '[data-greese-theme=brand]'
566
+ // Sass drops the attribute quotes; createTheme's font stack keeps its own.
567
+ const norm = (list: Array<[string, string]>) =>
568
+ list.map(([p, v]) => [p, v.replace(/"/g, "'")])
569
+ expect(norm(declared(css, sass))).toEqual(norm(declared(theme.css, T)))
570
+ expect(
571
+ declared(css, `:root[data-greese-scheme=dark] ${sass}:not([data-greese-scheme])`)
572
+ ).toEqual(
573
+ declared(
574
+ theme.css,
575
+ `:root[data-greese-scheme='dark'] ${T}:not([data-greese-scheme])`
576
+ )
577
+ )
578
+ })
579
+
580
+ it('derives content colors from hex strings, as Theme Studio writes them', () => {
581
+ const css = compile(`@include greese.theme(brand) {
582
+ @include greese.tokens((color: (negative: (light: "#00ff00", dark: "#1e3a8a"), positive: "#fff", info: "#0369A1")));
583
+ }`)
584
+ const theme = createTheme({
585
+ name: 'brand',
586
+ color: {
587
+ negative: { light: '#00ff00', dark: '#1e3a8a' },
588
+ positive: '#fff',
589
+ info: '#0369A1',
590
+ },
591
+ })
592
+ const T = "[data-greese-theme='brand']"
593
+ const sass = '[data-greese-theme=brand]'
594
+ const norm = (list: Array<[string, string]>) =>
595
+ list.map(([p, v]) => [p, v.replace(/"/g, "'")])
596
+ const light = declared(css, sass)
597
+ expect(light).toContainEqual(['--greese-color-negative-content', '#11171c'])
598
+ expect(norm(light)).toEqual(norm(declared(theme.css, T)))
599
+ })
600
+
601
+ it('scopes component overrides to the theme', () => {
602
+ const css = compile(`@include greese.theme(brand) {
603
+ @include greese.component(Card) { padding: 0; }
604
+ @include greese.motion-defaults((distance: 2rem));
605
+ }`)
606
+ expect(rules(css, '[data-greese-theme=brand] .greese-Card')[0]?.declarations).toEqual(
607
+ [['padding', '0']]
608
+ )
609
+ expect(declared(css, '[data-greese-theme=brand]')).toEqual([
610
+ ['--greese-motion-distance', '2rem'],
611
+ ])
612
+ })
613
+
614
+ it('rejects unknown token groups and keys', () => {
615
+ expect(() => compile('@include greese.tokens((colr: (accent: red)));')).toThrow(
616
+ /no token group `colr`/
617
+ )
618
+ expect(() => compile('@include greese.tokens((radius: (huge: 1px)));')).toThrow(
619
+ /no `radius` token `huge`. Keys: none, sm, md, lg, xl, pill/
620
+ )
621
+ })
622
+ })
623
+
624
+ describe('SCSS kit: new tokens', () => {
625
+ const extended = `@include greese.tokens((), $extend: (
626
+ color: (brand-ink: (light: #1b1340, dark: #e5defc), brand-flat: #123456),
627
+ space: (9: 4rem),
628
+ shadow: (glow: 0 0 12px red),
629
+ duration: (glacial: 900ms),
630
+ ));`
631
+
632
+ it('declares them alongside the built-in tokens', () => {
633
+ const css = compile(extended)
634
+ expect(css).toContain('--greese-space-9: 4rem;')
635
+ expect(css).toContain('--greese-shadow-glow: 0 0 12px red;')
636
+ expect(css).toContain('--greese-color-brand-ink: #1b1340;')
637
+ expect(css).toContain('--greese-color-brand-flat: #123456;')
638
+ expect(css).toMatch(/--greese-color-brand-ink: #e5defc;/)
639
+ expect(css).toMatch(/prefers-reduced-motion[^}]*--greese-duration-glacial: 0\.01ms/s)
640
+ })
641
+
642
+ it('lets token() and component variables use them afterwards', () => {
643
+ const css = compile(`${extended}
644
+ @include greese.component(Button) {
645
+ color: greese.token(color, brand-ink);
646
+ @include greese.vars((tone: greese.token(color, brand-flat)));
647
+ }`)
648
+ expect(css).toContain('color: var(--greese-color-brand-ink)')
649
+ })
650
+
651
+ it('still stops the build on unknown names, groups and repeats', () => {
652
+ expect(() =>
653
+ compile('@include greese.tokens((), $extend: (color: (accent: red)));')
654
+ ).toThrow(/`color` token `accent` already exists/)
655
+ expect(() =>
656
+ compile('@include greese.tokens((), $extend: (colr: (x: red)));')
657
+ ).toThrow(/no token group `colr` to extend/)
658
+ expect(() =>
659
+ compile('@include greese.component(Button) { color: greese.token(color, nope); }')
660
+ ).toThrow(/no color token `nope`/)
661
+ })
662
+
663
+ it('declares them in a named theme too, in both schemes', () => {
664
+ const css = compile(`@include greese.theme(brand) { ${extended} }`)
665
+ expect(css).toContain('[data-greese-theme=brand]')
666
+ expect(css).toContain('--greese-color-brand-ink: #e5defc;')
667
+ expect(css).toContain('--greese-space-9: 4rem;')
668
+ })
669
+ })
670
+
671
+ describe('SCSS kit: typography()', () => {
672
+ it('writes a variant complete, so its other breakpoints stay in step', () => {
673
+ const css = compile(`@include greese.typography((
674
+ h1: (base: (font-size: 2rem), lg: (font-size: 4rem, letter-spacing: -0.03em)),
675
+ ));`)
676
+ expect(css).toContain('@layer greese.theme')
677
+ // base: the default h1's other values too, with the new size
678
+ expect(css).toMatch(
679
+ /--greese-type-h1-font-size: 2rem;[\s\S]*--greese-type-h1-font-weight: 700;/
680
+ )
681
+ // the default md step is kept, since the variant is written whole
682
+ expect(css).toMatch(
683
+ /@media \(width >= 900px\)[\s\S]*--greese-type-h1-font-size: 2\.75rem;/
684
+ )
685
+ expect(css).toMatch(
686
+ /@media \(width >= 1200px\)[\s\S]*--greese-type-h1-font-size: 4rem;/
687
+ )
688
+ expect(css).toContain('--greese-type-h1-letter-spacing: -0.03em;')
689
+ // other variants are left alone
690
+ expect(css).not.toContain('--greese-type-h2-')
691
+ })
692
+
693
+ it('takes other properties, and scopes to a theme', () => {
694
+ const css = compile(`@include greese.theme(brand) {
695
+ @include greese.typography((caption: (base: (text-transform: uppercase, font-family: ('Inter', sans-serif), color: red))));
696
+ }`)
697
+ expect(css).toContain('[data-greese-theme=brand]')
698
+ expect(css).toContain('--greese-type-caption-text-transform: uppercase;')
699
+ expect(css).toContain('--greese-type-caption-font-family: "Inter", sans-serif;')
700
+ expect(css).toContain('--greese-type-caption-color: red;')
701
+ })
702
+
703
+ it('stops the build on unknown variants, breakpoints and properties', () => {
704
+ expect(() =>
705
+ compile('@include greese.typography((h9: (base: (font-size: 1rem))));')
706
+ ).toThrow(/no type variant `h9`/)
707
+ expect(() =>
708
+ compile('@include greese.typography((h1: (huge: (font-size: 1rem))));')
709
+ ).toThrow(/no breakpoint `huge`/)
710
+ expect(() =>
711
+ compile('@include greese.typography((h1: (base: (colour: red))));')
712
+ ).toThrow(/no type property `colour`/)
713
+ })
714
+ })
715
+
716
+ describe('SCSS kit: keyframes()', () => {
717
+ it('writes keyframes in the utilities layer, from steps', () => {
718
+ const css = compile(`@include greese.keyframes(greese-float-up, (
719
+ from: (opacity: 0, translate: '0 2rem'),
720
+ '60%': (opacity: 1),
721
+ to: (opacity: 1, translate: '0 0'),
722
+ ));`)
723
+ expect(css).toContain('@layer greese.utilities')
724
+ const steps = blocks(css).filter(
725
+ (b) => b.within.at(-1) === '@keyframes greese-float-up'
726
+ )
727
+ expect(steps.map((b) => [b.prelude, b.declarations])).toEqual([
728
+ [
729
+ 'from',
730
+ [
731
+ ['opacity', '0'],
732
+ ['translate', '0 2rem'],
733
+ ],
734
+ ],
735
+ ['60%', [['opacity', '1']]],
736
+ [
737
+ 'to',
738
+ [
739
+ ['opacity', '1'],
740
+ ['translate', '0 0'],
741
+ ],
742
+ ],
743
+ ])
744
+ })
745
+
746
+ it('only takes names that start with greese-, and none of a built-in', () => {
747
+ expect(() =>
748
+ compile(`@include greese.keyframes(wobble, (from: (opacity: 0)));`)
749
+ ).toThrow(/start with `greese-`/)
750
+ expect(() =>
751
+ compile(`@include greese.keyframes(greese-fade-up, (from: (opacity: 0)));`)
752
+ ).toThrow(/built-in effect keeps its keyframes/)
753
+ })
754
+ })