@sableui/react 0.4.0 → 0.6.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/AGENTS.md +2 -2
- package/CHANGELOG.md +189 -0
- package/CONTRIBUTING.md +1 -1
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/bin/sable-lint.mjs +2 -1
- package/bin/sable.mjs +3 -2
- package/dist/{_contract-Cn3lUpih.d.ts → _contract-C2M1SjBf.d.ts} +20 -2
- package/dist/blocks/index.js +21 -53
- package/dist/{chunk-CCUDFLU3.js → chunk-7QSDYBXO.js} +652 -38
- package/dist/{chunk-FBSL2LZQ.js → chunk-EDXLM7YL.js} +11 -30
- package/dist/chunk-FKZXAFIY.js +12 -0
- package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
- package/dist/{chunk-NGBDP3BM.js → chunk-QJ6FWTUG.js} +1 -1
- package/dist/{chunk-D2ETQBHV.js → chunk-VVW4UCDI.js} +5 -5
- package/dist/{chunk-TH563WLW.js → chunk-XAZGMY73.js} +10 -3
- package/dist/foundations/index.js +6 -0
- package/dist/icon/index.js +2 -2
- package/dist/icon-sets/brands.d.ts +0 -27
- package/dist/icon-sets/brands.js +7 -1
- package/dist/icon-sets/reicon.js +1 -1
- package/dist/index.d.ts +164 -23
- package/dist/index.js +116 -29
- package/dist/ios/index.d.ts +6 -1
- package/dist/ios/index.js +4 -3
- package/dist/ios/styles.css +9 -0
- package/dist/patterns/index.js +24 -30
- package/dist/presets/index.d.ts +108 -5
- package/dist/presets/index.js +239 -6
- package/dist/styles.css +57 -0
- package/dist/{theme-provider-CGeiusgu.d.ts → theme-provider-kQszHYji.d.ts} +1 -1
- package/dist/themes/atelier/atelier.css +31 -3
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +38 -7
- 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 +33 -4
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +28 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +26 -0
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +24 -11
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +34 -4
- package/llms/blocks/danger-zone-block.md +1 -1
- package/llms/blocks/event-card-block.md +1 -1
- package/llms/blocks/feedback-form-block.md +1 -1
- package/llms/blocks/invite-team-block.md +1 -1
- package/llms/blocks/media-player-block.md +3 -16
- package/llms/blocks/notification-list-block.md +3 -3
- package/llms/blocks/product-card-block.md +3 -17
- package/llms/blocks/profile-card-block.md +4 -17
- package/llms/blocks/profile-form-block.md +2 -2
- package/llms/blocks/team-members-block.md +1 -1
- package/llms/blocks/testimonial-block.md +1 -1
- package/llms/components/avatar.md +13 -1
- package/llms/components/card.md +1 -1
- package/llms/components/gradient-art.md +80 -0
- package/llms/components/sheet.md +1 -1
- package/llms/components/tabs.md +1 -1
- package/llms/components/toast.md +12 -1
- package/llms/foundations/accessibility.md +1 -0
- package/llms/patterns/auth-page.md +5 -19
- package/llms/patterns/brand-landing-page.md +1 -1
- package/llms/patterns/chat-panel.md +1 -1
- package/llms/patterns/dashboard-page.md +4 -4
- package/llms/patterns/feed-page.md +4 -4
- package/llms/patterns/landing-page.md +1 -1
- package/llms/patterns/list-detail-page.md +3 -2
- package/llms/patterns/settings-page.md +3 -3
- package/llms/reference/token-intent.md +9 -1
- package/llms/tasks/auditing-an-app.md +6 -0
- package/llms/tasks/building-an-ios-prototype.md +3 -1
- package/llms/tasks/converting-an-app.md +1 -1
- package/llms/tasks/creating-a-preset.md +24 -0
- package/llms/tasks/reviewing-a-design.md +9 -0
- package/llms/tasks/using-presets.md +6 -1
- package/llms-full.txt +214 -106
- package/llms.txt +4 -3
- package/package.json +9 -7
- package/registry.json +310 -32
- package/scripts/design-presets.mjs +27 -4
- package/scripts/grade.mjs +2 -1
- package/scripts/label-contrast.mjs +138 -0
- package/scripts/lint-rules.mjs +23 -2
- package/skills/sable/SKILL.md +3 -1
- package/targets/ios.json +18 -2
- package/third-party/paper-shaders-LICENSE.txt +201 -0
- package/third-party/paper-shaders-NOTICE.txt +5 -0
package/AGENTS.md
CHANGED
|
@@ -45,11 +45,11 @@ These are short on purpose. Each links to a foundations doc with the full vocabu
|
|
|
45
45
|
6. **Motion has a reason and a recipe.** Apply a named `motion-*` recipe; drop to `duration-(--motion-duration-*)` + `ease-*` tokens only when none fits. Never Tailwind's `animate-*`, never a raw `duration-150`. Exits are faster than entrances and never overshoot. → `sable docs motion`
|
|
46
46
|
7. **Icons by semantic name.** `<Icon name="check" />`; size with `text-{size}`, never type utilities. A mark attached to a line of type (dot, bullet, leading icon) sits in an `h-[1lh] items-center` box and is sized in em, never nudged with px. → `sable docs typography`
|
|
47
47
|
8. **Every scroll container declares a policy:** `scrollbar-quiet` (vertical) or `scrollbar-hidden` (horizontal, with an edge fade carrying the affordance).
|
|
48
|
-
9. **Accessible by construction.** WCAG AA contrast, 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
|
|
48
|
+
9. **Accessible by construction.** WCAG AA contrast (and a label on a brand or status fill also clears APCA Lc 60 — `label-contrast`), 44px hit areas, visible focus, logical properties, live regions that exist before they speak. → `sable docs accessibility`
|
|
49
49
|
|
|
50
50
|
## Customizing: themes are presets
|
|
51
51
|
|
|
52
|
-
Pick a theme as a starting point, then override tokens on top of it. Every token a theme sets can be reset by the app on `:root` (or any subtree) after the theme CSS loads — radius, density, type scale, motion scale, accent — and components follow, because they only read tokens. The docs site's **Customize** panel does exactly this live and exports the result as CSS; paste it after your Sable imports. Retrieve the overridable set with `npx sable token --list`.
|
|
52
|
+
Pick a theme as a starting point, then override tokens on top of it. Every token a theme sets can be reset by the app on `:root` (or any subtree) after the theme CSS loads — radius, density, type scale, motion scale, accent — and components follow, because they only read tokens. The docs site's **Customize** panel does exactly this live and exports the result as CSS; paste it after your Sable imports. Retrieve the overridable set with `npx sable token --list`. When you retune a filled role (`--primary`, `--sidebar-primary`, `--highlight`, `--destructive`, `--success`, `--tone-warning`, `--tone-info`), check the label on it: `sable lint` measures the pair against the installed theme and suggests one that reads. If the pair is a deliberate brand choice, keep it with `/* design-lint-disable-next-line label-contrast -- <reason> */`. Status colours have a second token for the colour drawn on a surface — `--destructive-text`, `--success-text`, `--tone-warning-text`, `--tone-info-text` — which `text-`, `border-` and `ring-<status>` read; only `bg-<status>` reads the fill. Retune both.
|
|
53
53
|
|
|
54
54
|
## Wire it up
|
|
55
55
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,194 @@
|
|
|
1
1
|
# What's new
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Art from the theme
|
|
6
|
+
|
|
7
|
+
- **`GradientArt`, placeholder artwork painted by a shader.** A static WebGL
|
|
8
|
+
mesh gradient (Paper Shaders' static mesh gradient; `variant="grain"` for
|
|
9
|
+
their grain gradient) painted from the theme, for covers, banners, tiles and
|
|
10
|
+
avatars with no image yet. `seed` fixes each item's composition and leading
|
|
11
|
+
colour, so a row of covers or avatars stays individually recognisable in
|
|
12
|
+
every theme; children render above the art. It paints once on mount, on
|
|
13
|
+
resize and when the theme changes, through one WebGL context shared by every
|
|
14
|
+
instance, so a page of avatars costs one context. A CSS gradient stays
|
|
15
|
+
underneath for prerendered pages and browsers without WebGL2.
|
|
16
|
+
- **Each preset has its own art direction.** New optional `--art-*` tokens
|
|
17
|
+
(`art-1` … `art-6`, `art-ground`, `art-warp`, `art-softness`, `art-grain`;
|
|
18
|
+
`ART_TOKENS` in the contract) set the art's palette and character, through
|
|
19
|
+
`--theme-art-*` hooks like the workspace surfaces. Ember burns (fire falling
|
|
20
|
+
off into warm black), Poppy is candy blobs, Prism airbrushed pastels, Verdant
|
|
21
|
+
stained glass, Meridian grainy monochrome, Atelier a dark studio with one
|
|
22
|
+
amber glow; Sable keeps its tones. The colours derive from each theme's own
|
|
23
|
+
tokens where its art direction allows, so a brand change carries into the
|
|
24
|
+
art. Presets may declare the hooks too.
|
|
25
|
+
- **`Avatar variant="art"`.** The fallback paints the initials over
|
|
26
|
+
`GradientArt` seeded by the person (the initials, then `alt`, or `seed`), so
|
|
27
|
+
everyone without a photo gets their own colour and keeps it wherever they
|
|
28
|
+
appear. The default stays `neutral`.
|
|
29
|
+
- **Blocks and patterns use it.** The product card, media player and profile
|
|
30
|
+
card blocks and the auth page paint their placeholder art with
|
|
31
|
+
`GradientArt` instead of hand-written CSS gradients, and the avatars across
|
|
32
|
+
the blocks and page patterns use `variant="art"`.
|
|
33
|
+
- **New dependency:** `@paper-design/shaders` 0.0.81 (Apache-2.0), pinned
|
|
34
|
+
exactly while it is pre-1.0. Only the two fragment shaders and the noise
|
|
35
|
+
texture are imported; the notice is in `THIRD_PARTY_NOTICES.md`.
|
|
36
|
+
|
|
37
|
+
### Labels on filled colours
|
|
38
|
+
|
|
39
|
+
- **Labels on brand and status fills now have to pass APCA as well as WCAG.** A
|
|
40
|
+
filled control's label is held to 4.5:1 and APCA Lc 60 in both modes, for
|
|
41
|
+
`primary`, `sidebar-primary`, `highlight`, `destructive`, `success`,
|
|
42
|
+
`tone-warning` and `tone-info`. The ratio alone let dark ink on a bright red
|
|
43
|
+
through at Lc 38–46, which reads muddy. Every shipped theme is tested against
|
|
44
|
+
both floors, with no exceptions.
|
|
45
|
+
- **Status colours have a separate token for text.** `--destructive-text`,
|
|
46
|
+
`--success-text`, `--tone-warning-text` and `--tone-info-text`.
|
|
47
|
+
`text-destructive`, `border-destructive`, `ring-destructive`, `outline-`,
|
|
48
|
+
`decoration-`, `fill-` and `stroke-` now read the `-text` token; only
|
|
49
|
+
`bg-destructive` reads the fill. The class names are unchanged. A dark-mode
|
|
50
|
+
fill can now sit low enough for a white label while error text stays light
|
|
51
|
+
enough to read on a dark card. Badge's soft status tones and the destructive
|
|
52
|
+
Menu item mix their ink from the `-text` token too: from the fill, Meridian's
|
|
53
|
+
dark destructive menu item measured 3.8:1; every theme now clears 4.5:1.
|
|
54
|
+
**Migration:** if your app retunes `--destructive` (or `--success`,
|
|
55
|
+
`--tone-warning`, `--tone-info`), set its `-text` as well, or text, borders
|
|
56
|
+
and rings keep the theme's colour. Presets saved on 0.5 keep working: a
|
|
57
|
+
missing `-text` token takes its fill, which is what text read before.
|
|
58
|
+
- **Destructive buttons have white labels in dark mode.** In Sable, Atelier,
|
|
59
|
+
Ember, Meridian and Verdant the dark destructive fill moved down (Sable
|
|
60
|
+
68%→59%, Meridian 66%→55%, Atelier 66%→58.5%, Verdant 64%→57.5%, Ember
|
|
61
|
+
62%→58%) and its label went from dark to white. Error text reads the lighter
|
|
62
|
+
`-text` value, so it is no darker than before.
|
|
63
|
+
- **Dark highlights have white labels in Sable and Ember.** Sable's dark
|
|
64
|
+
`--highlight` went from 70% to 57% and Ember's from 65.2% to 58%; dark ink on
|
|
65
|
+
the brighter fill read at Lc 42–46.
|
|
66
|
+
- **Small status fixes.** In Ember, Atelier, Meridian and Verdant, dark
|
|
67
|
+
`success` and `tone-info` are a little lighter so the dark label clears Lc
|
|
68
|
+
60. In light mode, Sable's `text-tone-info` is one step darker and Verdant's
|
|
69
|
+
`text-tone-warning` is 51%; both measured just under 4.5:1 on `muted`.
|
|
70
|
+
|
|
71
|
+
### Lint and CLI
|
|
72
|
+
|
|
73
|
+
- **New `sable lint` rule: `label-contrast`.** It finds an app's overrides of a
|
|
74
|
+
brand or status fill or its `-foreground`, completes the pair from the
|
|
75
|
+
installed theme and measures both modes. Findings carry file:line and the
|
|
76
|
+
nearest pair that reads: the fill's lightness moves, the hue stays. Keep a
|
|
77
|
+
deliberate brand pair with `/* design-lint-disable-next-line label-contrast
|
|
78
|
+
-- <reason> */`. `sable grade` runs the same rule. Apps that passed lint on
|
|
79
|
+
0.5 can fail it now.
|
|
80
|
+
- **`sable preset --check` measures labels on fills.** A failing pair stops the
|
|
81
|
+
check with `ERR_PRESET_CONTRAST`, the suggested pair and a ready-to-paste
|
|
82
|
+
`exceptions` entry. Presets accept an optional `exceptions` array (`rule`,
|
|
83
|
+
`mode`, `token`, `reason`); an accepted pair is reported with its reason on
|
|
84
|
+
every check instead of failing. `@sableui/react/presets` exports
|
|
85
|
+
`presetLabelContrast()` and the measuring helpers (`checkLabelledFills`,
|
|
86
|
+
`fitFillToInk`, `inkFor`, `inkPolarity`, `labelContrast`, `LABELLED_FILLS`,
|
|
87
|
+
`BRAND_FILLS`, `STATUS_FILLS`).
|
|
88
|
+
|
|
89
|
+
### Sheets own their actions
|
|
90
|
+
|
|
91
|
+
- **A sheet lays out the actions inside it.** `CardFooter` has an `actions`
|
|
92
|
+
prop for a footer that holds only buttons. Inside a bottom `SheetContent` or
|
|
93
|
+
an `IosSheet`, that footer becomes a full-width stack pinned to the bottom:
|
|
94
|
+
the primary on top, every button at the cta height. On a page nothing
|
|
95
|
+
changes. `SheetFooter` stacks the same way in a bottom sheet, capped at
|
|
96
|
+
`max-w-sm` and centred; side sheets keep the right-aligned row.
|
|
97
|
+
DangerZoneBlock, FeedbackFormBlock and ProfileFormBlock mark their footers.
|
|
98
|
+
- **Only the sheet's own content.** A Dialog, Popover or side Sheet opened from
|
|
99
|
+
inside a bottom sheet presents a page again, so its footer is a row. Only the
|
|
100
|
+
outermost flat Card in a sheet bleeds into the sheet's padding (lining its
|
|
101
|
+
content up with the sheet's title and close button), and only that card's
|
|
102
|
+
footer stacks; a card nested inside it is left alone.
|
|
103
|
+
- **`PresentationProvider`, `usePresentation`** and the `Presentation` type let
|
|
104
|
+
a custom container declare itself a sheet.
|
|
105
|
+
- **`IosSheet background="opaque"`** keeps the floating medium-detent shape but
|
|
106
|
+
fills it solid, like SwiftUI's `.presentationBackground`, for forms whose
|
|
107
|
+
fields would otherwise sit on whatever the glass blurs. In an iOS sheet, a
|
|
108
|
+
form or grid between the content and its action bar keeps its own layout.
|
|
109
|
+
The iOS target is marked beta, and `sable target ios` prints its layout rules.
|
|
110
|
+
|
|
111
|
+
### Components
|
|
112
|
+
|
|
113
|
+
- **Tab triggers space a badge or icon from the label.** Pill and underline
|
|
114
|
+
`TabsTrigger` use `--control-gap`, as Button does; the label's trimmed span
|
|
115
|
+
had dropped a typed space, so a badge touched the text.
|
|
116
|
+
- **NotificationListBlock puts the unread dot before the time**, so the times
|
|
117
|
+
line up on one right edge.
|
|
118
|
+
|
|
119
|
+
### On sableui.com
|
|
120
|
+
|
|
121
|
+
- **Customize was redesigned.** Simple is gestures over previews: the character
|
|
122
|
+
pad, one shape preview for corners and depth together, a live palette strip
|
|
123
|
+
of what the page is painted with, single-line font rows. Advanced is a
|
|
124
|
+
property grid in the workspace register (compact density, `PanelSection`,
|
|
125
|
+
`PropertyRow`/`PropertyPair`, `ScrubField`, `Select`): one exact anchor per
|
|
126
|
+
domain with the ladder it resolves to beneath it. Mode, Reset all and Export
|
|
127
|
+
sit in a footer both views share, and every section resets on its own.
|
|
128
|
+
- **Status colours lean toward the brand within their meaning.** Destructive,
|
|
129
|
+
warning and success each move toward the brand inside a hue range the
|
|
130
|
+
presets already ship (destructive 5–30°, warning 50–90°, success 140–175°),
|
|
131
|
+
warning keeps 30° from destructive, and saturation follows the brand at half
|
|
132
|
+
strength with a floor. Advanced has a Status lean control.
|
|
133
|
+
- **Customize fits labels to brand and status fills**: the label ink is chosen
|
|
134
|
+
once, from the brand as picked, and each mode's fill moves to meet it.
|
|
135
|
+
- The homepage now says what is true: Sable is open source under MIT and
|
|
136
|
+
installs from npm.
|
|
137
|
+
|
|
138
|
+
## 0.5.0
|
|
139
|
+
|
|
140
|
+
- **Stacked toasts.** `<ToastProvider stacked>` stacks toasts like a deck of
|
|
141
|
+
cards: the newest in front, older ones peeking out behind it a step smaller,
|
|
142
|
+
so a burst of toasts no longer takes over the corner of the page. Hovering
|
|
143
|
+
or focusing the stack fans it out into the list. It works at every
|
|
144
|
+
`position`. `ToastContent` is the new part that holds a toast's row; in a
|
|
145
|
+
stacked viewport it hides the cards behind the front one. Collapsed, the
|
|
146
|
+
deck casts one shadow rather than one per card, so a theme with hard offset
|
|
147
|
+
shadows (Poppy) stays clean.
|
|
148
|
+
- **Depth in Customize.** On sableui.com, Customize has a Depth control (Flat ↔
|
|
149
|
+
Lifted) and a Shadow style (Preset, Soft, Crisp, Offset). They write the five
|
|
150
|
+
`--theme-shadow-*` tokens for light and dark, so the CSS export and preset
|
|
151
|
+
download carry them. Depth scales the reach and strength of every step
|
|
152
|
+
together; lifting a flat preset such as Meridian brings in soft shadows. The
|
|
153
|
+
shadow takes the preset's neutral hue. Advanced replaces the slider with
|
|
154
|
+
exact numbers set on the largest step, which the others follow: light angle
|
|
155
|
+
(90° is overhead), distance, blur and opacity. Blur takes one
|
|
156
|
+
blur-to-distance ratio, so an Offset shadow can be softened; opacity is set
|
|
157
|
+
in light mode and dark mode darkens in proportion.
|
|
158
|
+
- **Knob shadows stay on their track.** The Switch thumb, Slider thumb and
|
|
159
|
+
ColorField handle used `shadow-sm`, the elevation step for raised surfaces.
|
|
160
|
+
Where a theme or a Depth setting made that a long offset (Poppy's hard
|
|
161
|
+
drops), the shadow fell outside the switch's track. Knobs now take
|
|
162
|
+
`--knob-shadow`, one small contact shadow that themes can set through
|
|
163
|
+
`--theme-knob-shadow`. In Sable it looks the same as before.
|
|
164
|
+
- **Pill tabs take the theme's button shape.** `TabsList variant="pill"`
|
|
165
|
+
triggers used the small radius, so in Sable they were rounded rectangles
|
|
166
|
+
beside pill buttons. They now read `--button-radius`
|
|
167
|
+
as `Button` does: pills in Sable, 12px in Poppy, square in Meridian, and a
|
|
168
|
+
theme's own button shape wherever it sets one.
|
|
169
|
+
- **CodeBlock no longer scrolls code under its copy button.** Without a
|
|
170
|
+
filename header, the copy button floated over the code, so on a narrow
|
|
171
|
+
screen a long line ran underneath it. It now has its own column beside the
|
|
172
|
+
code. The code's edges fade while there is more to scroll, like `Scroller`'s.
|
|
173
|
+
- **Scroller's edge fades follow content and size changes.** They were measured
|
|
174
|
+
on mount and on scroll only, so a list that grew, shrank or was resized kept
|
|
175
|
+
the fade it started with until it was scrolled.
|
|
176
|
+
- **Icons no longer redraw on every render.** The default Reicon set and the
|
|
177
|
+
brand set rewrote their SVG each time a parent re-rendered (React 19
|
|
178
|
+
re-applies `dangerouslySetInnerHTML` when the object changes). Inside a
|
|
179
|
+
toast that never settled and froze the page.
|
|
180
|
+
- **A card that is only a header keeps its bottom padding.** `CardHeader` leaves
|
|
181
|
+
the bottom padding to the content that follows it; with nothing after it (a
|
|
182
|
+
stat card: label, number, sparkline) the number sat on the card's edge.
|
|
183
|
+
- **Line and area sparklines span their own range.** They used to include zero,
|
|
184
|
+
so a run from 12 to 26 used only the top half of the box: a flat squiggle over
|
|
185
|
+
a solid block. Bar sparklines keep the zero baseline, since a bar's length is
|
|
186
|
+
the value.
|
|
187
|
+
- **Combobox chips are balanced.** The remove button was a full 28px icon
|
|
188
|
+
button, which swelled each chip into a 34px pill and left more room after the
|
|
189
|
+
label than before it. It is now drawn to the chip (20px, with a 14px X) and
|
|
190
|
+
keeps a 28px pointer target, so the label and the X sit evenly in a 23px chip.
|
|
191
|
+
|
|
3
192
|
## 0.4.0
|
|
4
193
|
|
|
5
194
|
### The default theme is now called Sable
|
package/CONTRIBUTING.md
CHANGED
|
@@ -117,7 +117,7 @@ deliberately reverses: `DESIGN.md → Registers`. What a contributor needs to kn
|
|
|
117
117
|
|
|
118
118
|
## Theme contract
|
|
119
119
|
|
|
120
|
-
A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/sable/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
|
|
120
|
+
A theme is a CSS file that defines every token in `themes/_contract.ts` for `:root` (light) and `.dark` (dark), plus an optional TS manifest. Components depend on the **semantic** layer; each theme supplies its own palette underneath. Alternate themes activate through the root `data-sable-theme` attribute; consuming apps should usually let `<SableThemeProvider>` own that wiring and read/update it through `useSableTheme()`. To make a new theme: copy `themes/sable/`, swap the palette + semantic values, keep the token names identical, add the manifest to `themes/registry.ts`, import its CSS from `src/themes.css`, add `./theme/<id>` + `./theme/<id>.css` to both `exports` and `publishConfig.exports`, and cover it in `themes.test.ts` — which holds every label on a brand or status fill (`primary`, `sidebar-primary`, `highlight`, `destructive`, `success`, `tone-warning`, `tone-info`) to 4.5:1 **and** APCA Lc 60 with no exceptions; apps and presets may record exceptions, Sable's own themes may not. Each status colour also has a `-text` token (the colour on a surface, read by `text-`/`border-`/`ring-<status>` through Tailwind's per-property namespaces in styles.css), gated as text on `background`, `card`, `popover` and `muted`. `build-registry` fails on a theme missing from any of those, and generates `registry.json → themes.available` from the manifests — never hand-edit that list. Re-skinning never touches component code.
|
|
121
121
|
|
|
122
122
|
**Themes are presets, not the unit of customization.** Every token a theme sets can also be set by an app (or by the docs customizer) on `:root` after the theme loads. A theme is the starting point a consumer picks; the customizer exports the deltas on top of it. Keep that working: a theme must not set a token with `!important` or under a selector more specific than `[data-sable-theme="<id>"]`, or a consumer override cannot win.
|
|
123
123
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -16,10 +16,21 @@ Third-party software, fonts, and trademarks retain their own terms.
|
|
|
16
16
|
| [React](https://github.com/facebook/react) | MIT | Component runtime |
|
|
17
17
|
| [Reicon](https://github.com/dqev/reicon) | MIT | Default interface icons and bundled brand icons |
|
|
18
18
|
| [Konsta UI](https://github.com/konstaui/konsta) | MIT | Optional native controls for iOS prototypes |
|
|
19
|
+
| [Paper Shaders](https://github.com/paper-design/shaders) | Apache-2.0 | GradientArt's mesh and grain fragment shaders |
|
|
19
20
|
|
|
20
21
|
Library dependencies are external to Sable's library build and carry their
|
|
21
22
|
license notices in their distributions.
|
|
22
23
|
|
|
24
|
+
## Paper Shaders
|
|
25
|
+
|
|
26
|
+
GradientArt renders fragment shaders from `@paper-design/shaders` 0.0.81, a
|
|
27
|
+
dependency. Its vertex shader is adapted from that package's
|
|
28
|
+
`dist/vertex-shader.js` (outputs the two fragment shaders do not read are
|
|
29
|
+
removed) and lives in `src/components/gradient-art/renderer.ts`. Copyright 2026
|
|
30
|
+
Paper, "Powered by Paper Shaders" (https://shaders.paper.design); the license
|
|
31
|
+
and NOTICE are included as `third-party/paper-shaders-LICENSE.txt` and
|
|
32
|
+
`third-party/paper-shaders-NOTICE.txt`.
|
|
33
|
+
|
|
23
34
|
## Brand icons
|
|
24
35
|
|
|
25
36
|
The 23 bundled brand marks come from `reicon-brands` 1.0.2. Sable normalizes them
|
package/bin/sable-lint.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
|
27
27
|
import { dirname, join, relative, resolve, sep } from "node:path"
|
|
28
28
|
import { fileURLToPath } from "node:url"
|
|
29
29
|
import { ignoreGlob, lintText, makeRules, report } from "../scripts/lint-rules.mjs"
|
|
30
|
+
import { labelContrastScanner } from "../scripts/label-contrast.mjs"
|
|
30
31
|
|
|
31
32
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
32
33
|
const PKG_ROOT = join(HERE, "..")
|
|
@@ -53,7 +54,7 @@ const motionTokens = new Set([
|
|
|
53
54
|
REGISTRY.tokens?.motion?.scale,
|
|
54
55
|
].filter(Boolean))
|
|
55
56
|
|
|
56
|
-
const rules = makeRules({ semanticTokens, motionTokens }).filter((r) => r.scope !== "package")
|
|
57
|
+
const rules = makeRules({ semanticTokens, motionTokens, labelContrast: await labelContrastScanner(PKG_ROOT, REGISTRY.themes?.default) }).filter((r) => r.scope !== "package")
|
|
57
58
|
|
|
58
59
|
/* ── Args. ─────────────────────────────────────────────────────────────────── */
|
|
59
60
|
const args = process.argv.slice(2)
|
package/bin/sable.mjs
CHANGED
|
@@ -84,7 +84,7 @@ const COMMANDS = [
|
|
|
84
84
|
arg: null,
|
|
85
85
|
flags: [
|
|
86
86
|
{ name: "--template", value: null, describe: "Print a complete editable Sable preset JSON record." },
|
|
87
|
-
{ name: "--check", value: "file.json", describe: "Validate the schema
|
|
87
|
+
{ name: "--check", value: "file.json", describe: "Validate the schema, the Sable token contract, and labels on brand and status fills (accept a pair with `exceptions`)." },
|
|
88
88
|
{ name: "--css", value: "file.json", describe: "Generate scoped theme CSS from a validated preset." },
|
|
89
89
|
],
|
|
90
90
|
returns: ["preset.template", "preset.check", "preset.css"],
|
|
@@ -282,6 +282,7 @@ const COMMANDS = [
|
|
|
282
282
|
const ERROR_CODES = [
|
|
283
283
|
{ code: "ERR_TARGET_MISSING", when: "The installed package is missing its optional target mapping artifact." },
|
|
284
284
|
{ code: "ERR_PRESET_INVALID", when: "A preset is missing required metadata, semantic values, or uses unsupported tokens or CSS." },
|
|
285
|
+
{ code: "ERR_PRESET_CONTRAST", when: "A label on a brand or status fill (primary, sidebar-primary, highlight, destructive, success, tone-warning, tone-info) misses 4.5:1 or APCA Lc 60. Apply the suggested pair, or record an `exceptions` entry with a reason." },
|
|
285
286
|
{ code: "ERR_AGENT_SETUP", when: "The packaged skill is missing, the destination is linked, or existing local edits would be overwritten." },
|
|
286
287
|
{ code: "ERR_UNKNOWN_COMMAND", when: "The first argument is not a command." },
|
|
287
288
|
{ code: "ERR_MISSING_ARGUMENT", when: "A command that requires an argument got none." },
|
|
@@ -1213,7 +1214,7 @@ const HANDLERS = {
|
|
|
1213
1214
|
const contexts = (match.contexts ?? []).map(c => `${c.role}: ${c.strategy} → ${c.importFrom} (${c.components.join(" + ")})`).join("\n")
|
|
1214
1215
|
ok("target.component", match, `${match.source}: ${match.strategy}\n${contexts || `${match.importFrom ?? "app"} → ${match.components.join(" + ") || match.strategy}`}\n${match.guidance}`)
|
|
1215
1216
|
}
|
|
1216
|
-
ok("target", target, `iOS ${target.platformVersion}: ${target.designPolicy.principle}\n${target.designPolicy.rule}\n` + Object.values(target.components).map(c => `${c.source}: ${c.strategy} → ${c.strategy === "contextual" ? "choose by role" : c.components.join(" + ") || "redesign for phone"}`).join("\n"))
|
|
1217
|
+
ok("target", target, `iOS ${target.platformVersion}${target.status ? ` (${target.status})` : ""}: ${target.designPolicy.principle}\n${target.statusNote ? `${target.statusNote}\n` : ""}${target.designPolicy.rule}\n${(target.designPolicy.layout ?? []).map((l) => `Layout: ${l}\n`).join("")}` + Object.values(target.components).map(c => `${c.source}: ${c.strategy} → ${c.strategy === "contextual" ? "choose by role" : c.components.join(" + ") || "redesign for phone"}`).join("\n"))
|
|
1217
1218
|
},
|
|
1218
1219
|
preset: () => cmdDesignTool("design-presets.mjs"),
|
|
1219
1220
|
setup: () => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* To author a new theme: copy `themes/sable/`, keep these token names, and
|
|
8
8
|
* supply your own palette + values. See CONTRIBUTING.md → "Theme contract".
|
|
9
9
|
*/
|
|
10
|
-
declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-standard", "tone-collectible", "tone-edition", "tone-info", "tone-warning", "highlight", "tone-standard-foreground", "tone-collectible-foreground", "tone-edition-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
|
|
10
|
+
declare const SEMANTIC_TOKENS: readonly ["background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "muted", "muted-foreground", "border", "border-strong", "input", "control-border", "ring", "primary", "primary-foreground", "secondary", "secondary-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "success", "success-foreground", "tone-standard", "tone-collectible", "tone-edition", "tone-info", "tone-warning", "highlight", "tone-standard-foreground", "tone-collectible-foreground", "tone-edition-foreground", "tone-info-foreground", "tone-warning-foreground", "highlight-foreground", "destructive-text", "success-text", "tone-warning-text", "tone-info-text", "overlay", "overlay-foreground", "sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground", "sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring"];
|
|
11
11
|
type SemanticToken = (typeof SEMANTIC_TOKENS)[number];
|
|
12
12
|
/**
|
|
13
13
|
* Motion tokens (mode-independent). A theme MAY override these to retune its
|
|
@@ -106,6 +106,24 @@ type DensityToken = (typeof DENSITY_TOKENS)[number];
|
|
|
106
106
|
*/
|
|
107
107
|
declare const WORKSPACE_SURFACE_TOKENS: readonly ["canvas", "canvas-foreground", "panel", "panel-foreground", "panel-raised", "panel-field", "panel-field-hover", "hairline", "hairline-strong"];
|
|
108
108
|
type WorkspaceSurfaceToken = (typeof WORKSPACE_SURFACE_TOKENS)[number];
|
|
109
|
+
/**
|
|
110
|
+
* Art direction for generated imagery (GradientArt) — like the workspace
|
|
111
|
+
* surfaces, every theme gets them from styles.css with a `--theme-*` hook, so a
|
|
112
|
+
* theme declares only what makes its art its own.
|
|
113
|
+
*
|
|
114
|
+
* - `art-1` … `art-6` are the colours the art is painted from, in placement
|
|
115
|
+
* order; unset slots are transparent and skipped. The default is the four
|
|
116
|
+
* decorative tones, lifted to an art strength.
|
|
117
|
+
* - `art-ground` is what the colours glow over (grain's ground, the CSS
|
|
118
|
+
* fallback's base). The default is the brand steeped in the first tone.
|
|
119
|
+
* - `art-warp` (0–1) bends the field; `art-softness` (0–1) is how far colours
|
|
120
|
+
* melt into each other, 0 crisp blobs to 1 one wash; `art-grain` (0–1) is a
|
|
121
|
+
* film-grain overlay for themes whose imagery carries one.
|
|
122
|
+
*
|
|
123
|
+
* Derive the colours from the theme's own tokens (`oklch(from var(--primary)
|
|
124
|
+
* …)`) where the art direction allows, so a brand change carries into the art.
|
|
125
|
+
*/
|
|
126
|
+
declare const ART_TOKENS: readonly ["art-1", "art-2", "art-3", "art-4", "art-5", "art-6", "art-ground", "art-warp", "art-softness", "art-grain"];
|
|
109
127
|
/**
|
|
110
128
|
* UI type tiers (mode-independent) — the chrome typography register.
|
|
111
129
|
*
|
|
@@ -152,4 +170,4 @@ interface ThemeManifest {
|
|
|
152
170
|
};
|
|
153
171
|
}
|
|
154
172
|
|
|
155
|
-
export { DENSITY_TOKENS as D, MOTION_TOKENS as M, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, type DensityToken as b, type MotionToken as c, type RadiusToken as d, type SemanticToken as e, TYPE_OVERLINE_TOKENS as f, TYPE_REFINEMENT_TOKENS as g, TYPE_SCALE_TOKENS as h, TYPE_UI_TOKENS as i, type TypeOverlineToken as j, type TypeRefinementToken as k, type TypeScaleToken as l, type TypeUiToken as m, type WorkspaceSurfaceToken as n };
|
|
173
|
+
export { ART_TOKENS as A, DENSITY_TOKENS as D, MOTION_TOKENS as M, RADIUS_TOKENS as R, SEMANTIC_TOKENS as S, type ThemeManifest as T, WORKSPACE_SURFACE_TOKENS as W, type ThemeMode as a, type DensityToken as b, type MotionToken as c, type RadiusToken as d, type SemanticToken as e, TYPE_OVERLINE_TOKENS as f, TYPE_REFINEMENT_TOKENS as g, TYPE_SCALE_TOKENS as h, TYPE_UI_TOKENS as i, type TypeOverlineToken as j, type TypeRefinementToken as k, type TypeScaleToken as l, type TypeUiToken as m, type WorkspaceSurfaceToken as n };
|
package/dist/blocks/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Kbd, CommandMenuPanel, ToggleGroup, Toggle, BarChart, DateTile, Accordion, AccordionItem, AccordionTrigger, AccordionPanel, RadialChart, Sparkline, Slider, Stepper, StepperItem, SegmentedBar, Checkbox, PieChart, AreaChart, OtpField } from '../chunk-
|
|
2
|
-
import { Card, CardHeader, CardTitle, CardDescription, CardAction, Button, CardList, Entity, OverflowMenu, MenuItem, MenuSeparator, Code, CardContent, Message, Bubble, CardFooter, DestructiveActionModal, Badge, AvatarGroup, Avatar, Textarea, CodeBlock, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, StatusDot, Switch, Form, Field, Select, SelectTrigger, SelectContent, SelectItem, Separator, Label, Attachment } from '../chunk-
|
|
1
|
+
import { Kbd, CommandMenuPanel, ToggleGroup, Toggle, BarChart, DateTile, Accordion, AccordionItem, AccordionTrigger, AccordionPanel, RadialChart, Sparkline, Slider, Stepper, StepperItem, SegmentedBar, Checkbox, PieChart, AreaChart, OtpField } from '../chunk-EDXLM7YL.js';
|
|
2
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardAction, Button, CardList, Entity, OverflowMenu, MenuItem, MenuSeparator, Code, CardContent, Message, Bubble, CardFooter, DestructiveActionModal, Badge, AvatarGroup, Avatar, Textarea, CodeBlock, Input, Table, TableHeader, TableRow, TableHead, TableBody, TableCell, GradientArt, StatusDot, Switch, Form, Field, Select, SelectTrigger, SelectContent, SelectItem, Separator, Label, Attachment } from '../chunk-7QSDYBXO.js';
|
|
3
|
+
import '../chunk-FKZXAFIY.js';
|
|
3
4
|
import { Region, Stack, Row, Grid } from '../chunk-3IWZYBGH.js';
|
|
4
5
|
import { cn } from '../chunk-SRQWQUTW.js';
|
|
5
|
-
import { Icon } from '../chunk-
|
|
6
|
-
import '../chunk-
|
|
6
|
+
import { Icon } from '../chunk-QJ6FWTUG.js';
|
|
7
|
+
import '../chunk-XAZGMY73.js';
|
|
7
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
9
|
import { useId, useState } from 'react';
|
|
9
10
|
|
|
@@ -162,7 +163,7 @@ function DangerZoneBlock({ className, framed = true, onDelete }) {
|
|
|
162
163
|
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Delete account" }),
|
|
163
164
|
/* @__PURE__ */ jsx(CardDescription, { children: "Permanently remove your profile, editions, and sales history. Collectors keep the editions they own." })
|
|
164
165
|
] }),
|
|
165
|
-
/* @__PURE__ */ jsx(CardFooter, { className: "justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => setOpen(true), children: "Delete account" }) })
|
|
166
|
+
/* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => setOpen(true), children: "Delete account" }) })
|
|
166
167
|
] }),
|
|
167
168
|
/* @__PURE__ */ jsx(
|
|
168
169
|
DestructiveActionModal,
|
|
@@ -300,7 +301,7 @@ function EventCardBlock({ className, framed = true, onRespond }) {
|
|
|
300
301
|
] })
|
|
301
302
|
] }),
|
|
302
303
|
/* @__PURE__ */ jsxs(CardFooter, { className: "flex-wrap justify-between gap-3", children: [
|
|
303
|
-
/* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${event.total} attendees`, total: event.total, size: "sm", surface: framed ? "card" : void 0, fit: true, children: event.attendees.map((initials) => /* @__PURE__ */ jsx(Avatar, { fallback: initials, alt: initials }, initials)) }),
|
|
304
|
+
/* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${event.total} attendees`, total: event.total, size: "sm", surface: framed ? "card" : void 0, fit: true, children: event.attendees.map((initials) => /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: initials, alt: initials }, initials)) }),
|
|
304
305
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
305
306
|
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => onRespond?.("decline"), children: "Decline" }),
|
|
306
307
|
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => onRespond?.("join"), children: "Join" })
|
|
@@ -434,7 +435,7 @@ function FeedbackFormBlock({ className, framed = true, onDone }) {
|
|
|
434
435
|
/* @__PURE__ */ jsx(ToggleGroup, { value: [score], onValueChange: (v) => v[0] && setScore(String(v[0])), "aria-label": "Rating", className: "w-full", children: scale.map((n) => /* @__PURE__ */ jsx(Toggle, { value: n, className: "flex-1", children: n }, n)) }),
|
|
435
436
|
/* @__PURE__ */ jsx(Textarea, { placeholder: "What worked, what didn\u2019t\u2026", "aria-label": "Notes", rows: 3, value: notes, onChange: (e) => setNotes(e.target.value) })
|
|
436
437
|
] }),
|
|
437
|
-
/* @__PURE__ */ jsxs(CardFooter, { className: "justify-end", children: [
|
|
438
|
+
/* @__PURE__ */ jsxs(CardFooter, { actions: true, className: "justify-end", children: [
|
|
438
439
|
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDone?.(false), children: "Skip" }),
|
|
439
440
|
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => onDone?.(true, { score: Number(score), notes }), children: "Send feedback" })
|
|
440
441
|
] })
|
|
@@ -543,7 +544,7 @@ function InviteTeamBlock({ className, framed = true, onSend }) {
|
|
|
543
544
|
/* @__PURE__ */ jsx(CardDescription, { children: "Collaborators can edit every project." })
|
|
544
545
|
] }),
|
|
545
546
|
/* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
|
|
546
|
-
/* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${collaborators.length} collaborators`, size: "sm", surface: framed ? "card" : void 0, children: collaborators.map((initials) => /* @__PURE__ */ jsx(Avatar, { fallback: initials, alt: initials }, initials)) }),
|
|
547
|
+
/* @__PURE__ */ jsx(AvatarGroup, { "aria-label": `${collaborators.length} collaborators`, size: "sm", surface: framed ? "card" : void 0, children: collaborators.map((initials) => /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: initials, alt: initials }, initials)) }),
|
|
547
548
|
/* @__PURE__ */ jsxs(
|
|
548
549
|
"form",
|
|
549
550
|
{
|
|
@@ -665,17 +666,6 @@ var kpiSparklineBlockMeta = {
|
|
|
665
666
|
]
|
|
666
667
|
};
|
|
667
668
|
var track = { title: "Low tide, high noon", artist: "Juno Avery", position: 38, elapsed: "1:24", remaining: "-2:16" };
|
|
668
|
-
var ART_TONES = ["info", "collectible", "edition"];
|
|
669
|
-
var ART_SPOTS = [[20, 20, 70], [85, 30, 60], [60, 90, 65]];
|
|
670
|
-
var cover = [
|
|
671
|
-
...ART_SPOTS.map(
|
|
672
|
-
([x, y, reach], i) => (
|
|
673
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
674
|
-
`radial-gradient(in oklab at ${x}% ${y}%, oklch(from var(--tone-${ART_TONES[i % ART_TONES.length]}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent ${reach}%)`
|
|
675
|
-
)
|
|
676
|
-
),
|
|
677
|
-
"var(--primary)"
|
|
678
|
-
].join(", ");
|
|
679
669
|
function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPrevious, onNext }) {
|
|
680
670
|
const [playing, setPlaying] = useState(true);
|
|
681
671
|
return /* @__PURE__ */ jsx(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs", className), children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
|
|
@@ -683,7 +673,7 @@ function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPreviou
|
|
|
683
673
|
Entity,
|
|
684
674
|
{
|
|
685
675
|
className: "p-0",
|
|
686
|
-
leading: /* @__PURE__ */ jsx(
|
|
676
|
+
leading: /* @__PURE__ */ jsx(GradientArt, { "aria-hidden": true, seed: track.title, className: "size-14 rounded-md" }),
|
|
687
677
|
title: track.title,
|
|
688
678
|
subtitle: track.artist,
|
|
689
679
|
action: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Like", children: /* @__PURE__ */ jsx(Icon, { name: "heart" }) })
|
|
@@ -821,15 +811,15 @@ function NotificationListBlock({ className, framed = true }) {
|
|
|
821
811
|
return /* @__PURE__ */ jsx(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: /* @__PURE__ */ jsx(CardList, { children: notifications.map((n) => /* @__PURE__ */ jsx(
|
|
822
812
|
Entity,
|
|
823
813
|
{
|
|
824
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: n.initials, alt: n.name }),
|
|
814
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: n.initials, alt: n.name }),
|
|
825
815
|
title: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
826
816
|
n.name,
|
|
827
817
|
n.unread ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (unread)" }) : null
|
|
828
818
|
] }),
|
|
829
819
|
subtitle: n.what,
|
|
830
820
|
action: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 text-caption text-muted-foreground", children: [
|
|
831
|
-
n.
|
|
832
|
-
n.
|
|
821
|
+
n.unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "info" }) : null,
|
|
822
|
+
n.when
|
|
833
823
|
] })
|
|
834
824
|
},
|
|
835
825
|
n.name
|
|
@@ -846,7 +836,7 @@ var notificationListBlockMeta = {
|
|
|
846
836
|
intent: "Reach for this for a notifications popover, a recent-activity card or an inbox preview. A full inbox with selection and bulk actions is a ListDetailPage.",
|
|
847
837
|
notes: [
|
|
848
838
|
"Rows are Card + CardList + Entity, inset from the card's padding at any density.",
|
|
849
|
-
"Unread is a quiet info dot
|
|
839
|
+
"Unread is a quiet info dot before the time, so the times keep one right edge \u2014 the list stays calm when most items are unread.",
|
|
850
840
|
"framed={false} drops the surface for a Popover, which already is one."
|
|
851
841
|
],
|
|
852
842
|
a11y: [
|
|
@@ -1071,20 +1061,9 @@ var pricingTableBlockMeta = {
|
|
|
1071
1061
|
]
|
|
1072
1062
|
};
|
|
1073
1063
|
var product = { title: "Untitled (gold rush)", maker: "Mara Okafor", edition: "12 of 50", price: "0.42 SOL" };
|
|
1074
|
-
var ART_TONES2 = ["info", "collectible", "edition", "standard"];
|
|
1075
|
-
var ART_SPOTS2 = [[18, 22, 58], [82, 18, 55], [70, 88, 60], [12, 92, 55]];
|
|
1076
|
-
var artwork = [
|
|
1077
|
-
...ART_SPOTS2.map(
|
|
1078
|
-
([x, y, reach], i) => (
|
|
1079
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
1080
|
-
`radial-gradient(in oklab at ${x}% ${y}%, oklch(from var(--tone-${ART_TONES2[i % ART_TONES2.length]}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent ${reach}%)`
|
|
1081
|
-
)
|
|
1082
|
-
),
|
|
1083
|
-
"var(--primary)"
|
|
1084
|
-
].join(", ");
|
|
1085
1064
|
function ProductCardBlock({ className, framed = true, onCollect }) {
|
|
1086
1065
|
return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: [
|
|
1087
|
-
/* @__PURE__ */ jsx(
|
|
1066
|
+
/* @__PURE__ */ jsx(GradientArt, { "aria-hidden": true, seed: product.title, className: "aspect-[4/3]" }),
|
|
1088
1067
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
1089
1068
|
/* @__PURE__ */ jsx(CardTitle, { children: product.title }),
|
|
1090
1069
|
/* @__PURE__ */ jsx(CardDescription, { children: product.maker }),
|
|
@@ -1125,24 +1104,13 @@ var profile = {
|
|
|
1125
1104
|
{ value: "4.2k", label: "followers" }
|
|
1126
1105
|
]
|
|
1127
1106
|
};
|
|
1128
|
-
var ART_TONES3 = ["info", "collectible", "edition", "standard"];
|
|
1129
|
-
var ART_SPOTS3 = [[10, 50, 60], [50, 0, 55], [90, 60, 60], [40, 100, 50]];
|
|
1130
|
-
var banner = [
|
|
1131
|
-
...ART_SPOTS3.map(
|
|
1132
|
-
([x, y, reach], i) => (
|
|
1133
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
1134
|
-
`radial-gradient(in oklab at ${x}% ${y}%, oklch(from var(--tone-${ART_TONES3[i % ART_TONES3.length]}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent ${reach}%)`
|
|
1135
|
-
)
|
|
1136
|
-
),
|
|
1137
|
-
"var(--primary)"
|
|
1138
|
-
].join(", ");
|
|
1139
1107
|
function ProfileCardBlock({ className, framed = true, onFollowChange, onShare }) {
|
|
1140
1108
|
const [following, setFollowing] = useState(false);
|
|
1141
1109
|
return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-xs overflow-hidden", className), children: [
|
|
1142
|
-
/* @__PURE__ */ jsx(
|
|
1110
|
+
/* @__PURE__ */ jsx(GradientArt, { "aria-hidden": true, seed: profile.handle, className: "h-20" }),
|
|
1143
1111
|
/* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-3 pt-0", children: [
|
|
1144
1112
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between", children: [
|
|
1145
|
-
/* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: profile.initials, alt: profile.name, className: "-mt-6 ring-4 ring-card" }),
|
|
1113
|
+
/* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: profile.initials, alt: profile.name, className: "-mt-6 ring-4 ring-card" }),
|
|
1146
1114
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-1.5 pt-3", children: [
|
|
1147
1115
|
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Share profile", onClick: onShare, children: /* @__PURE__ */ jsx(Icon, { name: "share" }) }),
|
|
1148
1116
|
/* @__PURE__ */ jsx(
|
|
@@ -1222,7 +1190,7 @@ function ProfileFormBlock({ className, framed = true, onSave }) {
|
|
|
1222
1190
|
children: [
|
|
1223
1191
|
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
1224
1192
|
/* @__PURE__ */ jsxs(Row, { gap: 2, children: [
|
|
1225
|
-
/* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: profile2.initials, alt: profile2.name }),
|
|
1193
|
+
/* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: profile2.initials, alt: profile2.name }),
|
|
1226
1194
|
/* @__PURE__ */ jsxs(Stack, { gap: 0.5, align: "start", children: [
|
|
1227
1195
|
/* @__PURE__ */ jsxs(Row, { gap: 1, children: [
|
|
1228
1196
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", children: "Upload photo" }),
|
|
@@ -1241,7 +1209,7 @@ function ProfileFormBlock({ className, framed = true, onSave }) {
|
|
|
1241
1209
|
/* @__PURE__ */ jsx(SelectContent, { children: timezones.map((zone) => /* @__PURE__ */ jsx(SelectItem, { value: zone.value, children: zone.label }, zone.value)) })
|
|
1242
1210
|
] }) }) })
|
|
1243
1211
|
] }) }),
|
|
1244
|
-
/* @__PURE__ */ jsx(CardFooter, { className: "justify-end", children: /* @__PURE__ */ jsx(Button, { type: "submit", children: "Save profile" }) })
|
|
1212
|
+
/* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: /* @__PURE__ */ jsx(Button, { type: "submit", children: "Save profile" }) })
|
|
1245
1213
|
]
|
|
1246
1214
|
}
|
|
1247
1215
|
)
|
|
@@ -1639,7 +1607,7 @@ function TeamMembersBlock({ className, framed = true, onInvite, onMemberAction }
|
|
|
1639
1607
|
/* @__PURE__ */ jsx(CardList, { children: members.map((m) => /* @__PURE__ */ jsx(
|
|
1640
1608
|
Entity,
|
|
1641
1609
|
{
|
|
1642
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: m.initials, alt: m.name }),
|
|
1610
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: m.initials, alt: m.name }),
|
|
1643
1611
|
title: m.name,
|
|
1644
1612
|
subtitle: m.email,
|
|
1645
1613
|
action: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
@@ -1697,7 +1665,7 @@ function TestimonialBlock({ className }) {
|
|
|
1697
1665
|
] })
|
|
1698
1666
|
] }),
|
|
1699
1667
|
/* @__PURE__ */ jsxs("figcaption", { className: "flex items-center gap-3", children: [
|
|
1700
|
-
/* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: story.initials, alt: story.name }),
|
|
1668
|
+
/* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: story.initials, alt: story.name }),
|
|
1701
1669
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
1702
1670
|
/* @__PURE__ */ jsx("p", { className: "text-title-sm", children: story.name }),
|
|
1703
1671
|
/* @__PURE__ */ jsx("p", { className: "text-body-sm text-muted-foreground", children: story.role })
|