@sableui/react 0.5.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 +135 -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 +19 -51
- package/dist/{chunk-RCLKXF2N.js → chunk-7QSDYBXO.js} +610 -30
- package/dist/{chunk-GU3T7SBA.js → chunk-EDXLM7YL.js} +1 -1
- package/dist/chunk-FKZXAFIY.js +12 -0
- package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
- package/dist/{chunk-ZBZMKOO7.js → chunk-VVW4UCDI.js} +3 -3
- package/dist/foundations/index.js +6 -0
- package/dist/index.d.ts +127 -11
- package/dist/index.js +23 -13
- 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 +22 -28
- package/dist/presets/index.d.ts +108 -5
- package/dist/presets/index.js +239 -6
- package/dist/styles.css +50 -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/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 +201 -104
- package/llms.txt +3 -2
- package/package.json +9 -7
- package/registry.json +279 -30
- 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/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sable (@sableui/react)
|
|
2
2
|
|
|
3
|
-
> An owned, themeable React component system. Default theme: **Base** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter.
|
|
3
|
+
> An owned, themeable React component system. Default theme: **Base** (dark-by-default, content-first, fluid product UI). Behavior from Base UI; styling via Tailwind v4 + semantic CSS-variable tokens; Reicon icons via a pluggable adapter. 106 components, 13 layout primitives, and a **patterns** tier of canonical page shells. The full, per-component contract is the generated `registry.json`.
|
|
4
4
|
|
|
5
5
|
## Start here — brief, composition, evidence
|
|
6
6
|
|
|
@@ -423,6 +423,12 @@ drift; repeated overrides can indicate a missing system hook rather than app mis
|
|
|
423
423
|
Run relevant `sable doctor`, `sable lint <paths> --json` and `sable grade <paths>
|
|
424
424
|
--json` checks. Lint is lexical; source grades and pattern similarity do not judge
|
|
425
425
|
taste. Deliberately static motion slots remain valid with appropriate feedback.
|
|
426
|
+
Lint's `label-contrast` measures an app's overrides of the brand fills (`--primary`,
|
|
427
|
+
`--sidebar-primary`, `--highlight`) and status fills (`--destructive`, `--success`,
|
|
428
|
+
`--tone-warning`, `--tone-info`) against their labels on top of the installed theme, and
|
|
429
|
+
names the nearest pair that reads. Report it with that pair as a recommendation.
|
|
430
|
+
A reasoned `design-lint-disable-next-line label-contrast` is the app's recorded
|
|
431
|
+
choice: list it as intentional, not as a defect.
|
|
426
432
|
|
|
427
433
|
## Deliver evidence-backed findings
|
|
428
434
|
|
|
@@ -917,7 +923,9 @@ Entity rows in a Card) or `redesign` (desktop workspace tools).
|
|
|
917
923
|
3. Compose: `NativeApp` at the root (`mode` sets light or dark, also on
|
|
918
924
|
`<html>` so portaled Sable popups follow); one `NavigationStack` per tab,
|
|
919
925
|
each holding an `IosPage` with an `IosNavbar` and Sable content; an
|
|
920
|
-
`IosTabbar` of `IosTab`s with Sable `Icon`s; `IosSheet` around Sable forms
|
|
926
|
+
`IosTabbar` of `IosTab`s with Sable `Icon`s; `IosSheet` around Sable forms
|
|
927
|
+
(`background="opaque"` for a form at the medium detent, and the block
|
|
928
|
+
`framed={false}`: the sheet is already its surface).
|
|
921
929
|
Keep the app's `IconProvider`.
|
|
922
930
|
4. Frame it on a host page with `PhoneFrame` from `@sableui/react/ios/preview`
|
|
923
931
|
and `@sableui/react/ios/frame.css` (Sable's Phone around an iframe at
|
|
@@ -1173,7 +1181,7 @@ button and text node takes a minute and settles the argument.
|
|
|
1173
1181
|
|
|
1174
1182
|
## 1. Wire the package
|
|
1175
1183
|
|
|
1176
|
-
Follow **Consuming** in `llms.txt`:
|
|
1184
|
+
Follow **Consuming** in `llms.txt`: `pnpm add`, React dedupe, the CSS
|
|
1177
1185
|
import order, `@source`, fonts, providers. React dedupe is the one that fails
|
|
1178
1186
|
loudest and least clearly ("invalid hook call").
|
|
1179
1187
|
|
|
@@ -1319,6 +1327,30 @@ light and dark modes and test long text, focus, disabled controls, floating
|
|
|
1319
1327
|
surfaces, and errors. Token validation checks the contract and safe CSS structure;
|
|
1320
1328
|
rendered review establishes contrast, coherence, and suitability.
|
|
1321
1329
|
|
|
1330
|
+
`--check` also measures the label on every filled role against its `-foreground`,
|
|
1331
|
+
in both modes: the brand fills `primary`, `sidebar-primary` and `highlight`, and the
|
|
1332
|
+
status fills `destructive`, `success`, `tone-warning` and `tone-info`. A label must
|
|
1333
|
+
clear 4.5:1 and APCA Lc 60, because a pair can pass the ratio and still read muddy
|
|
1334
|
+
(dark ink on a bright red-orange). A failing pair stops the check with
|
|
1335
|
+
`ERR_PRESET_CONTRAST` and the nearest pair that reads, with the fill's lightness
|
|
1336
|
+
moved and its hue kept. A fill keeps the ink its light-mode colour takes, so a
|
|
1337
|
+
colour with a white label keeps white in dark mode and its fill comes down to meet
|
|
1338
|
+
it. Offer that pair to the user.
|
|
1339
|
+
|
|
1340
|
+
Each status colour has two tokens. The fill (`destructive`) carries its label;
|
|
1341
|
+
`destructive-text` is the same colour drawn on a surface, and `text-destructive`,
|
|
1342
|
+
`border-destructive` and `ring-destructive` read it. In dark mode the two usually
|
|
1343
|
+
differ: a saturated fill low enough for white, and a lighter text value that reads
|
|
1344
|
+
on a dark card. Author both; lowering the fill never darkens the page's error text.
|
|
1345
|
+
If the user chose the failing pair on purpose (a brand book, a reference to match),
|
|
1346
|
+
keep it and record the decision in the preset rather than overriding it:
|
|
1347
|
+
|
|
1348
|
+
```json
|
|
1349
|
+
"exceptions": [{ "rule": "label-contrast", "mode": "dark", "token": "primary", "reason": "Brand book specifies dark ink on the bright fill." }]
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
An accepted pair is reported with its reason on every check; it is never silent.
|
|
1353
|
+
|
|
1322
1354
|
Canvas's Export view downloads this same format with resolved theme values and
|
|
1323
1355
|
the current customizations. Enter a name and design guidance, save the JSON, and
|
|
1324
1356
|
place it in the target project. The browser cannot write into that project for
|
|
@@ -1950,6 +1982,15 @@ Use opaque `focus-visible:ring-ring` or `ring-sidebar-ring`; reducing the ring t
|
|
|
1950
1982
|
`faded-focus-ring` rule catches those classes. A token contrast report establishes
|
|
1951
1983
|
the tested pairs, not the entire rendered product's accessibility.
|
|
1952
1984
|
|
|
1985
|
+
A label on a filled role (a primary or destructive Button, the sidebar's active
|
|
1986
|
+
item, a highlight, a success or warning badge) should clear 4.5:1 and APCA Lc 60 in
|
|
1987
|
+
both modes. A colour retuned for dark mode often lands where dark ink passes the
|
|
1988
|
+
ratio but reads muddy. `label-contrast`
|
|
1989
|
+
in `sable lint` and `sable preset --check` find it and suggest the nearest pair
|
|
1990
|
+
that reads; offer that pair. Keep a pair the user chose deliberately, recorded with
|
|
1991
|
+
its reason (a lint disable or a preset `exceptions` entry), instead of overriding
|
|
1992
|
+
it to finish.
|
|
1993
|
+
|
|
1953
1994
|
Describe a defect as a visible or behavioral consequence: "The heading wraps to
|
|
1954
1995
|
six lines at 390px and pushes the action below the intended first decision point."
|
|
1955
1996
|
Then record the repair and before/after artifact IDs in `iterations`. Avoid vague
|
|
@@ -2175,7 +2216,12 @@ Font stacks are requirements, not downloaded fonts. Declare matching stacks in
|
|
|
2175
2216
|
the font metadata and tokens. Resolve internal palette variables before saving;
|
|
2176
2217
|
portable presets may reference public Sable tokens, not private palette names.
|
|
2177
2218
|
The validator rejects unknown tokens and CSS that can escape its declaration.
|
|
2178
|
-
|
|
2219
|
+
`sable preset --check` also holds labels on brand and status fills to 4.5:1 and APCA Lc 60
|
|
2220
|
+
and suggests a pair that reads; a preset may keep a pair deliberately through
|
|
2221
|
+
`exceptions` with a reason (see `sable docs creating-a-preset`). Neither certifies
|
|
2222
|
+
the rendered product's contrast or that a CSS value makes visual sense. Do not
|
|
2223
|
+
change a supplied preset's colours to clear a finding without asking; report the
|
|
2224
|
+
finding and the suggested pair.
|
|
2179
2225
|
|
|
2180
2226
|
Motion durations are nominal times at scale 1. `presetCss()` and the CLI connect
|
|
2181
2227
|
them to the live `--motion-scale` policy, so reduced motion still stops recipes.
|
|
@@ -2870,6 +2916,7 @@ Sable's components ship accessible behavior from Base UI. These rules cover what
|
|
|
2870
2916
|
|
|
2871
2917
|
- **Never:** Color as the only signal (a red border with no message). **Instead:** Color plus text or an icon. Roughly 1 in 12 men cannot tell red from green.
|
|
2872
2918
|
- **Never:** Opacity on the focus ring. **Instead:** ring-ring at full opacity; tune the token in the theme. A faded ring loses the contrast the theme tested. *Enforced by sable-lint: faded-focus-ring.*
|
|
2919
|
+
- **Never:** A brand or status fill retuned under its old label — dark ink on a bright red that passes 4.5:1 at APCA Lc 42. **Instead:** A label that clears 4.5:1 and APCA Lc 60 in both modes. Move the fill's lightness, not its hue, keeping the ink its colour takes. A status colour on a surface is its own token (`--destructive-text`), so the fill can move freely. The ratio alone passes labels that read muddy. A deliberate brand pair is allowed with a recorded reason (lint disable or preset exceptions). *Enforced by sable-lint: label-contrast.*
|
|
2873
2920
|
- **Never:** Blocking paste, or type="number" for codes and card numbers. **Instead:** type="text" inputMode="numeric" with the right autocomplete. Blocking paste breaks password managers; number inputs scroll-change identifiers.
|
|
2874
2921
|
- **Never:** forced-color-adjust: none to 'fix' high-contrast mode. **Instead:** Let the system colors apply; verify the result. It removes the user's chosen colors exactly where they need them.
|
|
2875
2922
|
- **Never:** outline: none without a replacement. **Instead:** focus-visible:ring-2 ring-ring. Keyboard users lose their place.
|
|
@@ -2932,22 +2979,7 @@ Split sign-in page: a form column (brand mark, heading, email + password, primar
|
|
|
2932
2979
|
|
|
2933
2980
|
```tsx
|
|
2934
2981
|
import { useState } from "react"
|
|
2935
|
-
import { Avatar, Badge, Button, Card, CardContent, Entity, Field, Form, Icon, Input, Region, Separator, Stack } from "@sableui/react"
|
|
2936
|
-
|
|
2937
|
-
// A soft mesh of the theme's decorative tones over its brand colour, so the
|
|
2938
|
-
// artwork follows every theme and mode. Each tone is lifted from its text
|
|
2939
|
-
// strength (dark, restrained) to an art strength.
|
|
2940
|
-
const artwork = [
|
|
2941
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
2942
|
-
"radial-gradient(in oklab at 18% 22%, oklch(from var(--tone-info) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 58%)",
|
|
2943
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
2944
|
-
"radial-gradient(in oklab at 82% 18%, oklch(from var(--tone-collectible) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)",
|
|
2945
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
2946
|
-
"radial-gradient(in oklab at 70% 88%, oklch(from var(--tone-edition) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 60%)",
|
|
2947
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
2948
|
-
"radial-gradient(in oklab at 12% 92%, oklch(from var(--tone-standard) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)",
|
|
2949
|
-
"var(--primary)",
|
|
2950
|
-
].join(", ")
|
|
2982
|
+
import { Avatar, Badge, Button, Card, CardContent, Entity, Field, Form, GradientArt, Icon, Input, Region, Separator, Stack } from "@sableui/react"
|
|
2951
2983
|
|
|
2952
2984
|
export function AuthPage() {
|
|
2953
2985
|
const [pending, setPending] = useState(false)
|
|
@@ -3035,7 +3067,8 @@ export function AuthPage() {
|
|
|
3035
3067
|
|
|
3036
3068
|
{/* The brand panel is decoration: on a phone the form is the whole screen. */}
|
|
3037
3069
|
<aside aria-label="Featured release" className="hidden p-3 lg:flex">
|
|
3038
|
-
|
|
3070
|
+
{/* Art painted from the theme's tones over its brand, so it follows every look. */}
|
|
3071
|
+
<GradientArt seed="Untitled (gold rush)" className="flex flex-1 flex-col justify-end rounded-xl p-8">
|
|
3039
3072
|
<Card className="max-w-md">
|
|
3040
3073
|
<CardContent>
|
|
3041
3074
|
<Stack gap={3}>
|
|
@@ -3047,14 +3080,14 @@ export function AuthPage() {
|
|
|
3047
3080
|
</p>
|
|
3048
3081
|
<Entity
|
|
3049
3082
|
className="p-0"
|
|
3050
|
-
leading={<Avatar fallback="MO" />}
|
|
3083
|
+
leading={<Avatar variant="art" fallback="MO" />}
|
|
3051
3084
|
title="Mara Okafor"
|
|
3052
3085
|
subtitle="Untitled (gold rush), 2026"
|
|
3053
3086
|
/>
|
|
3054
3087
|
</Stack>
|
|
3055
3088
|
</CardContent>
|
|
3056
3089
|
</Card>
|
|
3057
|
-
</
|
|
3090
|
+
</GradientArt>
|
|
3058
3091
|
</aside>
|
|
3059
3092
|
</main>
|
|
3060
3093
|
)
|
|
@@ -3442,7 +3475,7 @@ export function BrandLandingPage() {
|
|
|
3442
3475
|
</blockquote>
|
|
3443
3476
|
</Stack>
|
|
3444
3477
|
<figcaption className="flex items-center gap-3">
|
|
3445
|
-
<Avatar size="lg" fallback="MO" />
|
|
3478
|
+
<Avatar variant="art" size="lg" fallback="MO" />
|
|
3446
3479
|
<div>
|
|
3447
3480
|
<p className="text-title-sm">Maya Okafor</p>
|
|
3448
3481
|
<p className="text-body-sm text-muted-foreground">Head of operations, Fielddesk</p>
|
|
@@ -3664,7 +3697,7 @@ export function ChatPanel() {
|
|
|
3664
3697
|
// at that size reads as a speck); `alt` names it for a screen reader, which
|
|
3665
3698
|
// otherwise gets nothing from a fallback-only avatar.
|
|
3666
3699
|
const assistant = (
|
|
3667
|
-
<Avatar
|
|
3700
|
+
<Avatar variant="art"
|
|
3668
3701
|
size="sm"
|
|
3669
3702
|
alt="Assistant"
|
|
3670
3703
|
fallback={<Icon name="sparkles" className="text-base" />}
|
|
@@ -3992,7 +4025,7 @@ export function DashboardPage() {
|
|
|
3992
4025
|
<span className="text-title-sm">Atelier</span>
|
|
3993
4026
|
</Row>
|
|
3994
4027
|
}
|
|
3995
|
-
actions={<Avatar size="sm" fallback="AL" alt="Ada Lovelace" />}
|
|
4028
|
+
actions={<Avatar variant="art" size="sm" fallback="AL" alt="Ada Lovelace" />}
|
|
3996
4029
|
/>
|
|
3997
4030
|
}
|
|
3998
4031
|
bottomNav={
|
|
@@ -4027,7 +4060,7 @@ export function DashboardPage() {
|
|
|
4027
4060
|
<SidebarFooter>
|
|
4028
4061
|
<Entity
|
|
4029
4062
|
className="[--entity-py:0]"
|
|
4030
|
-
leading={<Avatar size="sm" fallback="AL" alt="" />}
|
|
4063
|
+
leading={<Avatar variant="art" size="sm" fallback="AL" alt="" />}
|
|
4031
4064
|
title="Ada Lovelace"
|
|
4032
4065
|
subtitle="@ada"
|
|
4033
4066
|
action={
|
|
@@ -4199,7 +4232,7 @@ export function DashboardPage() {
|
|
|
4199
4232
|
{collectors.map((person) => (
|
|
4200
4233
|
<Entity
|
|
4201
4234
|
key={person.id}
|
|
4202
|
-
leading={<Avatar size="sm" fallback={person.initials} alt="" />}
|
|
4235
|
+
leading={<Avatar variant="art" size="sm" fallback={person.initials} alt="" />}
|
|
4203
4236
|
title={person.name}
|
|
4204
4237
|
subtitle={`${person.handle} · ${person.pieces} pieces`}
|
|
4205
4238
|
action={<span className="text-mono-sm">{usd.format(person.spent)}</span>}
|
|
@@ -4246,7 +4279,7 @@ export function DashboardPage() {
|
|
|
4246
4279
|
{activity.map((item) => (
|
|
4247
4280
|
<Entity
|
|
4248
4281
|
key={item.id}
|
|
4249
|
-
leading={<Avatar size="sm" fallback={item.initials} alt="" />}
|
|
4282
|
+
leading={<Avatar variant="art" size="sm" fallback={item.initials} alt="" />}
|
|
4250
4283
|
title={item.who}
|
|
4251
4284
|
subtitle={item.what}
|
|
4252
4285
|
action={<RelativeTime date={Date.now() - item.when} className="text-caption text-muted-foreground" />}
|
|
@@ -4360,7 +4393,7 @@ const renderPost = (post: Post) => (
|
|
|
4360
4393
|
<Card>
|
|
4361
4394
|
<CardList>
|
|
4362
4395
|
<Entity
|
|
4363
|
-
leading={<Avatar fallback={post.initials} alt={post.author} />}
|
|
4396
|
+
leading={<Avatar variant="art" fallback={post.initials} alt={post.author} />}
|
|
4364
4397
|
title={post.author}
|
|
4365
4398
|
subtitle={`${post.handle} · ${post.time}`}
|
|
4366
4399
|
action={
|
|
@@ -4441,7 +4474,7 @@ export function FeedPage() {
|
|
|
4441
4474
|
<Card>
|
|
4442
4475
|
<CardContent>
|
|
4443
4476
|
<Row gap={1.5} align="start">
|
|
4444
|
-
<Avatar fallback="AL" alt="Ada Lovelace" />
|
|
4477
|
+
<Avatar variant="art" fallback="AL" alt="Ada Lovelace" />
|
|
4445
4478
|
<Stack gap={1.5} className="min-w-0 flex-1">
|
|
4446
4479
|
<Textarea
|
|
4447
4480
|
autoResize
|
|
@@ -4508,7 +4541,7 @@ export function FeedPage() {
|
|
|
4508
4541
|
{suggestions.map((person) => (
|
|
4509
4542
|
<Entity
|
|
4510
4543
|
key={person.id}
|
|
4511
|
-
leading={<Avatar fallback={person.initials} alt={person.name} />}
|
|
4544
|
+
leading={<Avatar variant="art" fallback={person.initials} alt={person.name} />}
|
|
4512
4545
|
title={person.name}
|
|
4513
4546
|
subtitle={person.handle}
|
|
4514
4547
|
action={
|
|
@@ -4528,7 +4561,7 @@ export function FeedPage() {
|
|
|
4528
4561
|
{upcoming.map((drop) => (
|
|
4529
4562
|
<Entity
|
|
4530
4563
|
key={drop.id}
|
|
4531
|
-
leading={<Avatar fallback={drop.initials} alt={drop.artist} />}
|
|
4564
|
+
leading={<Avatar variant="art" fallback={drop.initials} alt={drop.artist} />}
|
|
4532
4565
|
title={drop.title}
|
|
4533
4566
|
subtitle={`${drop.artist} · ${drop.date}`}
|
|
4534
4567
|
action={
|
|
@@ -4861,7 +4894,7 @@ export function LandingPage() {
|
|
|
4861
4894
|
{deploys.map((deploy) => (
|
|
4862
4895
|
<Entity
|
|
4863
4896
|
key={deploy.meta}
|
|
4864
|
-
leading={<Avatar size="sm" fallback={deploy.initials} alt={deploy.name} />}
|
|
4897
|
+
leading={<Avatar variant="art" size="sm" fallback={deploy.initials} alt={deploy.name} />}
|
|
4865
4898
|
title={deploy.title}
|
|
4866
4899
|
subtitle={deploy.meta}
|
|
4867
4900
|
action={
|
|
@@ -5287,7 +5320,8 @@ export function ListDetailPage() {
|
|
|
5287
5320
|
selected={isSelected}
|
|
5288
5321
|
onSelect={() => select(m.id)}
|
|
5289
5322
|
leading={
|
|
5290
|
-
<Avatar
|
|
5323
|
+
<Avatar variant="art"
|
|
5324
|
+
seed={m.name}
|
|
5291
5325
|
fallback={m.pending ? <Icon name="mail" className="size-5" /> : m.initials}
|
|
5292
5326
|
status={m.lastActive === "Online now" ? <StatusDot tone="success" /> : undefined}
|
|
5293
5327
|
// The row's title already names it: keep the initials out of the row's name.
|
|
@@ -5349,7 +5383,7 @@ export function ListDetailPage() {
|
|
|
5349
5383
|
<Card>
|
|
5350
5384
|
<CardHeader>
|
|
5351
5385
|
<Row gap={2}>
|
|
5352
|
-
<Avatar
|
|
5386
|
+
<Avatar variant="art"
|
|
5353
5387
|
size="lg"
|
|
5354
5388
|
fallback={selected.pending ? <Icon name="mail" className="size-6" /> : selected.initials}
|
|
5355
5389
|
status={selected.lastActive === "Online now" ? <StatusDot tone="success" /> : undefined}
|
|
@@ -5562,7 +5596,7 @@ export function SettingsPage() {
|
|
|
5562
5596
|
<span className="text-title-sm">Atelier</span>
|
|
5563
5597
|
</Row>
|
|
5564
5598
|
}
|
|
5565
|
-
actions={<Avatar size="sm" fallback="AL" alt="Ada Lovelace" />}
|
|
5599
|
+
actions={<Avatar variant="art" size="sm" fallback="AL" alt="Ada Lovelace" />}
|
|
5566
5600
|
/>
|
|
5567
5601
|
}
|
|
5568
5602
|
bottomNav={
|
|
@@ -5597,7 +5631,7 @@ export function SettingsPage() {
|
|
|
5597
5631
|
<SidebarFooter>
|
|
5598
5632
|
<Entity
|
|
5599
5633
|
className="[--entity-py:0]"
|
|
5600
|
-
leading={<Avatar size="sm" fallback="AL" alt="" />}
|
|
5634
|
+
leading={<Avatar variant="art" size="sm" fallback="AL" alt="" />}
|
|
5601
5635
|
title="Ada Lovelace"
|
|
5602
5636
|
subtitle="@ada"
|
|
5603
5637
|
action={
|
|
@@ -5647,7 +5681,7 @@ export function SettingsPage() {
|
|
|
5647
5681
|
<CardContent>
|
|
5648
5682
|
<Stack gap={3}>
|
|
5649
5683
|
<Row gap={2}>
|
|
5650
|
-
<Avatar size="lg" fallback="AL" alt="Ada Lovelace" />
|
|
5684
|
+
<Avatar variant="art" size="lg" fallback="AL" alt="Ada Lovelace" />
|
|
5651
5685
|
<Stack gap={0.5} align="start">
|
|
5652
5686
|
<Row gap={1}>
|
|
5653
5687
|
<Button type="button" variant="outline" size="sm">
|
|
@@ -6108,7 +6142,7 @@ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZo
|
|
|
6108
6142
|
Permanently remove your profile, editions, and sales history. Collectors keep the editions they own.
|
|
6109
6143
|
</CardDescription>
|
|
6110
6144
|
</CardHeader>
|
|
6111
|
-
<CardFooter className="justify-end">
|
|
6145
|
+
<CardFooter actions className="justify-end">
|
|
6112
6146
|
<Button variant="destructive" onClick={() => setOpen(true)}>
|
|
6113
6147
|
Delete account
|
|
6114
6148
|
</Button>
|
|
@@ -6296,7 +6330,7 @@ export function EventCardBlock({ className, framed = true, onRespond }: EventCar
|
|
|
6296
6330
|
<CardFooter className="flex-wrap justify-between gap-3">
|
|
6297
6331
|
<AvatarGroup aria-label={`${event.total} attendees`} total={event.total} size="sm" surface={framed ? "card" : undefined} fit>
|
|
6298
6332
|
{event.attendees.map((initials) => (
|
|
6299
|
-
<Avatar key={initials} fallback={initials} alt={initials} />
|
|
6333
|
+
<Avatar variant="art" key={initials} fallback={initials} alt={initials} />
|
|
6300
6334
|
))}
|
|
6301
6335
|
</AvatarGroup>
|
|
6302
6336
|
<div className="flex gap-2">
|
|
@@ -6522,7 +6556,7 @@ export function FeedbackFormBlock({ className, framed = true, onDone }: Feedback
|
|
|
6522
6556
|
</ToggleGroup>
|
|
6523
6557
|
<Textarea placeholder="What worked, what didn’t…" aria-label="Notes" rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} />
|
|
6524
6558
|
</CardContent>
|
|
6525
|
-
<CardFooter className="justify-end">
|
|
6559
|
+
<CardFooter actions className="justify-end">
|
|
6526
6560
|
<Button variant="ghost" size="sm" onClick={() => onDone?.(false)}>
|
|
6527
6561
|
Skip
|
|
6528
6562
|
</Button>
|
|
@@ -6705,7 +6739,7 @@ export function InviteTeamBlock({ className, framed = true, onSend }: InviteTeam
|
|
|
6705
6739
|
<CardContent className="flex flex-col gap-4">
|
|
6706
6740
|
<AvatarGroup aria-label={`${collaborators.length} collaborators`} size="sm" surface={framed ? "card" : undefined}>
|
|
6707
6741
|
{collaborators.map((initials) => (
|
|
6708
|
-
<Avatar key={initials} fallback={initials} alt={initials} />
|
|
6742
|
+
<Avatar variant="art" key={initials} fallback={initials} alt={initials} />
|
|
6709
6743
|
))}
|
|
6710
6744
|
</AvatarGroup>
|
|
6711
6745
|
<form
|
|
@@ -6896,24 +6930,10 @@ A now-playing card: cover, title and artist as an Entity with a Like action, a p
|
|
|
6896
6930
|
|
|
6897
6931
|
```tsx
|
|
6898
6932
|
import { useState } from "react"
|
|
6899
|
-
import { Button, Card, CardContent, cn, Entity, Icon, Slider } from "@sableui/react"
|
|
6933
|
+
import { Button, Card, CardContent, cn, Entity, GradientArt, Icon, Slider } from "@sableui/react"
|
|
6900
6934
|
|
|
6901
6935
|
const track = { title: "Low tide, high noon", artist: "Juno Avery", position: 38, elapsed: "1:24", remaining: "-2:16" }
|
|
6902
6936
|
|
|
6903
|
-
/**
|
|
6904
|
-
* Placeholder cover painted from the palette — the theme's decorative tones
|
|
6905
|
-
* over the brand, so it follows every look. Replace with the cover image.
|
|
6906
|
-
*/
|
|
6907
|
-
const ART_TONES = ["info", "collectible", "edition"]
|
|
6908
|
-
const ART_SPOTS = [[20, 20, 70], [85, 30, 60], [60, 90, 65]]
|
|
6909
|
-
const cover = [
|
|
6910
|
-
...ART_SPOTS.map(
|
|
6911
|
-
([x, y, reach], i) =>
|
|
6912
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
6913
|
-
`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}%)`,
|
|
6914
|
-
),
|
|
6915
|
-
"var(--primary)",
|
|
6916
|
-
].join(", ")
|
|
6917
6937
|
|
|
6918
6938
|
export interface MediaPlayerBlockProps {
|
|
6919
6939
|
className?: string
|
|
@@ -6934,7 +6954,8 @@ export function MediaPlayerBlock({ className, framed = true, onPlayingChange, on
|
|
|
6934
6954
|
card already pads it. */}
|
|
6935
6955
|
<Entity
|
|
6936
6956
|
className="p-0"
|
|
6937
|
-
|
|
6957
|
+
// Placeholder cover painted from the theme. Replace with the cover image.
|
|
6958
|
+
leading={<GradientArt aria-hidden seed={track.title} className="size-14 rounded-md" />}
|
|
6938
6959
|
title={track.title}
|
|
6939
6960
|
subtitle={track.artist}
|
|
6940
6961
|
action={
|
|
@@ -7113,7 +7134,7 @@ An activity list: Entity rows with the actor's Avatar, who did what, and a compa
|
|
|
7113
7134
|
## Notes
|
|
7114
7135
|
|
|
7115
7136
|
- Rows are Card + CardList + Entity, inset from the card's padding at any density.
|
|
7116
|
-
- Unread is a quiet info dot
|
|
7137
|
+
- Unread is a quiet info dot before the time, so the times keep one right edge — the list stays calm when most items are unread.
|
|
7117
7138
|
- framed={false} drops the surface for a Popover, which already is one.
|
|
7118
7139
|
|
|
7119
7140
|
## Accessibility
|
|
@@ -7144,7 +7165,7 @@ export function NotificationListBlock({ className, framed = true }: Notification
|
|
|
7144
7165
|
{notifications.map((n) => (
|
|
7145
7166
|
<Entity
|
|
7146
7167
|
key={n.name}
|
|
7147
|
-
leading={<Avatar size="sm" fallback={n.initials} alt={n.name} />}
|
|
7168
|
+
leading={<Avatar variant="art" size="sm" fallback={n.initials} alt={n.name} />}
|
|
7148
7169
|
title={
|
|
7149
7170
|
<>
|
|
7150
7171
|
{n.name}
|
|
@@ -7154,8 +7175,8 @@ export function NotificationListBlock({ className, framed = true }: Notification
|
|
|
7154
7175
|
subtitle={n.what}
|
|
7155
7176
|
action={
|
|
7156
7177
|
<span className="flex items-center gap-1.5 text-caption text-muted-foreground">
|
|
7157
|
-
{n.when}
|
|
7158
7178
|
{n.unread ? <StatusDot tone="info" /> : null}
|
|
7179
|
+
{n.when}
|
|
7159
7180
|
</span>
|
|
7160
7181
|
}
|
|
7161
7182
|
/>
|
|
@@ -7529,25 +7550,10 @@ A product card: edge-to-edge artwork, title and maker, an edition Badge, and the
|
|
|
7529
7550
|
## Source
|
|
7530
7551
|
|
|
7531
7552
|
```tsx
|
|
7532
|
-
import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn } from "@sableui/react"
|
|
7553
|
+
import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn, GradientArt } from "@sableui/react"
|
|
7533
7554
|
|
|
7534
7555
|
const product = { title: "Untitled (gold rush)", maker: "Mara Okafor", edition: "12 of 50", price: "0.42 SOL" }
|
|
7535
7556
|
|
|
7536
|
-
/**
|
|
7537
|
-
* Placeholder artwork painted from the palette — a soft mesh of the theme's
|
|
7538
|
-
* decorative tones over the brand, so it follows every look. Replace the art
|
|
7539
|
-
* <div> with your product's image.
|
|
7540
|
-
*/
|
|
7541
|
-
const ART_TONES = ["info", "collectible", "edition", "standard"]
|
|
7542
|
-
const ART_SPOTS = [[18, 22, 58], [82, 18, 55], [70, 88, 60], [12, 92, 55]]
|
|
7543
|
-
const artwork = [
|
|
7544
|
-
...ART_SPOTS.map(
|
|
7545
|
-
([x, y, reach], i) =>
|
|
7546
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
7547
|
-
`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}%)`,
|
|
7548
|
-
),
|
|
7549
|
-
"var(--primary)",
|
|
7550
|
-
].join(", ")
|
|
7551
7557
|
|
|
7552
7558
|
export interface ProductCardBlockProps {
|
|
7553
7559
|
className?: string
|
|
@@ -7560,7 +7566,8 @@ export interface ProductCardBlockProps {
|
|
|
7560
7566
|
export function ProductCardBlock({ className, framed = true, onCollect }: ProductCardBlockProps) {
|
|
7561
7567
|
return (
|
|
7562
7568
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs overflow-hidden", className)}>
|
|
7563
|
-
|
|
7569
|
+
{/* Placeholder art painted from the theme, so it follows every look. Replace with the product image. */}
|
|
7570
|
+
<GradientArt aria-hidden seed={product.title} className="aspect-[4/3]" />
|
|
7564
7571
|
<CardHeader>
|
|
7565
7572
|
<CardTitle>{product.title}</CardTitle>
|
|
7566
7573
|
<CardDescription>{product.maker}</CardDescription>
|
|
@@ -7602,7 +7609,7 @@ A profile card: a cover banner, an avatar rising into it, Share and Follow, the
|
|
|
7602
7609
|
|
|
7603
7610
|
```tsx
|
|
7604
7611
|
import { useState } from "react"
|
|
7605
|
-
import { Avatar, Button, Card, CardContent, CardDescription, CardTitle, cn, Icon } from "@sableui/react"
|
|
7612
|
+
import { Avatar, Button, Card, CardContent, CardDescription, CardTitle, cn, GradientArt, Icon } from "@sableui/react"
|
|
7606
7613
|
|
|
7607
7614
|
const profile = {
|
|
7608
7615
|
name: "Juno Avery",
|
|
@@ -7615,20 +7622,6 @@ const profile = {
|
|
|
7615
7622
|
],
|
|
7616
7623
|
}
|
|
7617
7624
|
|
|
7618
|
-
/**
|
|
7619
|
-
* Placeholder banner painted from the palette — the theme's decorative tones
|
|
7620
|
-
* over the brand, so it follows every look. Replace with the person's cover image.
|
|
7621
|
-
*/
|
|
7622
|
-
const ART_TONES = ["info", "collectible", "edition", "standard"]
|
|
7623
|
-
const ART_SPOTS = [[10, 50, 60], [50, 0, 55], [90, 60, 60], [40, 100, 50]]
|
|
7624
|
-
const banner = [
|
|
7625
|
-
...ART_SPOTS.map(
|
|
7626
|
-
([x, y, reach], i) =>
|
|
7627
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
7628
|
-
`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}%)`,
|
|
7629
|
-
),
|
|
7630
|
-
"var(--primary)",
|
|
7631
|
-
].join(", ")
|
|
7632
7625
|
|
|
7633
7626
|
export interface ProfileCardBlockProps {
|
|
7634
7627
|
className?: string
|
|
@@ -7643,12 +7636,13 @@ export function ProfileCardBlock({ className, framed = true, onFollowChange, onS
|
|
|
7643
7636
|
const [following, setFollowing] = useState(false)
|
|
7644
7637
|
return (
|
|
7645
7638
|
<Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs overflow-hidden", className)}>
|
|
7646
|
-
|
|
7639
|
+
{/* Placeholder banner painted from the theme, so it follows every look. Replace with the person's cover image. */}
|
|
7640
|
+
<GradientArt aria-hidden seed={profile.handle} className="h-20" />
|
|
7647
7641
|
{/* pt-0: the avatar rises into the banner above, so the content
|
|
7648
7642
|
starts at the banner's edge rather than a full pad below it. */}
|
|
7649
7643
|
<CardContent className="flex flex-col gap-3 pt-0">
|
|
7650
7644
|
<div className="flex items-start justify-between">
|
|
7651
|
-
<Avatar size="lg" fallback={profile.initials} alt={profile.name} className="-mt-6 ring-4 ring-card" />
|
|
7645
|
+
<Avatar variant="art" size="lg" fallback={profile.initials} alt={profile.name} className="-mt-6 ring-4 ring-card" />
|
|
7652
7646
|
<div className="flex gap-1.5 pt-3">
|
|
7653
7647
|
<Button variant="ghost" size="icon-sm" aria-label="Share profile" onClick={onShare}>
|
|
7654
7648
|
<Icon name="share" />
|
|
@@ -7751,7 +7745,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
|
|
|
7751
7745
|
<CardContent>
|
|
7752
7746
|
<Stack gap={3}>
|
|
7753
7747
|
<Row gap={2}>
|
|
7754
|
-
<Avatar size="lg" fallback={profile.initials} alt={profile.name} />
|
|
7748
|
+
<Avatar variant="art" size="lg" fallback={profile.initials} alt={profile.name} />
|
|
7755
7749
|
<Stack gap={0.5} align="start">
|
|
7756
7750
|
<Row gap={1}>
|
|
7757
7751
|
<Button type="button" variant="outline" size="sm">
|
|
@@ -7791,7 +7785,7 @@ export function ProfileFormBlock({ className, framed = true, onSave }: ProfileFo
|
|
|
7791
7785
|
</Grid>
|
|
7792
7786
|
</Stack>
|
|
7793
7787
|
</CardContent>
|
|
7794
|
-
<CardFooter className="justify-end">
|
|
7788
|
+
<CardFooter actions className="justify-end">
|
|
7795
7789
|
<Button type="submit">Save profile</Button>
|
|
7796
7790
|
</CardFooter>
|
|
7797
7791
|
</Form>
|
|
@@ -8400,7 +8394,7 @@ export function TeamMembersBlock({ className, framed = true, onInvite, onMemberA
|
|
|
8400
8394
|
{members.map((m) => (
|
|
8401
8395
|
<Entity
|
|
8402
8396
|
key={m.id}
|
|
8403
|
-
leading={<Avatar size="sm" fallback={m.initials} alt={m.name} />}
|
|
8397
|
+
leading={<Avatar variant="art" size="sm" fallback={m.initials} alt={m.name} />}
|
|
8404
8398
|
title={m.name}
|
|
8405
8399
|
subtitle={m.email}
|
|
8406
8400
|
action={
|
|
@@ -8490,7 +8484,7 @@ export function TestimonialBlock({ className }: TestimonialBlockProps) {
|
|
|
8490
8484
|
<blockquote className="text-balance text-heading-3 sm:text-heading-2">“{story.quote}”</blockquote>
|
|
8491
8485
|
</Stack>
|
|
8492
8486
|
<figcaption className="flex items-center gap-3">
|
|
8493
|
-
<Avatar size="lg" fallback={story.initials} alt={story.name} />
|
|
8487
|
+
<Avatar variant="art" size="lg" fallback={story.initials} alt={story.name} />
|
|
8494
8488
|
<div>
|
|
8495
8489
|
<p className="text-title-sm">{story.name}</p>
|
|
8496
8490
|
<p className="text-body-sm text-muted-foreground">{story.role}</p>
|
|
@@ -9993,6 +9987,7 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
9993
9987
|
|
|
9994
9988
|
### Best practices
|
|
9995
9989
|
|
|
9990
|
+
- Do: Use `variant="art"` where people without photos sit side by side — a member list, an activity feed, a chat. Seeded by the person, each gets their own colour, so faces stay tellable apart at a glance; the neutral fallback makes everyone the same grey circle.
|
|
9996
9991
|
- Do: Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person.
|
|
9997
9992
|
- Do: Use initials or a simple glyph fallback so missing images still communicate identity.
|
|
9998
9993
|
- Do: Use consistent sizes within a group; reserve lg for profile headers or major identity moments.
|
|
@@ -10031,14 +10026,17 @@ Use Avatar to represent a person, team, or account with a compact visual identit
|
|
|
10031
10026
|
| fallbackDelay | number | — | Delay before showing the fallback, in ms — avoids a flash on fast loads. |
|
|
10032
10027
|
| status | ReactNode | — | A presence indicator pinned to the bottom-end edge, usually <StatusDot tone="success" />. It sits outside the clipped circle and wears a ring in the surface colour (--avatar-group-ring, else the page background). Use this instead of absolutely positioning a dot over an Avatar. |
|
|
10033
10028
|
| size | "sm" \| "md" \| "lg" | "md" | Square footprint: 32px / 40px / 48px. |
|
|
10029
|
+
| variant | "neutral" \| "art" | "neutral" | What the fallback looks like. neutral: initials on the muted surface. art: initials over GradientArt seeded by the person, so each person without a photo gets their own stable colour from the theme. |
|
|
10030
|
+
| seed | string \| number | — | The art variant's seed. Defaults to the fallback text, then `alt`, so a person keeps one colour wherever they appear; pass a user id to tell apart two people who share initials. |
|
|
10034
10031
|
|
|
10035
10032
|
## Variants
|
|
10036
10033
|
|
|
10037
10034
|
- **size**: sm, md, lg
|
|
10035
|
+
- **variant**: neutral, art
|
|
10038
10036
|
|
|
10039
10037
|
## Tokens used
|
|
10040
10038
|
|
|
10041
|
-
`muted`, `muted-foreground`, `background`, `card`, `popover`
|
|
10039
|
+
`muted`, `muted-foreground`, `background`, `card`, `popover`, `overlay`, `overlay-foreground`
|
|
10042
10040
|
|
|
10043
10041
|
## Accessibility
|
|
10044
10042
|
|
|
@@ -10071,6 +10069,14 @@ import { Avatar } from "@sableui/react"
|
|
|
10071
10069
|
<Avatar fallback="AM" alt="Alex Morgan" />
|
|
10072
10070
|
```
|
|
10073
10071
|
|
|
10072
|
+
### Art fallback
|
|
10073
|
+
|
|
10074
|
+
```tsx
|
|
10075
|
+
import { Avatar } from "@sableui/react"
|
|
10076
|
+
|
|
10077
|
+
<Avatar variant="art" fallback="AM" alt="Alex Morgan" />
|
|
10078
|
+
```
|
|
10079
|
+
|
|
10074
10080
|
### Sizes
|
|
10075
10081
|
|
|
10076
10082
|
```tsx
|
|
@@ -11185,7 +11191,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
|
|
|
11185
11191
|
| CardAction | ComponentPropsWithRef<"div"> | Header-aligned top-right action slot (e.g. an overflow menu, switch, or close button). Place inside CardHeader, which becomes a 2-column grid when present and collapses to a single column otherwise. |
|
|
11186
11192
|
| CardContent | ComponentPropsWithRef<"div"> & { flush?: boolean } | The body region. Padding follows the density knob on all sides — does not assume a preceding CardHeader, so it is safe to use standalone (e.g. no CardHeader in the card). When paired with CardHeader, the seam stays tight because CardHeader zeroes its own bottom padding. `flush` runs it edge to edge for a table: the Table insets its first and last columns by exactly the card padding, so it lines up with the header — never hand-set pl-6/pr-6 on columns. |
|
|
11187
11193
|
| CardList | ComponentPropsWithRef<"div"> | Rows that run edge to edge in the card (Entity rows, most often), divided by hairlines. Insets each row to the card's own padding, so a list card lines up with every other card at any density — no hand-tuned px-* on the rows. Under a CardHeader it opens with a hairline and a gap; alone it fills the card. |
|
|
11188
|
-
| CardFooter | ComponentPropsWithRef<"div"> | Bottom row for actions/meta. Padding follows the density knob. |
|
|
11194
|
+
| CardFooter | ComponentPropsWithRef<"div"> & { actions?: boolean } | Bottom row for actions/meta. Padding follows the density knob. `actions` marks it as the card's action row (buttons only): in a sheet (IosSheet, a bottom SheetContent) it becomes a full-width stack pinned to the bottom, primary on top; on a page nothing changes. |
|
|
11189
11195
|
|
|
11190
11196
|
## Props
|
|
11191
11197
|
|
|
@@ -13637,6 +13643,89 @@ import { GlidingHighlight } from "@sableui/react"
|
|
|
13637
13643
|
|
|
13638
13644
|
---
|
|
13639
13645
|
|
|
13646
|
+
# GradientArt
|
|
13647
|
+
|
|
13648
|
+
**Status:** beta
|
|
13649
|
+
|
|
13650
|
+
Placeholder artwork painted by a static WebGL shader from the preset's own art direction (the --art-* tokens: palette, ground, warp, softness, grain) — Ember burns, Prism airbrushes, Meridian is grainy monochrome — with a CSS gradient underneath for prerendered pages and browsers without WebGL2.
|
|
13651
|
+
|
|
13652
|
+
**Keywords:** gradient, mesh gradient, grain, shader, artwork, placeholder image, cover, banner, avatar fallback, generative
|
|
13653
|
+
|
|
13654
|
+
## Usage
|
|
13655
|
+
|
|
13656
|
+
Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable.
|
|
13657
|
+
|
|
13658
|
+
### Best practices
|
|
13659
|
+
|
|
13660
|
+
- Do: Pass a stable `seed` per item — the name for an avatar, the title for a cover. Each item then keeps the same art across renders, reloads and themes, which is what lets someone recognise it.
|
|
13661
|
+
- Do: Size it with layout classes (`aspect-[4/3]`, `h-20`, `size-10 rounded-full`) and put any glyph or initials inside as children; they render above the art.
|
|
13662
|
+
- Do: Keep the default `mesh` for covers, tiles and avatars. Reach for `grain` where a printed, tactile surface suits — a banner, a hero card — and keep one variant per surface type so art reads as a system.
|
|
13663
|
+
- Don't: Do not put body text over it. The art spans the palette's lightest and most saturated tones, so no single ink is guaranteed to clear contrast across it; reserve overlays for a short label on a scrim or an avatar's initials.
|
|
13664
|
+
- Don't: Do not pass colours, and reach for `tones` rarely. The art follows the preset's --art-* tokens on purpose; to change how a product's art looks, set the --theme-art-* hooks in its theme or preset, so every cover, banner and avatar changes together.
|
|
13665
|
+
- Don't: Do not use it as a page or hero background. It paints at the element's pixel size, so a full-viewport instance costs a large frame on every theme change; it is sized for covers, banners and tiles.
|
|
13666
|
+
|
|
13667
|
+
## Props
|
|
13668
|
+
|
|
13669
|
+
| Name | Type | Default | Description |
|
|
13670
|
+
| --- | --- | --- | --- |
|
|
13671
|
+
| variant | "mesh" \| "grain" | "mesh" | mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture. |
|
|
13672
|
+
| tones | ("info" \| "standard" \| "collectible" \| "edition" \| "warning")[] | — | Paint these `--tone-*` colours instead of the preset's art palette. Leave unset to follow the preset's art direction. |
|
|
13673
|
+
| seed | string \| number | — | Fixes the composition: the same seed paints the same art in every theme. Pass a stable id. |
|
|
13674
|
+
| children | ReactNode | — | Rendered above the art — initials, a glyph, a short label. |
|
|
13675
|
+
| className | string | — | Size and shape: aspect, height, rounding. |
|
|
13676
|
+
|
|
13677
|
+
## Variants
|
|
13678
|
+
|
|
13679
|
+
- **variant**: mesh, grain
|
|
13680
|
+
|
|
13681
|
+
## Tokens used
|
|
13682
|
+
|
|
13683
|
+
`art-1`, `art-2`, `art-3`, `art-4`, `art-5`, `art-6`, `art-ground`, `art-warp`, `art-softness`, `art-grain`, `primary`, `tone-info`, `tone-standard`, `tone-collectible`, `tone-edition`, `tone-warning`
|
|
13684
|
+
|
|
13685
|
+
## Motion
|
|
13686
|
+
|
|
13687
|
+
- **arrival** · canvas · `motion-interactive` — The shader frame fades in over the CSS gradient it replaces, so the swap from fallback to shader reads as the art settling rather than a flash.
|
|
13688
|
+
- **response** · root · _no motion_ — Artwork, not a control: it does not react to the pointer.
|
|
13689
|
+
- **transition** · canvas · _custom (outside the recipe layer)_ — A theme change repaints the frame in place on the next animation frame; there are no states to move between.
|
|
13690
|
+
- **activity** · root · _no motion_ — Static by design: nothing animates between paints, so it costs nothing while idle and respects reduced motion without a branch.
|
|
13691
|
+
|
|
13692
|
+
## Accessibility
|
|
13693
|
+
|
|
13694
|
+
- Decorative by default: the canvas is aria-hidden. When the art stands for something (an avatar), label the element that contains it, not the art.
|
|
13695
|
+
- Children render above the art in the normal reading order, so initials or a label are announced as usual.
|
|
13696
|
+
|
|
13697
|
+
## Related
|
|
13698
|
+
|
|
13699
|
+
- Components: Avatar, Card, AspectBox
|
|
13700
|
+
|
|
13701
|
+
## Examples
|
|
13702
|
+
|
|
13703
|
+
### Product cover
|
|
13704
|
+
|
|
13705
|
+
```tsx
|
|
13706
|
+
import { GradientArt } from "@sableui/react"
|
|
13707
|
+
|
|
13708
|
+
<GradientArt seed="Untitled (gold rush)" className="aspect-[4/3] rounded-lg" />
|
|
13709
|
+
```
|
|
13710
|
+
|
|
13711
|
+
### Grain banner
|
|
13712
|
+
|
|
13713
|
+
```tsx
|
|
13714
|
+
import { GradientArt } from "@sableui/react"
|
|
13715
|
+
|
|
13716
|
+
<GradientArt variant="grain" seed="juno" tones={["info", "edition"]} className="h-20 rounded-md" />
|
|
13717
|
+
```
|
|
13718
|
+
|
|
13719
|
+
### Avatar with initials
|
|
13720
|
+
|
|
13721
|
+
```tsx
|
|
13722
|
+
import { GradientArt } from "@sableui/react"
|
|
13723
|
+
|
|
13724
|
+
<GradientArt seed="Mara Okafor" className="grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground">MO</GradientArt>
|
|
13725
|
+
```
|
|
13726
|
+
|
|
13727
|
+
---
|
|
13728
|
+
|
|
13640
13729
|
# InlineStatus
|
|
13641
13730
|
|
|
13642
13731
|
**Status:** stable
|
|
@@ -17337,7 +17426,7 @@ Use Sheet for secondary workflows that slide in while preserving context from th
|
|
|
17337
17426
|
| SheetHeader | ComponentProps<"div"> | Top section holding SheetTitle/SheetDescription. |
|
|
17338
17427
|
| SheetTitle | Base UI Drawer.Title props + className | The sheet's accessible name — wires aria-labelledby on the popup. |
|
|
17339
17428
|
| SheetDescription | Base UI Drawer.Description props + className | Muted supporting line; wires aria-describedby. |
|
|
17340
|
-
| SheetFooter | ComponentProps<"div"> | Bottom row for actions, pinned after the scrollable body. |
|
|
17429
|
+
| SheetFooter | ComponentProps<"div"> | Bottom row for actions, pinned after the scrollable body. In a bottom sheet it stacks: full width, the primary on top, every button at the cta height. |
|
|
17341
17430
|
|
|
17342
17431
|
## Props
|
|
17343
17432
|
|
|
@@ -19998,7 +20087,7 @@ import { PanelGroup, Workspace, WorkspaceBar, WorkspaceCanvas } from "@sableui/r
|
|
|
19998
20087
|
|
|
19999
20088
|
Which token to reach for, and what it is most often confused with. Generated from `src/themes/token-docs.ts`; the full data is `registry.json → tokens`.
|
|
20000
20089
|
|
|
20001
|
-
### semantic (
|
|
20090
|
+
### semantic (49)
|
|
20002
20091
|
|
|
20003
20092
|
- **`background`** — The page. The furthest-back surface in the page register. *Use for:* The document body, and any surface meant to read as 'the page itself'. Never anything that floats: background is the BOTTOM of the surface stack (background < card < popover in every dark mode), so a floating panel painted with it reads as a hole beneath the cards it hovers over.
|
|
20004
20093
|
- **Not** `popover` for anything floating, `card` for a bounded block on the page. `canvas`. `canvas` is the *work* surface in a tool — the artboard being edited — which is not the page behind the UI. Using `background` for an artboard makes the thing being edited indistinguishable from the chrome around it.
|
|
@@ -20070,6 +20159,14 @@ Which token to reach for, and what it is most often confused with. Generated fro
|
|
|
20070
20159
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
20071
20160
|
- **`highlight-foreground`** — Ink paired with `highlight`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `highlight`. Use them as a pair — `bg-highlight text-highlight-foreground` — so a theme can retune the surface and the ink together.
|
|
20072
20161
|
- **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
|
|
20162
|
+
- **`destructive-text`** — `destructive` drawn on a surface: text, icons, borders and focus rings. *Use for:* Write `text-destructive`, `border-destructive` or `ring-destructive` — they read this token; only `bg-destructive` reads the fill.
|
|
20163
|
+
- **Not** `destructive` itself for marks on a page. The fill is tuned to carry a label (dark mode lowers it under white ink), which can leave it too dark to read on a dark card.
|
|
20164
|
+
- **`success-text`** — `success` drawn on a surface: text, icons, borders and focus rings. *Use for:* Write `text-success`, `border-success` or `ring-success` — they read this token; only `bg-success` reads the fill.
|
|
20165
|
+
- **Not** `success` itself for marks on a page. The fill is tuned to carry a label (dark mode lowers it under white ink), which can leave it too dark to read on a dark card.
|
|
20166
|
+
- **`tone-warning-text`** — `tone-warning` drawn on a surface: text, icons, borders and focus rings. *Use for:* Write `text-tone-warning`, `border-tone-warning` or `ring-tone-warning` — they read this token; only `bg-tone-warning` reads the fill.
|
|
20167
|
+
- **Not** `tone-warning` itself for marks on a page. The fill is tuned to carry a label (dark mode lowers it under white ink), which can leave it too dark to read on a dark card.
|
|
20168
|
+
- **`tone-info-text`** — `tone-info` drawn on a surface: text, icons, borders and focus rings. *Use for:* Write `text-tone-info`, `border-tone-info` or `ring-tone-info` — they read this token; only `bg-tone-info` reads the fill.
|
|
20169
|
+
- **Not** `tone-info` itself for marks on a page. The fill is tuned to carry a label (dark mode lowers it under white ink), which can leave it too dark to read on a dark card.
|
|
20073
20170
|
- **`overlay`** — A scrim over the page behind a modal, and the wash behind media chrome. *Use for:* Dialog and sheet backdrops, letterboxing, a pill sitting on top of an image.
|
|
20074
20171
|
- **Not** `bg-black/50`. Pure black is never right — it crushes whatever is behind it, and a theme cannot tune it. This token is the sanctioned answer, which is why `raw-black-white` is a lint rule.
|
|
20075
20172
|
- **`overlay-foreground`** — Ink paired with `overlay`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `overlay`. Use them as a pair — `bg-overlay text-overlay-foreground` — so a theme can retune the surface and the ink together.
|