@sableui/react 0.14.0 → 0.16.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/CHANGELOG.md +131 -0
- package/DESIGN.md +3 -1
- package/bin/sable-lint.mjs +5 -1
- package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
- package/dist/blocks/index.d.ts +12 -1
- package/dist/blocks/index.js +4 -0
- package/dist/blocks/message-reactions-block.js +87 -0
- package/dist/blocks/message-reactions-block.meta.js +23 -0
- package/dist/blocks/wallet-block.js +2 -2
- package/dist/components/app-shell/app-shell-frame.js +16 -0
- package/dist/components/app-shell/app-shell-main.js +12 -0
- package/dist/components/app-shell/app-shell.js +28 -4
- package/dist/components/app-shell/app-shell.meta.js +88 -4
- package/dist/components/area-chart/area-chart.meta.js +6 -1
- package/dist/components/attachment/attachment.meta.js +2 -1
- package/dist/components/attachment/attachment.variants.js +1 -1
- package/dist/components/badge/badge.meta.js +8 -1
- package/dist/components/badge/badge.variants.js +7 -7
- package/dist/components/banner/banner.meta.js +6 -1
- package/dist/components/banner/banner.variants.js +5 -5
- package/dist/components/bar-chart/bar-chart.meta.js +6 -1
- package/dist/components/bubble/bubble.js +105 -5
- package/dist/components/bubble/bubble.meta.js +42 -6
- package/dist/components/bubble/bubble.variants.js +1 -1
- package/dist/components/bubble/index.js +2 -0
- package/dist/components/caret-menu/caret-menu.js +5 -6
- package/dist/components/chat-composer/chat-composer.meta.js +2 -2
- package/dist/components/chat-composer/chat-composer.variants.js +1 -1
- package/dist/components/color-field/color-field.js +6 -8
- package/dist/components/combobox/combobox.js +27 -5
- package/dist/components/combobox/combobox.meta.js +10 -2
- package/dist/components/command-menu/command-menu.js +14 -3
- package/dist/components/command-menu/command-menu.meta.js +9 -1
- package/dist/components/date-picker/date-picker.meta.js +2 -2
- package/dist/components/feedback/feedback.js +9 -2
- package/dist/components/feedback/feedback.meta.js +8 -1
- package/dist/components/gradient-art/gradient-art.js +8 -5
- package/dist/components/gradient-art/gradient-art.meta.js +35 -2
- package/dist/components/input/input.meta.js +11 -3
- package/dist/components/input-group/input-group.js +1 -1
- package/dist/components/input-group/input-group.meta.js +1 -1
- package/dist/components/like-button/index.js +7 -0
- package/dist/components/like-button/like-button.js +89 -0
- package/dist/components/like-button/like-button.meta.js +132 -0
- package/dist/components/line-chart/line-chart.meta.js +6 -1
- package/dist/components/menu/menu.js +1 -1
- package/dist/components/meter/meter.js +53 -7
- package/dist/components/meter/meter.meta.js +51 -4
- package/dist/components/meter/meter.variants.js +43 -7
- package/dist/components/note/note.meta.js +5 -1
- package/dist/components/note/note.variants.js +4 -4
- package/dist/components/number-field/number-field.meta.js +2 -2
- package/dist/components/otp-field/otp-field.meta.js +2 -2
- package/dist/components/panel-group/panel-group.js +5 -8
- package/dist/components/pie-chart/pie-chart.meta.js +6 -1
- package/dist/components/radar-chart/radar-chart.meta.js +6 -1
- package/dist/components/radial-chart/radial-chart.meta.js +6 -1
- package/dist/components/reaction-picker/index.js +13 -0
- package/dist/components/reaction-picker/reaction-picker.js +104 -0
- package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
- package/dist/components/search-input/search-input.meta.js +11 -3
- package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
- package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
- package/dist/components/select/select.js +1 -1
- package/dist/components/select/select.meta.js +2 -2
- package/dist/components/sheet/sheet.js +5 -4
- package/dist/components/sidebar/sidebar.js +16 -1
- package/dist/components/sidebar/sidebar.meta.js +7 -2
- package/dist/components/sparkline/sparkline.meta.js +8 -1
- package/dist/components/status-dot/status-dot.meta.js +8 -1
- package/dist/components/status-dot/status-dot.variants.js +7 -7
- package/dist/components/stepper/stepper.meta.js +2 -1
- package/dist/components/stepper/stepper.variants.js +1 -1
- package/dist/components/table/table.meta.js +4 -4
- package/dist/components/textarea/textarea.js +5 -0
- package/dist/components/textarea/textarea.meta.js +3 -3
- package/dist/components/toast/toast.js +25 -13
- package/dist/components/toast/toast.meta.js +7 -1
- package/dist/file-hashes.json +112 -100
- package/dist/foundations/accessibility.js +5 -0
- package/dist/foundations/motion.js +21 -1
- package/dist/icon-sets/reicon.js +11 -1
- package/dist/index.d.ts +425 -84
- package/dist/index.js +11 -1
- package/dist/lib/chart/types.js +8 -8
- package/dist/lib/chart/use-arrival.js +5 -4
- package/dist/lib/field-shell.js +1 -1
- package/dist/lib/highlight-glide.js +6 -6
- package/dist/lib/scrub-gesture.js +6 -6
- package/dist/lib/taxonomy.js +2 -0
- package/dist/lib/use-caret-trigger.js +11 -18
- package/dist/lib/use-match-highlight.js +69 -0
- package/dist/lib/use-smooth-wheel.js +10 -8
- package/dist/motion/Burst.js +79 -0
- package/dist/motion/index.js +2 -0
- package/dist/presets/index.d.ts +13 -2
- package/dist/presets/index.js +17 -1
- package/dist/styles.css +241 -0
- package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
- package/dist/themes/_contract.js +14 -0
- package/dist/themes/atelier/atelier.css +18 -0
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/contrast.js +48 -1
- package/dist/themes/ember/ember.css +18 -0
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +18 -0
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +18 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +18 -0
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +18 -0
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +18 -0
- package/llms/blocks/message-reactions-block.md +116 -0
- package/llms/blocks/wallet-block.md +5 -4
- package/llms/components/app-shell.md +72 -3
- package/llms/components/area-chart.md +1 -1
- package/llms/components/attachment.md +1 -1
- package/llms/components/badge.md +1 -1
- package/llms/components/banner.md +1 -1
- package/llms/components/bar-chart.md +1 -1
- package/llms/components/bubble.md +17 -7
- package/llms/components/chat-composer.md +1 -1
- package/llms/components/combobox.md +2 -1
- package/llms/components/command-menu.md +3 -2
- package/llms/components/date-picker.md +1 -1
- package/llms/components/feedback.md +2 -1
- package/llms/components/gradient-art.md +28 -1
- package/llms/components/input-group.md +1 -1
- package/llms/components/input.md +4 -2
- package/llms/components/like-button.md +91 -0
- package/llms/components/line-chart.md +1 -1
- package/llms/components/meter.md +33 -4
- package/llms/components/note.md +1 -1
- package/llms/components/number-field.md +1 -1
- package/llms/components/otp-field.md +1 -1
- package/llms/components/pie-chart.md +1 -1
- package/llms/components/radar-chart.md +1 -1
- package/llms/components/radial-chart.md +1 -1
- package/llms/components/reaction-picker.md +115 -0
- package/llms/components/search-input.md +20 -2
- package/llms/components/segmented-bar.md +1 -1
- package/llms/components/select.md +1 -1
- package/llms/components/sidebar.md +3 -2
- package/llms/components/sparkline.md +1 -1
- package/llms/components/status-dot.md +1 -1
- package/llms/components/stepper.md +1 -1
- package/llms/components/table.md +2 -2
- package/llms/components/textarea.md +2 -2
- package/llms/components/toast.md +2 -1
- package/llms/foundations/accessibility.md +1 -0
- package/llms/foundations/motion.md +18 -2
- package/llms/reference/token-intent.md +22 -2
- package/llms/reference/utilities.md +9 -3
- package/llms-full.txt +596 -61
- package/llms.txt +10 -7
- package/package.json +1 -1
- package/registry.json +968 -68
- package/scripts/lint-rules.mjs +6 -3
- package/targets/ios.json +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,136 @@
|
|
|
1
1
|
# What's new
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
Five pieces of the calm, product-chrome look that AI and B2B tools share:
|
|
6
|
+
tick-bar budgets, pastel art, filled fields, an inset frame and a dither
|
|
7
|
+
texture. Each is told through the theme, so every preset can wear it.
|
|
8
|
+
|
|
9
|
+
- **New: segmented meters.** `<Meter segments={40} />` draws the bar as a row
|
|
10
|
+
of ticks — a spend limit, credits left, a lead score, months of data — read
|
|
11
|
+
at a glance. Any reading above `min` shows at least one tick, and when the
|
|
12
|
+
value changes the fill runs through the ticks from the edge it is leaving.
|
|
13
|
+
Size the count to the track (about one tick per 8px) so a tick stays taller
|
|
14
|
+
than it is wide. `shape="ring"` ignores it.
|
|
15
|
+
- **New: `GradientArt intensity="soft"`.** The same art as a pastel of itself:
|
|
16
|
+
lifted and quieted, bright in dark mode too. It is a stage for floating
|
|
17
|
+
chips, logo tiles and card header bands, not a surface for text.
|
|
18
|
+
- **New: the field tokens, `field` and `field-border`.** Every text field —
|
|
19
|
+
Input, Textarea, Select, Combobox, SearchInput, NumberField, OTPField,
|
|
20
|
+
DatePicker, InputGroup, ChatComposer and CommandMenuTrigger — now paints
|
|
21
|
+
this pair, outlined on the card by default exactly as before. A theme fills
|
|
22
|
+
them all at once with `--theme-field: var(--muted)` and
|
|
23
|
+
`--theme-field-border: transparent`. Canvas has a matching **Fields**
|
|
24
|
+
control (Preset / Outline / Filled), and presets may set both hooks.
|
|
25
|
+
A hand-built field wearing `border-field-border` is caught by
|
|
26
|
+
`lookalike-control` like one wearing `border-input`.
|
|
27
|
+
- **New: `AppShell frame="inset"`.** From `lg`, the rail and a small gap sit on
|
|
28
|
+
one ground and main becomes a rounded, bordered panel that scrolls inside
|
|
29
|
+
itself while the rail stays put. The Sidebar in the slot drops its border
|
|
30
|
+
and fills the row, and its footer rule spans the content column rather than
|
|
31
|
+
running out into the gap; below `lg` nothing changes. The ground is a new derived
|
|
32
|
+
token, `shell-ground` — one step off the rail, so the panel reads as lifted
|
|
33
|
+
even in themes whose rail is the page colour; tune it with
|
|
34
|
+
`--theme-shell-ground`. Inset moves the desktop scroll from `window` to
|
|
35
|
+
`#main-content`, so scroll listeners (restoration, scroll-to-top, scrollspy)
|
|
36
|
+
must target `main`.
|
|
37
|
+
- **New utility: `texture-dither`.** A faint field of tiny squares that
|
|
38
|
+
dissolves, painted in `border-strong` so it follows every theme — decoration
|
|
39
|
+
for behind an empty state or a page header. `--dither-fade`,
|
|
40
|
+
`--dither-size` and `--dither-color` tune it.
|
|
41
|
+
- **Status and tone colours at graphic strength.** Meters, segmented bars,
|
|
42
|
+
status dots, chart series and the soft tints behind badges, banners and
|
|
43
|
+
notes were painted with the label fills — dark enough to carry white text,
|
|
44
|
+
so in light mode they read brown, forest green and plum. They now read new
|
|
45
|
+
`-mark` tokens (`success-mark`, `tone-warning-mark`, `destructive-mark`,
|
|
46
|
+
`tone-info-mark`, `tone-1/2/3-mark`): the same hue lifted and saturated,
|
|
47
|
+
held to a 2:1 visibility floor on the page instead of a text ratio. Every
|
|
48
|
+
bundled theme tunes its own in both modes, and keeps its character —
|
|
49
|
+
Ember's olive, Poppy's pastels and Atelier's grey tone stay theirs; a
|
|
50
|
+
preset sets `--theme-*-mark` or gets one derived from its fill. Labels are
|
|
51
|
+
unchanged: solid badges, banners and buttons keep their fills, and text
|
|
52
|
+
keeps `-text`. `markFor` (from `@sableui/react/presets`) derives a mark the
|
|
53
|
+
same way, and Canvas moves the marks with any tone it generates.
|
|
54
|
+
- **Canvas: a brand colour no longer flattens the tones.** Info is treated
|
|
55
|
+
as the status it is: it leans toward the brand within the blues instead of
|
|
56
|
+
being repainted (Brand only made it the brand, so a green brand's info read
|
|
57
|
+
as success; the schemes spun it anywhere). A status also keeps 25° off the
|
|
58
|
+
brand's own hue when its band allows, so a green brand gets a teal success
|
|
59
|
+
rather than a second copy of itself. And Brand only is a family you can tell
|
|
60
|
+
apart — a deep brand, a lighter neighbour and a muted sibling — instead of
|
|
61
|
+
one hue in four near-identical depths.
|
|
62
|
+
- **Table:** `numeric` on TableHead and TableCell is now documented.
|
|
63
|
+
- **New: search matches light up.** Combobox and CommandMenu paint the typed
|
|
64
|
+
text in each row's label, on a tint of the theme's `highlight`. It uses the CSS
|
|
65
|
+
Custom Highlight API, so rows get no `<mark>` wrappers and their accessible
|
|
66
|
+
names stay whole; browsers without it simply show no paint. Combobox lights
|
|
67
|
+
only what was typed, not a label a pick filled in. `highlightMatches={false}`
|
|
68
|
+
turns it off on either.
|
|
69
|
+
- **New hook: `useMatchHighlight(query, { selector })`.** The same paint for a
|
|
70
|
+
results list you filter yourself, such as a SearchInput over a table: it
|
|
71
|
+
returns a callback ref for the results container.
|
|
72
|
+
- **Textarea:** text you are editing no longer rewraps above the caret when
|
|
73
|
+
the field sits inside a `text-pretty` or `text-balance` block
|
|
74
|
+
(`text-wrap-style: stable`). ChatComposer gets the same fix.
|
|
75
|
+
|
|
76
|
+
## 0.15.0
|
|
77
|
+
|
|
78
|
+
Motion with some life in it: small things that turn on now land like events,
|
|
79
|
+
told through the theme rather than one-off styles, and chat reactions that
|
|
80
|
+
answer you the way a messaging app does.
|
|
81
|
+
|
|
82
|
+
- **New: `LikeButton`.** A pressed-or-not toggle for a like, a star or a
|
|
83
|
+
save, with its count. Pressing it on fills the glyph, which blooms past
|
|
84
|
+
its size and settles while a ring opens and dots spray out in the theme's
|
|
85
|
+
palette families, and the count rolls up a digit; pressing it off is quiet.
|
|
86
|
+
It shows the viewer's like at once and goes back to `count` exactly
|
|
87
|
+
whenever `count` changes, so pass the server's total. `icon`, `tone` (a
|
|
88
|
+
palette family) and `label` make it a star or a save; `motion="subtle"`
|
|
89
|
+
keeps the fill and drops the flourish.
|
|
90
|
+
- **New recipe: `motion-bloom`.** A small glyph landing in its on state grows
|
|
91
|
+
from nothing to 1.2 and settles. Remount the glyph (a changing `key`) to
|
|
92
|
+
replay it; `--motion-bloom-delay` lets a ring lead. Expressive.
|
|
93
|
+
- **New: `<Burst>`.** The follow-through behind a bloom — a disc that hollows
|
|
94
|
+
into a ring, then dots thrown outward — coloured by palette families, so it
|
|
95
|
+
re-themes. Plays when its `trigger` changes, never on mount, and renders
|
|
96
|
+
nothing below full motion.
|
|
97
|
+
- **Toast: a status glyph instead of an accent bar.** A typed toast now leads
|
|
98
|
+
with the same glyph and ink as Note and InlineStatus (success, info,
|
|
99
|
+
warning, and the destructive glyph for errors), and it blooms in once the
|
|
100
|
+
card has landed — and again in the spinner's place when a loading toast
|
|
101
|
+
settles. The coloured bar down the leading edge told the type by colour
|
|
102
|
+
alone. A stacked deck now springs as cards step back and fan out; leaving
|
|
103
|
+
toasts still take the standard curve.
|
|
104
|
+
- **Feedback:** the chosen rating mark blooms as it is picked.
|
|
105
|
+
- **Icons: `replay` and `motion`.** A single circular arrow for watching
|
|
106
|
+
something again from its start (`refresh` reads as fetching again), and
|
|
107
|
+
a play mark in a dashed ring for a motion or playback setting (`sparkles`
|
|
108
|
+
reads as AI).
|
|
109
|
+
- **New: `ReactionPicker` and `MenuReactions`.** The quick row of emoji you
|
|
110
|
+
react to a message with. `ReactionPicker` is a toolbar for a popover or a
|
|
111
|
+
hover bar; `MenuReactions` is the same row as checkbox menu items, for the
|
|
112
|
+
top of the menu a message opens on right click or long press. The emoji
|
|
113
|
+
bloom in one after another as it opens and magnify under the pointer.
|
|
114
|
+
- **Bubble reactions celebrate.** When a `BubbleReaction` becomes the
|
|
115
|
+
reader's own — pressed, or added already on to a row that has rendered —
|
|
116
|
+
the emoji blooms and copies of it spray out, and the count rolls. Taking
|
|
117
|
+
a reaction back is quiet, and the reactions a page loads with stay still.
|
|
118
|
+
New `label` names a pill ("Party, 3"); new `BubbleReactionAdd` is the
|
|
119
|
+
smiley pill that opens a picker. `BubbleReactions` now hides itself while
|
|
120
|
+
empty, so render it for every message: staying mounted is how the first
|
|
121
|
+
reaction still celebrates. A Bubble shows a focus ring when it is made
|
|
122
|
+
focusable as a menu trigger.
|
|
123
|
+
- **New block: `MessageReactionsBlock`.** A thread you can react to from a
|
|
124
|
+
right-click / long-press menu or the add-reaction pill.
|
|
125
|
+
- **`<Burst>`: `particle` and `playOnMount`.** Throw copies of a glyph
|
|
126
|
+
instead of dots (no ring, travelling upright), and play once on mount for
|
|
127
|
+
a glyph that exists because of the action.
|
|
128
|
+
- **Icon: `emoji`.** A smiley, for an add-reaction control.
|
|
129
|
+
- **Motion guidance:** amplitude scales with smallness (a glyph travels far, a
|
|
130
|
+
surface barely), a moment is staged (main object first, follow-through
|
|
131
|
+
after), and only a person's own choice is celebrated. In `sable docs
|
|
132
|
+
motion` and DESIGN.md.
|
|
133
|
+
|
|
3
134
|
## 0.14.0
|
|
4
135
|
|
|
5
136
|
Motion that is crafted by default and calm on request, and the first pieces for agent products.
|
package/DESIGN.md
CHANGED
|
@@ -481,6 +481,8 @@ Motion is a first-class token layer, not an afterthought. **Crafted by default,
|
|
|
481
481
|
|
|
482
482
|
**The principle behind the craft: one thing travels; two things do not blink.** When a selection, a value or a mark changes in place, move the element that represents it. And finish the story — a refused answer shakes *and* says why, a success confirms where it happened, an armed action asks again before it acts.
|
|
483
483
|
|
|
484
|
+
**Amplitude scales with smallness.** Motion felt lifeless when one amplitude served everything: the 4% a dialog needs is invisible on a 16px icon, so a heart turning on read as a colour swap. Small glyphs that a person turns on now travel far — grow from nothing past their size and settle — while surfaces keep their 0.96 → 1 and presses their 0.97. A moment is staged rather than flashed: the main object lands, then its follow-through (LikeButton's ring opens, the heart blooms 90ms later, dots are thrown after it). And only a person's own choice is celebrated, only at full motion: a burst on a system event or on load is a notification wearing confetti. The look of all of it still comes from the theme — palette families, tone glyphs, the radius scale — never from one-off values.
|
|
485
|
+
|
|
484
486
|
**Two levels.** *Essential* motion answers the user — colour and focus feedback, press, overlay arrival, disclosure height, loaders, chart hover. *Expressive* motion narrates — glides, rolling numbers, drawn ticks, shakes, staggers, chart arrival and morph, scroll reveals, spring overshoot. Expressive durations are multiplied by a second factor, `--motion-expressive`, which `data-motion="subtle"` sets to 0 (and which takes the overshoot out of `ease-spring`). So the policy ladder is `full` (default: both) → `subtle` (essential only) → `off` (nothing moves; every state still changes). Set it app-wide on `<MotionProvider policy>`, for a region with `<MotionScope level>`, or per component with the `motion` prop that every component owning a flourish takes. Scopes and props can only turn motion **down** — `full` on a component means "inherit", so nothing below the provider can override a reader's reduce-motion setting.
|
|
485
487
|
|
|
486
488
|
**Three parts:**
|
|
@@ -492,7 +494,7 @@ Motion is a first-class token layer, not an afterthought. **Crafted by default,
|
|
|
492
494
|
|
|
493
495
|
2. **Recipes** (`styles.css`) — named, reusable behaviours a component opts into with one class instead of a bespoke transition string:
|
|
494
496
|
- `motion-interactive` (hover/focus colour + transform transitions, including `rotate`) · `motion-press` (tactile press settle) · `motion-fade` (scrims) · `motion-pop` (anchored/centred surfaces — gentle spring on open) · `motion-slide-{up,down,left,right}` (directional fade-slide) · `motion-disclosure` (height) · `motion-draw` (SVG line-art draw-on — a `pathLength="1"` path inside a `<Reveal>` draws in when scrolled into view; stagger siblings via `--motion-draw-delay`) · loaders `motion-spin`/`motion-pulse`/`motion-shimmer`/`motion-dot-bounce`. Enter/exit recipes drive off Base UI's `data-[starting-style]`/`data-[ending-style]`.
|
|
495
|
-
- Expressive: `motion-glide` (one indicator travelling to the selection) · `motion-shake` (a refused answer, driven by `useShake()`) · `motion-tick` (a mark that draws itself in) · `motion-appear` (a small control mounting via `@starting-style`) · `motion-arrive` (items a user action adds to a list, staggered) · `motion-size` (easing between intrinsic sizes via `interpolate-size`). `NumberTicker` is the rolling-number primitive
|
|
497
|
+
- Expressive: `motion-glide` (one indicator travelling to the selection) · `motion-shake` (a refused answer, driven by `useShake()`) · `motion-tick` (a mark that draws itself in) · `motion-appear` (a small control mounting via `@starting-style`) · `motion-arrive` (items a user action adds to a list, staggered) · `motion-size` (easing between intrinsic sizes via `interpolate-size`) · `motion-bloom` (a small glyph landing in its on state, 0 → 1.2 → 1). `NumberTicker` is the rolling-number primitive; `<Burst>` is the follow-through behind a bloom (a ring and thrown dots in the theme's palette families), used by `LikeButton`.
|
|
496
498
|
|
|
497
499
|
3. **Policy** — `prefers-reduced-motion` and `<MotionProvider>` collapse `--motion-scale` to 0 in one place: transitions resolve instantly, loaders hold their first frame. `subtle` zeroes `--motion-expressive` instead, so essential motion keeps playing. In-app, `useMotion()` reads and sets the policy; `useMotionLevel(motion)` resolves a component's own prop against everything around it, and JS-driven flourishes gate on its `expressive`.
|
|
498
500
|
|
package/bin/sable-lint.mjs
CHANGED
|
@@ -44,7 +44,11 @@ if (!existsSync(registryPath)) {
|
|
|
44
44
|
}
|
|
45
45
|
const REGISTRY = JSON.parse(readFileSync(registryPath, "utf8"))
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Derived colours (`tone-warning-mark`, `field`, `shell-ground`…) are real tokens
|
|
48
|
+
// too: every theme gets them, so a utility naming one is not a typo.
|
|
49
|
+
const semanticTokens = new Set(
|
|
50
|
+
[...(REGISTRY.tokens?.semantic ?? []), ...(REGISTRY.tokens?.derived ?? [])].map((t) => (typeof t === "string" ? t : t.name)),
|
|
51
|
+
)
|
|
48
52
|
// Motion token NAMES are reassembled from the registry's durations/easings lists.
|
|
49
53
|
// A test in the package asserts this reconstruction equals _contract.ts exactly,
|
|
50
54
|
// so the consumer linter can never validate against a drifted vocabulary.
|
|
@@ -147,6 +147,52 @@ declare const ART_TOKENS: readonly ["art-1", "art-2", "art-3", "art-4", "art-5",
|
|
|
147
147
|
* 2 doubles it, for a theme that wants every colour to read as its own.
|
|
148
148
|
*/
|
|
149
149
|
declare const PALETTE_TOKENS: readonly ["palette-harmony"];
|
|
150
|
+
/**
|
|
151
|
+
* The text field's surface — derived like the edges, with a `--theme-*` hook.
|
|
152
|
+
*
|
|
153
|
+
* - `field` is the fill of every text-entry field (Input, Textarea, Select,
|
|
154
|
+
* Combobox, SearchInput, InputGroup, ChatComposer…). It defaults to `card`.
|
|
155
|
+
* - `field-border` is that field's outline. It defaults to `input`.
|
|
156
|
+
*
|
|
157
|
+
* Together they are the one choice a theme makes about how a field reads:
|
|
158
|
+
* outlined on the card (the default), or filled — `--theme-field: var(--muted)`
|
|
159
|
+
* with `--theme-field-border: transparent` — the borderless grey well some
|
|
160
|
+
* product UIs use for every input. Every field wears the pair through
|
|
161
|
+
* lib/field-shell, so the choice reaches them all at once instead of being
|
|
162
|
+
* restyled per control.
|
|
163
|
+
*/
|
|
164
|
+
declare const FIELD_TOKENS: readonly ["field", "field-border"];
|
|
165
|
+
/**
|
|
166
|
+
* The ground of an inset app frame — derived, with a `--theme-*` hook.
|
|
167
|
+
*
|
|
168
|
+
* `<AppShell frame="inset">` lifts main into a rounded panel and paints the
|
|
169
|
+
* rail and the gap around it as one surface. That surface cannot simply be
|
|
170
|
+
* `sidebar`: most themes paint the rail the page's own colour, so the panel
|
|
171
|
+
* sat on a ground of the same colour and only its hairline told them apart.
|
|
172
|
+
* `shell-ground` is one small step off the rail toward its ink, so the panel
|
|
173
|
+
* reads as lifted in every theme, and a theme sets `--theme-shell-ground` for
|
|
174
|
+
* a stronger or tinted ground.
|
|
175
|
+
*/
|
|
176
|
+
declare const APP_SHELL_TOKENS: readonly ["shell-ground"];
|
|
177
|
+
/**
|
|
178
|
+
* Status and tone colours at GRAPHIC strength — derived, with `--theme-*` hooks.
|
|
179
|
+
*
|
|
180
|
+
* Each status colour already comes in two: the fill, which carries a label
|
|
181
|
+
* (dark enough for white ink at 4.5:1 in light mode), and `-text`, the colour
|
|
182
|
+
* as ink on a surface. Neither is right for a mark that carries no text — a
|
|
183
|
+
* meter's fill, a segmented bar, a status dot, a chart series, the soft tint
|
|
184
|
+
* behind a badge. Those were painted with the label fill, so in light mode
|
|
185
|
+
* every one of them was a deep brown, forest green or plum: muddy, and 5–9:1
|
|
186
|
+
* against the page when a graphic needs far less.
|
|
187
|
+
*
|
|
188
|
+
* A mark is the same hue lifted and saturated. It is held to a VISIBILITY floor
|
|
189
|
+
* of 2:1 against `background` and `card` (NON_TEXT_HARD_FLOOR, the `graphic`
|
|
190
|
+
* tier in contrast.ts), not to a text ratio: every mark that uses it sits
|
|
191
|
+
* beside text that states the same thing (a meter's reading, a dot's label).
|
|
192
|
+
* A graphic that is the ONLY carrier of its meaning still owes WCAG 1.4.11's
|
|
193
|
+
* 3:1 — use the fill, or add the text.
|
|
194
|
+
*/
|
|
195
|
+
declare const MARK_TOKENS: readonly ["destructive-mark", "success-mark", "tone-warning-mark", "tone-info-mark", "tone-1-mark", "tone-2-mark", "tone-3-mark"];
|
|
150
196
|
/**
|
|
151
197
|
* UI type tiers (mode-independent) — the chrome typography register.
|
|
152
198
|
*
|
|
@@ -193,4 +239,4 @@ interface ThemeManifest {
|
|
|
193
239
|
};
|
|
194
240
|
}
|
|
195
241
|
|
|
196
|
-
export {
|
|
242
|
+
export { APP_SHELL_TOKENS as A, DENSITY_TOKENS as D, FIELD_TOKENS as F, MARK_TOKENS as M, PALETTE_TOKENS as P, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, ART_TOKENS as b, type DensityToken as c, MOTION_TOKENS as d, type MotionToken as e, RHYTHM_TOKENS as f, type RadiusToken as g, type RhythmToken as h, type SemanticToken as i, TYPE_OVERLINE_TOKENS as j, TYPE_REFINEMENT_TOKENS as k, TYPE_SCALE_TOKENS as l, TYPE_UI_TOKENS as m, type TypeOverlineToken as n, type TypeRefinementToken as o, type TypeScaleToken as p, type TypeUiToken as q, type WorkspaceSurfaceToken as r };
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -170,6 +170,17 @@ declare function MediaPlayerBlock({ className, framed, onPlayingChange, onPrevio
|
|
|
170
170
|
|
|
171
171
|
declare const mediaPlayerBlockMeta: BlockMeta;
|
|
172
172
|
|
|
173
|
+
interface MessageReactionsBlockProps {
|
|
174
|
+
className?: string;
|
|
175
|
+
/** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
|
|
176
|
+
framed?: boolean;
|
|
177
|
+
/** Called when the reader adds or removes a reaction, with the message, the emoji and whether it is now on. */
|
|
178
|
+
onReact?: (messageId: string, emoji: string, on: boolean) => void;
|
|
179
|
+
}
|
|
180
|
+
declare function MessageReactionsBlock({ className, framed, onReact }: MessageReactionsBlockProps): react.JSX.Element;
|
|
181
|
+
|
|
182
|
+
declare const messageReactionsBlockMeta: BlockMeta;
|
|
183
|
+
|
|
173
184
|
interface NewsletterBlockProps {
|
|
174
185
|
className?: string;
|
|
175
186
|
/** false drops the card's surface and border — for a footer or muted section that already frames it. */
|
|
@@ -408,4 +419,4 @@ declare function WalletBlock({ className, framed, onSend, onReceive }: WalletBlo
|
|
|
408
419
|
|
|
409
420
|
declare const walletBlockMeta: BlockMeta;
|
|
410
421
|
|
|
411
|
-
export { ApiKeysBlock, ApprovalBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, NewsletterBlock, NotFoundBlock, NotificationListBlock, NotificationSettingsBlock, OrderTrackingBlock, PricingCardBlock, PricingTableBlock, ProductCardBlock, ProfileCardBlock, ProfileFormBlock, RevenueBreakdownBlock, SignInBlock, SignUpBlock, SiteFooterBlock, StatRowBlock, StepsBlock, TaskListBlock, TeamMembersBlock, TestimonialBlock, TrafficSourcesBlock, TrafficTrendBlock, UploadListBlock, VerifyCodeBlock, WalletBlock, apiKeysBlockMeta, approvalBlockMeta, chatThreadBlockMeta, commandPaletteBlockMeta, ctaBandBlockMeta, dangerZoneBlockMeta, deviceComparisonBlockMeta, eventCardBlockMeta, faqBlockMeta, featureGridBlockMeta, feedbackFormBlockMeta, goalProgressBlockMeta, heroBlockMeta, inviteTeamBlockMeta, invoiceHistoryBlockMeta, kpiSparklineBlockMeta, mediaPlayerBlockMeta, newsletterBlockMeta, notFoundBlockMeta, notificationListBlockMeta, notificationSettingsBlockMeta, orderTrackingBlockMeta, pricingCardBlockMeta, pricingTableBlockMeta, productCardBlockMeta, profileCardBlockMeta, profileFormBlockMeta, revenueBreakdownBlockMeta, signInBlockMeta, signUpBlockMeta, siteFooterBlockMeta, statRowBlockMeta, stepsBlockMeta, taskListBlockMeta, teamMembersBlockMeta, testimonialBlockMeta, trafficSourcesBlockMeta, trafficTrendBlockMeta, uploadListBlockMeta, verifyCodeBlockMeta, walletBlockMeta };
|
|
422
|
+
export { ApiKeysBlock, ApprovalBlock, ChatThreadBlock, CommandPaletteBlock, CtaBandBlock, DangerZoneBlock, DeviceComparisonBlock, EventCardBlock, FaqBlock, FeatureGridBlock, FeedbackFormBlock, GoalProgressBlock, HeroBlock, InviteTeamBlock, InvoiceHistoryBlock, KpiSparklineBlock, MediaPlayerBlock, MessageReactionsBlock, NewsletterBlock, NotFoundBlock, NotificationListBlock, NotificationSettingsBlock, OrderTrackingBlock, PricingCardBlock, PricingTableBlock, ProductCardBlock, ProfileCardBlock, ProfileFormBlock, RevenueBreakdownBlock, SignInBlock, SignUpBlock, SiteFooterBlock, StatRowBlock, StepsBlock, TaskListBlock, TeamMembersBlock, TestimonialBlock, TrafficSourcesBlock, TrafficTrendBlock, UploadListBlock, VerifyCodeBlock, WalletBlock, apiKeysBlockMeta, approvalBlockMeta, chatThreadBlockMeta, commandPaletteBlockMeta, ctaBandBlockMeta, dangerZoneBlockMeta, deviceComparisonBlockMeta, eventCardBlockMeta, faqBlockMeta, featureGridBlockMeta, feedbackFormBlockMeta, goalProgressBlockMeta, heroBlockMeta, inviteTeamBlockMeta, invoiceHistoryBlockMeta, kpiSparklineBlockMeta, mediaPlayerBlockMeta, messageReactionsBlockMeta, newsletterBlockMeta, notFoundBlockMeta, notificationListBlockMeta, notificationSettingsBlockMeta, orderTrackingBlockMeta, pricingCardBlockMeta, pricingTableBlockMeta, productCardBlockMeta, profileCardBlockMeta, profileFormBlockMeta, revenueBreakdownBlockMeta, signInBlockMeta, signUpBlockMeta, siteFooterBlockMeta, statRowBlockMeta, stepsBlockMeta, taskListBlockMeta, teamMembersBlockMeta, testimonialBlockMeta, trafficSourcesBlockMeta, trafficTrendBlockMeta, uploadListBlockMeta, verifyCodeBlockMeta, walletBlockMeta };
|
package/dist/blocks/index.js
CHANGED
|
@@ -33,6 +33,8 @@ import { KpiSparklineBlock } from "./kpi-sparkline-block.js";
|
|
|
33
33
|
import { kpiSparklineBlockMeta } from "./kpi-sparkline-block.meta.js";
|
|
34
34
|
import { MediaPlayerBlock } from "./media-player-block.js";
|
|
35
35
|
import { mediaPlayerBlockMeta } from "./media-player-block.meta.js";
|
|
36
|
+
import { MessageReactionsBlock } from "./message-reactions-block.js";
|
|
37
|
+
import { messageReactionsBlockMeta } from "./message-reactions-block.meta.js";
|
|
36
38
|
import { NewsletterBlock } from "./newsletter-block.js";
|
|
37
39
|
import { newsletterBlockMeta } from "./newsletter-block.meta.js";
|
|
38
40
|
import { NotFoundBlock } from "./not-found-block.js";
|
|
@@ -99,6 +101,7 @@ export {
|
|
|
99
101
|
InvoiceHistoryBlock,
|
|
100
102
|
KpiSparklineBlock,
|
|
101
103
|
MediaPlayerBlock,
|
|
104
|
+
MessageReactionsBlock,
|
|
102
105
|
NewsletterBlock,
|
|
103
106
|
NotFoundBlock,
|
|
104
107
|
NotificationListBlock,
|
|
@@ -140,6 +143,7 @@ export {
|
|
|
140
143
|
invoiceHistoryBlockMeta,
|
|
141
144
|
kpiSparklineBlockMeta,
|
|
142
145
|
mediaPlayerBlockMeta,
|
|
146
|
+
messageReactionsBlockMeta,
|
|
143
147
|
newsletterBlockMeta,
|
|
144
148
|
notFoundBlockMeta,
|
|
145
149
|
notificationListBlockMeta,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/blocks/message-reactions-block.tsx
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Bubble, BubbleReaction, BubbleReactionAdd, BubbleReactions } from "../components/bubble/index.js";
|
|
6
|
+
import { Card, CardContent, CardFooter } from "../components/card/index.js";
|
|
7
|
+
import {
|
|
8
|
+
ContextMenu,
|
|
9
|
+
ContextMenuContent,
|
|
10
|
+
ContextMenuItem,
|
|
11
|
+
ContextMenuSeparator,
|
|
12
|
+
ContextMenuTrigger
|
|
13
|
+
} from "../components/context-menu/index.js";
|
|
14
|
+
import { Message } from "../components/message/index.js";
|
|
15
|
+
import { DEFAULT_REACTIONS, MenuReactions } from "../components/reaction-picker/index.js";
|
|
16
|
+
import { cn } from "../lib/cn.js";
|
|
17
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
var messages = [
|
|
19
|
+
{ id: "m1", text: "The new onboarding flow is live on staging \u{1F680}", mine: false, reactions: { "\u{1F389}": 2, "\u{1F440}": 1 } },
|
|
20
|
+
{ id: "m2", text: "Clicked through it twice. The progress bar is so smooth.", mine: true, reactions: {} },
|
|
21
|
+
{ id: "m3", text: "Ship it Friday?", mine: false, reactions: { "\u{1F44D}": 1 } }
|
|
22
|
+
];
|
|
23
|
+
var hint = "Right-click or long-press a message to react.";
|
|
24
|
+
var labels = {
|
|
25
|
+
...Object.fromEntries(DEFAULT_REACTIONS.map((r) => [r.emoji, r.label])),
|
|
26
|
+
"\u{1F440}": "Eyes"
|
|
27
|
+
};
|
|
28
|
+
function MessageReactionsBlock({ className, framed = true, onReact }) {
|
|
29
|
+
const [mine, setMine] = useState({ m1: ["\u{1F389}"] });
|
|
30
|
+
const toggle = (messageId, emoji) => {
|
|
31
|
+
const on = !(mine[messageId] ?? []).includes(emoji);
|
|
32
|
+
setMine((prev) => {
|
|
33
|
+
const current = prev[messageId] ?? [];
|
|
34
|
+
const next = current.includes(emoji) ? current.filter((e) => e !== emoji) : [...current, emoji];
|
|
35
|
+
return { ...prev, [messageId]: next };
|
|
36
|
+
});
|
|
37
|
+
onReact?.(messageId, emoji, on);
|
|
38
|
+
};
|
|
39
|
+
return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-sm", className), children: [
|
|
40
|
+
/* @__PURE__ */ jsx(CardContent, { className: "flex flex-col gap-3", children: messages.map((m) => {
|
|
41
|
+
const align = m.mine ? "end" : "start";
|
|
42
|
+
const chosen = mine[m.id] ?? [];
|
|
43
|
+
const counts = { ...m.reactions };
|
|
44
|
+
for (const emoji of chosen) counts[emoji] = counts[emoji] ?? 0;
|
|
45
|
+
const pills = Object.entries(counts).map(([emoji, others]) => ({
|
|
46
|
+
emoji,
|
|
47
|
+
active: chosen.includes(emoji),
|
|
48
|
+
count: others + (chosen.includes(emoji) ? 1 : 0)
|
|
49
|
+
}));
|
|
50
|
+
return /* @__PURE__ */ jsxs(Message, { messageId: m.id, align: m.mine ? "end" : void 0, children: [
|
|
51
|
+
/* @__PURE__ */ jsxs(ContextMenu, { children: [
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
ContextMenuTrigger,
|
|
54
|
+
{
|
|
55
|
+
render: /* @__PURE__ */ jsx(Bubble, { tabIndex: 0, variant: m.mine ? "primary" : void 0, align }),
|
|
56
|
+
children: m.text
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
/* @__PURE__ */ jsxs(ContextMenuContent, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(MenuReactions, { selected: chosen, onSelect: (emoji) => toggle(m.id, emoji) }),
|
|
61
|
+
/* @__PURE__ */ jsx(ContextMenuSeparator, {}),
|
|
62
|
+
/* @__PURE__ */ jsx(ContextMenuItem, { children: "Reply" }),
|
|
63
|
+
/* @__PURE__ */ jsx(ContextMenuItem, { children: "Copy text" })
|
|
64
|
+
] })
|
|
65
|
+
] }),
|
|
66
|
+
/* @__PURE__ */ jsxs(BubbleReactions, { align, children: [
|
|
67
|
+
pills.map((p) => /* @__PURE__ */ jsx(
|
|
68
|
+
BubbleReaction,
|
|
69
|
+
{
|
|
70
|
+
emoji: p.emoji,
|
|
71
|
+
label: labels[p.emoji],
|
|
72
|
+
count: p.count,
|
|
73
|
+
active: p.active,
|
|
74
|
+
onClick: () => toggle(m.id, p.emoji)
|
|
75
|
+
},
|
|
76
|
+
p.emoji
|
|
77
|
+
)),
|
|
78
|
+
pills.length > 0 ? /* @__PURE__ */ jsx(BubbleReactionAdd, { selected: chosen, onSelect: (emoji) => toggle(m.id, emoji) }) : null
|
|
79
|
+
] })
|
|
80
|
+
] }, m.id);
|
|
81
|
+
}) }),
|
|
82
|
+
/* @__PURE__ */ jsx(CardFooter, { className: "text-caption text-muted-foreground", children: hint })
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
MessageReactionsBlock
|
|
87
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/blocks/message-reactions-block.meta.ts
|
|
2
|
+
var messageReactionsBlockMeta = {
|
|
3
|
+
name: "MessageReactionsBlock",
|
|
4
|
+
family: "Social & messaging",
|
|
5
|
+
kind: "card",
|
|
6
|
+
width: { min: "18rem", max: "24rem" },
|
|
7
|
+
summary: "A short thread you can react to: right-click or long-press a bubble for a menu with a row of quick reactions above Reply and Copy, or use the add-reaction pill under a message. A new reaction blooms in and sprays copies of its emoji.",
|
|
8
|
+
intent: "Reach for this when a conversation excerpt should be reactable \u2014 a team thread, a comment card, a support transcript. A read-only excerpt is ChatThreadBlock; a working chat with a composer and history is the ChatPanel page shell.",
|
|
9
|
+
notes: [
|
|
10
|
+
"Two ways in, on purpose: the right-click / long-press menu is fast once you know it is there, and the add-reaction pill is how a first-time reader finds reactions at all.",
|
|
11
|
+
"The quick row in the menu is MenuReactions (checkbox menu items) and the one in the pill's popover is ReactionPicker (a toolbar), so each is reachable by the keyboard model of the surface it sits in.",
|
|
12
|
+
"Only the reader's own reaction celebrates. Counts from other people roll quietly, and the reactions a page loads with never play.",
|
|
13
|
+
"The add-reaction pill appears only on messages that already have reactions; a message with none is reacted to from its menu, which keeps quiet rows quiet."
|
|
14
|
+
],
|
|
15
|
+
a11y: [
|
|
16
|
+
"Each bubble is focusable, and its menu opens on right click, long press, Shift+F10 or the Menu key; its reactions are checkbox items reporting whether the reader has used each one.",
|
|
17
|
+
'Each reaction pill is a toggle named by its meaning and count ("Party, 3") with `aria-pressed`; the emoji and the burst are hidden from assistive tech.',
|
|
18
|
+
"Under reduced motion the reaction appears and the count changes at once; nothing else is lost."
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
export {
|
|
22
|
+
messageReactionsBlockMeta
|
|
23
|
+
};
|
|
@@ -11,6 +11,7 @@ var activity = [
|
|
|
11
11
|
{ title: "Refund", detail: "Order 1042 \xB7 Yesterday", amount: "-24.00", tone: "2" },
|
|
12
12
|
{ title: "Payout", detail: "To checking \xB7 Mon", amount: "-850.00", tone: "3" }
|
|
13
13
|
];
|
|
14
|
+
var TONE_MARK = { "1": "bg-tone-1-mark", "2": "bg-tone-2-mark", "3": "bg-tone-3-mark" };
|
|
14
15
|
function WalletBlock({ className, framed = true, onSend, onReceive }) {
|
|
15
16
|
return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs", className), children: [
|
|
16
17
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
@@ -30,8 +31,7 @@ function WalletBlock({ className, framed = true, onSend, onReceive }) {
|
|
|
30
31
|
"span",
|
|
31
32
|
{
|
|
32
33
|
"aria-hidden": true,
|
|
33
|
-
className: "block size-8 rounded-full",
|
|
34
|
-
style: { background: `oklch(from var(--tone-${item.tone}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h)` }
|
|
34
|
+
className: cn("block size-8 rounded-full", TONE_MARK[item.tone])
|
|
35
35
|
}
|
|
36
36
|
),
|
|
37
37
|
title: item.title,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/app-shell/app-shell-frame.tsx
|
|
4
|
+
import { createContext, useContext } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var AppShellFrameContext = createContext(null);
|
|
7
|
+
function AppShellFrameProvider({ frame, children }) {
|
|
8
|
+
return /* @__PURE__ */ jsx(AppShellFrameContext.Provider, { value: frame, children });
|
|
9
|
+
}
|
|
10
|
+
function useAppShellFrame() {
|
|
11
|
+
return useContext(AppShellFrameContext);
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
AppShellFrameProvider,
|
|
15
|
+
useAppShellFrame
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/app-shell/app-shell-main.tsx
|
|
4
|
+
import { useScrollActivity } from "../../lib/use-scroll-activity.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
function AppShellMain({ scrolls, ...props }) {
|
|
7
|
+
const trackScroll = useScrollActivity();
|
|
8
|
+
return /* @__PURE__ */ jsx("main", { ref: scrolls ? trackScroll : void 0, ...props });
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
AppShellMain
|
|
12
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/components/app-shell/app-shell.tsx
|
|
2
2
|
import { cn } from "../../lib/cn.js";
|
|
3
3
|
import { AppShellDock } from "./app-shell-dock.js";
|
|
4
|
+
import { AppShellFrameProvider } from "./app-shell-frame.js";
|
|
5
|
+
import { AppShellMain } from "./app-shell-main.js";
|
|
4
6
|
import { SkipLink } from "./skip-link.js";
|
|
5
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
8
|
function AppShell({
|
|
@@ -10,18 +12,27 @@ function AppShell({
|
|
|
10
12
|
bottomNav,
|
|
11
13
|
dock,
|
|
12
14
|
overlays,
|
|
15
|
+
frame = "flush",
|
|
13
16
|
className,
|
|
14
17
|
children,
|
|
15
18
|
...rest
|
|
16
19
|
}) {
|
|
20
|
+
const inset = frame === "inset";
|
|
17
21
|
return /* @__PURE__ */ jsxs(
|
|
18
22
|
"div",
|
|
19
23
|
{
|
|
24
|
+
"data-frame": inset ? "inset" : void 0,
|
|
20
25
|
className: cn(
|
|
21
26
|
// `overflow-x-clip` (not `-hidden`) contains horizontal bleed WITHOUT
|
|
22
27
|
// establishing a scroll container — `-hidden` forces `overflow-y: auto`,
|
|
23
28
|
// which breaks `position: sticky` on a descendant Sidebar.
|
|
24
29
|
"flex min-h-screen flex-col overflow-x-clip bg-background text-foreground",
|
|
30
|
+
// Inset, from lg: the shell IS the viewport and never scrolls — main
|
|
31
|
+
// does. The ground is the rail's surface, so the rail and the gap
|
|
32
|
+
// around the panel read as one plane. A page-surface rail (Sidebar
|
|
33
|
+
// surface="page") is page-coloured, so the ground follows it and the
|
|
34
|
+
// panel's border carries the lift.
|
|
35
|
+
inset && "lg:h-dvh lg:min-h-0 lg:bg-shell-ground lg:[&:has(>[data-app-shell-body]>[data-surface=page])]:bg-background",
|
|
25
36
|
className
|
|
26
37
|
),
|
|
27
38
|
...rest,
|
|
@@ -29,16 +40,29 @@ function AppShell({
|
|
|
29
40
|
/* @__PURE__ */ jsx(SkipLink, {}),
|
|
30
41
|
banners ? /* @__PURE__ */ jsx("div", { className: "relative z-(--z-nav)", children: banners }) : null,
|
|
31
42
|
header,
|
|
32
|
-
/* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1", children: [
|
|
33
|
-
sidebar,
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { "data-app-shell-body": inset ? "" : void 0, className: "flex min-h-0 flex-1", children: [
|
|
44
|
+
inset ? /* @__PURE__ */ jsx(AppShellFrameProvider, { frame: "inset", children: sidebar }) : sidebar,
|
|
34
45
|
/* @__PURE__ */ jsx(
|
|
35
|
-
|
|
46
|
+
AppShellMain,
|
|
36
47
|
{
|
|
37
48
|
id: "main-content",
|
|
49
|
+
scrolls: inset,
|
|
50
|
+
tabIndex: inset ? -1 : void 0,
|
|
38
51
|
style: { containerType: "inline-size" },
|
|
39
52
|
className: cn(
|
|
40
53
|
"min-w-0 flex-1",
|
|
41
|
-
bottomNav && "pb-[calc(var(--nav-height)+env(safe-area-inset-bottom,0px))] lg:pb-0"
|
|
54
|
+
bottomNav && "pb-[calc(var(--nav-height)+env(safe-area-inset-bottom,0px))] lg:pb-0",
|
|
55
|
+
// The inset panel. A spacing-step gap on the open sides (the rail
|
|
56
|
+
// is the leading one). Banners and the dock are in flow in the
|
|
57
|
+
// shell's column, so the bottom gap already sits above a dock.
|
|
58
|
+
// Sticky children stick to the panel's top, and the overflow
|
|
59
|
+
// clips them to its rounded corners. A resting surface: a hairline
|
|
60
|
+
// edge and the subtlest raise, never a floating shadow.
|
|
61
|
+
inset && cn(
|
|
62
|
+
"outline-none lg:m-2 lg:overflow-y-auto lg:overscroll-contain scrollbar-quiet",
|
|
63
|
+
"lg:rounded-lg lg:border lg:border-border lg:bg-background lg:shadow-sm",
|
|
64
|
+
sidebar && "lg:ms-0"
|
|
65
|
+
)
|
|
42
66
|
),
|
|
43
67
|
children
|
|
44
68
|
}
|