@teamofsilicons/silicon-ui 0.2.0 → 0.4.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/README.md +4 -3
- package/check.mjs +14 -5
- package/components/activity-heatmap/activity-heatmap.module.css +4 -4
- package/components/alert/alert.module.css +1 -1
- package/components/animated-counter/animated-counter.js +1 -1
- package/components/announcement-bar/announcement-bar.module.css +6 -6
- package/components/avatar/avatar.d.ts +2 -2
- package/components/avatar/avatar.js +1 -1
- package/components/avatar/avatar.module.css +5 -2
- package/components/avatar-group/avatar-group.module.css +4 -4
- package/components/badge/badge.module.css +4 -4
- package/components/bar-chart/bar-chart.js +1 -1
- package/components/bar-chart/bar-chart.module.css +4 -4
- package/components/billing-toggle/billing-toggle.js +2 -2
- package/components/billing-toggle/billing-toggle.module.css +10 -10
- package/components/bottom-sheet/bottom-sheet.js +1 -1
- package/components/bottom-sheet/bottom-sheet.module.css +4 -5
- package/components/breadcrumb/breadcrumb.d.ts +8 -2
- package/components/breadcrumb/breadcrumb.js +21 -9
- package/components/breadcrumb/breadcrumb.module.css +7 -3
- package/components/brush-chart/brush-chart.module.css +9 -9
- package/components/button/button.d.ts +7 -2
- package/components/button/button.js +3 -1
- package/components/button/button.module.css +10 -1
- package/components/button-group/button-group.module.css +7 -7
- package/components/calendar/calendar.js +15 -5
- package/components/calendar/calendar.module.css +13 -10
- package/components/card/card.js +4 -4
- package/components/card/card.module.css +2 -2
- package/components/card-stack/card-stack.js +2 -2
- package/components/card-stack/card-stack.module.css +2 -3
- package/components/carousel/carousel.module.css +3 -3
- package/components/chat-thread/chat-thread.module.css +41 -41
- package/components/checkbox/checkbox.module.css +1 -1
- package/components/chip-group/chip-group.js +1 -1
- package/components/chip-group/chip-group.module.css +2 -2
- package/components/code-block/code-block.module.css +7 -14
- package/components/color-picker/color-picker.module.css +20 -21
- package/components/combobox/combobox.js +5 -1
- package/components/combobox/combobox.module.css +2 -2
- package/components/command-palette/command-palette.js +18 -11
- package/components/command-palette/command-palette.module.css +18 -18
- package/components/comment-thread/comment-thread.module.css +35 -35
- package/components/confirm-morph/confirm-morph.js +5 -5
- package/components/confirm-morph/confirm-morph.module.css +9 -9
- package/components/context-menu/context-menu.js +10 -28
- package/components/context-menu/context-menu.module.css +3 -3
- package/components/copy-button/copy-button.js +4 -4
- package/components/countdown/countdown.module.css +4 -4
- package/components/date-picker/date-picker.js +3 -3
- package/components/date-picker/date-picker.module.css +7 -7
- package/components/date-range-picker/date-range-picker.js +13 -10
- package/components/date-range-picker/date-range-picker.module.css +23 -22
- package/components/dialog/dialog.module.css +2 -2
- package/components/donut-chart/donut-chart.module.css +5 -5
- package/components/drawer/drawer.module.css +7 -7
- package/components/dropdown-menu/dropdown-menu.d.ts +71 -1
- package/components/dropdown-menu/dropdown-menu.js +100 -6
- package/components/dropdown-menu/dropdown-menu.module.css +15 -5
- package/components/elastic-slider/elastic-slider.js +2 -2
- package/components/elastic-slider/elastic-slider.module.css +4 -4
- package/components/expandable-card/expandable-card.js +1 -1
- package/components/expandable-card/expandable-card.module.css +1 -1
- package/components/expanding-button-group/expanding-button-group.module.css +3 -3
- package/components/expanding-search/expanding-search.js +3 -3
- package/components/expanding-search/expanding-search.module.css +9 -9
- package/components/file-dropzone/file-dropzone.js +3 -3
- package/components/file-dropzone/file-dropzone.module.css +21 -21
- package/components/file-upload/file-upload.module.css +7 -7
- package/components/filter-toolbar/filter-toolbar.js +7 -3
- package/components/filter-toolbar/filter-toolbar.module.css +9 -9
- package/components/floating-button-group/floating-button-group.module.css +7 -7
- package/components/gauge/gauge.js +1 -1
- package/components/gauge/gauge.module.css +2 -2
- package/components/hover-card/hover-card.js +2 -2
- package/components/hover-card/hover-card.module.css +2 -2
- package/components/image-compare/image-compare.module.css +4 -4
- package/components/inline-edit/inline-edit.module.css +12 -12
- package/components/json-viewer/json-viewer.module.css +12 -12
- package/components/line-chart/line-chart.js +1 -1
- package/components/line-chart/line-chart.module.css +8 -8
- package/components/mention-input/mention-input.js +4 -2
- package/components/mention-input/mention-input.module.css +8 -8
- package/components/metric-card/metric-card.module.css +1 -1
- package/components/money-input/money-input.js +6 -3
- package/components/money-input/money-input.module.css +10 -10
- package/components/morph-select/morph-select.js +7 -5
- package/components/morph-select/morph-select.module.css +13 -13
- package/components/multi-select/multi-select.js +4 -2
- package/components/multi-select/multi-select.module.css +6 -6
- package/components/nav-list/nav-list.d.ts +64 -0
- package/components/nav-list/nav-list.js +96 -0
- package/components/nav-list/nav-list.module.css +25 -0
- package/components/notification-center/notification-center.module.css +27 -27
- package/components/number-field/number-field.js +3 -3
- package/components/number-field/number-field.module.css +3 -3
- package/components/otp-input/otp-input.module.css +4 -4
- package/components/pagination/pagination.module.css +1 -1
- package/components/password-field/password-field.module.css +2 -2
- package/components/password-strength/password-strength.module.css +8 -8
- package/components/phone-input/phone-input.js +9 -7
- package/components/phone-input/phone-input.module.css +19 -19
- package/components/popover/popover.module.css +2 -2
- package/components/progress/progress.module.css +1 -1
- package/components/radio-cards/radio-cards.module.css +8 -8
- package/components/radio-group/radio-group.js +4 -1
- package/components/radio-group/radio-group.module.css +4 -3
- package/components/resizable-panels/resizable-panels.module.css +5 -5
- package/components/rich-text-editor/rich-text-editor.js +3 -3
- package/components/rich-text-editor/rich-text-editor.module.css +12 -12
- package/components/ridgeline/ridgeline.module.css +7 -7
- package/components/scroll-area/scroll-area.module.css +8 -8
- package/components/segmented-control/segmented-control.d.ts +3 -1
- package/components/segmented-control/segmented-control.js +5 -2
- package/components/segmented-control/segmented-control.module.css +9 -4
- package/components/select/select.module.css +5 -5
- package/components/shortcut-recorder/shortcut-recorder.js +5 -5
- package/components/shortcut-recorder/shortcut-recorder.module.css +16 -16
- package/components/signature-pad/signature-pad.module.css +11 -11
- package/components/slider/slider.module.css +4 -4
- package/components/slope-chart/slope-chart.module.css +7 -7
- package/components/slot-text/slot-text.js +2 -1
- package/components/sortable-data-table/sortable-data-table.module.css +4 -4
- package/components/sparkline/sparkline.js +1 -1
- package/components/split-button/split-button.module.css +6 -6
- package/components/stepper/stepper.module.css +3 -3
- package/components/streamgraph/streamgraph.module.css +7 -7
- package/components/swipe-actions/swipe-actions.js +2 -2
- package/components/swipe-actions/swipe-actions.module.css +5 -5
- package/components/switch/switch.js +2 -2
- package/components/switch/switch.module.css +1 -1
- package/components/tabs/tabs.d.ts +9 -2
- package/components/tabs/tabs.js +11 -5
- package/components/tabs/tabs.module.css +10 -3
- package/components/tag-input/tag-input.module.css +4 -4
- package/components/text-reveal/text-reveal.js +1 -1
- package/components/time-picker/time-picker.js +4 -2
- package/components/time-picker/time-picker.module.css +3 -3
- package/components/timeline/timeline.js +4 -4
- package/components/timeline/timeline.module.css +3 -3
- package/components/toast/toast.js +1 -1
- package/components/toast/toast.module.css +5 -5
- package/components/toast-stack/toast-stack.js +1 -1
- package/components/toast-stack/toast-stack.module.css +5 -5
- package/components/tooltip/tooltip.module.css +1 -1
- package/components/tree-view/tree-view.js +16 -12
- package/components/tree-view/tree-view.module.css +10 -8
- package/components/treemap/treemap.module.css +13 -13
- package/components/usage-meter/usage-meter.js +1 -1
- package/components/usage-meter/usage-meter.module.css +5 -5
- package/components/user-menu/user-menu.js +16 -14
- package/components/user-menu/user-menu.module.css +22 -22
- package/components/waffle-chart/waffle-chart.module.css +7 -7
- package/components.json +416 -1
- package/eslint.js +35 -3
- package/lib/fluid-hover.d.ts +83 -0
- package/lib/fluid-hover.js +184 -0
- package/lib/menu-highlight.d.ts +24 -11
- package/lib/menu-highlight.js +29 -28
- package/lib/motion-tokens.d.ts +76 -18
- package/lib/motion-tokens.js +45 -10
- package/package.json +1 -1
- package/styles.css +50 -7
package/README.md
CHANGED
|
@@ -40,8 +40,9 @@ components you use. Theme, accent and corners are set on `<html>`:
|
|
|
40
40
|
`data-theme` is `light` or `dark`. `data-accent` is `neutral`, `violet`, `blue`, `green`, `amber`, `orange`,
|
|
41
41
|
`coral` or `rose`. `data-corners` is `squircle` or `round`.
|
|
42
42
|
|
|
43
|
-
Fonts: the tokens use `--font-geist` and `--font-
|
|
44
|
-
Geist Variable,
|
|
43
|
+
Fonts: the tokens use `--font-geist` and `--font-hanken-grotesk` when set (for example by `next/font`), and fall back
|
|
44
|
+
to Geist Variable, Hanken Grotesk Variable (the names `@fontsource-variable/geist` and
|
|
45
|
+
`@fontsource-variable/hanken-grotesk` register), then sans-serif. Load the fonts in the app.
|
|
45
46
|
|
|
46
47
|
## Links and images
|
|
47
48
|
|
|
@@ -71,7 +72,7 @@ export default [...yourConfig, ...siliconUi()];
|
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
```sh
|
|
74
|
-
npx silicon-ui-check # raw spacing, radius, colour
|
|
75
|
+
npx silicon-ui-check # raw spacing, radius, colour, font size or z-index in CSS modules fails
|
|
75
76
|
npx silicon-ui-check --update-baseline # tolerate today's legacy values; only new drift fails
|
|
76
77
|
```
|
|
77
78
|
|
package/check.mjs
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* silicon-ui-check: keeps an app's CSS on Silicon UI's tokens.
|
|
4
4
|
*
|
|
5
|
-
* Fails a CSS file that sets spacing, a corner radius, a colour
|
|
5
|
+
* Fails a CSS file that sets spacing, a corner radius, a colour, a font size or a stacking layer with a raw value
|
|
6
|
+
* instead of a token:
|
|
6
7
|
*
|
|
7
8
|
* spacing padding, margin, gap, inset, top/right/bottom/left, scroll-margin/padding → var(--space-*)
|
|
8
9
|
* radius border-radius and its corners → var(--radius-*)
|
|
9
10
|
* color hex, rgb(), hsl(), oklch(), oklab(), lab(), lch(), color() → var(--…)
|
|
10
11
|
* font-size px or rem → var(--text-*)
|
|
12
|
+
* z-index 10 and above (or -10 and below) → var(--layer-*)
|
|
11
13
|
*
|
|
12
14
|
* Allowed everywhere: 0, auto, inherit and other keywords, anything inside var() (including fallbacks), and
|
|
13
|
-
* font-relative or layout-relative units (em, %, ch, vw, vh, cq*).
|
|
15
|
+
* font-relative or layout-relative units (em, %, ch, vw, vh, cq*). 1px and -1px are allowed in spacing: they are
|
|
16
|
+
* hairline alignments against a border, not space. z-index from -9 to 9 is a local order inside a component. Mask properties are not colours (only their alpha is used). Custom property definitions (--x: …) are not
|
|
14
17
|
* checked: tokens are defined that way, in the files you list under `ignore`. The visually hidden block (.srOnly,
|
|
15
18
|
* .visuallyHidden, .sr-only) and @keyframes are skipped.
|
|
16
19
|
*
|
|
@@ -43,10 +46,11 @@ const LENGTH = /(?<![\w.#-])(-?(?:\d*\.)?\d+)(px|rem)\b/g;
|
|
|
43
46
|
const COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\(/i;
|
|
44
47
|
const HIDDEN = /\.(?:srOnly|visuallyHidden|sr-only|visually-hidden)\b/;
|
|
45
48
|
const FIX = {
|
|
46
|
-
spacing: "use a spacing token: var(--space-tight|related|group|inset|comfort|section|block|page|stage|screen)",
|
|
49
|
+
spacing: "use a spacing token: var(--space-hair|tight|related|group|inset|comfort|section|block|page|stage|screen)",
|
|
47
50
|
radius: "use a radius token: var(--radius-detail|control|container|overlay|full)",
|
|
48
51
|
color: "use a colour token: var(--foreground), var(--text-secondary), var(--border), var(--accent) …",
|
|
49
52
|
"font-size": "use a type token: var(--text-xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)",
|
|
53
|
+
layer: "use a layer token: var(--layer-raised|sticky|overlay|dialog|popover|dropdown|toast|tooltip); -9 to 9 is fine for order inside a component",
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
/** Removes every var(...) (with nested parentheses), so tokens and their fallbacks are never flagged. */
|
|
@@ -102,10 +106,15 @@ export function checkCss(css) {
|
|
|
102
106
|
const bare = withoutVars(value);
|
|
103
107
|
for (const [rule, pattern] of Object.entries(RULES)) {
|
|
104
108
|
if (!pattern.test(property)) continue;
|
|
105
|
-
const
|
|
109
|
+
const hairline = match => rule === "spacing" && match[2] === "px" && Math.abs(Number(match[1])) === 1;
|
|
110
|
+
const raw = [...bare.matchAll(LENGTH)].filter(match => Number(match[1]) !== 0 && !hairline(match)).map(match => match[0]);
|
|
106
111
|
if (raw.length) problems.push({ rule, line, property, value, raw: raw.join(", ") });
|
|
107
112
|
}
|
|
108
|
-
|
|
113
|
+
if (property === "z-index") {
|
|
114
|
+
const raw = [...bare.matchAll(/(?<![\w.-])-?\d+(?![\w.])/g)].map(match => match[0]).filter(number => Math.abs(Number(number)) >= 10);
|
|
115
|
+
if (raw.length) problems.push({ rule: "layer", line, property, value, raw: raw.join(", ") });
|
|
116
|
+
}
|
|
117
|
+
const color = /^(?:-webkit-)?mask/.test(property) ? null : bare.match(COLOR);
|
|
109
118
|
if (color) problems.push({ rule: "color", line, property, value, raw: color[0] });
|
|
110
119
|
}
|
|
111
120
|
return problems;
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
.rise { position: relative; display: inline-flex; }
|
|
14
14
|
|
|
15
15
|
/* The scroller starts at its right edge on narrow screens (the most recent weeks) without script, through its writing direction. */
|
|
16
|
-
.scroller { direction: rtl; overflow-x: auto; overflow-y: hidden; margin: -
|
|
16
|
+
.scroller { direction: rtl; overflow-x: auto; overflow-y: hidden; margin: calc(var(--space-related) * -1); padding: var(--space-related); overscroll-behavior-x: contain; scrollbar-color: var(--border-strong) transparent; scrollbar-width: thin; }
|
|
17
17
|
.canvas { --gap: 3px; --label: 30px; --cell: clamp(9px, calc((100cqi - var(--label) - var(--gap) * 53) / 53), 15px); --step: calc(var(--cell) + var(--gap)); direction: ltr; display: grid; width: max-content; grid-template-columns: var(--label) auto; grid-template-rows: auto auto; column-gap: var(--gap); row-gap: var(--space-related); }
|
|
18
18
|
.months { position: relative; grid-row: 1; grid-column: 2; height: 1rem; color: var(--text-muted); font-size: var(--text-xs); line-height: 1rem; }
|
|
19
19
|
/* Month labels glide to their new week when a range starts on a different weekday. */
|
|
20
20
|
.month { position: absolute; top: 0; left: 0; white-space: nowrap; transform: translateX(calc(var(--col) * var(--step))); transition: transform var(--duration-spring) var(--ease-spring); }
|
|
21
21
|
/* Weekday labels stay pinned while a narrow grid scrolls under them. Set --heatmap-surface when the heatmap sits on another surface. */
|
|
22
|
-
.weekdays { position: sticky; z-index: 1; left: -
|
|
22
|
+
.weekdays { position: sticky; z-index: 1; left: calc(var(--space-related) * -1); display: grid; grid-row: 1 / 3; grid-column: 1; grid-template-rows: repeat(7, var(--cell)); align-content: end; row-gap: var(--gap); margin: 0 calc(var(--gap) * -1) 0 calc(var(--space-related) * -1); padding: 0 var(--gap) 0 var(--space-related); background: var(--heatmap-surface, var(--surface)); color: var(--text-muted); font-size: var(--text-xs); }
|
|
23
23
|
.weekday { display: flex; height: var(--cell); align-items: center; white-space: nowrap; line-height: 1; }
|
|
24
24
|
.plot { position: relative; grid-row: 2; grid-column: 2; }
|
|
25
25
|
.grid { display: grid; gap: var(--gap); outline: none; }
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
@media (hover: hover) and (pointer: fine) { .cell:not([data-empty]):hover { outline-color: color-mix(in oklab, var(--foreground) 45%, transparent); } }
|
|
43
43
|
|
|
44
44
|
/* The selection ring glides between days, and follows its day when the range changes. */
|
|
45
|
-
.ring { position: absolute; top: -
|
|
45
|
+
.ring { position: absolute; top: calc(var(--space-tight) * -1); left: calc(var(--space-tight) * -1); width: calc(var(--cell) + 6px); height: calc(var(--cell) + 6px); border: 1.5px solid var(--foreground); border-radius: calc(var(--cell) * .28 + var(--space-tight)); opacity: 0; pointer-events: none; transform: translate(calc(var(--col) * var(--step)), calc(var(--row) * var(--step))); transition: transform var(--duration-spring) var(--ease-spring), opacity var(--duration-fast) var(--ease-standard); }
|
|
46
46
|
.ring[data-shown] { opacity: 1; }
|
|
47
47
|
|
|
48
48
|
.legend { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: var(--space-group); color: var(--text-muted); font-size: var(--text-xs); }
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
/* One bubble for the whole grid. The outer layer glides between cells; the bubble fades and scales from its tail. */
|
|
67
67
|
.tip { position: absolute; z-index: 5; top: 0; left: 0; width: 0; height: 0; pointer-events: none; }
|
|
68
|
-
.bubble { position: absolute; bottom:
|
|
68
|
+
.bubble { position: absolute; bottom: var(--space-related); left: 0; display: block; padding: var(--space-related) var(--space-group) var(--space-related); border: 1px solid color-mix(in oklab, var(--background) 14%, var(--foreground)); border-radius: var(--radius-control); background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); transform-origin: 50% 100%; will-change: transform, opacity; }
|
|
69
69
|
.tipBody { position: relative; display: block; overflow: clip; overflow-clip-margin: 8px; }
|
|
70
70
|
.tipMeasure { position: absolute; top: 0; left: 0; display: grid; width: max-content; visibility: hidden; }
|
|
71
71
|
.tipLines { display: grid; width: max-content; }
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
.line { display: block; }
|
|
10
10
|
.description { position: relative; margin: var(--space-tight) 0 0; color: var(--text-secondary); font-size: var(--text-xs); }
|
|
11
11
|
.info .icon { color: var(--accent); } .success .icon { color: var(--success); } .warning .icon { color: var(--warning); } .danger .icon { color: var(--danger); }
|
|
12
|
-
.dismiss { display: grid; width: 28px; height: 28px; flex: 0 0 auto; place-items: center; margin: -
|
|
12
|
+
.dismiss { display: grid; width: 28px; height: 28px; flex: 0 0 auto; place-items: center; margin: calc(var(--space-tight) * -1) calc(var(--space-related) * -1) calc(var(--space-tight) * -1) 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
|
|
13
13
|
.dismiss:active { transform: scale(.96); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
|
|
14
14
|
@media (hover: hover) and (pointer: fine) { .dismiss:hover { background: var(--surface-muted); color: var(--foreground); } }
|
|
15
15
|
@media (prefers-reduced-motion: reduce) { .dismiss { transition: none; } .dismiss:active { transform: none; } }
|
|
@@ -7,7 +7,7 @@ import styles from "./animated-counter.module.css";
|
|
|
7
7
|
const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
8
8
|
const rise = { hidden: { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } } };
|
|
9
9
|
const fade = { hidden: { opacity: 0, y: 0, filter: "blur(0px)" }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } } };
|
|
10
|
-
const reveal = { ...motionTokens.spring.smooth, visualDuration: motionTokens.duration.
|
|
10
|
+
const reveal = { ...motionTokens.spring.smooth, visualDuration: motionTokens.duration.expressive };
|
|
11
11
|
/** Split a formatted number into columns keyed by place value, so 999 → 1,000 keeps the ones column the ones column. */
|
|
12
12
|
function partsFor(value, decimals, locale) {
|
|
13
13
|
const parts = new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, numberingSystem: "latn" }).formatToParts(value);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.collapse { overflow: hidden; container-type: inline-size; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
3
3
|
|
|
4
4
|
/* The side column mirrors the controls, so the message stays centered on the page rather than in the leftover space. */
|
|
5
|
-
.bar { --icon: 32px; --control-gap: 2px; --muted: var(--text-secondary); display: grid; grid-template-columns: calc(var(--controls, 0) * var(--icon) + max(var(--controls, 0) - 1, 0) * var(--control-gap)) minmax(0, 1fr) auto; align-items: center; gap: var(--space-group); padding:
|
|
5
|
+
.bar { --icon: 32px; --control-gap: 2px; --muted: var(--text-secondary); display: grid; grid-template-columns: calc(var(--controls, 0) * var(--icon) + max(var(--controls, 0) - 1, 0) * var(--control-gap)) minmax(0, 1fr) auto; align-items: center; gap: var(--space-group); padding: var(--space-related) var(--space-related) var(--space-related) var(--space-inset); border-bottom: 1px solid var(--border); background: var(--surface-muted); color: var(--foreground); }
|
|
6
6
|
.bar[data-tone="inverted"] { --muted: color-mix(in oklab, var(--background) 66%, transparent); border-bottom-color: transparent; background: var(--foreground); color: var(--background); }
|
|
7
7
|
|
|
8
8
|
.side { min-width: 0; }
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
/* Faces overlap in place; the entering one reports its height and the viewport springs to it. */
|
|
12
12
|
.face { position: absolute; top: 0; right: 0; left: 0; display: flex; min-height: 32px; align-items: center; justify-content: center; }
|
|
13
|
-
.message { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; gap:
|
|
13
|
+
.message { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; gap: var(--space-hair) var(--space-group); margin: 0; padding: var(--space-related) 0; font-size: var(--text-sm); line-height: var(--leading-body); text-align: center; text-wrap: balance; }
|
|
14
14
|
.text { min-width: 0; }
|
|
15
15
|
|
|
16
16
|
/* The countdown is one run of text: every cell shares the line box, so digits, colons, and the day mark sit on the message baseline. */
|
|
17
|
-
.countdown { display: inline-flex; align-items: baseline; gap:
|
|
17
|
+
.countdown { display: inline-flex; align-items: baseline; gap: var(--space-related); color: var(--muted); white-space: nowrap; }
|
|
18
18
|
.countdownLabel { color: inherit; }
|
|
19
19
|
.time { display: inline-flex; align-items: baseline; color: var(--foreground); font-variant-numeric: tabular-nums; font-weight: 500; line-height: inherit; }
|
|
20
20
|
.time[data-pending] { opacity: 0; }
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
.sep { display: inline-block; padding-inline: 1px; color: var(--muted); font-feature-settings: "case"; font-weight: 400; }
|
|
29
29
|
.gap { display: inline-block; width: .4em; }
|
|
30
30
|
|
|
31
|
-
.cta { display: inline-flex; align-items: center; gap:
|
|
31
|
+
.cta { display: inline-flex; align-items: center; gap: var(--space-tight); padding: 0; border: 0; background: none; color: inherit; cursor: pointer; font: inherit; font-weight: 500; text-decoration: none; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
|
|
32
32
|
.cta svg { transition: transform var(--duration-fast) var(--ease-standard); }
|
|
33
33
|
|
|
34
34
|
.controls { display: flex; align-items: center; gap: var(--control-gap); }
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
|
|
51
51
|
/* Narrow bars drop the mirror column and read from the start edge. */
|
|
52
52
|
@container (max-width: 560px) {
|
|
53
|
-
.bar { grid-template-columns: 0 minmax(0, 1fr) auto; gap: 0 var(--space-related); padding-left:
|
|
53
|
+
.bar { grid-template-columns: 0 minmax(0, 1fr) auto; gap: 0 var(--space-related); padding-left: var(--space-inset); }
|
|
54
54
|
.face { justify-content: flex-start; }
|
|
55
|
-
.message { justify-content: flex-start; column-gap:
|
|
55
|
+
.message { justify-content: flex-start; column-gap: var(--space-group); text-align: left; }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type HTMLAttributes } from "react";
|
|
2
2
|
export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
|
|
3
3
|
name: string;
|
|
4
|
-
src?: string;
|
|
5
|
-
size?: "sm" | "md" | "lg" | "xl";
|
|
4
|
+
src?: string; /** `xs` (20px) is for dense lists such as space icons in a tree; `sm` 28, `md` 36, `lg` 48, `xl` 88. */
|
|
5
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
6
6
|
status?: "online" | "offline";
|
|
7
7
|
}
|
|
8
8
|
export declare function Avatar({ name, src, size, status, className, ...props }: AvatarProps): import("react").JSX.Element;
|
|
@@ -14,5 +14,5 @@ export function Avatar({ name, src, size = "md", status, className, ...props })
|
|
|
14
14
|
useLayoutEffect(() => { const node = image.current; if (node && !node.complete)
|
|
15
15
|
node.dataset.loading = ""; }, [src]);
|
|
16
16
|
const showImage = src && failedSrc !== src;
|
|
17
|
-
return _jsxs("span", { ...props, className: [styles.avatar, styles[size], className].filter(Boolean).join(" "), role: "img", "aria-label": `${name}${status ? `, ${status}` : ""}`, children: [showImage ? _jsx(SiliconImage, { ref: image, src: src, alt: "", fill: true, sizes: size === "xl" ? "88px" : size === "lg" ? "48px" : size === "md" ? "36px" : "28px", onLoad: event => { delete event.currentTarget.dataset.loading; }, onError: () => setFailedSrc(src) }, src) : _jsx("span", { className: src ? styles.fallback : undefined, "aria-hidden": "true", children: initials }), _jsx(AnimatePresence, { initial: false, children: status && _jsx(motion.i, { className: [styles.status, styles[status]].join(" "), "aria-hidden": "true", initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: reduceMotion ? 0 : motionTokens.duration.fast } }, transition: reduceMotion ? { duration: 0 } : motionTokens.spring.snappy }, status) })] });
|
|
17
|
+
return _jsxs("span", { ...props, className: [styles.avatar, styles[size], className].filter(Boolean).join(" "), role: "img", "aria-label": `${name}${status ? `, ${status}` : ""}`, children: [showImage ? _jsx(SiliconImage, { ref: image, src: src, alt: "", fill: true, sizes: size === "xl" ? "88px" : size === "lg" ? "48px" : size === "md" ? "36px" : size === "sm" ? "28px" : "20px", onLoad: event => { delete event.currentTarget.dataset.loading; }, onError: () => setFailedSrc(src) }, src) : _jsx("span", { className: src ? styles.fallback : undefined, "aria-hidden": "true", children: initials }), _jsx(AnimatePresence, { initial: false, children: status && _jsx(motion.i, { className: [styles.status, styles[status]].join(" "), "aria-hidden": "true", initial: { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: .6, transition: { duration: reduceMotion ? 0 : motionTokens.duration.fast } }, transition: reduceMotion ? { duration: 0 } : motionTokens.spring.snappy }, status) })] });
|
|
18
18
|
}
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
|
|
29
29
|
.fallback { animation: avatar-fallback-in var(--duration-standard) var(--ease-enter) both; }
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/* Initials below the smallest type step, so two letters fit 20px. */
|
|
32
|
+
.xs { width: 20px; height: 20px; font-size: calc(var(--text-xs) * .8); }
|
|
33
|
+
.sm { width: 28px; height: 28px; font-size: var(--text-xs); }
|
|
32
34
|
.md { width: 36px; height: 36px; }
|
|
33
35
|
.lg { width: 48px; height: 48px; font-size: var(--text-sm); }
|
|
34
36
|
.xl { width: 88px; height: 88px; font-size: var(--text-xl); }
|
|
@@ -45,9 +47,10 @@
|
|
|
45
47
|
box-shadow: 0 0 0 1px color-mix(in oklab, var(--border) 70%, transparent);
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
.xs .status { width: 6px; height: 6px; border-width: 1px; }
|
|
48
51
|
.sm .status { width: 8px; height: 8px; border-width: 1.5px; }
|
|
49
52
|
.lg .status { width: 12px; height: 12px; }
|
|
50
|
-
.xl .status { width: 14px; height: 14px; right:
|
|
53
|
+
.xl .status { width: 14px; height: 14px; right: var(--space-tight); bottom: var(--space-tight); }
|
|
51
54
|
.online { background: var(--success); }
|
|
52
55
|
.offline { background: var(--text-muted); }
|
|
53
56
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
.group { --avatar-size: 36px; display: inline-flex; height: var(--avatar-size); align-items: center; padding-left:
|
|
1
|
+
.group { --avatar-size: 36px; display: inline-flex; height: var(--avatar-size); align-items: center; padding-left: var(--space-related); }
|
|
2
2
|
.group.sm { --avatar-size: 28px; }
|
|
3
3
|
.group.lg { --avatar-size: 48px; }
|
|
4
4
|
/* Each slot is the avatar's visible width, so a slot that opens or closes moves the rest of the stack smoothly. */
|
|
5
5
|
.slot { position: relative; display: inline-flex; flex: 0 0 auto; height: var(--avatar-size); align-items: center; }
|
|
6
6
|
/* One wrapper per person carries every hover move as a single translate, so the stack never changes size. */
|
|
7
|
-
.lift { --fan: 0px; --aside: 0px; --rise: 0px; position: relative; display: inline-flex; margin-left: -
|
|
7
|
+
.lift { --fan: 0px; --aside: 0px; --rise: 0px; position: relative; display: inline-flex; margin-left: calc(var(--space-related) * -1); border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape); transform: translate(calc(var(--fan) + var(--aside)), var(--rise)); transition: transform var(--duration-standard) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
|
|
8
8
|
.avatar, .overflow { border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border); transition: box-shadow var(--duration-fast) var(--ease-standard); }
|
|
9
9
|
.overflow { position: relative; display: inline-grid; place-items: center; overflow: hidden; border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
10
10
|
.count { display: inline-block; }
|
|
11
|
-
.overflow.sm { width: 28px; height: 28px; font-size:
|
|
11
|
+
.overflow.sm { width: 28px; height: 28px; font-size: var(--text-xs); }
|
|
12
12
|
.overflow.md { width: 36px; height: 36px; }
|
|
13
13
|
.overflow.lg { width: 48px; height: 48px; font-size: var(--text-sm); }
|
|
14
14
|
/* The name floats above the stack, outside the layout, so showing it never moves anything. */
|
|
15
|
-
.tip { position: absolute; z-index: 2; bottom: calc(100% +
|
|
15
|
+
.tip { position: absolute; z-index: 2; bottom: calc(100% + var(--space-related)); left: 50%; padding: var(--space-tight) var(--space-related); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; pointer-events: none; opacity: 0; transform: translate(-50%, 3px); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }
|
|
16
16
|
/* Pointing at the stack loosens its overlap from the center; the person under the pointer lifts with a crisp ring and names themselves, and their neighbors ease aside. */
|
|
17
17
|
@media (hover: hover) and (pointer: fine) {
|
|
18
18
|
.group:hover .lift { --fan: calc((var(--index) - (var(--count) - 1) / 2) * 4px); }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
align-items: center;
|
|
5
5
|
border: 1px solid var(--badge-border, var(--border));
|
|
6
6
|
border-radius: var(--radius-full); corner-shape: round;
|
|
7
|
-
padding: 0
|
|
7
|
+
padding: 0 var(--space-group);
|
|
8
8
|
color: var(--badge-foreground, var(--foreground));
|
|
9
9
|
background: var(--badge-background, var(--surface));
|
|
10
10
|
font-size: var(--text-xs);
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
@media (hover: hover) and (pointer: fine) {
|
|
21
21
|
.badge:hover { border-color: color-mix(in oklab, var(--badge-color, var(--foreground)) 32%, var(--border)); background: color-mix(in oklab, var(--badge-color, var(--foreground)) 10%, var(--surface)); }
|
|
22
22
|
}
|
|
23
|
-
.sm { min-height: 22px; padding: 0
|
|
23
|
+
.sm { min-height: 22px; padding: 0 var(--space-related); font-size: var(--text-xs); }
|
|
24
24
|
.md { font-size: var(--text-xs); }
|
|
25
25
|
|
|
26
26
|
.body { display: block; }
|
|
27
|
-
.content { position: relative; display: inline-flex; width: max-content; align-items: center; gap:
|
|
28
|
-
.sm .content { gap:
|
|
27
|
+
.content { position: relative; display: inline-flex; width: max-content; align-items: center; gap: var(--space-tight); }
|
|
28
|
+
.sm .content { gap: var(--space-tight); }
|
|
29
29
|
.label, .glyph { position: relative; display: inline-flex; align-items: center; }
|
|
30
30
|
.text { display: block; }
|
|
31
31
|
|
|
@@ -8,7 +8,7 @@ const { spring, duration, ease, blur, stagger } = motionTokens;
|
|
|
8
8
|
/** Motion drops inherited velocity on time-defined springs, so values that retarget mid-flight run the same springs written as stiffness and damping. */
|
|
9
9
|
const physical = ({ visualDuration, bounce }, restDelta = .01) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
|
|
10
10
|
const settle = physical(spring.smooth);
|
|
11
|
-
const grow = physical(spring.
|
|
11
|
+
const grow = physical(spring.expressive);
|
|
12
12
|
const glide = physical(spring.snappy);
|
|
13
13
|
/** The zoom runs in fractions of the plot, so it rests far below a pixel. */
|
|
14
14
|
const zoom = physical(spring.smooth, .00001);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
.figure { display: grid; min-width: 0; gap: var(--space-comfort); margin: 0; color: var(--foreground); }
|
|
2
2
|
|
|
3
3
|
/* The headline is the readout: the average at rest, the scrubbed bar while exploring. Each line keeps its height, so swaps never move the plot. */
|
|
4
|
-
.header { display: grid; min-width: 0; gap:
|
|
4
|
+
.header { display: grid; min-width: 0; gap: var(--space-hair); }
|
|
5
5
|
.kind, .when { display: block; min-height: calc(1em * var(--leading-body)); font-size: var(--text-sm); line-height: var(--leading-body); }
|
|
6
6
|
.kind { color: var(--text-secondary); }
|
|
7
7
|
.when { color: var(--text-muted); }
|
|
8
|
-
.value { display: flex; min-width: 0; align-items: baseline; gap:
|
|
8
|
+
.value { display: flex; min-width: 0; align-items: baseline; gap: var(--space-related); font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); font-variant-numeric: tabular-nums; }
|
|
9
9
|
.unit { color: var(--text-secondary); font-family: var(--font-body); font-size: var(--text-base); font-weight: 400; letter-spacing: var(--tracking-body); }
|
|
10
10
|
.swap { position: relative; display: block; min-width: 0; }
|
|
11
11
|
.swapText { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
.chart .bar[data-active] { fill: var(--accent); }
|
|
34
34
|
|
|
35
35
|
.gutter { position: relative; grid-area: 1 / 2; min-width: 0; }
|
|
36
|
-
.tick, .meanLabel { position: absolute; top: 0; left:
|
|
36
|
+
.tick, .meanLabel { position: absolute; top: 0; left: var(--space-group); display: block; height: 0; white-space: nowrap; pointer-events: none; }
|
|
37
37
|
.tickText { display: block; transform: translateY(-50%); color: var(--text-muted); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }
|
|
38
38
|
.meanLabel .tickText { color: var(--foreground); font-weight: 500; }
|
|
39
39
|
|
|
40
40
|
.axis { position: relative; grid-area: 2 / 1; min-width: 0; overflow: hidden; }
|
|
41
|
-
.axisLabel { position: absolute; top:
|
|
41
|
+
.axisLabel { position: absolute; top: var(--space-related); left: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
42
42
|
|
|
43
43
|
/* The scrubber covers the plot and its axes. Horizontal drags scrub, vertical swipes still scroll the page. */
|
|
44
44
|
.scrubber { position: absolute; inset: 0; border-radius: calc(var(--radius-control) / 2); outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
|
|
@@ -7,7 +7,7 @@ import { motionTokens } from "../../lib/motion-tokens.js";
|
|
|
7
7
|
import styles from "./billing-toggle.module.css";
|
|
8
8
|
const { duration, ease, blur } = motionTokens;
|
|
9
9
|
/** Critically damped: the thumb glides and lands without overshoot. */
|
|
10
|
-
const glide = { type: "spring", visualDuration:
|
|
10
|
+
const glide = { type: "spring", visualDuration: motionTokens.spring.moderate.visualDuration, bounce: 0 };
|
|
11
11
|
const DEFAULT_OPTIONS = [
|
|
12
12
|
{ value: "monthly", label: "Monthly" },
|
|
13
13
|
{ value: "yearly", label: "Yearly", badge: "Save 20%" },
|
|
@@ -66,7 +66,7 @@ export function BillingToggle({ value, onValueChange, options = DEFAULT_OPTIONS,
|
|
|
66
66
|
return (_jsxs("button", { ref: node => { refs.current[index] = node; }, type: "button", role: "radio", "aria-checked": selected, tabIndex: selected ? 0 : -1, className: styles.option, "data-selected": selected || undefined, onClick: () => onValueChange(option.value), onKeyDown: event => onKeyDown(event, index), children: [selected && !thumb ? _jsx("span", { className: styles.thumbStatic, "aria-hidden": "true" }) : null, _jsxs("span", { className: styles.content, children: [_jsx("span", { className: styles.label, children: option.label }), badgeText ? (_jsx("span", { className: styles.badge, "data-active": selected || undefined, children: _jsx(StableSwap, { text: badgeText, candidates: candidates, reduced: reduced }) })) : null] })] }, option.value));
|
|
67
67
|
})] }));
|
|
68
68
|
}
|
|
69
|
-
const clip = { type: "spring", visualDuration:
|
|
69
|
+
const clip = { type: "spring", visualDuration: motionTokens.spring.moderate.visualDuration, bounce: 0 };
|
|
70
70
|
/** A price that rolls to its new amount. The old price and the period ease their width, so nothing beside them jumps. */
|
|
71
71
|
export function BillingPrice({ amount, currency = "$", period, was, decimals = 0, className }) {
|
|
72
72
|
const reduced = !!useReducedMotion();
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
z-index: -1;
|
|
27
27
|
border-radius: var(--radius-full); corner-shape: round;
|
|
28
28
|
background: var(--surface-raised);
|
|
29
|
-
box-shadow: 0 0 0 1px var(--border-subtle),
|
|
29
|
+
box-shadow: 0 0 0 1px var(--border-subtle), var(--shadow-thumb);
|
|
30
30
|
pointer-events: none;
|
|
31
31
|
}
|
|
32
32
|
.thumb { top: var(--_pad); bottom: var(--_pad); left: 0; will-change: transform, width; }
|
|
33
33
|
.thumbStatic { inset: 0; }
|
|
34
34
|
:global(:root[data-theme="dark"]) .thumb,
|
|
35
35
|
:global(:root[data-theme="dark"]) .thumbStatic {
|
|
36
|
-
box-shadow: 0 0 0 1px var(--border),
|
|
36
|
+
box-shadow: 0 0 0 1px var(--border), var(--shadow-thumb);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.option {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
min-width: 0;
|
|
43
43
|
height: var(--_h);
|
|
44
44
|
align-items: center;
|
|
45
|
-
padding: 0
|
|
45
|
+
padding: 0 var(--space-inset);
|
|
46
46
|
border: 0;
|
|
47
47
|
border-radius: var(--radius-full); corner-shape: round;
|
|
48
48
|
background: transparent;
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
-webkit-tap-highlight-color: transparent;
|
|
56
56
|
}
|
|
57
57
|
.option[data-selected] { color: var(--foreground); cursor: default; }
|
|
58
|
-
.option:has(.badge) { padding-right:
|
|
59
|
-
.root[data-size="lg"] .option { padding-inline:
|
|
60
|
-
.root[data-size="lg"] .option:has(.badge) { padding-right:
|
|
58
|
+
.option:has(.badge) { padding-right: var(--space-tight); }
|
|
59
|
+
.root[data-size="lg"] .option { padding-inline: var(--space-comfort); }
|
|
60
|
+
.root[data-size="lg"] .option:has(.badge) { padding-right: var(--space-related); }
|
|
61
61
|
|
|
62
62
|
@media (hover: hover) and (pointer: fine) {
|
|
63
63
|
.option:not([data-selected]):hover { color: var(--text-secondary); }
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
position: relative;
|
|
68
68
|
display: inline-flex;
|
|
69
69
|
align-items: center;
|
|
70
|
-
gap:
|
|
70
|
+
gap: var(--space-related);
|
|
71
71
|
transition: transform var(--duration-fast) var(--ease-standard);
|
|
72
72
|
}
|
|
73
73
|
.option:not([data-selected]):active .content { transform: scale(.97); }
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
display: inline-flex;
|
|
80
80
|
height: calc(var(--_h) - 10px);
|
|
81
81
|
align-items: center;
|
|
82
|
-
padding: 0
|
|
82
|
+
padding: 0 var(--space-related);
|
|
83
83
|
border-radius: var(--radius-full); corner-shape: round;
|
|
84
84
|
background: color-mix(in oklab, var(--foreground) 6%, transparent);
|
|
85
85
|
color: var(--text-secondary);
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
|
|
109
109
|
/* ------------------------------------------------------------------------ Price */
|
|
110
110
|
|
|
111
|
-
.price { display: inline-flex; min-width: 0; flex-wrap: wrap; align-items: flex-end; gap:
|
|
111
|
+
.price { display: inline-flex; min-width: 0; flex-wrap: wrap; align-items: flex-end; gap: var(--space-tight) var(--space-group); }
|
|
112
112
|
.amount {
|
|
113
113
|
display: inline-flex;
|
|
114
114
|
align-items: flex-start;
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
white-space: nowrap;
|
|
134
134
|
}
|
|
135
135
|
.wasSlot { display: inline-block; overflow-x: clip; }
|
|
136
|
-
.was { display: inline-block; padding-right:
|
|
136
|
+
.was { display: inline-block; padding-right: var(--space-related); color: var(--text-muted); font-variant-numeric: tabular-nums; text-decoration-thickness: 1px; }
|
|
137
137
|
.period { position: relative; display: inline-block; overflow-x: clip; overflow-clip-margin: 2px; }
|
|
138
138
|
.periodSizer { display: inline-block; visibility: hidden; }
|
|
139
139
|
.periodText { position: absolute; top: 0; left: 0; display: inline-block; }
|
|
@@ -22,7 +22,7 @@ const STRETCH = 120;
|
|
|
22
22
|
const LOW_DIM = .78;
|
|
23
23
|
const settle = motionTokens.spring.smooth;
|
|
24
24
|
/** Leaving is shorter than arriving; a flick keeps its velocity through the same spring. */
|
|
25
|
-
const leave = { ...motionTokens.spring.smooth, visualDuration: .
|
|
25
|
+
const leave = { ...motionTokens.spring.smooth, visualDuration: motionTokens.spring.moderate.visualDuration };
|
|
26
26
|
const fade = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
|
|
27
27
|
/** iOS-style resistance: follows the finger at first, then approaches STRETCH. */
|
|
28
28
|
const rubber = (distance) => (1 - 1 / (distance * .55 / STRETCH + 1)) * STRETCH;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* The dim follows the sheet's position in JS; only its color lives here. */
|
|
2
|
-
.overlay { position: fixed; inset: 0; z-index:
|
|
3
|
-
:global(:root[data-theme="dark"]) .overlay { background: oklch(0% 0 0 / .52); }
|
|
2
|
+
.overlay { position: fixed; inset: 0; z-index: var(--layer-overlay); background: var(--scrim); }
|
|
4
3
|
/* While it leaves, nothing underneath is blocked and the sheet cannot be grabbed again.
|
|
5
4
|
Radix sets pointer-events: auto inline on the overlay, so only !important lets a click reach the trigger and reverse the exit. */
|
|
6
5
|
.overlay[data-state="closed"], .sheet[data-state="closed"] { pointer-events: none !important; }
|
|
@@ -8,7 +7,7 @@
|
|
|
8
7
|
/* The sheet is as tall as its largest detent plus a hidden extension below the fold; detents are translateY offsets from there. */
|
|
9
8
|
.sheet {
|
|
10
9
|
position: fixed;
|
|
11
|
-
z-index:
|
|
10
|
+
z-index: var(--layer-dialog);
|
|
12
11
|
right: 0;
|
|
13
12
|
bottom: calc(var(--sheet-extension) * -1);
|
|
14
13
|
left: 0;
|
|
@@ -47,9 +46,9 @@
|
|
|
47
46
|
.headRow { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-inset); }
|
|
48
47
|
.headText { min-width: 0; }
|
|
49
48
|
.title { margin: 0; font-family: var(--font-body); font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
|
|
50
|
-
.description { margin:
|
|
49
|
+
.description { margin: var(--space-hair) 0 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
|
|
51
50
|
|
|
52
|
-
.close { display: grid; flex: none; width: 2rem; height: 2rem; place-items: center; margin-top: -
|
|
51
|
+
.close { display: grid; flex: none; width: 2rem; height: 2rem; place-items: center; margin-top: calc(var(--space-hair) * -1); padding: 0; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
|
|
53
52
|
@media (hover: hover) and (pointer: fine) { .close:hover { color: var(--foreground); border-color: var(--border-strong); } }
|
|
54
53
|
.close:active { color: var(--foreground); }
|
|
55
54
|
|
|
@@ -6,8 +6,14 @@ export interface BreadcrumbItem {
|
|
|
6
6
|
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
|
7
7
|
}
|
|
8
8
|
export interface BreadcrumbProps {
|
|
9
|
+
/** The path from the top level to the current page, which is the last item. */
|
|
9
10
|
items: BreadcrumbItem[];
|
|
11
|
+
/** The navigation landmark's name (default "Breadcrumb"). */
|
|
10
12
|
ariaLabel?: string;
|
|
13
|
+
/** Experimental. The most crumbs to show, at least 2. A longer path shows the first crumb, a "…" menu of the levels
|
|
14
|
+
* between, and the last `maxItems - 1` crumbs. By default the path never collapses. */
|
|
15
|
+
maxItems?: number;
|
|
11
16
|
}
|
|
12
|
-
/** Crumbs present on first render stay still; crumbs added later slide in from the path before them.
|
|
13
|
-
|
|
17
|
+
/** Crumbs present on first render stay still; crumbs added later slide in from the path before them.
|
|
18
|
+
* With maxItems, the levels a long path hides move into a "…" menu, where they stay links. */
|
|
19
|
+
export declare function Breadcrumb({ items, ariaLabel, maxItems }: BreadcrumbProps): import("react").JSX.Element;
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { ChevronRight as NavArrowRight } from "lucide-react";
|
|
3
|
+
import { Ellipsis, ChevronRight as NavArrowRight } from "lucide-react";
|
|
4
4
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
5
|
+
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuRoot, DropdownMenuTrigger } from "../dropdown-menu/dropdown-menu.js";
|
|
5
6
|
import { SiliconLink } from "../../lib/silicon-provider.js";
|
|
6
7
|
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
8
|
import styles from "./breadcrumb.module.css";
|
|
8
|
-
/** Crumbs present on first render stay still; crumbs added later slide in from the path before them.
|
|
9
|
-
|
|
9
|
+
/** Crumbs present on first render stay still; crumbs added later slide in from the path before them.
|
|
10
|
+
* With maxItems, the levels a long path hides move into a "…" menu, where they stay links. */
|
|
11
|
+
export function Breadcrumb({ items, ariaLabel = "Breadcrumb", maxItems }) {
|
|
10
12
|
const reduced = useReducedMotion() ?? false;
|
|
11
13
|
const still = { duration: 0 };
|
|
12
14
|
const path = items.map(item => item.label).join("/");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// The first crumb and the current page always show. Crumbs keep their index in `items` as their key, so the ones
|
|
16
|
+
// that stay visible never remount when the path collapses or grows.
|
|
17
|
+
const shown = Math.max(2, Math.floor(maxItems ?? Infinity));
|
|
18
|
+
const hidden = items.length > shown ? items.slice(1, items.length - shown + 1) : [];
|
|
19
|
+
const crumb = (key, first, children) => _jsxs(motion.li, { layout: reduced ? false : "position", layoutDependency: path, initial: reduced ? false : { opacity: 0, x: -8, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, x: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: still } : { opacity: 0, x: -4, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, transition: reduced ? still : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter], layout: motionTokens.spring.smooth }, children: [!first && _jsx(NavArrowRight, { width: 14, height: 14, "aria-hidden": "true" }), children] }, key);
|
|
20
|
+
const crumbs = items.map((item, index) => {
|
|
21
|
+
const current = index === items.length - 1;
|
|
22
|
+
return crumb(`${item.label}-${index}`, index === 0, !current && item.href ? _jsx(SiliconLink, { href: item.href, "data-label": item.label, onClick: item.onClick, children: item.label })
|
|
23
|
+
: !current && item.onClick ? _jsx("button", { type: "button", "data-label": item.label, onClick: item.onClick, children: item.label })
|
|
24
|
+
: _jsx("span", { "aria-current": current ? "page" : undefined, "data-label": item.label, children: item.label }));
|
|
25
|
+
});
|
|
26
|
+
if (hidden.length)
|
|
27
|
+
crumbs.splice(1, hidden.length, crumb("more", false, _jsxs(DropdownMenuRoot, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", className: styles.more, "aria-label": `Show ${hidden.length} more ${hidden.length === 1 ? "level" : "levels"}`, children: _jsx(Ellipsis, { width: 16, height: 16, "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "start", children: hidden.map((item, index) => item.href
|
|
28
|
+
? _jsx(DropdownMenuItem, { asChild: true, onClick: item.onClick, children: _jsx(SiliconLink, { href: item.href, children: item.label }) }, `${item.label}-${index + 1}`)
|
|
29
|
+
: _jsx(DropdownMenuItem, { disabled: !item.onClick, onClick: item.onClick, children: item.label }, `${item.label}-${index + 1}`)) })] })));
|
|
30
|
+
return _jsx("nav", { "aria-label": ariaLabel, children: _jsx("ol", { className: styles.list, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: crumbs }) }) });
|
|
19
31
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/* The gap between crumbs matches the gap inside one, so every chevron sits centered between its two labels. */
|
|
2
|
-
.list { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap:
|
|
3
|
-
.list li { display: inline-flex; align-items: center; gap:
|
|
2
|
+
.list { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-hair) var(--space-related); margin: 0; padding: 0; list-style: none; }
|
|
3
|
+
.list li { display: inline-flex; align-items: center; gap: var(--space-related); color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-body); white-space: nowrap; }
|
|
4
4
|
.list li > svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--border-strong); stroke-width: 1.75; }
|
|
5
5
|
/* Each crumb reserves the width of its medium weight label, so becoming the current page never shifts the path. */
|
|
6
|
-
.list a, .list li > span, .list li > button { display: inline-flex; flex-direction: column; border-radius: var(--radius-detail); padding:
|
|
6
|
+
.list a, .list li > span, .list li > button { display: inline-flex; flex-direction: column; border-radius: var(--radius-detail); padding: var(--space-tight) var(--space-hair); }
|
|
7
7
|
.list a::after, .list li > span::after, .list li > button::after { height: 0; overflow: hidden; content: attr(data-label); font-weight: 500; visibility: hidden; user-select: none; pointer-events: none; }
|
|
8
8
|
.list li > button { border: 0; margin: 0; background: none; font: inherit; line-height: inherit; text-align: start; cursor: pointer; }
|
|
9
9
|
.list a, .list li > button { color: var(--text-secondary); text-decoration: underline 1px; text-decoration-color: transparent; text-underline-offset: 4px; transition: color var(--duration-fast) var(--ease-standard), text-decoration-color var(--duration-fast) var(--ease-standard); }
|
|
10
10
|
.list [aria-current="page"] { color: var(--accent-strong); font-weight: 500; transition: color var(--duration-fast) var(--ease-standard); }
|
|
11
|
+
/* The "…" of a collapsed path: a 24px square target, accented while its menu of hidden levels is open. */
|
|
12
|
+
.list .more { padding: var(--space-tight); }
|
|
13
|
+
.list .more svg { width: 16px; height: 16px; stroke-width: 1.75; }
|
|
14
|
+
.list .more[data-state="open"] { color: var(--accent-strong); }
|
|
11
15
|
@media (hover: hover) and (pointer: fine) { .list a:hover, .list li > button:hover { color: var(--accent-strong); text-decoration-color: color-mix(in oklab, currentColor 45%, transparent); } }
|
|
12
16
|
@media (prefers-reduced-motion: reduce) { .list a, .list li > button, .list [aria-current="page"] { transition: none; } }
|