@greese/ui 0.0.0-stage → 1.1.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 (288) 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 +991 -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 +11919 -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 +3017 -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 +197 -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/candy.css +87 -0
  250. package/themes/candy.scss +32 -0
  251. package/themes/depth.css +123 -0
  252. package/themes/depth.scss +46 -0
  253. package/themes/extrude.css +219 -0
  254. package/themes/extrude.scss +255 -0
  255. package/themes/folio.css +986 -0
  256. package/themes/folio.scss +1047 -0
  257. package/themes/forest.css +87 -0
  258. package/themes/forest.scss +32 -0
  259. package/themes/gentle.css +66 -0
  260. package/themes/gentle.scss +39 -0
  261. package/themes/grape.css +87 -0
  262. package/themes/grape.scss +32 -0
  263. package/themes/greese.css +36 -0
  264. package/themes/greese.scss +21 -0
  265. package/themes/mono.css +87 -0
  266. package/themes/mono.scss +32 -0
  267. package/themes/ocean.css +87 -0
  268. package/themes/ocean.scss +32 -0
  269. package/themes/outline.css +127 -0
  270. package/themes/outline.scss +126 -0
  271. package/themes/pebble.css +65 -0
  272. package/themes/pebble.scss +69 -0
  273. package/themes/pocket.css +137 -0
  274. package/themes/pocket.scss +45 -0
  275. package/themes/ridge.css +927 -0
  276. package/themes/ridge.scss +998 -0
  277. package/themes/snappy.css +82 -0
  278. package/themes/snappy.scss +40 -0
  279. package/themes/soft.css +79 -0
  280. package/themes/soft.scss +84 -0
  281. package/themes/sunset.css +87 -0
  282. package/themes/sunset.scss +32 -0
  283. package/themes/ticket.css +960 -0
  284. package/themes/ticket.scss +1031 -0
  285. package/themes/tilt.css +148 -0
  286. package/themes/tilt.scss +169 -0
  287. package/themes/wipe.css +186 -0
  288. package/themes/wipe.scss +46 -0
package/dist/shapes.js ADDED
@@ -0,0 +1,852 @@
1
+ /**
2
+ * Shapes (ADR 0024): custom outlines for any element, as plain CSS.
3
+ *
4
+ * A shape is a list of points in the element's box. Each coordinate is a
5
+ * percentage of the box plus pixels plus multiples of named parameters, so a
6
+ * 12px corner stays 12px whatever the element's size. It compiles to:
7
+ *
8
+ * - **one geometry**: a `polygon()` clip on the element, and a ring (its
9
+ * border) on the element's own pseudo-element, computed from the same points;
10
+ * - **animated parameters**: each parameter is a registered custom property
11
+ * (`--greese-shape-<param>`). Hover, press, focus and entrance change the
12
+ * parameters, never `clip-path`, so the clip and its border interpolate
13
+ * together by construction, and transforms (the `greese` prop) move both;
14
+ * - **no runtime**: nothing in the browser, and no extra markup.
15
+ *
16
+ * This module only builds CSS: the Studio and build scripts import it, apps
17
+ * don't (it isn't in the package's main entry).
18
+ */
19
+ const BORDER = '__border';
20
+ const BLEED = '__bleed';
21
+ /** How far the clip opens past the box for a `box` focus ring: outline offset 2px + width 2px + 1px border + spare. */
22
+ const BLEED_PX = 8;
23
+ const FOCUS_RING_PX = 3;
24
+ /** The custom property behind a parameter. */
25
+ export const shapeVar = (param) => param === BORDER
26
+ ? '--greese-shape-border'
27
+ : param === BLEED
28
+ ? '--greese-shape-bleed'
29
+ : `--greese-shape-${param}`;
30
+ const lin = (l) => ({
31
+ pct: l.pct ?? 0,
32
+ px: l.px ?? 0,
33
+ terms: new Map(l.param ? [[l.param, l.k ?? 1]] : []),
34
+ });
35
+ const addTerm = (a, param, k) => {
36
+ const terms = new Map(a.terms);
37
+ terms.set(param, (terms.get(param) ?? 0) + k);
38
+ return { ...a, terms };
39
+ };
40
+ const addLin = (a, b) => b.param ? addTerm(a, b.param, b.k) : { ...a, px: a.px + (b.px ?? 0) * b.k };
41
+ const num = (n) => {
42
+ const r = Math.round(n * 1000) / 1000;
43
+ return Object.is(r, -0) ? '0' : String(r);
44
+ };
45
+ /** A Lin as CSS: `12px`, `100%`, or a `calc()`. */
46
+ const css = (a) => {
47
+ // Signed terms, so `- var(…)` reads as it should.
48
+ const terms = [];
49
+ if (a.pct)
50
+ terms.push([a.pct < 0 ? -1 : 1, `${num(Math.abs(a.pct))}%`]);
51
+ if (a.px)
52
+ terms.push([a.px < 0 ? -1 : 1, `${num(Math.abs(a.px))}px`]);
53
+ for (const [param, k] of a.terms) {
54
+ if (Math.abs(k) < 1e-9)
55
+ continue;
56
+ const size = Math.abs(k);
57
+ const v = `var(${shapeVar(param)})`;
58
+ terms.push([k < 0 ? -1 : 1, Math.abs(size - 1) < 1e-9 ? v : `${num(size)} * ${v}`]);
59
+ }
60
+ if (!terms.length)
61
+ return '0';
62
+ if (terms.length === 1 && terms[0][0] === 1 && !terms[0][1].includes('var('))
63
+ return terms[0][1];
64
+ const [first, ...rest] = terms;
65
+ const head = first[0] < 0 ? `-1 * ${first[1]}` : first[1];
66
+ return `calc(${[head, ...rest.map(([sign, t]) => `${sign < 0 ? '-' : '+'} ${t}`)].join(' ')})`;
67
+ };
68
+ const evaluate = (a, size, params) => {
69
+ let v = (a.pct / 100) * size + a.px;
70
+ for (const [param, k] of a.terms)
71
+ v += k * (params[param] ?? 0);
72
+ return v;
73
+ };
74
+ /** Where geometry is measured, to fix directions and offsets: a box of this size. */
75
+ const REF = { w: 320, h: 180 };
76
+ /** Every parameter at the largest value any state gives it, so no edge collapses. */
77
+ const largest = (def) => {
78
+ const out = {};
79
+ for (const set of [def.params, def.hover, def.active])
80
+ for (const [k, v] of Object.entries(set ?? {}))
81
+ out[k] = Math.max(out[k] ?? 0, Math.abs(v));
82
+ return out;
83
+ };
84
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
85
+ const len = (a) => Math.hypot(a[0], a[1]);
86
+ const unit = (a) => {
87
+ const l = len(a);
88
+ return l < 1e-9 ? [0, 0] : [a[0] / l, a[1] / l];
89
+ };
90
+ const at = (p, params, w = REF.w, h = REF.h) => [
91
+ evaluate(p.x, w, params),
92
+ evaluate(p.y, h, params),
93
+ ];
94
+ /** The nearest point before (`step` -1) or after (+1) `i` that isn't at the same place. */
95
+ const distinct = (pts, i, step) => {
96
+ const n = pts.length;
97
+ for (let j = 1; j < n; j++) {
98
+ const q = pts[(i + step * j + n * n) % n];
99
+ if (len(sub(q, pts[i])) > 1e-6)
100
+ return q;
101
+ }
102
+ return pts[i];
103
+ };
104
+ /** Flattens each rounded corner into an arc of points, exact while its edges keep their directions. */
105
+ const expand = (def) => {
106
+ const base = def.points.map((p) => ({ x: lin(p.x), y: lin(p.y) }));
107
+ const big = largest(def);
108
+ const ref = base.map((p) => at(p, big));
109
+ const out = [];
110
+ def.points.forEach((p, i) => {
111
+ const v = base[i];
112
+ if (p.round === undefined || p.round === 0)
113
+ return out.push(v);
114
+ const u1 = unit(sub(distinct(ref, i, -1), ref[i]));
115
+ const u2 = unit(sub(distinct(ref, i, 1), ref[i]));
116
+ const cos = u1[0] * u2[0] + u1[1] * u2[1];
117
+ const theta = Math.acos(Math.max(-1, Math.min(1, cos)));
118
+ if (theta < 1e-3 || Math.PI - theta < 1e-3)
119
+ return out.push(v);
120
+ // Per unit of radius: the tangent points, and the arc's center, from the vertex.
121
+ const t = 1 / Math.tan(theta / 2);
122
+ const bis = unit([u1[0] + u2[0], u1[1] + u2[1]]);
123
+ const c = [bis[0] / Math.sin(theta / 2), bis[1] / Math.sin(theta / 2)];
124
+ const a1 = Math.atan2(u1[1] * t - c[1], u1[0] * t - c[0]);
125
+ let a2 = Math.atan2(u2[1] * t - c[1], u2[0] * t - c[0]);
126
+ // The short way round.
127
+ while (a2 - a1 > Math.PI)
128
+ a2 -= 2 * Math.PI;
129
+ while (a1 - a2 > Math.PI)
130
+ a2 += 2 * Math.PI;
131
+ const r = typeof p.round === 'string' ? { param: p.round, k: 1 } : { px: p.round, k: 1 };
132
+ const steps = 6;
133
+ for (let s = 0; s <= steps; s++) {
134
+ const ang = a1 + ((a2 - a1) * s) / steps;
135
+ const dx = c[0] + Math.cos(ang);
136
+ const dy = c[1] + Math.sin(ang);
137
+ out.push({
138
+ x: addLin(v.x, { ...r, k: dx }),
139
+ y: addLin(v.y, { ...r, k: dy }),
140
+ });
141
+ }
142
+ });
143
+ return out;
144
+ };
145
+ /**
146
+ * Per point, how far it moves for 1px of inset, measured where every edge
147
+ * has its full length: the miter of the two edges' inward normals.
148
+ */
149
+ const miters = (pts, def) => {
150
+ const ref = pts.map((p) => at(p, largest(def)));
151
+ let area = 0;
152
+ ref.forEach((p, i) => {
153
+ const q = ref[(i + 1) % ref.length];
154
+ area += p[0] * q[1] - q[0] * p[1];
155
+ });
156
+ // Clockwise on screen (y down) has a positive sum; anticlockwise flips the normals.
157
+ const turn = area >= 0 ? 1 : -1;
158
+ return ref.map((p, i) => {
159
+ const e1 = unit(sub(p, distinct(ref, i, -1)));
160
+ const e2 = unit(sub(distinct(ref, i, 1), p));
161
+ const n1 = [-e1[1] * turn, e1[0] * turn];
162
+ const n2 = [-e2[1] * turn, e2[0] * turn];
163
+ const d = 1 + n1[0] * n2[0] + n1[1] * n2[1];
164
+ if (d < 0.25) {
165
+ // A spike: cap the miter instead of letting it run off.
166
+ const b = unit([n1[0] + n2[0], n1[1] + n2[1]]);
167
+ return [b[0] * 2, b[1] * 2];
168
+ }
169
+ return [(n1[0] + n2[0]) / d, (n1[1] + n2[1]) / d];
170
+ });
171
+ };
172
+ const polygon = (pts, fill) => `polygon(${fill ? `${fill}, ` : ''}${pts.map((p) => `${css(p.x)} ${css(p.y)}`).join(', ')})`;
173
+ /** The parameters a shape uses, in a stable order. */
174
+ export const shapeParams = (def) => {
175
+ const names = new Set();
176
+ for (const p of def.points) {
177
+ if (p.x.param)
178
+ names.add(p.x.param);
179
+ if (p.y.param)
180
+ names.add(p.y.param);
181
+ if (typeof p.round === 'string')
182
+ names.add(p.round);
183
+ }
184
+ for (const set of [def.params, def.hover, def.active])
185
+ for (const k of Object.keys(set ?? {}))
186
+ names.add(k);
187
+ return [...names].sort();
188
+ };
189
+ /** True when the shape with every parameter at 0 is its whole box, so `box` focus works. */
190
+ export const opensToBox = (def) => {
191
+ const zero = Object.fromEntries(shapeParams(def).map((k) => [k, 0]));
192
+ const pts = expand(def).map((p) => at(p, zero));
193
+ let area = 0;
194
+ pts.forEach((p, i) => {
195
+ const q = pts[(i + 1) % pts.length];
196
+ area += p[0] * q[1] - q[0] * p[1];
197
+ });
198
+ const inside = pts.every(([x, y]) => x > -0.01 && y > -0.01 && x < REF.w + 0.01 && y < REF.h + 0.01);
199
+ return inside && Math.abs(Math.abs(area / 2) - REF.w * REF.h) < 0.5;
200
+ };
201
+ /**
202
+ * The outline at one size, as numbers: for editors and previews. `inset`
203
+ * gives the border's inner edge instead.
204
+ */
205
+ export const shapePoints = (def, w, h, params = def.params ?? {}, inset = 0) => {
206
+ const pts = expand(def);
207
+ const m = miters(pts, def);
208
+ return pts.map((p, i) => {
209
+ const [x, y] = at(p, params, w, h);
210
+ return [x + m[i][0] * inset, y + m[i][1] * inset];
211
+ });
212
+ };
213
+ /** `@property` rules for the parameters: registered, so they animate. Once per page is enough. */
214
+ export const shapeProperties = (params) => [...new Set([...params, BORDER, BLEED])]
215
+ .sort()
216
+ .map((p) => `@property ${shapeVar(p)} {\n syntax: '<length>';\n inherits: false;\n initial-value: 0px;\n}`)
217
+ .join('\n') +
218
+ // The ring's color is read by the element's own border layer, so it
219
+ // inherits; every shaped element sets its own, so nesting can't leak.
220
+ `\n@property --greese-shape-ring {\n syntax: '<color>';\n inherits: true;\n initial-value: transparent;\n}`;
221
+ const decls = (d, indent = ' ') => Object.entries(d)
222
+ .map(([k, v]) => `${indent}${k}: ${v};`)
223
+ .join('\n');
224
+ const rule = (selector, d) => Object.keys(d).length ? `${selector} {\n${decls(d)}\n}` : '';
225
+ const paramDecls = (set) => Object.fromEntries(Object.entries(set ?? {}).map(([k, v]) => [shapeVar(k), `${num(v)}px`]));
226
+ /** The shape flipped start to end, still clockwise. */
227
+ export const mirrored = (def) => {
228
+ const flip = (l) => ({
229
+ pct: 100 - (l.pct ?? 0),
230
+ px: -(l.px ?? 0),
231
+ ...(l.param && { param: l.param, k: -(l.k ?? 1) }),
232
+ });
233
+ const points = def.points.map((p) => ({ ...p, x: flip(p.x) })).reverse();
234
+ return { ...def, points };
235
+ };
236
+ /** The clip and the ring of a shape, as `clip-path` values. */
237
+ const clips = (def) => {
238
+ const pts = expand(def);
239
+ const m = miters(pts, def);
240
+ // The clip, opened past the box by the bleed (0 except for a box focus ring).
241
+ const outer = pts.map((p, i) => ({
242
+ x: addTerm(p.x, BLEED, -m[i][0]),
243
+ y: addTerm(p.y, BLEED, -m[i][1]),
244
+ }));
245
+ // The ring: the outline, then down a seam to the inner edge, round it
246
+ // backwards and back to the seam, so the polygon closes along the seam
247
+ // (which evenodd cancels). Ending anywhere else would close with a line
248
+ // across the border, cutting a sliver out of it.
249
+ const inner = pts.map((p, i) => ({
250
+ x: addTerm(p.x, BORDER, m[i][0]),
251
+ y: addTerm(p.y, BORDER, m[i][1]),
252
+ }));
253
+ const ring = [...pts, pts[0], inner[0], ...inner.slice(1).reverse(), inner[0]];
254
+ return { outer: polygon(outer), ring: polygon(ring, 'evenodd') };
255
+ };
256
+ // A suffix with `&` anywhere in it is a selector of its own (`&--x:hover`,
257
+ // `.greese-OtpField[data-invalid] &`); any other is appended (`:hover`).
258
+ const join = (selector, suffix) => suffix.includes('&') ? suffix.replace(/&/g, selector) : `${selector}${suffix}`;
259
+ const block = (head, body) => body.trim()
260
+ ? `${head} {\n${body
261
+ .split('\n')
262
+ .map((l) => (l ? ` ${l}` : l))
263
+ .join('\n')}\n}`
264
+ : '';
265
+ const TIMING = 'var(--greese-duration-normal) var(--greese-easing-standard)';
266
+ /**
267
+ * The CSS for a set of shapes and the elements that take them. Each shape's
268
+ * geometry is written once, on every element that takes it, as two custom
269
+ * properties computed on the element itself (`--greese-shape-clip` and
270
+ * `--greese-shape-ring-clip`), so its border layer inherits the ring already
271
+ * resolved with the element's parameters, mid-transition included. The
272
+ * mechanics (the clip, the border layer, transitions) are written once per
273
+ * kind of element. Pair it with `shapeProperties()` once.
274
+ */
275
+ export const shapesCss = (shapes, uses) => {
276
+ const live = uses.filter((u) => shapes[u.shape]);
277
+ if (!live.length)
278
+ return '';
279
+ const out = [];
280
+ // Mechanics, grouped by element kinds that share them.
281
+ const transitionsOf = (u) => {
282
+ const params = shapeParams(shapes[u.shape]);
283
+ return [
284
+ ...(u.transitions ?? []),
285
+ ...[...params.map(shapeVar), shapeVar(BORDER), '--greese-shape-ring'].map((p) => `${p} ${TIMING}`),
286
+ `${shapeVar(BLEED)} 0s linear`,
287
+ ];
288
+ };
289
+ const groups = new Map();
290
+ for (const u of live) {
291
+ const transitions = u.still ? [] : transitionsOf(u);
292
+ const how = [
293
+ u.pseudo,
294
+ u.positioned,
295
+ u.borderColor,
296
+ u.variants,
297
+ u.variantTransitions,
298
+ u.touch,
299
+ u.still,
300
+ u.ring,
301
+ ];
302
+ const key = JSON.stringify({ how, transitions });
303
+ const group = groups.get(key) ?? { uses: [], transitions };
304
+ group.uses.push(u);
305
+ groups.set(key, group);
306
+ }
307
+ for (const { uses: group, transitions } of groups.values()) {
308
+ const u = group[0];
309
+ const sels = group.map((g) => g.selector);
310
+ const list = sels.join(',\n');
311
+ const pseudo = u.pseudo ?? '::after';
312
+ const ring = u.ring !== false;
313
+ out.push(rule(list, ring
314
+ ? {
315
+ '--greese-shape-ring': u.borderColor ?? 'var(--greese-color-border)',
316
+ ...(u.positioned ? {} : { position: 'relative' }),
317
+ // The border layer's z-index stays inside the element.
318
+ isolation: 'isolate',
319
+ // The ring draws the border, in the shape; the box's own stays for layout.
320
+ 'border-color': 'transparent',
321
+ 'clip-path': 'var(--greese-shape-clip) padding-box',
322
+ }
323
+ : { 'clip-path': 'var(--greese-shape-clip) border-box' }));
324
+ if (ring)
325
+ out.push(rule(sels.map((s) => `${s}${pseudo}`).join(',\n'), {
326
+ position: 'absolute',
327
+ inset: '0',
328
+ // The pseudo-element may be the component's touch target (Button's
329
+ // ::after on coarse pointers), centred by a translate and sized past
330
+ // the box: undone, or the ring draws offset across the element.
331
+ 'inline-size': 'auto',
332
+ 'block-size': 'auto',
333
+ translate: 'none',
334
+ 'z-index': '1',
335
+ 'pointer-events': 'none',
336
+ content: "''",
337
+ background: 'var(--greese-shape-ring)',
338
+ 'clip-path': 'var(--greese-shape-ring-clip)',
339
+ }));
340
+ for (const [variant, d] of Object.entries(u.variants ?? {}))
341
+ out.push(rule(sels.map((s) => join(s, variant)).join(',\n'), d));
342
+ if (transitions.length)
343
+ out.push(block('@media (prefers-reduced-motion: no-preference)', [
344
+ rule(list, { transition: transitions.join(',\n ') }),
345
+ ...Object.entries(u.variantTransitions ?? {}).map(([variant, extra]) => rule(sels.map((s) => join(s, variant)).join(',\n'), {
346
+ // After the shape's own, whose last (the bleed) the focus
347
+ // rule's delays count on; the extra ones cycle to 0s delays.
348
+ transition: [...transitions, ...extra].join(',\n '),
349
+ })),
350
+ ].join('\n')));
351
+ if (u.touch)
352
+ out.push(block('@media (pointer: coarse)', rule(list, { 'min-block-size': '44px', 'min-inline-size': '44px' })));
353
+ // Forced colors: the system draws borders and outlines; a shape would hide them.
354
+ out.push(block('@media (forced-colors: active)', [
355
+ rule(list, {
356
+ 'clip-path': 'none',
357
+ ...(ring && { 'border-color': 'CanvasText' }),
358
+ }),
359
+ ring
360
+ ? rule(sels.map((s) => `${s}${pseudo}`).join(',\n'), { display: 'none' })
361
+ : '',
362
+ ]
363
+ .filter(Boolean)
364
+ .join('\n')));
365
+ }
366
+ // Each shape's geometry and parameters, once, for every element that takes it.
367
+ for (const [name, def] of Object.entries(shapes)) {
368
+ const mine = live.filter((u) => u.shape === name);
369
+ if (!mine.length)
370
+ continue;
371
+ const sels = mine.map((u) => u.selector);
372
+ const moving = mine.filter((u) => !u.still);
373
+ const { outer, ring } = clips(def);
374
+ const color = def.border?.color && def.border.color !== 'auto' ? def.border.color : undefined;
375
+ out.push(rule(sels.join(',\n'), {
376
+ ...paramDecls(def.params),
377
+ [shapeVar(BORDER)]: `${num(def.border?.width ?? 1)}px`,
378
+ ...(color && { '--greese-shape-ring': color }),
379
+ '--greese-shape-clip': outer,
380
+ '--greese-shape-ring-clip': ring,
381
+ }));
382
+ const turned = mine
383
+ .filter((u) => u.mirrorOn)
384
+ .map((u) => join(u.selector, u.mirrorOn));
385
+ if (def.mirror || turned.length) {
386
+ const flipped = clips(mirrored(def));
387
+ const as = (c) => ({
388
+ '--greese-shape-clip': c.outer,
389
+ '--greese-shape-ring-clip': c.ring,
390
+ });
391
+ // Mirrored in right-to-left text, and where it faces the other way;
392
+ // facing the other way in right-to-left text, it's back as drawn.
393
+ const mirroredSels = [
394
+ ...(def.mirror ? sels.map((s) => `${s}:dir(rtl)`) : []),
395
+ ...turned,
396
+ ];
397
+ out.push(rule(mirroredSels.join(',\n'), as(flipped)));
398
+ if (def.mirror && turned.length)
399
+ out.push(rule(turned.map((s) => `${s}:dir(rtl)`).join(',\n'), as({ outer, ring })));
400
+ }
401
+ // Every selector a state applies on: the element's own, then each scaled
402
+ // variant (smaller features), which come later so they win.
403
+ const variantsOf = (u) => [
404
+ [u.selector, 1],
405
+ ...Object.entries(u.scale ?? {}).map(([suffix, f]) => [join(u.selector, suffix), f]),
406
+ ];
407
+ const scaled = (set, f) => paramDecls(set && Object.fromEntries(Object.entries(set).map(([k, v]) => [k, v * f])));
408
+ for (const u of mine)
409
+ for (const [sel, f] of variantsOf(u).slice(1))
410
+ out.push(rule(sel, scaled(def.params, f)));
411
+ const stateRules = (set, pick, fallback) => {
412
+ if (!set)
413
+ return '';
414
+ const byFactor = new Map();
415
+ for (const u of moving) {
416
+ const suffix = pick(u);
417
+ if (suffix === false)
418
+ continue;
419
+ for (const [sel, f] of variantsOf(u))
420
+ byFactor.set(f, [...(byFactor.get(f) ?? []), join(sel, suffix ?? fallback)]);
421
+ }
422
+ return [...byFactor]
423
+ .map(([f, list]) => rule(list.join(',\n'), scaled(set, f)))
424
+ .join('\n');
425
+ };
426
+ const hover = stateRules(def.hover, (u) => u.hover, ':hover');
427
+ if (hover)
428
+ out.push(block('@media (hover: hover)', hover));
429
+ const active = stateRules(def.active, (u) => u.active, ':active');
430
+ if (active)
431
+ out.push(active);
432
+ // Focus: the shape opens to its box and the usual focus ring shows round
433
+ // it; or a ring inside the shape; or the element's own, drawn inside.
434
+ const own = def.focus ?? (opensToBox(def) ? 'box' : 'ring');
435
+ const modeOf = (u) => u.focus === 'keep'
436
+ ? 'keep'
437
+ : u.focus === 'ring' && u.ring !== false
438
+ ? 'ring'
439
+ : own === 'ring' && u.ring === false
440
+ ? 'keep'
441
+ : (u.focus ?? own);
442
+ const focusSel = (u) => join(u.selector, u.focusOn ?? ':focus-visible');
443
+ const rings = mine.filter((u) => modeOf(u) === 'ring').map(focusSel);
444
+ if (rings.length)
445
+ out.push(rule(rings.join(',\n'), {
446
+ [shapeVar(BORDER)]: `${FOCUS_RING_PX}px`,
447
+ '--greese-shape-ring': 'var(--greese-color-focus-ring)',
448
+ // The ring is the focus indicator; an outline would be clipped anyway.
449
+ 'outline-color': 'transparent',
450
+ }));
451
+ // The bleed (the clip opening past the box) waits until the shape has
452
+ // opened, so no background shows past its cuts: the last transition.
453
+ const byLength = new Map();
454
+ for (const u of mine.filter((m) => modeOf(m) === 'box')) {
455
+ const n = u.still ? 0 : transitionsOf(u).length;
456
+ byLength.set(n, [...(byLength.get(n) ?? []), focusSel(u)]);
457
+ }
458
+ for (const [n, focusSels] of byLength)
459
+ out.push(rule(focusSels.join(',\n'), {
460
+ ...Object.fromEntries(shapeParams(def).map((p) => [shapeVar(p), '0px'])),
461
+ [shapeVar(BLEED)]: `${BLEED_PX}px`,
462
+ ...(n && {
463
+ 'transition-delay': [
464
+ ...Array(n - 1).fill('0s'),
465
+ 'var(--greese-duration-normal)',
466
+ ].join(', '),
467
+ }),
468
+ }));
469
+ }
470
+ return out.filter(Boolean).join('\n');
471
+ };
472
+ /** One shape on one element: `shapesCss` for a single use. */
473
+ export const shapeCss = (def, target) => shapesCss({ shape: def }, [{ ...target, shape: 'shape' }]);
474
+ const fast = (...properties) => properties.map((p) => `${p} var(--greese-duration-fast) var(--greese-easing-standard)`);
475
+ /** A field: focused while it's used, so focus is a ring inside the shape. */
476
+ const FIELD = {
477
+ hover: false,
478
+ active: false,
479
+ focus: 'ring',
480
+ focusOn: ':has(:focus-visible)',
481
+ borderColor: 'var(--greese-color-border-strong)',
482
+ variants: {
483
+ '&:hover': { '--greese-shape-ring': 'var(--greese-color-fg-muted)' },
484
+ '&[aria-invalid=true], &[data-invalid]': {
485
+ '--greese-shape-ring': 'var(--greese-color-negative)',
486
+ },
487
+ },
488
+ transitions: fast('box-shadow'),
489
+ };
490
+ /**
491
+ * How each component takes a shape: which element, whether a pseudo-element
492
+ * is free for the border, how it shows focus, its own transitions. A
493
+ * component not listed takes the defaults (its root, `::after`, the border
494
+ * token). Clipping removes shadows and anything drawn outside the box, so an
495
+ * entry also says what replaces them.
496
+ */
497
+ export const SHAPE_TARGETS = {
498
+ // ::before draws the loading spinner, so the border takes ::after, the
499
+ // touch target: the clip would cut that off anyway. A shaped button keeps
500
+ // its size on touch screens, as every theme's does (ADR 0024 addendum).
501
+ Button: {
502
+ pseudo: '::after',
503
+ borderColor: 'var(--greese-button-border, transparent)',
504
+ transitions: fast('background-color', 'border-color', 'color'),
505
+ scale: {
506
+ '&.greese-Button--size-sm': 0.7,
507
+ '&.greese-Button--size-xs': 0.5,
508
+ '&.greese-IconButton': 0.5,
509
+ },
510
+ },
511
+ Card: {
512
+ hover: '&.greese-Card--interactive:hover',
513
+ active: '&.greese-Card--interactive:active',
514
+ borderColor: 'transparent',
515
+ variants: {
516
+ '&.greese-Card--outline': { '--greese-shape-ring': 'var(--greese-color-border)' },
517
+ // A clip removes the shadow, so elevation is a stronger edge.
518
+ '&.greese-Card--elevated': {
519
+ '--greese-shape-ring': 'var(--greese-color-border-strong)',
520
+ },
521
+ },
522
+ // Only an interactive card moves on its own (it lifts when hovered), as
523
+ // without a shape: on every card, `translate` would lag the runtime's
524
+ // effects behind their masks (a pocket's window would drift with it).
525
+ variantTransitions: {
526
+ '&.greese-Card--interactive': [
527
+ 'box-shadow var(--greese-duration-normal) var(--greese-easing-standard)',
528
+ 'border-color var(--greese-duration-normal) var(--greese-easing-standard)',
529
+ 'translate var(--greese-duration-normal) var(--greese-easing-standard)',
530
+ ],
531
+ },
532
+ },
533
+ Alert: {
534
+ hover: false,
535
+ active: false,
536
+ borderColor: 'color-mix(in oklab, var(--greese-alert-tone) 30%, transparent)',
537
+ },
538
+ Badge: {
539
+ hover: false,
540
+ active: false,
541
+ borderColor: 'color-mix(in oklab, var(--greese-badge-tone) 35%, transparent)',
542
+ },
543
+ Chip: {
544
+ borderColor: 'var(--greese-color-border)',
545
+ transitions: fast('background-color', 'color'),
546
+ },
547
+ Tile: { hover: false, active: false, borderColor: 'var(--greese-color-border)' },
548
+ // Fields stay focused while they're used: focus is a ring inside the shape.
549
+ Input: FIELD,
550
+ Select: FIELD,
551
+ Textarea: FIELD,
552
+ // A code's boxes are drawn for the one input over them: they never take
553
+ // focus, the box being typed in shows it. Its caret is ::after.
554
+ OtpField: {
555
+ select: '.greese-OtpField-slot',
556
+ pseudo: '::before',
557
+ hover: false,
558
+ active: false,
559
+ focus: 'keep',
560
+ borderColor: 'var(--greese-color-border-strong)',
561
+ variants: {
562
+ '.greese-OtpField[data-invalid] &': {
563
+ '--greese-shape-ring': 'var(--greese-color-negative)',
564
+ },
565
+ '&.greese-OtpField-slot--active': {
566
+ '--greese-shape-ring': 'var(--greese-color-focus-ring)',
567
+ '--greese-shape-border': '2px',
568
+ },
569
+ },
570
+ transitions: fast('box-shadow'),
571
+ },
572
+ // A collapsible's header, the part you press (themes give it a surface).
573
+ Collapsible: {
574
+ select: '.greese-Collapsible-trigger',
575
+ active: false,
576
+ borderColor: 'var(--greese-collapsible-border, var(--greese-color-border))',
577
+ transitions: fast('background-color'),
578
+ },
579
+ SegmentedControl: {
580
+ select: '.greese-SegmentedControl-track',
581
+ hover: false,
582
+ active: false,
583
+ borderColor: 'var(--greese-color-border)',
584
+ },
585
+ // The chosen segment's edge is a hook a color variant sets (Outline draws
586
+ // it in the primary): drawn along the shape, not clipped by it.
587
+ Segment: {
588
+ hover: false,
589
+ active: false,
590
+ focus: 'ring',
591
+ focusOn: ':has(:focus-visible)',
592
+ borderColor: 'transparent',
593
+ variants: {
594
+ '&:has(:checked)': {
595
+ '--greese-shape-ring': 'var(--greese-segment-current-ring, transparent)',
596
+ },
597
+ },
598
+ transitions: fast('background-color', 'color'),
599
+ },
600
+ // The current tab's edge, like the chosen segment's, is a hook a color
601
+ // variant sets (Outline draws it in the primary): drawn along the shape,
602
+ // as thick as the variant's own edge, so a cut corner doesn't cut it.
603
+ Tab: {
604
+ focus: 'keep',
605
+ active: false,
606
+ borderColor: 'transparent',
607
+ variants: {
608
+ "&[aria-selected='true']": {
609
+ '--greese-shape-ring': 'var(--greese-tab-current-ring, transparent)',
610
+ '--greese-shape-border': '1.5px',
611
+ },
612
+ },
613
+ transitions: fast('color', 'background-color', 'box-shadow'),
614
+ },
615
+ // An item's panel takes the shape, not the <details> around it: its header
616
+ // is a Collapsible's, shaped as one.
617
+ AccordionItem: {
618
+ select: '.greese-AccordionItem > .greese-Collapsible-content',
619
+ hover: false,
620
+ active: false,
621
+ borderColor: 'var(--greese-color-border)',
622
+ },
623
+ // A bubble's tail is drawn outside it (::before), so its shape takes the
624
+ // tail in; an end bubble's points the other way. It has no border.
625
+ ChatBubble: {
626
+ select: '.greese-ChatBubble-bubble',
627
+ mirrorOn: '.greese-ChatBubble--end &',
628
+ ring: false,
629
+ still: true,
630
+ },
631
+ // A pulsing status draws its pulse outside the dot: it keeps its own round.
632
+ Status: {
633
+ select: '.greese-Status:not(.greese-Status--pulse)',
634
+ ring: false,
635
+ still: true,
636
+ },
637
+ // In a group, avatars are separated by a ring drawn outside them: those stay round.
638
+ Avatar: {
639
+ select: '.greese-Avatar:not(.greese-AvatarGroup-item > .greese-Avatar)',
640
+ ring: false,
641
+ still: true,
642
+ },
643
+ // Overlays move as the theme says (ADR 0025): a transition here would
644
+ // replace the one that keeps them rendered while they leave, so their
645
+ // shapes stay still.
646
+ Dialog: {
647
+ positioned: true,
648
+ still: true,
649
+ borderColor: 'var(--greese-color-border)',
650
+ },
651
+ // Overlays take a border along the shape, or a clip cuts theirs off where
652
+ // it cuts a corner. A popover scrolls its own content only once it's taller
653
+ // than the screen; then its border layer scrolls with it. A menu scrolls a
654
+ // viewport inside it, and a tooltip doesn't scroll.
655
+ Popover: { positioned: true, still: true, borderColor: 'var(--greese-color-border)' },
656
+ Menu: { positioned: true, still: true, borderColor: 'var(--greese-color-border)' },
657
+ // No border of its own: a color variant can give it one (the hook).
658
+ Tooltip: {
659
+ positioned: true,
660
+ still: true,
661
+ borderColor: 'var(--greese-tooltip-border, transparent)',
662
+ },
663
+ // A clip removes the toast's shadow and its tone's thick start edge (a
664
+ // border): the edge is drawn inside instead, and the border leans to it.
665
+ Toast: {
666
+ still: true,
667
+ borderColor: 'color-mix(in oklab, var(--greese-toast-edge) 45%, var(--greese-color-border-strong))',
668
+ variants: {
669
+ '&': { 'box-shadow': 'inset 3px 0 0 var(--greese-toast-edge)' },
670
+ '&:dir(rtl)': { 'box-shadow': 'inset -3px 0 0 var(--greese-toast-edge)' },
671
+ },
672
+ },
673
+ };
674
+ /** The element a component's shape goes on. */
675
+ export const shapeSelector = (component) => SHAPE_TARGETS[component]?.select ?? `.greese-${component}`;
676
+ /** The class that gives any element a shape: `greese-shape-<name>`. */
677
+ export const shapeClass = (name) => `greese-shape-${name}`;
678
+ /** A component's target for a shape, or the default for an element with the shape's class. */
679
+ export const shapeTarget = (component) => (component && SHAPE_TARGETS[component]) || {};
680
+ // ---------------------------------------------------------------------------
681
+ // Presets: starting points for the Studio and the shape themes.
682
+ // ---------------------------------------------------------------------------
683
+ const P = (x, y, round) => round === undefined ? { x, y } : { x, y, round };
684
+ const end = (px = 0, param, k = -1) => ({ pct: 100, px, param, k });
685
+ const start = (px = 0, param, k = 1) => ({ pct: 0, px, param, k });
686
+ const mid = (param, k = 1) => ({ pct: 50, param, k });
687
+ /** The box itself. */
688
+ export const rectangle = () => ({
689
+ points: [P(start(), start()), P(end(), start()), P(end(), end()), P(start(), end())],
690
+ });
691
+ /** Corners cut on the diagonal: `a` top-start and bottom-end, `b` the other two. */
692
+ export const bevel = (a = 12, b = 0) => ({
693
+ points: [
694
+ P(start(0, 'a'), start()),
695
+ P(end(0, 'b'), start()),
696
+ P(end(), start(0, 'b')),
697
+ P(end(), end(0, 'a')),
698
+ P(end(0, 'a'), end()),
699
+ P(start(0, 'b'), end()),
700
+ P(start(), end(0, 'b')),
701
+ P(start(), start(0, 'a')),
702
+ ],
703
+ params: { a, b },
704
+ });
705
+ /** Square notches out of every corner. */
706
+ export const notch = (n = 10) => ({
707
+ points: [
708
+ P(start(0, 'n'), start()),
709
+ P(end(0, 'n'), start()),
710
+ P(end(0, 'n'), start(0, 'n')),
711
+ P(end(), start(0, 'n')),
712
+ P(end(), end(0, 'n')),
713
+ P(end(0, 'n'), end(0, 'n')),
714
+ P(end(0, 'n'), end()),
715
+ P(start(0, 'n'), end()),
716
+ P(start(0, 'n'), end(0, 'n')),
717
+ P(start(), end(0, 'n')),
718
+ P(start(), start(0, 'n')),
719
+ P(start(0, 'n'), start(0, 'n')),
720
+ ],
721
+ params: { n },
722
+ });
723
+ /** A tag: square at the start, pointed at the end by `t`. */
724
+ export const tag = (t = 12) => ({
725
+ points: [
726
+ P(start(), start()),
727
+ P(end(0, 't'), start()),
728
+ P(end(), mid()),
729
+ P(end(0, 't'), end()),
730
+ P(start(), end()),
731
+ ],
732
+ params: { t },
733
+ mirror: true,
734
+ });
735
+ /** Pointed at both ends: a hexagon, `h` deep. */
736
+ export const hexagon = (h = 10) => ({
737
+ points: [
738
+ P(start(0, 'h'), start()),
739
+ P(end(0, 'h'), start()),
740
+ P(end(), mid()),
741
+ P(end(0, 'h'), end()),
742
+ P(start(0, 'h'), end()),
743
+ P(start(), mid()),
744
+ ],
745
+ params: { h },
746
+ });
747
+ /** Leaning like italic type: top edge shifted by `s` toward the end. */
748
+ export const slant = (s = 8) => ({
749
+ points: [
750
+ P(start(0, 's'), start()),
751
+ P(end(), start()),
752
+ P(end(0, 's'), end()),
753
+ P(start(), end()),
754
+ ],
755
+ params: { s },
756
+ mirror: true,
757
+ });
758
+ /** The top-end corner folded down by `f`. */
759
+ export const dogEar = (f = 18) => ({
760
+ points: [
761
+ P(start(), start()),
762
+ P(end(0, 'f'), start()),
763
+ P(end(), start(0, 'f')),
764
+ P(end(), end()),
765
+ P(start(), end()),
766
+ ],
767
+ params: { f },
768
+ mirror: true,
769
+ });
770
+ /** A ticket: a half-round notch `r` deep in each side, halfway down. */
771
+ export const ticket = (r = 12) => {
772
+ // Each notch is a half circle flattened into points around (side, 50%): k·r from it.
773
+ const arc = (side) => Array.from({ length: 9 }, (_, i) => {
774
+ const a = (Math.PI * i) / 8;
775
+ // Down the end side (top to bottom), up the start side.
776
+ const dy = side === 'end' ? -Math.cos(a) : Math.cos(a);
777
+ const dx = Math.sin(a);
778
+ return P(side === 'end' ? { pct: 100, param: 'r', k: -dx } : { pct: 0, param: 'r', k: dx }, { pct: 50, param: 'r', k: dy });
779
+ });
780
+ return {
781
+ points: [
782
+ P(start(), start()),
783
+ P(end(), start()),
784
+ ...arc('end'),
785
+ P(end(), end()),
786
+ P(start(), end()),
787
+ ...arc('start'),
788
+ ],
789
+ params: { r },
790
+ };
791
+ };
792
+ /** Top corners cut by `c`: a tab, or a folder's. */
793
+ export const tabTop = (c = 8) => ({
794
+ points: [
795
+ P(start(0, 'c'), start()),
796
+ P(end(0, 'c'), start()),
797
+ P(end(), start(0, 'c')),
798
+ P(end(), end()),
799
+ P(start(), end()),
800
+ P(start(), start(0, 'c')),
801
+ ],
802
+ params: { c },
803
+ });
804
+ /** A zigzag hem along the bottom edge: `teeth` teeth, `z` deep. */
805
+ export const zigzag = (z = 6, teeth = 16) => ({
806
+ points: [
807
+ P(start(), start()),
808
+ P(end(), start()),
809
+ P(end(), end(0, 'z')),
810
+ ...Array.from({ length: teeth * 2 - 1 }, (_, i) => {
811
+ const k = teeth * 2 - 1 - i;
812
+ return P({ pct: (100 * k) / (teeth * 2) }, k % 2 ? end() : end(0, 'z'));
813
+ }),
814
+ P(start(), end(0, 'z')),
815
+ ],
816
+ params: { z },
817
+ });
818
+ /** A diamond: the midpoints of the box's sides, for marks (avatars, status dots). */
819
+ export const diamond = () => ({
820
+ points: [
821
+ P({ pct: 50 }, { pct: 0 }),
822
+ P({ pct: 100 }, { pct: 50 }),
823
+ P({ pct: 50 }, { pct: 100 }),
824
+ P({ pct: 0 }, { pct: 50 }),
825
+ ],
826
+ });
827
+ /** A shield: free-form, in percentages, rounded at the top corners. */
828
+ export const shield = () => ({
829
+ points: [
830
+ P({ pct: 0 }, { pct: 0 }, 'r'),
831
+ P({ pct: 100 }, { pct: 0 }, 'r'),
832
+ P({ pct: 100 }, { pct: 55 }),
833
+ P({ pct: 50 }, { pct: 100 }),
834
+ P({ pct: 0 }, { pct: 55 }),
835
+ ],
836
+ params: { r: 10 },
837
+ });
838
+ /** The presets by name, for pickers. */
839
+ export const SHAPE_PRESETS = {
840
+ rectangle,
841
+ bevel: () => bevel(),
842
+ notch: () => notch(),
843
+ tag: () => tag(),
844
+ hexagon: () => hexagon(),
845
+ slant: () => slant(),
846
+ 'dog-ear': () => dogEar(),
847
+ ticket: () => ticket(),
848
+ 'tab-top': () => tabTop(),
849
+ zigzag: () => zigzag(),
850
+ diamond,
851
+ shield,
852
+ };