@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.
- package/LICENSE +21 -0
- package/README.md +55 -2
- package/bin/greese-utilities.mjs +56 -0
- package/dist/_layers.scss +2 -0
- package/dist/_maps.scss +1884 -0
- package/dist/_tokens-base.scss +174 -0
- package/dist/_tokens.scss +2140 -0
- package/dist/base.css +67 -0
- package/dist/colorScheme.d.ts +16 -0
- package/dist/colorScheme.js +13 -0
- package/dist/contrast.d.ts +15 -0
- package/dist/contrast.js +43 -0
- package/dist/css/Accordion.css +9 -0
- package/dist/css/ActionSelect.css +6 -0
- package/dist/css/Alert.css +84 -0
- package/dist/css/AppBar.css +35 -0
- package/dist/css/AspectRatio.css +40 -0
- package/dist/css/AutoLayout.css +508 -0
- package/dist/css/Avatar.css +88 -0
- package/dist/css/Badge.css +66 -0
- package/dist/css/Box.css +640 -0
- package/dist/css/Breadcrumbs.css +57 -0
- package/dist/css/Button.css +275 -0
- package/dist/css/ButtonGroup.css +40 -0
- package/dist/css/Card.css +101 -0
- package/dist/css/Carousel.css +170 -0
- package/dist/css/Center.css +11 -0
- package/dist/css/ChatBubble.css +128 -0
- package/dist/css/Checkbox.css +65 -0
- package/dist/css/Chip.css +124 -0
- package/dist/css/ChoiceGroup.css +57 -0
- package/dist/css/Code.css +29 -0
- package/dist/css/Collapsible.css +126 -0
- package/dist/css/ColorPicker.css +191 -0
- package/dist/css/Combobox.css +90 -0
- package/dist/css/Container.css +30 -0
- package/dist/css/Countdown.css +25 -0
- package/dist/css/DateField.css +15 -0
- package/dist/css/Dialog.css +640 -0
- package/dist/css/Diff.css +103 -0
- package/dist/css/Divider.css +19 -0
- package/dist/css/Dock.css +91 -0
- package/dist/css/Field.css +33 -0
- package/dist/css/Fieldset.css +54 -0
- package/dist/css/FileInput.css +65 -0
- package/dist/css/Footer.css +72 -0
- package/dist/css/Grid.css +998 -0
- package/dist/css/Header.css +89 -0
- package/dist/css/Heading.css +6 -0
- package/dist/css/Icon.css +12 -0
- package/dist/css/IconButton.css +7 -0
- package/dist/css/Image.css +69 -0
- package/dist/css/Indicator.css +41 -0
- package/dist/css/Input.css +115 -0
- package/dist/css/Key.css +42 -0
- package/dist/css/Link.css +72 -0
- package/dist/css/List.css +52 -0
- package/dist/css/Media.css +8 -0
- package/dist/css/Menu.css +161 -0
- package/dist/css/Motion.css +1356 -0
- package/dist/css/NumberField.css +49 -0
- package/dist/css/OtpField.css +90 -0
- package/dist/css/PageTransition.css +991 -0
- package/dist/css/Pagination.css +43 -0
- package/dist/css/Popover.css +93 -0
- package/dist/css/Progress.css +101 -0
- package/dist/css/RadialProgress.css +44 -0
- package/dist/css/Radio.css +41 -0
- package/dist/css/Rating.css +100 -0
- package/dist/css/ScrollSpy.css +6 -0
- package/dist/css/SegmentedControl.css +101 -0
- package/dist/css/Select.css +115 -0
- package/dist/css/Sidebar.css +207 -0
- package/dist/css/Skeleton.css +48 -0
- package/dist/css/Slider.css +75 -0
- package/dist/css/Spacer.css +34 -0
- package/dist/css/SpeedDial.css +165 -0
- package/dist/css/Spinner.css +44 -0
- package/dist/css/Stack.css +528 -0
- package/dist/css/Stat.css +107 -0
- package/dist/css/Status.css +86 -0
- package/dist/css/Steps.css +201 -0
- package/dist/css/Swap.css +49 -0
- package/dist/css/Switch.css +66 -0
- package/dist/css/Table.css +269 -0
- package/dist/css/Tabs.css +119 -0
- package/dist/css/Text.css +60 -0
- package/dist/css/TextRotate.css +147 -0
- package/dist/css/Textarea.css +107 -0
- package/dist/css/ThemeProvider.css +6 -0
- package/dist/css/Tile.css +18 -0
- package/dist/css/Toast.css +110 -0
- package/dist/css/ToggleButton.css +20 -0
- package/dist/css/Tooltip.css +94 -0
- package/dist/css/Type.css +211 -0
- package/dist/css/Visibility.css +78 -0
- package/dist/css/VisuallyHidden.css +15 -0
- package/dist/css/core.css +251 -0
- package/dist/css.d.ts +29 -0
- package/dist/css.js +28 -0
- package/dist/effect-classes.d.ts +8 -0
- package/dist/effect-classes.js +5 -0
- package/dist/effects/css.d.ts +71 -0
- package/dist/effects/css.js +184 -0
- package/dist/effects/definitions.d.ts +555 -0
- package/dist/effects/definitions.js +379 -0
- package/dist/effects/frames.d.ts +18 -0
- package/dist/effects/frames.js +68 -0
- package/dist/effects/sequence.d.ts +33 -0
- package/dist/effects/sequence.js +106 -0
- package/dist/effects.d.ts +30 -0
- package/dist/effects.js +30 -0
- package/dist/greese.css +11919 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/kebab.d.ts +2 -0
- package/dist/kebab.js +2 -0
- package/dist/palette.css +249 -0
- package/dist/palette.d.ts +599 -0
- package/dist/palette.js +334 -0
- package/dist/shapes.d.ts +193 -0
- package/dist/shapes.js +852 -0
- package/dist/storage.d.ts +47 -0
- package/dist/storage.js +118 -0
- package/dist/style-manifest.json +3017 -0
- package/dist/theme.d.ts +71 -0
- package/dist/theme.js +180 -0
- package/dist/tokens.css +253 -0
- package/dist/tokens.d.ts +343 -0
- package/dist/tokens.js +179 -0
- package/dist/typography.d.ts +55 -0
- package/dist/typography.js +144 -0
- package/dist/utilities.css +6975 -0
- package/dist/utilities.d.ts +82 -0
- package/dist/utilities.js +636 -0
- package/dist/utilitiesFiles.d.ts +18 -0
- package/dist/utilitiesFiles.js +26 -0
- package/dist/vite.d.ts +40 -0
- package/dist/vite.js +43 -0
- package/package.json +131 -4
- package/src/components/Accordion/Accordion.scss +9 -0
- package/src/components/ActionSelect/ActionSelect.scss +6 -0
- package/src/components/Alert/Alert.scss +71 -0
- package/src/components/AppBar/AppBar.scss +43 -0
- package/src/components/AspectRatio/AspectRatio.scss +22 -0
- package/src/components/AutoLayout/AutoLayout.scss +90 -0
- package/src/components/Avatar/Avatar.scss +91 -0
- package/src/components/Badge/Badge.scss +52 -0
- package/src/components/Box/Box.scss +63 -0
- package/src/components/Breadcrumbs/Breadcrumbs.scss +59 -0
- package/src/components/Button/Button.scss +231 -0
- package/src/components/ButtonGroup/ButtonGroup.scss +56 -0
- package/src/components/Card/Card.scss +99 -0
- package/src/components/Carousel/Carousel.scss +161 -0
- package/src/components/Center/Center.scss +11 -0
- package/src/components/ChatBubble/ChatBubble.scss +119 -0
- package/src/components/Checkbox/Checkbox.scss +66 -0
- package/src/components/Chip/Chip.scss +119 -0
- package/src/components/ChoiceGroup/ChoiceGroup.scss +69 -0
- package/src/components/Code/Code.scss +28 -0
- package/src/components/Collapsible/Collapsible.scss +136 -0
- package/src/components/ColorPicker/ColorPicker.scss +235 -0
- package/src/components/Combobox/Combobox.scss +83 -0
- package/src/components/Container/Container.scss +28 -0
- package/src/components/Countdown/Countdown.scss +28 -0
- package/src/components/DateField/DateField.scss +19 -0
- package/src/components/Dialog/Dialog.scss +268 -0
- package/src/components/Diff/Diff.scss +125 -0
- package/src/components/Divider/Divider.scss +22 -0
- package/src/components/Dock/Dock.scss +86 -0
- package/src/components/Field/Field.scss +38 -0
- package/src/components/Fieldset/Fieldset.scss +63 -0
- package/src/components/FileInput/FileInput.scss +66 -0
- package/src/components/Footer/Footer.scss +79 -0
- package/src/components/Grid/Grid.scss +86 -0
- package/src/components/Header/Header.scss +111 -0
- package/src/components/Heading/Heading.scss +7 -0
- package/src/components/Icon/Icon.scss +12 -0
- package/src/components/IconButton/IconButton.scss +6 -0
- package/src/components/Image/Image.scss +46 -0
- package/src/components/Indicator/Indicator.scss +48 -0
- package/src/components/Input/Input.scss +58 -0
- package/src/components/Key/Key.scss +48 -0
- package/src/components/Link/Link.scss +50 -0
- package/src/components/List/List.scss +62 -0
- package/src/components/Media/Media.scss +7 -0
- package/src/components/Menu/Menu.scss +91 -0
- package/src/components/Motion/Motion.scss +106 -0
- package/src/components/NumberField/NumberField.scss +44 -0
- package/src/components/OtpField/OtpField.scss +91 -0
- package/src/components/PageTransition/PageTransition.scss +197 -0
- package/src/components/Pagination/Pagination.scss +55 -0
- package/src/components/Popover/Popover.scss +13 -0
- package/src/components/Progress/Progress.scss +126 -0
- package/src/components/RadialProgress/RadialProgress.scss +65 -0
- package/src/components/Radio/Radio.scss +36 -0
- package/src/components/Rating/Rating.scss +121 -0
- package/src/components/ScrollSpy/ScrollSpy.scss +6 -0
- package/src/components/SegmentedControl/SegmentedControl.scss +107 -0
- package/src/components/Select/Select.scss +59 -0
- package/src/components/Sidebar/Sidebar.scss +88 -0
- package/src/components/Skeleton/Skeleton.scss +60 -0
- package/src/components/Slider/Slider.scss +96 -0
- package/src/components/Spacer/Spacer.scss +15 -0
- package/src/components/SpeedDial/SpeedDial.scss +109 -0
- package/src/components/Spinner/Spinner.scss +43 -0
- package/src/components/Stack/Stack.scss +57 -0
- package/src/components/Stat/Stat.scss +96 -0
- package/src/components/Status/Status.scss +60 -0
- package/src/components/Steps/Steps.scss +216 -0
- package/src/components/Swap/Swap.scss +62 -0
- package/src/components/Switch/Switch.scss +63 -0
- package/src/components/Table/Table.scss +183 -0
- package/src/components/Tabs/Tabs.scss +108 -0
- package/src/components/Text/Text.scss +34 -0
- package/src/components/TextRotate/TextRotate.scss +51 -0
- package/src/components/Textarea/Textarea.scss +56 -0
- package/src/components/ThemeProvider/ThemeProvider.scss +7 -0
- package/src/components/Tile/Tile.scss +23 -0
- package/src/components/Toast/Toast.scss +93 -0
- package/src/components/ToggleButton/ToggleButton.scss +23 -0
- package/src/components/Tooltip/Tooltip.scss +21 -0
- package/src/components/Type/Type.scss +35 -0
- package/src/components/Visibility/Visibility.scss +39 -0
- package/src/components/VisuallyHidden/VisuallyHidden.scss +17 -0
- package/src/styles/_breakpoint-vars.scss +16 -0
- package/src/styles/_config.scss +48 -0
- package/src/styles/_controls.scss +88 -0
- package/src/styles/_core.scss +4 -0
- package/src/styles/_effect.scss +51 -0
- package/src/styles/_floating.scss +141 -0
- package/src/styles/_kit.scss +1548 -0
- package/src/styles/_manifest.scss +670 -0
- package/src/styles/_mixins.scss +261 -0
- package/src/styles/_tones.scss +63 -0
- package/src/styles/_type-emit.scss +76 -0
- package/src/styles/_typography-vars.scss +8 -0
- package/src/styles/base.scss +80 -0
- package/src/styles/breakpoints.test.ts +73 -0
- package/src/styles/entryPoints.test.ts +68 -0
- package/src/styles/index.scss +93 -0
- package/src/styles/kit.test.ts +754 -0
- package/src/styles/public/_controls.scss +3 -0
- package/src/styles/public/_floating.scss +3 -0
- package/src/styles/public/_mixins.scss +5 -0
- package/src/styles/public/_tones.scss +4 -0
- package/themes/bevel.css +1077 -0
- package/themes/bevel.scss +1166 -0
- package/themes/candy.css +87 -0
- package/themes/candy.scss +32 -0
- package/themes/depth.css +123 -0
- package/themes/depth.scss +46 -0
- package/themes/extrude.css +219 -0
- package/themes/extrude.scss +255 -0
- package/themes/folio.css +986 -0
- package/themes/folio.scss +1047 -0
- package/themes/forest.css +87 -0
- package/themes/forest.scss +32 -0
- package/themes/gentle.css +66 -0
- package/themes/gentle.scss +39 -0
- package/themes/grape.css +87 -0
- package/themes/grape.scss +32 -0
- package/themes/greese.css +36 -0
- package/themes/greese.scss +21 -0
- package/themes/mono.css +87 -0
- package/themes/mono.scss +32 -0
- package/themes/ocean.css +87 -0
- package/themes/ocean.scss +32 -0
- package/themes/outline.css +127 -0
- package/themes/outline.scss +126 -0
- package/themes/pebble.css +65 -0
- package/themes/pebble.scss +69 -0
- package/themes/pocket.css +137 -0
- package/themes/pocket.scss +45 -0
- package/themes/ridge.css +927 -0
- package/themes/ridge.scss +998 -0
- package/themes/snappy.css +82 -0
- package/themes/snappy.scss +40 -0
- package/themes/soft.css +79 -0
- package/themes/soft.scss +84 -0
- package/themes/sunset.css +87 -0
- package/themes/sunset.scss +32 -0
- package/themes/ticket.css +960 -0
- package/themes/ticket.scss +1031 -0
- package/themes/tilt.css +148 -0
- package/themes/tilt.scss +169 -0
- package/themes/wipe.css +186 -0
- 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
|
+
};
|