@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,1548 @@
1
+ // greese ui's SCSS kit for themes and component overrides, compiled in your
2
+ // build (docs/adr/0010-theme-kit-and-studio.md). For example:
3
+ // @use 'pkg:@greese/ui/scss/kit' as greese;
4
+ // @include greese.tokens((color: (primary: (light: lavender-600, dark: lavender-300))));
5
+ // @include greese.component(Button) {
6
+ // font-weight: 700;
7
+ // @include greese.vars((tone: greese.token(color, positive)));
8
+ // @include greese.modifier(outline) { border-width: 2px; }
9
+ // @include greese.part(icon) { opacity: 0.8; }
10
+ // @include greese.motion((durations: (fast: 80ms)));
11
+ // }
12
+ // You write names (`Button`, `icon`, `outline`); the kit adds the `greese-`
13
+ // prefix and the BEM separators, and stops the build on a name the library
14
+ // doesn't have. Token overrides go in `@layer greese.theme`, component
15
+ // overrides in `@layer greese.overrides`: above component styles, below style
16
+ // props and the `greese` prop, and below any unlayered CSS of yours.
17
+ // Using this module emits no CSS by itself. Call its mixins at the top level
18
+ // of a stylesheet, or inside `theme()`.
19
+ @use 'sass:color';
20
+ @use 'sass:list';
21
+ @use 'sass:map';
22
+ @use 'sass:math';
23
+ @use 'sass:meta';
24
+ @use 'sass:string';
25
+ @use '../../dist/maps' as defaults;
26
+ @use 'manifest';
27
+ @use 'config';
28
+ @use 'effect';
29
+ @use 'type-emit';
30
+ @forward 'mixins' show up, down, hover, focus-ring, touch-target, motion-safe;
31
+
32
+ /// Stop the build on unknown components, parts, modifiers, variables and
33
+ /// tokens. Turn off to target classes this version doesn't know yet.
34
+ $strict: true !default;
35
+ $_ordered: false;
36
+ $_theme: null;
37
+
38
+ // Tokens declared with `tokens(…, $extend: …)`, by group, so `token()` and
39
+ // `component()` variables accept them from then on.
40
+ $_extended: ();
41
+ $_block: null;
42
+ $_part: null;
43
+ $_scheme-attr: 'data-greese-scheme';
44
+ $_theme-attr: 'data-greese-theme';
45
+
46
+ // Effects defined with `effect()`, by kebab-case name, over the built-in ones.
47
+ $_defined: ();
48
+ $_eases: ('linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'in', 'out', 'in-out');
49
+
50
+ // --- helpers ---------------------------------------------------------------
51
+
52
+ /// `primaryContent` → `primary-content`, `lineHeight` → `line-height`; kebab-case stays.
53
+ @function _kebab($name) {
54
+ $name: '#{$name}';
55
+ $out: '';
56
+
57
+ @for $i from 1 through string.length($name) {
58
+ $char: string.slice($name, $i, $i);
59
+ $lower: string.to-lower-case($char);
60
+
61
+ @if $char != $lower {
62
+ $out: $out + '-' + $lower;
63
+ } @else {
64
+ $out: $out + $char;
65
+ }
66
+ }
67
+
68
+ @return string.unquote($out);
69
+ }
70
+
71
+ @function _join($items) {
72
+ $out: '';
73
+
74
+ @each $item in $items {
75
+ @if $out == '' {
76
+ $out: '#{$item}';
77
+ } @else {
78
+ $out: '#{$out}, #{$item}';
79
+ }
80
+ }
81
+
82
+ @if $out == '' {
83
+ @return '(none)';
84
+ }
85
+
86
+ @return $out;
87
+ }
88
+
89
+ @function _check($ok, $message) {
90
+ @if not $ok and $strict {
91
+ @error 'greese ui kit: #{$message}';
92
+ }
93
+
94
+ @return $ok;
95
+ }
96
+
97
+ /// A value as CSS text: quoted strings unquote, lists keep inner quotes, and
98
+ /// a palette name (`lavender-600`) becomes its color.
99
+ @function _css($value) {
100
+ @if meta.type-of($value) == string {
101
+ @return _palette-name($value) or string.unquote($value);
102
+ }
103
+
104
+ @if meta.type-of($value) == list {
105
+ @return string.unquote(meta.inspect($value));
106
+ }
107
+
108
+ @return $value;
109
+ }
110
+
111
+ @function _in($list, $item) {
112
+ @return list.index($list, $item) != null;
113
+ }
114
+
115
+ @function _info($block) {
116
+ @return map.get(manifest.$blocks, $block);
117
+ }
118
+
119
+ @function _scoped($selector) {
120
+ @if $_theme {
121
+ @return "[#{$_theme-attr}='#{$_theme}'] #{$selector}";
122
+ }
123
+
124
+ @return $selector;
125
+ }
126
+
127
+ @function _scheme($scheme) {
128
+ @return "[#{$_scheme-attr}='#{$scheme}']";
129
+ }
130
+
131
+ /// The layer order, once per stylesheet, before the first rule: every
132
+ /// greese ui sheet starts with it, so whichever loads first the order holds.
133
+ /// Without it, a theme loaded before greese ui's CSS would make
134
+ /// `greese.theme` the first, weakest layer.
135
+ @mixin _layer-order {
136
+ @if not $_ordered {
137
+ $_ordered: true !global;
138
+
139
+ // stylelint-disable-next-line layer-name-pattern -- the whole list, interpolated
140
+ @layer #{defaults.$layer-order};
141
+ }
142
+ }
143
+
144
+ // --- names -----------------------------------------------------------------
145
+
146
+ @function _block-name($name) {
147
+ $name: '#{$name}';
148
+ $known: map.has-key(manifest.$blocks, $name);
149
+ $checked: _check(
150
+ $known,
151
+ 'no component `#{$name}`. Components are PascalCase, as in the docs: #{_join(map.keys(manifest.$blocks))}.'
152
+ );
153
+
154
+ @return string.unquote($name);
155
+ }
156
+
157
+ @function _part-name($block, $part) {
158
+ $part: _kebab($part);
159
+ $info: _info($block);
160
+
161
+ @if $info {
162
+ $parts: map.get($info, parts);
163
+ $checked: _check(
164
+ _in($parts, $part),
165
+ '`#{$block}` has no part `#{$part}`. Parts: #{_join($parts)}.'
166
+ );
167
+ }
168
+
169
+ @return $part;
170
+ }
171
+
172
+ @function _modifier-name($block, $part, $modifier) {
173
+ $modifier: _kebab($modifier);
174
+ $info: _info($block);
175
+
176
+ @if $info {
177
+ @if $part {
178
+ $mods: map.get($info, part-modifiers, $part) or ();
179
+ $checked: _check(
180
+ _in($mods, $modifier),
181
+ '`#{$block}-#{$part}` has no modifier `#{$modifier}`. Modifiers: #{_join($mods)}.'
182
+ );
183
+ } @else {
184
+ $mods: map.get($info, modifiers);
185
+ $checked: _check(
186
+ _in($mods, $modifier),
187
+ '`#{$block}` has no modifier `#{$modifier}`. Modifiers: #{_join($mods)}.'
188
+ );
189
+ }
190
+ }
191
+
192
+ @return $modifier;
193
+ }
194
+
195
+ /// The selector for a component, part or modifier, prefix included:
196
+ /// `class(Button, $part: icon)` is `.greese-Button-icon`. For your own
197
+ /// selectors: `#{greese.class(Card)} > p { … }`.
198
+ @function class($component, $part: null, $modifier: null) {
199
+ $block: _block-name($component);
200
+ $selector: 'greese-#{$block}';
201
+
202
+ @if $part {
203
+ $part: _part-name($block, $part);
204
+ $selector: '#{$selector}-#{$part}';
205
+ }
206
+
207
+ @if $modifier {
208
+ $selector: '#{$selector}--#{_modifier-name($block, $part, $modifier)}';
209
+ }
210
+
211
+ @return string.unquote('.#{$selector}');
212
+ }
213
+
214
+ /// A palette color: `palette(lavender, 600)` is `#7e5bd0`. As a token or
215
+ /// variable value, `lavender-600` does the same.
216
+ @function palette($hue, $shade) {
217
+ $shades: map.get(defaults.$palette, '#{$hue}');
218
+ $checked: _check(
219
+ $shades,
220
+ 'no palette hue `#{$hue}`. Hues: #{_join(map.keys(defaults.$palette))}.'
221
+ );
222
+ $value: null;
223
+
224
+ @if $shades {
225
+ $value: map.get($shades, $shade);
226
+ }
227
+
228
+ $checked: _check(
229
+ $value,
230
+ 'no `#{$hue}` shade `#{$shade}`. Shades: #{_join(map.keys($shades or ()))}.'
231
+ );
232
+
233
+ @return $value;
234
+ }
235
+
236
+ /// `lavender-600` → `#7e5bd0`; null for anything else. No CSS value looks like
237
+ /// a hue and a shade, so this can't capture one.
238
+ @function _palette-name($value) {
239
+ $value: '#{$value}';
240
+ $dash: string.index($value, '-');
241
+
242
+ @if not $dash or $dash == 1 {
243
+ @return null;
244
+ }
245
+
246
+ $shades: map.get(defaults.$palette, string.slice($value, 1, $dash - 1));
247
+
248
+ @if not $shades {
249
+ @return null;
250
+ }
251
+
252
+ $shade: string.slice($value, $dash + 1);
253
+
254
+ @each $key, $color in $shades {
255
+ @if '#{$key}' == $shade {
256
+ @return $color;
257
+ }
258
+ }
259
+
260
+ @return null;
261
+ }
262
+
263
+ @function _is-extended($group, $key) {
264
+ @return list.index(map.get($_extended, '#{$group}') or (), '#{$key}') != null;
265
+ }
266
+
267
+ /// A design token: `token(color, accent)` is `var(--greese-color-accent)`.
268
+ @function token($group, $key) {
269
+ $group: _kebab($group);
270
+ $key: _kebab($key);
271
+
272
+ @if _is-extended($group, $key) {
273
+ @return var(--greese-#{$group}-#{$key});
274
+ }
275
+
276
+ @if $group == color {
277
+ $checked: _check(
278
+ map.has-key(defaults.$color, $key),
279
+ 'no color token `#{$key}`. Colors: #{_join(map.keys(defaults.$color))}.'
280
+ );
281
+ } @else {
282
+ $values: map.get(defaults.$scalars, $group);
283
+ $checked: _check(
284
+ $values,
285
+ 'no token group `#{$group}`. Groups: color, #{_join(map.keys(defaults.$scalars))}.'
286
+ );
287
+
288
+ @if $values {
289
+ $checked: _check(
290
+ map.has-key($values, '#{$key}'),
291
+ 'no `#{$group}` token `#{$key}`. Keys: #{_join(map.keys($values))}.'
292
+ );
293
+ }
294
+ }
295
+
296
+ @return var(--greese-#{$group}-#{$key});
297
+ }
298
+
299
+ @function _is-token-var($name) {
300
+ @each $group, $keys in $_extended {
301
+ @each $key in $keys {
302
+ @if $name == '--greese-#{$group}-#{$key}' {
303
+ @return true;
304
+ }
305
+ }
306
+ }
307
+
308
+ @each $role, $unused in defaults.$color {
309
+ @if $name == '--greese-color-#{$role}' {
310
+ @return true;
311
+ }
312
+ }
313
+
314
+ @each $group, $values in defaults.$scalars {
315
+ @each $key, $unused in $values {
316
+ @if $name == '--greese-#{$group}-#{$key}' {
317
+ @return true;
318
+ }
319
+ }
320
+ }
321
+
322
+ @return false;
323
+ }
324
+
325
+ /// A component variable's full name. `tone` inside `component(Button)` is
326
+ /// `--greese-button-tone`; full names (`button-tone`, `radius-md`) work
327
+ /// anywhere. Tokens are allowed too: they rescope to that component.
328
+ @function _var-name($name) {
329
+ $name: _kebab($name);
330
+ $full: '--greese-#{$name}';
331
+
332
+ @if _is-token-var($full) {
333
+ @return $full;
334
+ }
335
+
336
+ @if $_block {
337
+ $vars: map.get(_info($_block), variables) or ();
338
+ $short: '--greese-#{string.to-lower-case($_block)}-#{$name}';
339
+
340
+ @if _in($vars, $short) {
341
+ @return $short;
342
+ }
343
+
344
+ $checked: _check(
345
+ _in($vars, $full),
346
+ '`#{$_block}` has no variable `#{$name}`. Its variables: #{_join($vars)}; or any token, such as `radius-md`.'
347
+ );
348
+ } @else {
349
+ $all: ();
350
+
351
+ @each $_, $info in manifest.$blocks {
352
+ $all: list.join($all, map.get($info, variables));
353
+ }
354
+
355
+ $checked: _check(_in($all, $full), 'no variable `#{$full}`.');
356
+ }
357
+
358
+ @return $full;
359
+ }
360
+
361
+ /// A component variable or token, as `var()`: `variable(button-tone)`.
362
+ @function variable($name) {
363
+ @return var(#{_var-name($name)});
364
+ }
365
+
366
+ // --- component overrides ---------------------------------------------------
367
+
368
+ /// Styles for one component, in `@layer greese.overrides`. Nest `part()`,
369
+ /// `modifier()`, `vars()`, `motion()` and plain declarations inside.
370
+ @mixin component($name) {
371
+ $block: _block-name($name);
372
+ $outer-block: $_block;
373
+ $outer-part: $_part;
374
+ $_block: $block !global;
375
+ $_part: null !global;
376
+
377
+ @include _layer-order;
378
+
379
+ @layer greese.overrides {
380
+ #{_scoped('.greese-#{$block}')} {
381
+ @content;
382
+ }
383
+ }
384
+
385
+ $_block: $outer-block !global;
386
+ $_part: $outer-part !global;
387
+ }
388
+
389
+ @function _require-component($mixin) {
390
+ @return _check($_block, '#{$mixin}() goes inside component().') or true;
391
+ }
392
+
393
+ /// A part of the component: `part(icon)` is `& .greese-Button-icon`.
394
+ @mixin part($name) {
395
+ $checked: _require-component('part');
396
+ $part: _part-name($_block, $name);
397
+ $outer: $_part;
398
+ $_part: $part !global;
399
+
400
+ & .greese-#{$_block}-#{$part} {
401
+ @content;
402
+ }
403
+
404
+ $_part: $outer !global;
405
+ }
406
+
407
+ /// A modifier of the component, or of the enclosing part:
408
+ /// `modifier(solid)` is `&.greese-Button--solid`.
409
+ @mixin modifier($name) {
410
+ $checked: _require-component('modifier');
411
+ $modifier: _modifier-name($_block, $_part, $name);
412
+ $base: 'greese-#{$_block}';
413
+
414
+ @if $_part {
415
+ $base: '#{$base}-#{$_part}';
416
+ }
417
+
418
+ &.#{$base}--#{$modifier} {
419
+ @content;
420
+ }
421
+ }
422
+
423
+ @function _is-duration($name) {
424
+ @return string.index($name, '--greese-duration-') == 1;
425
+ }
426
+
427
+ /// Custom properties, prefix added: `vars((tone: red, radius-md: 2px))`.
428
+ /// A `(light: …, dark: …)` value becomes `light-dark()`, which follows the
429
+ /// nearest color scheme. Duration tokens stay out of reduced motion.
430
+ @mixin vars($values) {
431
+ $calm: ();
432
+
433
+ @each $name, $value in $values {
434
+ $property: _var-name($name);
435
+
436
+ @if meta.type-of($value) == map {
437
+ $value: light-dark(#{_css(map.get($value, light))}, #{_css(map.get($value, dark))});
438
+ }
439
+
440
+ @if _is-duration($property) {
441
+ $calm: map.set($calm, $property, $value);
442
+ } @else {
443
+ #{$property}: #{_css($value)};
444
+ }
445
+ }
446
+
447
+ @if list.length($calm) > 0 {
448
+ @media (prefers-reduced-motion: no-preference) {
449
+ @each $property, $value in $calm {
450
+ #{$property}: #{_css($value)};
451
+ }
452
+ }
453
+ }
454
+ }
455
+
456
+ // --- motion ----------------------------------------------------------------
457
+
458
+ @function _ease($ease) {
459
+ @if meta.type-of($ease) == string and _in($_eases, _kebab($ease)) {
460
+ @return var(--greese-ease-#{_kebab($ease)});
461
+ }
462
+
463
+ @return _css($ease);
464
+ }
465
+
466
+ /// Theme-wide motion for the component: what the `greese` prop does per
467
+ /// instance, as a default for every instance. Pure CSS, behind
468
+ /// `prefers-reduced-motion: no-preference`.
469
+ ///
470
+ /// enter a preset (`fade-up`, `scale-in`, …) played when it appears
471
+ /// keyframes your own `@keyframes` name, instead of a preset
472
+ /// exit how an overlay leaves: fade-out, fade-out-up, fade-out-down, scale-out
473
+ /// duration, delay, ease, distance tune the entrance (`ease: out` or a cubic-bezier())
474
+ /// durations the component's transition speeds: (fast: 80ms, normal: 150ms)
475
+ /// easings its transition curves: (standard: cubic-bezier(…))
476
+ ///
477
+ /// An instance's own `greese` entrance replaces this one; its `duration`,
478
+ /// `delay` and `ease` tune it.
479
+ @mixin motion($options) {
480
+ $checked: _require-component('motion');
481
+ $known: (enter, keyframes, exit, duration, delay, ease, distance, durations, easings);
482
+
483
+ @each $key, $unused in $options {
484
+ $checked: _check(
485
+ _in($known, $key),
486
+ 'motion() has no option `#{$key}`. Options: #{_join($known)}.'
487
+ );
488
+ }
489
+
490
+ $enter: map.get($options, enter);
491
+ $name: map.get($options, keyframes);
492
+
493
+ @if $enter {
494
+ $enter: _kebab($enter);
495
+ $checked: _check(
496
+ _in(_effects-with(enter), $enter),
497
+ 'no entrance `#{$enter}`. Effects that end at rest: #{_join(_effects-with(enter))}.'
498
+ );
499
+ $name: greese-#{$enter};
500
+ }
501
+
502
+ @if map.has-key($options, distance) {
503
+ --greese-motion-distance: #{_css(map.get($options, distance))};
504
+ }
505
+
506
+ $exit: map.get($options, exit);
507
+
508
+ @if $exit {
509
+ $exit: _kebab($exit);
510
+ $checked: _check(
511
+ _in(_effects-with(exit), $exit),
512
+ 'no exit `#{$exit}`. Effects that end away from rest: #{_join(_effects-with(exit))}.'
513
+ );
514
+
515
+ // As the `exit` classes in Motion.scss: only what the effect changes, so
516
+ // `fade-out` keeps the overlay's own exit.
517
+ @each $property, $value in map.get(map.get(_effects(), $exit), exit) {
518
+ --greese-motion-exit-#{$property}: #{string.unquote($value)};
519
+ }
520
+ }
521
+
522
+ $tokens: ();
523
+
524
+ @each $key, $value in map.get($options, durations) or () {
525
+ $tokens: map.set($tokens, duration-#{_kebab($key)}, $value);
526
+ }
527
+
528
+ @each $key, $value in map.get($options, easings) or () {
529
+ $tokens: map.set($tokens, easing-#{_kebab($key)}, $value);
530
+ }
531
+
532
+ @if list.length($tokens) > 0 {
533
+ @include vars($tokens);
534
+ }
535
+
536
+ @if $name {
537
+ $duration: _css(map.get($options, duration) or 0.5s);
538
+ $delay: _css(map.get($options, delay) or 0s);
539
+ $ease: _ease(map.get($options, ease) or out);
540
+
541
+ // `backwards`, as the presets: the first frame holds during the delay,
542
+ // then the component's own styles apply again.
543
+ @media (prefers-reduced-motion: no-preference) {
544
+ animation: #{$name} var(--greese-motion-duration, #{$duration})
545
+ var(--greese-motion-ease, #{$ease}) var(--greese-motion-delay, #{$delay})
546
+ backwards;
547
+ }
548
+ }
549
+ }
550
+
551
+ /// Every effect by kebab-case name: the built-in ones and those defined with
552
+ /// `effect()` (which can't take a built-in's name).
553
+ @function _effects() {
554
+ @return map.merge(defaults.$effects, $_defined);
555
+ }
556
+
557
+ /// The names of the effects whose plan has `$key`: `enter` (ends at rest, plays
558
+ /// as CSS) or `exit` (ends away from rest, an overlay can leave with it).
559
+ @function _effects-with($key) {
560
+ $out: ();
561
+
562
+ @each $name, $plan in _effects() {
563
+ @if map.has-key($plan, $key) {
564
+ $out: list.append($out, $name, comma);
565
+ }
566
+ }
567
+
568
+ @return $out;
569
+ }
570
+
571
+ /// The custom properties `motion-defaults()` writes for `$options`, checked.
572
+ @mixin _role-declarations($options, $lists) {
573
+ $known: map.get($lists, known);
574
+ $entrances: map.get($lists, entrances);
575
+ $exits: map.get($lists, exits);
576
+ $page-entrances: map.get($lists, page-entrances);
577
+ $page-exits: map.get($lists, page-exits);
578
+ $page-keywords: map.get($lists, page-keywords);
579
+ $durations: map.get($lists, durations);
580
+ $eases: map.get($lists, eases);
581
+
582
+ @each $key, $value in $options {
583
+ $checked: _check(
584
+ _in($known, $key),
585
+ 'motion-defaults() has no option `#{$key}`. Options: #{_join($known)}.'
586
+ );
587
+
588
+ @if _in($page-entrances, $key) or _in($page-exits, $key) {
589
+ // A page role names the keyframes its snapshot plays.
590
+ $name: _kebab($value);
591
+ $kind: enter;
592
+ $what: 'entrance';
593
+ $ends: 'at rest';
594
+
595
+ @if _in($page-exits, $key) {
596
+ $kind: exit;
597
+ $what: 'exit';
598
+ $ends: 'away from rest';
599
+ }
600
+
601
+ @if $name != none {
602
+ $checked: _check(
603
+ _in(_effects-with($kind), $name),
604
+ 'motion-defaults(): no #{$what} `#{$name}` for `#{$key}`. Effects that end #{$ends}: #{_join(_effects-with($kind))}.'
605
+ );
606
+ $name: greese-#{$name};
607
+ }
608
+
609
+ --greese-motion-#{$key}: #{$name};
610
+ } @else if map.has-key($page-keywords, $key) {
611
+ $allowed: map.get($page-keywords, $key);
612
+ $checked: _check(
613
+ _in($allowed, $value),
614
+ 'motion-defaults(): `#{$key}` is one of #{_join($allowed)}, not `#{$value}`.'
615
+ );
616
+
617
+ --greese-motion-#{$key}: #{$value};
618
+ } @else if _in($exits, $key) {
619
+ // An exit by name: everything it sets, under the role's prefix.
620
+ $name: _kebab($value);
621
+ $checked: _check(
622
+ _in(_effects-with(exit), $name),
623
+ 'motion-defaults(): no exit `#{$name}` for `#{$key}`. Effects that end away from rest: #{_join(_effects-with(exit))}.'
624
+ );
625
+
626
+ @each $property, $v in map.get(map.get(_effects(), $name), exit) {
627
+ --greese-motion-#{$key}-#{$property}: #{string.unquote($v)};
628
+ }
629
+ } @else {
630
+ @if $key == ease {
631
+ $value: _ease($value);
632
+ } @else if _in($entrances, $key) {
633
+ $name: _kebab($value);
634
+
635
+ @if $name == none {
636
+ $value: none;
637
+ } @else {
638
+ $checked: _check(
639
+ _in(_effects-with(enter), $name),
640
+ 'motion-defaults(): no entrance `#{$name}` for `#{$key}`. Effects that end at rest: #{_join(_effects-with(enter))}.'
641
+ );
642
+ $value: greese-#{$name};
643
+ }
644
+ } @else if _in($durations, $key) and _in((fast, normal, slow), $value) {
645
+ $value: var(--greese-duration-#{$value});
646
+ } @else if _in($eases, $key) and _in((standard, enter, exit), $value) {
647
+ $value: var(--greese-easing-#{$value});
648
+ } @else if _in($eases, $key) {
649
+ $value: _ease($value);
650
+ }
651
+
652
+ --greese-motion-#{$key}: #{_css($value)};
653
+ }
654
+ }
655
+ }
656
+
657
+ /// Tunes every `greese` prop entrance on the page (or in the theme), and
658
+ /// assigns the theme's motion roles (ADR 0025):
659
+ ///
660
+ /// duration, delay, ease, distance, stagger every `greese` entrance's pacing
661
+ /// theme the entrance `greese="theme"` plays (base: fade-up)
662
+ /// content a tab panel's, an opening accordion's children (base: a short rise)
663
+ /// rows a table's rows, one after another (base: none)
664
+ /// toast, backdrop their entrances
665
+ /// overlay every overlay's entrance but a drawer's; `dialog`, `popover`
666
+ /// (menus and speed-dial actions too), `tooltip` and `drawer`
667
+ /// (the sidebar too) for one kind
668
+ /// overlay-exit, dialog-exit, popover-exit, tooltip-exit, drawer-exit
669
+ /// how they leave: an exit by name
670
+ /// content-duration, rows-duration, overlay-duration, toast-duration,
671
+ /// backdrop-duration, exit-duration a duration token (`fast`,
672
+ /// `normal`, `slow`) or a time
673
+ /// enter-ease, exit-ease an easing token (`standard`,
674
+ /// `enter`, `exit`), an ease name or a curve
675
+ /// page, page-back a page transition's entrance,
676
+ /// going forward and back (ADR 0029; base: fade-in)
677
+ /// page-exit, page-back-exit how the old page leaves: an exit
678
+ /// by name (base: fade-out)
679
+ /// page-duration, share-duration a duration token or a time:
680
+ /// the page's, and a shared element's morph
681
+ /// page-ease, share-ease an easing, as above
682
+ /// page-stagger seconds between a live exit's
683
+ /// members (base: the entrance stagger)
684
+ /// page-exit-order `reverse` (base), `document` or
685
+ /// `together`: the order a live exit's members leave in
686
+ /// page-mode `auto` (base), `snapshot` or
687
+ /// `live`: how a page change plays
688
+ ///
689
+ /// `motion-defaults((content: wipe-open-x, dialog: iris-in, dialog-exit: iris-out,
690
+ /// content-duration: slow))`. A role points at an effect or a token: the
691
+ /// effects keep their keyframes and the tokens their values.
692
+ @mixin motion-defaults($options) {
693
+ $pacing: (duration, delay, ease, distance, stagger);
694
+ $entrances: (
695
+ theme,
696
+ content,
697
+ rows,
698
+ toast,
699
+ backdrop,
700
+ overlay,
701
+ dialog,
702
+ popover,
703
+ tooltip,
704
+ drawer
705
+ );
706
+ $exits: (overlay-exit, dialog-exit, popover-exit, tooltip-exit, drawer-exit);
707
+
708
+ // A page transition's roles (ADR 0029): its entrances and exits are
709
+ // `@keyframes` names, played on the view transition's snapshots.
710
+ $page-entrances: (page, page-back);
711
+ $page-exits: (page-exit, page-back-exit);
712
+ $page-keywords: (
713
+ page-exit-order: (
714
+ reverse,
715
+ document,
716
+ together,
717
+ ),
718
+ page-mode: (
719
+ auto,
720
+ snapshot,
721
+ live,
722
+ ),
723
+ );
724
+ $durations: (
725
+ content-duration,
726
+ rows-duration,
727
+ overlay-duration,
728
+ toast-duration,
729
+ backdrop-duration,
730
+ exit-duration,
731
+ page-duration,
732
+ share-duration
733
+ );
734
+ $eases: (enter-ease, exit-ease, page-ease, share-ease);
735
+ $known: list.join(
736
+ list.join(list.join($pacing, $entrances), $exits),
737
+ list.join($durations, $eases)
738
+ );
739
+ $known: list.join(
740
+ $known,
741
+ list.join(
742
+ list.join($page-entrances, $page-exits),
743
+ list.join((page-stagger), map.keys($page-keywords))
744
+ )
745
+ );
746
+ $root: ':root';
747
+
748
+ @include _layer-order;
749
+
750
+ @if $_theme {
751
+ $root: "[#{$_theme-attr}='#{$_theme}']";
752
+ }
753
+
754
+ $lists: (
755
+ known: $known,
756
+ entrances: $entrances,
757
+ exits: $exits,
758
+ page-entrances: $page-entrances,
759
+ page-exits: $page-exits,
760
+ page-keywords: $page-keywords,
761
+ durations: $durations,
762
+ eases: $eases,
763
+ );
764
+
765
+ @layer greese.overrides {
766
+ #{$root} {
767
+ @include _role-declarations($options, $lists);
768
+ }
769
+
770
+ // A theme scoped to an element: its page roles go on the root as well,
771
+ // while that element is on the page, because a view transition's
772
+ // snapshots inherit from the root, not from anything inside the page
773
+ // (ADR 0029).
774
+ @if $_theme {
775
+ $page: ();
776
+
777
+ @each $key, $value in $options {
778
+ @if string.index('#{$key}', 'page') == 1 or string.index('#{$key}', 'share') == 1
779
+ {
780
+ $page: map.set($page, $key, $value);
781
+ }
782
+ }
783
+
784
+ @if list.length($page) > 0 {
785
+ :root:has(#{$root}) {
786
+ @include _role-declarations($page, $lists);
787
+ }
788
+ }
789
+ }
790
+ }
791
+
792
+ // The keyframes of the entrances it assigns, so the theme works on a page
793
+ // that hasn't loaded the Motion styles: the effects' own, unchanged.
794
+ $played: ();
795
+
796
+ @each $key, $value in $options {
797
+ $role: _in($entrances, $key) or _in($page-entrances, $key) or _in($page-exits, $key);
798
+
799
+ @if $role and _kebab($value) != none {
800
+ $played: map.set($played, _kebab($value), true);
801
+ }
802
+ }
803
+
804
+ @if list.length($played) > 0 {
805
+ @layer greese.utilities {
806
+ @media (prefers-reduced-motion: no-preference) {
807
+ @each $name, $unused in $played {
808
+ @include effect.keyframes($name, map.get(_effects(), $name), $leave: true);
809
+ }
810
+ }
811
+ }
812
+ }
813
+ }
814
+
815
+ /// Selector rules for page transitions (ADR 0029): during a greese page
816
+ /// change, each selector's elements get a snapshot of their own, with their
817
+ /// own effects, rather than moving with the page.
818
+ ///
819
+ /// page-transitions((
820
+ /// 'header, footer': still,
821
+ /// '.greese-Card': (exit: scale-out, enter: pop-in, duration: fast),
822
+ /// '[data-hero]': (exit: fade-out-up, delay: 0.1s),
823
+ /// ))
824
+ ///
825
+ /// A rule is `still` (kept in place, not animated) or a map of `enter` (an
826
+ /// entrance), `exit` (an exit), `duration` (a token or a time), `delay` and
827
+ /// `ease`. What a rule leaves out follows the theme's page roles. The elements
828
+ /// are named only while a greese page change runs
829
+ /// (`:active-view-transition-type(greese-page)`), so other view transitions
830
+ /// never capture them; where that selector or `match-element` isn't supported
831
+ /// they move with the page. Every named element is a snapshot of its own:
832
+ /// name sections, not every item of a long list.
833
+ @mixin page-transitions($rules) {
834
+ $known: (enter, exit, duration, delay, ease);
835
+ $during: ':root:active-view-transition-type(greese-page)';
836
+ $prefix: base;
837
+ $played: ();
838
+ $i: 0;
839
+
840
+ @if $_theme {
841
+ $prefix: $_theme;
842
+ }
843
+
844
+ @include _layer-order;
845
+
846
+ @layer greese.overrides {
847
+ @each $selector, $rule in $rules {
848
+ $i: $i + 1;
849
+ $class: greese-page-#{$prefix}-#{$i};
850
+ $classes: string.unquote(
851
+ 'greese-PageTransition greese-PageTransition--theme #{$class}'
852
+ );
853
+
854
+ @if $rule == still {
855
+ $classes: string.unquote('greese-PageTransition greese-PageTransition--still');
856
+ } @else {
857
+ $checked: _check(
858
+ meta.type-of($rule) == map,
859
+ 'page-transitions(): the rule for `#{$selector}` is `still` or a map of #{_join($known)}.'
860
+ );
861
+
862
+ @each $key, $unused in $rule {
863
+ $checked: _check(
864
+ _in($known, $key),
865
+ 'page-transitions(): `#{$selector}` has no option `#{$key}`. Options: #{_join($known)}.'
866
+ );
867
+ }
868
+ }
869
+
870
+ // Named only while a greese page change runs; under a theme, only
871
+ // inside it (the attribute may be on the root itself).
872
+ @if $_theme {
873
+ #{$during} [#{$_theme-attr}='#{$_theme}'] :is(#{$selector}),
874
+ :root[#{$_theme-attr}='#{$_theme}']:active-view-transition-type(greese-page)
875
+ :is(#{$selector}) {
876
+ view-transition-name: match-element;
877
+ view-transition-class: $classes;
878
+ }
879
+ } @else {
880
+ #{$during} :is(#{$selector}) {
881
+ view-transition-name: match-element;
882
+ view-transition-class: $classes;
883
+ }
884
+ }
885
+
886
+ @if $rule != still {
887
+ $pacing: ();
888
+
889
+ @if map.has-key($rule, duration) {
890
+ $duration: map.get($rule, duration);
891
+
892
+ @if _in((fast, normal, slow), $duration) {
893
+ $duration: var(--greese-duration-#{$duration});
894
+ }
895
+
896
+ $pacing: map.set($pacing, animation-duration, $duration);
897
+ }
898
+
899
+ @if map.has-key($rule, delay) {
900
+ $pacing: map.set($pacing, animation-delay, map.get($rule, delay));
901
+ }
902
+
903
+ @if map.has-key($rule, ease) {
904
+ $pacing: map.set(
905
+ $pacing,
906
+ animation-timing-function,
907
+ _ease(map.get($rule, ease))
908
+ );
909
+ }
910
+
911
+ @each $side, $kind in (enter: enter, exit: exit) {
912
+ $name: null;
913
+
914
+ @if map.has-key($rule, $side) {
915
+ $name: _kebab(map.get($rule, $side));
916
+
917
+ @if $name != none {
918
+ $checked: _check(
919
+ _in(_effects-with($kind), $name),
920
+ 'page-transitions(): `#{$selector}` has no #{$side} `#{$name}`. Effects that #{$side}: #{_join(_effects-with($kind))}.'
921
+ );
922
+ $played: map.set($played, $name, true);
923
+ }
924
+ }
925
+
926
+ $pseudo: new;
927
+
928
+ @if $side == exit {
929
+ $pseudo: old;
930
+ }
931
+
932
+ @if $name or $pacing != () {
933
+ @media (prefers-reduced-motion: no-preference) {
934
+ html::view-transition-#{$pseudo}(*.#{$class}) {
935
+ @if $name == none {
936
+ animation-name: none;
937
+ } @else if $name {
938
+ animation-name: greese-#{$name};
939
+ }
940
+
941
+ @each $property, $value in $pacing {
942
+ #{$property}: #{_css($value)};
943
+ }
944
+ }
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ }
951
+
952
+ // The keyframes the rules play, so they work without PageTransition's
953
+ // stylesheet: the effects' own, unchanged.
954
+ @if list.length($played) > 0 {
955
+ @layer greese.utilities {
956
+ @media (prefers-reduced-motion: no-preference) {
957
+ @each $name, $unused in $played {
958
+ @include effect.keyframes($name, map.get(_effects(), $name), $leave: true);
959
+ }
960
+ }
961
+ }
962
+ }
963
+ }
964
+
965
+ /// Adds a `@keyframes` of your own, for a component's `motion((keyframes: …))`:
966
+ /// `keyframes(greese-float-up, (from: (opacity: 0, translate: 0 2rem), to:
967
+ /// (opacity: 1, translate: 0 0)))`. Steps are `from`, `to` or percentages
968
+ /// (quoted: `'60%'`). A built-in effect's name is refused: a built-in keeps its
969
+ /// keyframes, and a theme assigns another where it wants one (ADR 0025).
970
+ @mixin keyframes($name, $steps) {
971
+ $checked: _check(
972
+ string.index('#{$name}', 'greese-') == 1,
973
+ 'keyframes() names start with `greese-`, like `greese-float-up`.'
974
+ );
975
+ $checked: _check(
976
+ not map.has-key(defaults.$effects, string.slice('#{$name}', 8)),
977
+ 'keyframes(#{$name}): a built-in effect keeps its keyframes. Name yours differently and assign it.'
978
+ );
979
+
980
+ @include _layer-order;
981
+
982
+ @layer greese.utilities {
983
+ @keyframes #{$name} {
984
+ @each $at, $declarations in $steps {
985
+ #{$at} {
986
+ @each $property, $value in $declarations {
987
+ #{$property}: #{_css($value)};
988
+ }
989
+ }
990
+ }
991
+ }
992
+ }
993
+ }
994
+
995
+ /// Defines an effect as CSS, as the built-in ones are: `plan` is `(enter:
996
+ /// (from: (opacity: 0), '60%': (opacity: 1)))` for an effect that ends at the
997
+ /// element's resting state (its `@keyframes`, without the 100% step), or
998
+ /// `(exit: (scale: 0.9))` for one that ends away from it (what an overlay's exit
999
+ /// changes). Theme Studio writes these from its effects, with `cssPlan()` in
1000
+ /// @greese/ui, so a new effect works in `motion()` like a built-in, and a
1001
+ /// built-in's name is retuned. Names are kebab-case.
1002
+ @mixin effect($name, $plan) {
1003
+ $name: _kebab($name);
1004
+ $plan: _normal($plan);
1005
+
1006
+ // A built-in always does what its name says; a theme assigns another
1007
+ // effect where it wants one (ADR 0025), it doesn't rewrite this one.
1008
+ $checked: _check(
1009
+ not map.has-key(defaults.$effects, $name),
1010
+ 'effect(#{$name}): `#{$name}` is a built-in effect, which keeps its own plan. Name yours differently and assign it.'
1011
+ );
1012
+ $checked: _check(
1013
+ map.has-key($plan, enter) or map.has-key($plan, exit),
1014
+ 'effect(#{$name}) needs a plan with `enter` or `exit`.'
1015
+ );
1016
+ $_defined: map.set($_defined, $name, $plan) !global;
1017
+
1018
+ @include _layer-order;
1019
+
1020
+ @layer greese.utilities {
1021
+ @include effect.classes($name, $plan);
1022
+
1023
+ @media (prefers-reduced-motion: no-preference) {
1024
+ @include effect.keyframes($name, $plan, $leave: true);
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ // --- tokens and themes -----------------------------------------------------
1030
+
1031
+ /// Declarations for the token overrides, split by scheme.
1032
+ // --- derived brand colors (createTheme's deriveColors, in Sass) -------------
1033
+
1034
+ $_derived: (primary, secondary, tertiary, accent, negative);
1035
+
1036
+ // The WCAG formula's constants need their full precision, and the inks are
1037
+ // literal colors by definition (the same ones @greese/ui uses).
1038
+ // stylelint-disable number-max-precision, color-no-hex, color-hex-length
1039
+
1040
+ /// WCAG relative luminance of a color.
1041
+ @function _luminance($c) {
1042
+ $sum: 0;
1043
+
1044
+ @each $channel, $weight in ('red': 0.2126, 'green': 0.7152, 'blue': 0.0722) {
1045
+ $s: math.div(color.channel($c, $channel, $space: rgb), 255);
1046
+ $linear: math.div($s, 12.92);
1047
+
1048
+ @if $s > 0.04045 {
1049
+ $linear: math.pow(math.div($s + 0.055, 1.055), 2.4);
1050
+ }
1051
+
1052
+ $sum: $sum + $weight * $linear;
1053
+ }
1054
+
1055
+ @return $sum;
1056
+ }
1057
+
1058
+ @function _ratio($a, $b) {
1059
+ $la: _luminance($a);
1060
+ $lb: _luminance($b);
1061
+
1062
+ @return math.div(math.max($la, $lb) + 0.05, math.min($la, $lb) + 0.05);
1063
+ }
1064
+
1065
+ /// A color from a hex string (`"#c2410c"`, `"#fff"`), as Theme Studio and
1066
+ /// JSON-driven themes write them; colors pass through. Null otherwise.
1067
+ @function _hex-color($value) {
1068
+ @if meta.type-of($value) == color {
1069
+ @return $value;
1070
+ }
1071
+
1072
+ @if meta.type-of($value) != string {
1073
+ @return null;
1074
+ }
1075
+
1076
+ $s: string.to-lower-case(string.unquote('#{$value}'));
1077
+
1078
+ @if string.slice($s, 1, 1) != '#' {
1079
+ @return null;
1080
+ }
1081
+
1082
+ $hex: string.slice($s, 2);
1083
+
1084
+ @if string.length($hex) == 3 {
1085
+ $short: $hex;
1086
+ $hex: '';
1087
+
1088
+ @for $i from 1 through 3 {
1089
+ $hex: $hex + string.slice($short, $i, $i) + string.slice($short, $i, $i);
1090
+ }
1091
+ }
1092
+
1093
+ @if string.length($hex) != 6 {
1094
+ @return null;
1095
+ }
1096
+
1097
+ $digits: '0123456789abcdef';
1098
+ $channels: ();
1099
+
1100
+ @for $i from 0 through 2 {
1101
+ $high: string.index($digits, string.slice($hex, $i * 2 + 1, $i * 2 + 1));
1102
+ $low: string.index($digits, string.slice($hex, $i * 2 + 2, $i * 2 + 2));
1103
+
1104
+ @if not $high or not $low {
1105
+ @return null;
1106
+ }
1107
+
1108
+ $channels: list.append($channels, ($high - 1) * 16 + $low - 1);
1109
+ }
1110
+
1111
+ @return rgb(list.nth($channels, 1), list.nth($channels, 2), list.nth($channels, 3));
1112
+ }
1113
+
1114
+ // The content inks: white, and steel-999 (the dark scheme's content color), as
1115
+ // `CONTENT_LIGHT` and `CONTENT_DARK` in contrast.ts (a test keeps them equal).
1116
+ $_white: #fff;
1117
+ $_dark-ink: #11171c;
1118
+
1119
+ /// Text for content on `$c`: as `contentColor()` in @greese/ui, the
1120
+ /// more readable of white and near-black if it reaches AA, else black. Null
1121
+ /// for anything that isn't a color (a `var()`), which keeps the default.
1122
+ @function _content($c) {
1123
+ @if meta.type-of($c) != color {
1124
+ @return null;
1125
+ }
1126
+
1127
+ $light: _ratio($c, $_white);
1128
+ $dark: _ratio($c, $_dark-ink);
1129
+ $best: '#ffffff';
1130
+ $ratio: $light;
1131
+
1132
+ @if $dark > $light {
1133
+ $best: '#11171c';
1134
+ $ratio: $dark;
1135
+ }
1136
+
1137
+ @if $ratio < 4.5 {
1138
+ $best: '#000000';
1139
+ }
1140
+
1141
+ @return string.unquote($best);
1142
+ }
1143
+
1144
+ // stylelint-enable number-max-precision, color-no-hex, color-hex-length
1145
+
1146
+ /// Fills in `-content` and `-strong` for brand colors the overrides set,
1147
+ /// unless they set those too.
1148
+ @function _derive($colors) {
1149
+ @each $role in $_derived {
1150
+ $main: map.get($colors, '#{$role}');
1151
+
1152
+ @if $main {
1153
+ $pair: $main;
1154
+
1155
+ @if meta.type-of($pair) != map {
1156
+ $pair: (
1157
+ light: $main,
1158
+ dark: $main,
1159
+ );
1160
+ }
1161
+
1162
+ @if not map.has-key($colors, '#{$role}-content') {
1163
+ $fallback: map.get(defaults.$color, '#{$role}-content');
1164
+ $colors: map.set(
1165
+ $colors,
1166
+ '#{$role}-content',
1167
+ (
1168
+ light: _content(_hex-color(_css(map.get($pair, light)))) or
1169
+ map.get($fallback, light),
1170
+ dark: _content(_hex-color(_css(map.get($pair, dark)))) or
1171
+ map.get($fallback, dark),
1172
+ )
1173
+ );
1174
+ }
1175
+
1176
+ @if map.has-key(defaults.$color, '#{$role}-strong') and not
1177
+ map.has-key($colors, '#{$role}-strong')
1178
+ {
1179
+ $self: 'var(--greese-color-#{$role})';
1180
+ $colors: map.set(
1181
+ $colors,
1182
+ '#{$role}-strong',
1183
+ (
1184
+ light: string.unquote('color-mix(in oklab, #{$self} 80%, #000)'),
1185
+ dark: string.unquote('color-mix(in oklab, #{$self} 70%, #fff)'),
1186
+ )
1187
+ );
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ @return $colors;
1193
+ }
1194
+
1195
+ /// The extended tokens known so far, plus these.
1196
+ @function _register-extended($extend) {
1197
+ $known: $_extended;
1198
+
1199
+ @each $group, $values in $extend {
1200
+ $list: map.get($known, _kebab($group)) or ();
1201
+
1202
+ @each $key, $unused in $values {
1203
+ $list: list.append($list, '#{_kebab($key)}', comma);
1204
+ }
1205
+
1206
+ $known: map.set($known, _kebab($group), $list);
1207
+ }
1208
+
1209
+ @return $known;
1210
+ }
1211
+
1212
+ @function _token-decls($overrides, $complete, $extend: ()) {
1213
+ $base: ();
1214
+ $light: ();
1215
+ $dark: ();
1216
+ $durations: ();
1217
+ $type-base: ();
1218
+
1219
+ @each $group, $values in $overrides {
1220
+ $group: _kebab($group);
1221
+ $checked: _check(
1222
+ $group == color or map.has-key(defaults.$scalars, $group),
1223
+ 'no token group `#{$group}`. Groups: color, #{_join(map.keys(defaults.$scalars))}.'
1224
+ );
1225
+
1226
+ @each $key, $unused in $values {
1227
+ $key: '#{_kebab($key)}';
1228
+ $known: map.get(defaults.$scalars, $group) or ();
1229
+
1230
+ @if $group == color {
1231
+ $known: defaults.$color;
1232
+ }
1233
+
1234
+ $checked: _check(
1235
+ map.has-key($known, $key),
1236
+ 'no `#{$group}` token `#{$key}`. Keys: #{_join(map.keys($known))}.'
1237
+ );
1238
+ }
1239
+ }
1240
+
1241
+ // Normalised overrides: kebab keys, so they match the defaults.
1242
+ $colors: ();
1243
+ $scalars: ();
1244
+
1245
+ @each $group, $values in $overrides {
1246
+ @each $key, $value in $values {
1247
+ @if _kebab($group) == color {
1248
+ $colors: map.set($colors, '#{_kebab($key)}', $value);
1249
+ } @else {
1250
+ $scalars: map.set($scalars, '#{_kebab($group)}', '#{_kebab($key)}', $value);
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ $colors: _derive($colors);
1256
+
1257
+ @each $role, $pair in defaults.$color {
1258
+ $value: map.get($colors, $role);
1259
+
1260
+ @if $value or $complete {
1261
+ $value: $value or $pair;
1262
+
1263
+ @if meta.type-of($value) != map {
1264
+ $value: (
1265
+ light: $value,
1266
+ dark: $value,
1267
+ );
1268
+ }
1269
+
1270
+ $pair: $value;
1271
+ $light: map.set($light, --greese-color-#{$role}, _css(map.get($pair, light)));
1272
+ $dark: map.set($dark, --greese-color-#{$role}, _css(map.get($pair, dark)));
1273
+ }
1274
+ }
1275
+
1276
+ // A complete theme declares the type system too: its smallest-screen
1277
+ // values with the tokens, the changes in media queries (see `tokens()`).
1278
+ @if $complete {
1279
+ @each $property, $v in type-emit.base(config.$typography) {
1280
+ $type-base: map.set($type-base, $property, type-emit.value($v));
1281
+ }
1282
+ }
1283
+
1284
+ @each $group, $values in defaults.$scalars {
1285
+ @each $key, $fallback in $values {
1286
+ $value: map.get($scalars, $group, $key);
1287
+
1288
+ @if $value or $complete {
1289
+ $base: map.set($base, --greese-#{$group}-#{$key}, _css($value or $fallback));
1290
+
1291
+ @if $group == duration {
1292
+ $durations: map.set($durations, --greese-#{$group}-#{$key}, 0.01ms);
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ // New tokens: declared, never guessed. Each is `--greese-<group>-<key>`,
1299
+ // alongside the built-in ones; a color takes one value or a light/dark pair.
1300
+ @each $group, $values in $extend {
1301
+ $group: _kebab($group);
1302
+ $checked: _check(
1303
+ $group == color or map.has-key(defaults.$scalars, $group),
1304
+ 'no token group `#{$group}` to extend. Groups: color, #{_join(map.keys(defaults.$scalars))}.'
1305
+ );
1306
+
1307
+ @each $key, $value in $values {
1308
+ $key: '#{_kebab($key)}';
1309
+ $known: map.get(defaults.$scalars, $group) or ();
1310
+
1311
+ @if $group == color {
1312
+ $known: defaults.$color;
1313
+ }
1314
+
1315
+ $checked: _check(
1316
+ not map.has-key($known, $key),
1317
+ '`#{$group}` token `#{$key}` already exists: set it in tokens(), not $extend.'
1318
+ );
1319
+ $checked: _check(
1320
+ string.index($key, ' ') == null and string.length($key) > 0,
1321
+ 'new token names are kebab-case words, got `#{$key}`.'
1322
+ );
1323
+
1324
+ @if $group == color {
1325
+ @if meta.type-of($value) != map {
1326
+ $value: (
1327
+ light: $value,
1328
+ dark: $value,
1329
+ );
1330
+ }
1331
+
1332
+ $light: map.set($light, --greese-color-#{$key}, _css(map.get($value, light)));
1333
+ $dark: map.set($dark, --greese-color-#{$key}, _css(map.get($value, dark)));
1334
+ } @else {
1335
+ $base: map.set($base, --greese-#{$group}-#{$key}, _css($value));
1336
+
1337
+ @if $group == duration {
1338
+ $durations: map.set($durations, --greese-#{$group}-#{$key}, 0.01ms);
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ @return (
1345
+ base: map.merge($base, $type-base),
1346
+ light: $light,
1347
+ dark: $dark,
1348
+ durations: $durations
1349
+ );
1350
+ }
1351
+
1352
+ @mixin _declare($map) {
1353
+ @each $property, $value in $map {
1354
+ #{$property}: #{$value};
1355
+ }
1356
+ }
1357
+
1358
+ /// Token overrides, compiled: the build-time `createTheme()`. Takes the same
1359
+ /// groups as `createTheme` in kebab-case, and a color takes one value or a
1360
+ /// `(light: …, dark: …)` pair:
1361
+ ///
1362
+ /// @include greese.tokens((
1363
+ /// color: (primary: (light: #7c3aed, dark: #c4b5fd), primary-content: #fff),
1364
+ /// radius: (md: 12px),
1365
+ /// ));
1366
+ ///
1367
+ /// At the top level it changes the page's defaults, overriding only what you
1368
+ /// pass. Inside `theme(name)` it declares a complete theme, as `createTheme`
1369
+ /// does, for `data-greese-theme="name"`.
1370
+ ///
1371
+ /// To add tokens the system doesn't have, name them in `$extend` (same shape):
1372
+ ///
1373
+ /// @include greese.tokens((), $extend: (
1374
+ /// color: (brand-ink: (light: #1b1340, dark: #e5defc)),
1375
+ /// space: (9: 4rem),
1376
+ /// ));
1377
+ ///
1378
+ /// Each becomes `--greese-<group>-<key>` in the built CSS, and `token()` and
1379
+ /// component overrides accept it from then on. Names that already exist, or
1380
+ /// groups that don't, still stop the build. (A new `space` key is a variable
1381
+ /// only: the responsive classes come with the stylesheet's build.)
1382
+ @mixin tokens($overrides, $extend: ()) {
1383
+ $_extended: _register-extended($extend) !global;
1384
+ $d: _token-decls($overrides, $_theme != null, $extend);
1385
+ $base: map.get($d, base);
1386
+ $light: map.get($d, light);
1387
+ $dark: map.get($d, dark);
1388
+ $durations: map.get($d, durations);
1389
+
1390
+ @include _layer-order;
1391
+
1392
+ @layer greese.theme {
1393
+ @if $_theme {
1394
+ $t: "[#{$_theme-attr}='#{$_theme}']";
1395
+ $s: '[#{$_scheme-attr}]';
1396
+
1397
+ #{$t} {
1398
+ @include _declare($base);
1399
+ @include _declare($light);
1400
+ }
1401
+
1402
+ @media (prefers-color-scheme: dark) {
1403
+ #{$t}:not(#{_scheme(light)}) {
1404
+ @include _declare($dark);
1405
+ }
1406
+ }
1407
+
1408
+ #{$t}#{_scheme(dark)},
1409
+ :root#{_scheme(dark)} #{$t}:not(#{$s}) {
1410
+ @include _declare($dark);
1411
+ }
1412
+
1413
+ @include type-emit.emit-media(config.$typography, $t);
1414
+
1415
+ :root#{_scheme(light)} #{$t}:not(#{$s}) {
1416
+ @include _declare($light);
1417
+ }
1418
+
1419
+ @media (prefers-reduced-motion: reduce) {
1420
+ #{$t} {
1421
+ @include _declare($durations);
1422
+ }
1423
+ }
1424
+ } @else {
1425
+ // The selectors of the default tokens (tokens.css), one layer up.
1426
+ @if list.length($base) > 0 {
1427
+ :root {
1428
+ @include _declare($base);
1429
+ }
1430
+ }
1431
+
1432
+ @if list.length($light) > 0 {
1433
+ :root,
1434
+ #{_scheme(light)},
1435
+ #{_scheme(system)} {
1436
+ @include _declare($light);
1437
+ }
1438
+
1439
+ @media (prefers-color-scheme: dark) {
1440
+ :root:not(#{_scheme(light)}),
1441
+ #{_scheme(system)} {
1442
+ @include _declare($dark);
1443
+ }
1444
+ }
1445
+
1446
+ #{_scheme(dark)} {
1447
+ @include _declare($dark);
1448
+ }
1449
+ }
1450
+
1451
+ @if list.length($durations) > 0 {
1452
+ @media (prefers-reduced-motion: reduce) {
1453
+ :root {
1454
+ @include _declare($durations);
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ }
1460
+ }
1461
+
1462
+ /// The type system's values, for the page or (inside `theme(name)`) for that
1463
+ /// theme. Name a variant, then `base` or a breakpoint, then properties; what
1464
+ /// you set replaces the default at that point, and the rest stays:
1465
+ ///
1466
+ /// @include greese.typography((
1467
+ /// h1: (base: (font-size: 2rem), lg: (font-size: 4rem, letter-spacing: -0.03em)),
1468
+ /// caption: (base: (text-transform: uppercase)),
1469
+ /// ));
1470
+ ///
1471
+ /// Properties: the CSS names, font-size, font-weight, letter-spacing,
1472
+ /// line-height, and font-family, font-style, text-transform,
1473
+ /// font-feature-settings, font-variation-settings, font-stretch, text-wrap,
1474
+ /// word-spacing, font-variant-numeric, color. Variants: p1-p3,
1475
+ /// h1-h6, s1, s2, caption, button. Names that don't exist stop the build.
1476
+ @mixin typography($changes) {
1477
+ $merged: ();
1478
+
1479
+ @each $variant, $spec in $changes {
1480
+ $variant: '#{$variant}';
1481
+ $current: map.get(config.$typography, $variant);
1482
+ $checked: _check(
1483
+ $current != null,
1484
+ 'no type variant `#{$variant}`. Variants: #{_join(map.keys(config.$typography))}.'
1485
+ );
1486
+
1487
+ @each $point, $style in $spec {
1488
+ $point: '#{$point}';
1489
+ $checked: _check(
1490
+ $point == base or map.has-key(config.$breakpoints, $point),
1491
+ 'no breakpoint `#{$point}` for `#{$variant}`. Use base or #{_join(map.keys(config.$breakpoints))}.'
1492
+ );
1493
+
1494
+ @each $prop, $unused in $style {
1495
+ $checked: _check(
1496
+ list.index(defaults.$type-props, '#{$prop}') != null,
1497
+ 'no type property `#{$prop}`. Properties: #{_join(defaults.$type-props)}.'
1498
+ );
1499
+ }
1500
+ }
1501
+
1502
+ // Everything this variant has, so its breakpoints stay in step: a theme
1503
+ // layer rule at one point would otherwise beat the defaults above it.
1504
+ $merged: map.set($merged, $variant, map.deep-merge($current, _normal($spec)));
1505
+ }
1506
+
1507
+ @include _layer-order;
1508
+
1509
+ @layer greese.theme {
1510
+ @if $_theme {
1511
+ @include type-emit.emit($merged, "[#{$_theme-attr}='#{$_theme}']");
1512
+ } @else {
1513
+ @include type-emit.emit($merged);
1514
+ }
1515
+ }
1516
+ }
1517
+
1518
+ /// Keys as strings, so they match the defaults' whatever way they're written.
1519
+ @function _normal($map) {
1520
+ $out: ();
1521
+
1522
+ @each $key, $value in $map {
1523
+ @if meta.type-of($value) == map {
1524
+ $value: _normal($value);
1525
+ }
1526
+
1527
+ $out: map.set($out, '#{$key}', $value);
1528
+ }
1529
+
1530
+ @return $out;
1531
+ }
1532
+
1533
+ /// A named theme: `tokens()` inside declares it for
1534
+ /// `data-greese-theme="name"` (set it with `<ThemeProvider>`'s
1535
+ /// `data-greese-theme`, or on any element), and `component()`,
1536
+ /// `motion-defaults()` inside apply only within it.
1537
+ @mixin theme($name) {
1538
+ $checked: _check(
1539
+ meta.type-of($name) == string and string.length('#{$name}') > 0,
1540
+ 'theme() needs a name: letters, digits and `-`.'
1541
+ );
1542
+ $outer: $_theme;
1543
+ $_theme: '#{$name}' !global;
1544
+
1545
+ @content;
1546
+
1547
+ $_theme: $outer !global;
1548
+ }