@sableui/react 0.5.0 → 0.6.1
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 +148 -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 +283 -30
- package/scripts/design-presets.mjs +27 -4
- package/scripts/grade.mjs +2 -1
- package/scripts/label-contrast.mjs +174 -0
- package/scripts/lint-rules.mjs +39 -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/registry.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://sable.design/registry.schema.json",
|
|
3
3
|
"name": "@sableui/react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
|
|
6
6
|
"engine": {
|
|
7
7
|
"behavior": "@base-ui/react",
|
|
@@ -516,6 +516,54 @@
|
|
|
516
516
|
"highlight"
|
|
517
517
|
]
|
|
518
518
|
},
|
|
519
|
+
{
|
|
520
|
+
"name": "destructive-text",
|
|
521
|
+
"intent": "`destructive` drawn on a surface: text, icons, borders and focus rings.",
|
|
522
|
+
"whenToUse": "Write `text-destructive`, `border-destructive` or `ring-destructive` — they read this token; only `bg-destructive` reads the fill.",
|
|
523
|
+
"insteadOf": "`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.",
|
|
524
|
+
"pairsWith": [
|
|
525
|
+
"background",
|
|
526
|
+
"card",
|
|
527
|
+
"popover",
|
|
528
|
+
"muted"
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"name": "success-text",
|
|
533
|
+
"intent": "`success` drawn on a surface: text, icons, borders and focus rings.",
|
|
534
|
+
"whenToUse": "Write `text-success`, `border-success` or `ring-success` — they read this token; only `bg-success` reads the fill.",
|
|
535
|
+
"insteadOf": "`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.",
|
|
536
|
+
"pairsWith": [
|
|
537
|
+
"background",
|
|
538
|
+
"card",
|
|
539
|
+
"popover",
|
|
540
|
+
"muted"
|
|
541
|
+
]
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"name": "tone-warning-text",
|
|
545
|
+
"intent": "`tone-warning` drawn on a surface: text, icons, borders and focus rings.",
|
|
546
|
+
"whenToUse": "Write `text-tone-warning`, `border-tone-warning` or `ring-tone-warning` — they read this token; only `bg-tone-warning` reads the fill.",
|
|
547
|
+
"insteadOf": "`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.",
|
|
548
|
+
"pairsWith": [
|
|
549
|
+
"background",
|
|
550
|
+
"card",
|
|
551
|
+
"popover",
|
|
552
|
+
"muted"
|
|
553
|
+
]
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"name": "tone-info-text",
|
|
557
|
+
"intent": "`tone-info` drawn on a surface: text, icons, borders and focus rings.",
|
|
558
|
+
"whenToUse": "Write `text-tone-info`, `border-tone-info` or `ring-tone-info` — they read this token; only `bg-tone-info` reads the fill.",
|
|
559
|
+
"insteadOf": "`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.",
|
|
560
|
+
"pairsWith": [
|
|
561
|
+
"background",
|
|
562
|
+
"card",
|
|
563
|
+
"popover",
|
|
564
|
+
"muted"
|
|
565
|
+
]
|
|
566
|
+
},
|
|
519
567
|
{
|
|
520
568
|
"name": "overlay",
|
|
521
569
|
"intent": "A scrim over the page behind a modal, and the wash behind media chrome.",
|
|
@@ -1512,6 +1560,14 @@
|
|
|
1512
1560
|
{
|
|
1513
1561
|
"id": "native-ui-import",
|
|
1514
1562
|
"fails-on": "A direct import from konsta. In an iOS prototype, Sable components build every screen; Konsta supplies only system chrome, and that comes through @sableui/react/ios (NativeApp, IosPage, IosNavbar, IosBackLink, IosTabbar, IosTab, IosSheet, IosIconButton, NavigationStack). For a row, input, switch, slider, search, button, list or toast, use the Sable component — `sable target ios --component <Name>` says which."
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
"id": "label-contrast",
|
|
1566
|
+
"fails-on": "A label on a brand fill (`--primary`, `--sidebar-primary`, `--highlight`, each with its `-foreground`) should clear 4.5:1 AND APCA Lc 60 — a pair can pass the ratio and still read muddy (dark ink on a bright red-orange, Lc 42). The finding names the nearest pair that reads: the ink the colour takes as picked, with the fill's lightness moved to meet it, hue kept. If the pair is your brand's deliberate choice, keep it on the record: `/* design-lint-disable-next-line label-contrast -- <reason> */` above the declaration."
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
"id": "status-text-drift",
|
|
1570
|
+
"fails-on": "A status colour has two tokens: the fill (`--destructive`, `--success`, `--tone-warning`, `--tone-info`), which `bg-*` paints, and its `-text` (`--destructive-text` …), which `text-*`, `border-*`, `ring-*`, `outline-*`, `fill-*` and `stroke-*` read. Setting the fill alone leaves the text on the theme's colour, so a retuned red button sits beside the theme's red error message. Set the `-text` in the same theme and mode: the fill itself where it already reads on your surfaces, or a step of it moved away from them (lighter in dark mode) so it clears 4.5:1 on a card."
|
|
1515
1571
|
}
|
|
1516
1572
|
],
|
|
1517
1573
|
"escapeHatch": {
|
|
@@ -3264,6 +3320,10 @@
|
|
|
3264
3320
|
"usage": {
|
|
3265
3321
|
"description": "Use Avatar to represent a person, team, or account with a compact visual identity. Wrap several in AvatarGroup for collaborators, assignees, or attendees — it overlaps them, rings each in the surface colour, and collapses the rest into a \"+N\" count; use the low-level parts only when you need custom image/fallback layout.",
|
|
3266
3322
|
"bestPractices": [
|
|
3323
|
+
{
|
|
3324
|
+
"guidance": "do",
|
|
3325
|
+
"description": "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."
|
|
3326
|
+
},
|
|
3267
3327
|
{
|
|
3268
3328
|
"guidance": "do",
|
|
3269
3329
|
"description": "Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person."
|
|
@@ -3365,6 +3425,17 @@
|
|
|
3365
3425
|
"type": "\"sm\" | \"md\" | \"lg\"",
|
|
3366
3426
|
"default": "\"md\"",
|
|
3367
3427
|
"description": "Square footprint: 32px / 40px / 48px."
|
|
3428
|
+
},
|
|
3429
|
+
{
|
|
3430
|
+
"name": "variant",
|
|
3431
|
+
"type": "\"neutral\" | \"art\"",
|
|
3432
|
+
"default": "\"neutral\"",
|
|
3433
|
+
"description": "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."
|
|
3434
|
+
},
|
|
3435
|
+
{
|
|
3436
|
+
"name": "seed",
|
|
3437
|
+
"type": "string | number",
|
|
3438
|
+
"description": "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."
|
|
3368
3439
|
}
|
|
3369
3440
|
],
|
|
3370
3441
|
"subcomponents": [
|
|
@@ -3399,6 +3470,10 @@
|
|
|
3399
3470
|
"sm",
|
|
3400
3471
|
"md",
|
|
3401
3472
|
"lg"
|
|
3473
|
+
],
|
|
3474
|
+
"variant": [
|
|
3475
|
+
"neutral",
|
|
3476
|
+
"art"
|
|
3402
3477
|
]
|
|
3403
3478
|
},
|
|
3404
3479
|
"tokensUsed": [
|
|
@@ -3406,7 +3481,9 @@
|
|
|
3406
3481
|
"muted-foreground",
|
|
3407
3482
|
"background",
|
|
3408
3483
|
"card",
|
|
3409
|
-
"popover"
|
|
3484
|
+
"popover",
|
|
3485
|
+
"overlay",
|
|
3486
|
+
"overlay-foreground"
|
|
3410
3487
|
],
|
|
3411
3488
|
"a11y": [
|
|
3412
3489
|
"Base UI tracks the image loading status and swaps to the fallback on missing/loading/error — no manual onError wiring.",
|
|
@@ -3426,6 +3503,11 @@
|
|
|
3426
3503
|
"code": "<Avatar fallback=\"AM\" alt=\"Alex Morgan\" />",
|
|
3427
3504
|
"imports": "import { Avatar } from \"@sableui/react\""
|
|
3428
3505
|
},
|
|
3506
|
+
{
|
|
3507
|
+
"label": "Art fallback",
|
|
3508
|
+
"code": "<Avatar variant=\"art\" fallback=\"AM\" alt=\"Alex Morgan\" />",
|
|
3509
|
+
"imports": "import { Avatar } from \"@sableui/react\""
|
|
3510
|
+
},
|
|
3429
3511
|
{
|
|
3430
3512
|
"label": "Sizes",
|
|
3431
3513
|
"code": "<Avatar src=\"/pfp.png\" alt=\"Alex Morgan\" fallback=\"AM\" size=\"sm\" />\n<Avatar src=\"/pfp.png\" alt=\"Alex Morgan\" fallback=\"AM\" size=\"md\" />\n<Avatar src=\"/pfp.png\" alt=\"Alex Morgan\" fallback=\"AM\" size=\"lg\" />",
|
|
@@ -5150,8 +5232,8 @@
|
|
|
5150
5232
|
},
|
|
5151
5233
|
{
|
|
5152
5234
|
"name": "CardFooter",
|
|
5153
|
-
"type": "ComponentPropsWithRef<\"div\">",
|
|
5154
|
-
"description": "Bottom row for actions/meta. Padding follows the density knob."
|
|
5235
|
+
"type": "ComponentPropsWithRef<\"div\"> & { actions?: boolean }",
|
|
5236
|
+
"description": "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."
|
|
5155
5237
|
}
|
|
5156
5238
|
],
|
|
5157
5239
|
"variants": {
|
|
@@ -9096,6 +9178,161 @@
|
|
|
9096
9178
|
}
|
|
9097
9179
|
]
|
|
9098
9180
|
},
|
|
9181
|
+
{
|
|
9182
|
+
"name": "GradientArt",
|
|
9183
|
+
"status": "beta",
|
|
9184
|
+
"group": "Media & mockups",
|
|
9185
|
+
"register": "either",
|
|
9186
|
+
"path": "components/gradient-art",
|
|
9187
|
+
"summary": "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.",
|
|
9188
|
+
"keywords": [
|
|
9189
|
+
"gradient",
|
|
9190
|
+
"mesh gradient",
|
|
9191
|
+
"grain",
|
|
9192
|
+
"shader",
|
|
9193
|
+
"artwork",
|
|
9194
|
+
"placeholder image",
|
|
9195
|
+
"cover",
|
|
9196
|
+
"banner",
|
|
9197
|
+
"avatar fallback",
|
|
9198
|
+
"generative"
|
|
9199
|
+
],
|
|
9200
|
+
"usage": {
|
|
9201
|
+
"description": "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.",
|
|
9202
|
+
"bestPractices": [
|
|
9203
|
+
{
|
|
9204
|
+
"guidance": "do",
|
|
9205
|
+
"description": "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."
|
|
9206
|
+
},
|
|
9207
|
+
{
|
|
9208
|
+
"guidance": "do",
|
|
9209
|
+
"description": "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."
|
|
9210
|
+
},
|
|
9211
|
+
{
|
|
9212
|
+
"guidance": "do",
|
|
9213
|
+
"description": "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."
|
|
9214
|
+
},
|
|
9215
|
+
{
|
|
9216
|
+
"guidance": "dont",
|
|
9217
|
+
"description": "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."
|
|
9218
|
+
},
|
|
9219
|
+
{
|
|
9220
|
+
"guidance": "dont",
|
|
9221
|
+
"description": "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."
|
|
9222
|
+
},
|
|
9223
|
+
{
|
|
9224
|
+
"guidance": "dont",
|
|
9225
|
+
"description": "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."
|
|
9226
|
+
}
|
|
9227
|
+
]
|
|
9228
|
+
},
|
|
9229
|
+
"related": {
|
|
9230
|
+
"components": [
|
|
9231
|
+
"Avatar",
|
|
9232
|
+
"Card",
|
|
9233
|
+
"AspectBox"
|
|
9234
|
+
]
|
|
9235
|
+
},
|
|
9236
|
+
"props": [
|
|
9237
|
+
{
|
|
9238
|
+
"name": "variant",
|
|
9239
|
+
"type": "\"mesh\" | \"grain\"",
|
|
9240
|
+
"default": "\"mesh\"",
|
|
9241
|
+
"description": "mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture."
|
|
9242
|
+
},
|
|
9243
|
+
{
|
|
9244
|
+
"name": "tones",
|
|
9245
|
+
"type": "(\"info\" | \"standard\" | \"collectible\" | \"edition\" | \"warning\")[]",
|
|
9246
|
+
"description": "Paint these `--tone-*` colours instead of the preset's art palette. Leave unset to follow the preset's art direction."
|
|
9247
|
+
},
|
|
9248
|
+
{
|
|
9249
|
+
"name": "seed",
|
|
9250
|
+
"type": "string | number",
|
|
9251
|
+
"description": "Fixes the composition: the same seed paints the same art in every theme. Pass a stable id."
|
|
9252
|
+
},
|
|
9253
|
+
{
|
|
9254
|
+
"name": "children",
|
|
9255
|
+
"type": "ReactNode",
|
|
9256
|
+
"description": "Rendered above the art — initials, a glyph, a short label."
|
|
9257
|
+
},
|
|
9258
|
+
{
|
|
9259
|
+
"name": "className",
|
|
9260
|
+
"type": "string",
|
|
9261
|
+
"description": "Size and shape: aspect, height, rounding."
|
|
9262
|
+
}
|
|
9263
|
+
],
|
|
9264
|
+
"variants": {
|
|
9265
|
+
"variant": [
|
|
9266
|
+
"mesh",
|
|
9267
|
+
"grain"
|
|
9268
|
+
]
|
|
9269
|
+
},
|
|
9270
|
+
"tokensUsed": [
|
|
9271
|
+
"art-1",
|
|
9272
|
+
"art-2",
|
|
9273
|
+
"art-3",
|
|
9274
|
+
"art-4",
|
|
9275
|
+
"art-5",
|
|
9276
|
+
"art-6",
|
|
9277
|
+
"art-ground",
|
|
9278
|
+
"art-warp",
|
|
9279
|
+
"art-softness",
|
|
9280
|
+
"art-grain",
|
|
9281
|
+
"primary",
|
|
9282
|
+
"tone-info",
|
|
9283
|
+
"tone-standard",
|
|
9284
|
+
"tone-collectible",
|
|
9285
|
+
"tone-edition",
|
|
9286
|
+
"tone-warning"
|
|
9287
|
+
],
|
|
9288
|
+
"motion": [
|
|
9289
|
+
{
|
|
9290
|
+
"slot": "arrival",
|
|
9291
|
+
"recipe": "motion-interactive",
|
|
9292
|
+
"part": "canvas",
|
|
9293
|
+
"why": "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."
|
|
9294
|
+
},
|
|
9295
|
+
{
|
|
9296
|
+
"slot": "response",
|
|
9297
|
+
"recipe": "none",
|
|
9298
|
+
"part": "root",
|
|
9299
|
+
"why": "Artwork, not a control: it does not react to the pointer."
|
|
9300
|
+
},
|
|
9301
|
+
{
|
|
9302
|
+
"slot": "transition",
|
|
9303
|
+
"recipe": "custom",
|
|
9304
|
+
"part": "canvas",
|
|
9305
|
+
"why": "A theme change repaints the frame in place on the next animation frame; there are no states to move between."
|
|
9306
|
+
},
|
|
9307
|
+
{
|
|
9308
|
+
"slot": "activity",
|
|
9309
|
+
"recipe": "none",
|
|
9310
|
+
"part": "root",
|
|
9311
|
+
"why": "Static by design: nothing animates between paints, so it costs nothing while idle and respects reduced motion without a branch."
|
|
9312
|
+
}
|
|
9313
|
+
],
|
|
9314
|
+
"a11y": [
|
|
9315
|
+
"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.",
|
|
9316
|
+
"Children render above the art in the normal reading order, so initials or a label are announced as usual."
|
|
9317
|
+
],
|
|
9318
|
+
"examples": [
|
|
9319
|
+
{
|
|
9320
|
+
"label": "Product cover",
|
|
9321
|
+
"code": "<GradientArt seed=\"Untitled (gold rush)\" className=\"aspect-[4/3] rounded-lg\" />",
|
|
9322
|
+
"imports": "import { GradientArt } from \"@sableui/react\""
|
|
9323
|
+
},
|
|
9324
|
+
{
|
|
9325
|
+
"label": "Grain banner",
|
|
9326
|
+
"code": "<GradientArt variant=\"grain\" seed=\"juno\" tones={[\"info\", \"edition\"]} className=\"h-20 rounded-md\" />",
|
|
9327
|
+
"imports": "import { GradientArt } from \"@sableui/react\""
|
|
9328
|
+
},
|
|
9329
|
+
{
|
|
9330
|
+
"label": "Avatar with initials",
|
|
9331
|
+
"code": "<GradientArt seed=\"Mara Okafor\" className=\"grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground\">MO</GradientArt>",
|
|
9332
|
+
"imports": "import { GradientArt } from \"@sableui/react\""
|
|
9333
|
+
}
|
|
9334
|
+
]
|
|
9335
|
+
},
|
|
9099
9336
|
{
|
|
9100
9337
|
"name": "InlineStatus",
|
|
9101
9338
|
"status": "stable",
|
|
@@ -14854,7 +15091,7 @@
|
|
|
14854
15091
|
{
|
|
14855
15092
|
"name": "SheetFooter",
|
|
14856
15093
|
"type": "ComponentProps<\"div\">",
|
|
14857
|
-
"description": "Bottom row for actions, pinned after the scrollable body."
|
|
15094
|
+
"description": "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."
|
|
14858
15095
|
}
|
|
14859
15096
|
],
|
|
14860
15097
|
"variants": {
|
|
@@ -18733,8 +18970,8 @@
|
|
|
18733
18970
|
"The Forgot password and Create one links use `hit-target` to meet the 44px touch floor without changing layout.",
|
|
18734
18971
|
"The artwork swatches are aria-hidden decoration; the brand link's accessible name is the wordmark."
|
|
18735
18972
|
],
|
|
18736
|
-
"imports": "import { useState } from \"react\"\nimport { Avatar, Badge, Button, Card, CardContent, Entity, Field, Form, Icon, Input, Region, Separator, Stack } from \"@sableui/react\"",
|
|
18737
|
-
"code": "
|
|
18973
|
+
"imports": "import { useState } from \"react\"\nimport { Avatar, Badge, Button, Card, CardContent, Entity, Field, Form, GradientArt, Icon, Input, Region, Separator, Stack } from \"@sableui/react\"",
|
|
18974
|
+
"code": "export function AuthPage() {\n\tconst [pending, setPending] = useState(false)\n\tconst [errors, setErrors] = useState<Record<string, string>>({})\n\n\treturn (\n\t\t<main className=\"grid min-h-dvh bg-background lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]\">\n\t\t\t<Region max=\"form\" safeArea={[\"top\", \"bottom\"]} className=\"flex flex-col\">\n\t\t\t\t{/* safeArea owns the region's own padding, so the breathing room sits one level in. */}\n\t\t\t\t<Stack gap={0} className=\"flex-1 py-8 sm:py-10\">\n\t\t\t\t\t<a href=\"/\" className=\"flex items-center gap-2.5 self-start rounded-sm motion-interactive motion-press\">\n\t\t\t\t\t\t{/* The same mark the app shell's sidebar and header carry. */}\n\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-2xl\" />\n\t\t\t\t\t\t<span className=\"text-title-lg\">Atelier</span>\n\t\t\t\t\t</a>\n\n\t\t\t\t\t<Stack gap={4} justify=\"center\" className=\"flex-1 py-12\">\n\t\t\t\t\t\t<Stack gap={1.5}>\n\t\t\t\t\t\t\t<h1 className=\"text-heading-1\">Welcome back</h1>\n\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\tSign in to collect new releases and manage your editions.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t<Form\n\t\t\t\t\t\t\tclassName=\"gap-5\"\n\t\t\t\t\t\t\terrors={errors}\n\t\t\t\t\t\t\tonFormSubmit={() => {\n\t\t\t\t\t\t\t\tsetPending(true)\n\t\t\t\t\t\t\t\t// Replace with your auth call; a rejected sign-in comes back as a field error.\n\t\t\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\t\t\tsetPending(false)\n\t\t\t\t\t\t\t\t\tsetErrors({ password: \"That email and password don’t match. Try again or reset your password.\" })\n\t\t\t\t\t\t\t\t}, 900)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Field label=\"Email\" name=\"email\" validate={(value) => (value ? null : \"Enter your email address.\")}>\n\t\t\t\t\t\t\t\t<Input type=\"email\" autoComplete=\"email webauthn\" placeholder=\"you@example.com\" />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t<Stack gap={1.5}>\n\t\t\t\t\t\t\t\t<Field label=\"Password\" name=\"password\" validate={(value) => (value ? null : \"Enter your password.\")}>\n\t\t\t\t\t\t\t\t\t<Input type=\"password\" autoComplete=\"current-password\" />\n\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t<a href=\"/forgot-password\" className=\"hit-target self-end text-body-sm text-muted-foreground link-underline motion-interactive\">\n\t\t\t\t\t\t\t\t\tForgot password?\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Button type=\"submit\" size=\"cta\" loading={pending} loadingLabel=\"Signing in\" className=\"w-full\">\n\t\t\t\t\t\t\t\tSign in\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Form>\n\n\t\t\t\t\t\t<Separator label=\"or\" />\n\n\t\t\t\t\t\t<Stack gap={1.5}>\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"cta\" className=\"w-full\" iconStart={<Icon name=\"key\" />}>\n\t\t\t\t\t\t\t\tSign in with a passkey\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"cta\" className=\"w-full\" iconStart={<Icon name=\"wallet\" />}>\n\t\t\t\t\t\t\t\tConnect a wallet\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t<p className=\"text-body-md text-muted-foreground\">\n\t\t\t\t\t\t\tNo account?{\" \"}\n\t\t\t\t\t\t\t<a href=\"/signup\" className=\"hit-target link-underline motion-interactive text-foreground\">\n\t\t\t\t\t\t\t\tCreate one\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</Stack>\n\n\t\t\t\t\t<p className=\"text-body-xs text-muted-foreground\">\n\t\t\t\t\t\tBy signing in you agree to the{\" \"}\n\t\t\t\t\t\t<a href=\"/terms\" className=\"link-underline motion-interactive\">\n\t\t\t\t\t\t\tTerms\n\t\t\t\t\t\t</a>{\" \"}\n\t\t\t\t\t\tand{\" \"}\n\t\t\t\t\t\t<a href=\"/privacy\" className=\"link-underline motion-interactive\">\n\t\t\t\t\t\t\tPrivacy Policy\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t.\n\t\t\t\t\t</p>\n\t\t\t\t</Stack>\n\t\t\t</Region>\n\n\t\t\t{/* The brand panel is decoration: on a phone the form is the whole screen. */}\n\t\t\t<aside aria-label=\"Featured release\" className=\"hidden p-3 lg:flex\">\n\t\t\t\t{/* Art painted from the theme's tones over its brand, so it follows every look. */}\n\t\t\t\t<GradientArt seed=\"Untitled (gold rush)\" className=\"flex flex-1 flex-col justify-end rounded-xl p-8\">\n\t\t\t\t\t<Card className=\"max-w-md\">\n\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t<Badge tone=\"edition\" className=\"self-start\">\n\t\t\t\t\t\t\t\t\tEdition of 50\n\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t<p className=\"text-title-lg\">\n\t\t\t\t\t\t\t\t\t“Every edition is a conversation with whoever holds it next.”\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tclassName=\"p-0\"\n\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" fallback=\"MO\" />}\n\t\t\t\t\t\t\t\t\ttitle=\"Mara Okafor\"\n\t\t\t\t\t\t\t\t\tsubtitle=\"Untitled (gold rush), 2026\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t</Card>\n\t\t\t\t</GradientArt>\n\t\t\t</aside>\n\t\t</main>\n\t)\n}"
|
|
18738
18975
|
},
|
|
18739
18976
|
{
|
|
18740
18977
|
"name": "BrandLandingPage",
|
|
@@ -18761,7 +18998,7 @@
|
|
|
18761
18998
|
"Status dots and the status Badge are paired with visible text labels so color is never the only signal."
|
|
18762
18999
|
],
|
|
18763
19000
|
"imports": "import { Avatar, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Grid, Icon, Input, Region, Reveal, Row, Separator, Stack, StatusDot, TextReveal } from \"@sableui/react\"",
|
|
18764
|
-
"code": "const navItems = [\n\t{ label: \"Platform\", href: \"#platform\" },\n\t{ label: \"How it works\", href: \"#how-it-works\" },\n\t{ label: \"Customers\", href: \"#customers\" },\n\t{ label: \"Pricing\", href: \"/pricing\" },\n]\n\nconst spaces = [\n\t{ name: \"Waypoint\", value: \"98.4%\", label: \"clear routes\" },\n\t{ name: \"Fielddesk\", value: \"12\", label: \"open holds\" },\n\t{ name: \"Archive\", value: \"4.8k\", label: \"sealed records\" },\n]\n\nconst routes = [\n\t{ label: \"Intake\", state: \"Open\", tone: \"info\" as const },\n\t{ label: \"Review\", state: \"Live\", tone: \"edition\" as const },\n\t{ label: \"Archive\", state: \"Safe\", tone: \"success\" as const },\n]\n\n// Weekly approvals: past weeks recede, the current week carries the signal.\nconst bars = [\n\t{ week: \"w1\", height: \"38%\", current: false },\n\t{ week: \"w2\", height: \"56%\", current: false },\n\t{ week: \"w3\", height: \"44%\", current: false },\n\t{ week: \"w4\", height: \"68%\", current: false },\n\t{ week: \"w5\", height: \"52%\", current: false },\n\t{ week: \"w6\", height: \"78%\", current: false },\n\t{ week: \"w7\", height: \"60%\", current: false },\n\t{ week: \"w8\", height: \"72%\", current: false },\n\t{ week: \"w9\", height: \"88%\", current: true },\n]\n\nconst sceneMetrics = [\n\t{ label: \"Cleared\", value: \"1.8M\", detail: \"last 30 days\" },\n\t{ label: \"Review\", value: \"42s\", detail: \"median path\" },\n\t{ label: \"Spaces\", value: \"284\", detail: \"active teams\" },\n]\n\nconst values = [\n\t{\n\t\ticon: \"lock\" as const,\n\t\ttitle: \"Private by default\",\n\t\tbody: \"Roles, approvals, and handoff history stay with the people who need them. No shared inboxes, no forwarded screenshots.\",\n\t},\n\t{\n\t\ticon: \"bell\" as const,\n\t\ttitle: \"Signals before surprises\",\n\t\tbody: \"Cove watches every route and surfaces stalled reviews and expiring approvals while there is still time to act.\",\n\t},\n\t{\n\t\ticon: \"clock\" as const,\n\t\ttitle: \"A trail for every decision\",\n\t\tbody: \"Each approval keeps its context, reviewer, and timestamp, so an audit takes an afternoon instead of a quarter.\",\n\t},\n]\n\nconst steps = [\n\t{\n\t\tnumber: \"01\",\n\t\ttitle: \"Connect your spaces\",\n\t\tbody: \"Bring teams, vendors, and record stores into one workspace, with roles that mirror how you already work.\",\n\t},\n\t{\n\t\tnumber: \"02\",\n\t\ttitle: \"Route every approval\",\n\t\tbody: \"Requests follow the path you define, with reviewers, deadlines, and escalation built in from the start.\",\n\t},\n\t{\n\t\tnumber: \"03\",\n\t\ttitle: \"Seal the record\",\n\t\tbody: \"Approved work is archived with its full context and is ready for review the moment it closes.\",\n\t},\n]\n\nconst outcomes = [\n\t{ value: \"3.2×\", label: \"faster approvals in the first quarter\" },\n\t{ value: \"68%\", label: \"fewer follow-ups on handoffs\" },\n\t{ value: \"1 day\", label: \"to prepare a quarterly audit\" },\n]\n\nconst footerGroups = [\n\t{\n\t\ttitle: \"Platform\",\n\t\tlinks: [\n\t\t\t{ label: \"Overview\", href: \"#platform\" },\n\t\t\t{ label: \"Signals\", href: \"/signals\" },\n\t\t\t{ label: \"Approvals\", href: \"/approvals\" },\n\t\t\t{ label: \"Records\", href: \"/records\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Company\",\n\t\tlinks: [\n\t\t\t{ label: \"About\", href: \"/about\" },\n\t\t\t{ label: \"Security\", href: \"/security\" },\n\t\t\t{ label: \"Customers\", href: \"#customers\" },\n\t\t\t{ label: \"Careers\", href: \"/careers\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Resources\",\n\t\tlinks: [\n\t\t\t{ label: \"Guides\", href: \"/guides\" },\n\t\t\t{ label: \"Status\", href: \"/status\" },\n\t\t\t{ label: \"Changelog\", href: \"/changelog\" },\n\t\t\t{ label: \"Docs\", href: \"/docs\" },\n\t\t],\n\t},\n]\n\nconst legalLinks = [\n\t{ label: \"Privacy\", href: \"/privacy\" },\n\t{ label: \"Terms\", href: \"/terms\" },\n\t{ label: \"Contact\", href: \"/contact\" },\n]\n\nexport function BrandLandingPage() {\n\treturn (\n\t\t<div className=\"min-h-dvh overflow-hidden bg-background text-foreground\">\n\t\t\t<header className=\"relative z-10 pt-4\">\n\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t<nav\n\t\t\t\t\t\taria-label=\"Main\"\n\t\t\t\t\t\tclassName=\"flex min-h-15 items-center justify-between gap-3 rounded-full border border-border bg-card/90 px-3 py-2 shadow-sm backdrop-blur\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<a href=\"/\" className=\"flex items-center gap-2 rounded-full px-2 motion-interactive motion-press\">\n\t\t\t\t\t\t\t<span className=\"grid size-8 place-items-center rounded-full bg-primary text-primary-foreground\">\n\t\t\t\t\t\t\t\t<Icon name=\"sparkles\" className=\"text-sm\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className=\"text-title-lg\">Cove</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<div className=\"hidden lg:block\">\n\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t{navItems.map((item) => (\n\t\t\t\t\t\t\t\t\t<Button key={item.label} variant=\"ghost\" size=\"sm\" render={<a href={item.href} />}>\n\t\t\t\t\t\t\t\t\t\t{item.label}\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" className=\"max-sm:hidden\" render={<a href=\"/login\" />}>\n\t\t\t\t\t\t\t\tSign in\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button size=\"sm\" render={<a href=\"/demo\" />}>\n\t\t\t\t\t\t\t\tRequest access\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</nav>\n\t\t\t\t</Region>\n\t\t\t</header>\n\n\t\t\t<main>\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"pb-20 pt-14 sm:pt-20 lg:pb-28\">\n\t\t\t\t\t<div className=\"grid items-center gap-12 lg:grid-cols-[1fr_1.1fr] lg:gap-10\">\n\t\t\t\t\t\t<Stack gap={3} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t<Badge appearance=\"outline\" icon={<Icon name=\"lock\" className=\"text-sm\" />}>\n\t\t\t\t\t\t\t\tPrivate operations, made legible\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t<TextReveal\n\t\t\t\t\t\t\t\tas=\"h1\"\n\t\t\t\t\t\t\t\ttext=\"A calmer command center for high-trust work\"\n\t\t\t\t\t\t\t\tclassName=\"text-balance text-display-lg sm:text-display-2xl\"\n\t\t\t\t\t\t\t\tonce\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\tCove gives teams a polished front door and a serious operating\n\t\t\t\t\t\t\t\tsurface: approvals, signals, records, and handoffs in one\n\t\t\t\t\t\t\t\tunderstandable flow.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Row gap={2} wrap className=\"pt-3\">\n\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/demo\" />}>\n\t\t\t\t\t\t\t\t\tBook a walkthrough\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\" render={<a href=\"#platform\" />}>\n\t\t\t\t\t\t\t\t\tExplore the platform\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t{/* The product scene is a dark island: `dark` re-scopes every\n\t\t\t\t\t\t semantic token, so its surfaces, muted ink and status tones\n\t\t\t\t\t\t are the theme's own dark values in either page mode. It is a\n\t\t\t\t\t\t picture of the product, so it stays out of the tab order and\n\t\t\t\t\t\t the accessibility tree; the hero copy carries its meaning. */}\n\t\t\t\t\t\t<div className=\"dark\" aria-hidden inert>\n\t\t\t\t\t\t\t<div className=\"overflow-hidden rounded-xl border border-border-floating bg-popover p-3 text-popover-foreground shadow-xl\">\n\t\t\t\t\t\t\t\t{/* Nested surfaces take concentric corners: each level's radius is\n\t\t\t\t\t\t\t\t the shell's xl minus every inset between it and the shell edge. */}\n\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/3 border border-border bg-card\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-between border-b border-border px-4 py-3\">\n\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone=\"warning\" />\n\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone=\"edition\" />\n\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone=\"success\" />\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-label-sm text-muted-foreground\">Live workspace</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"grid gap-3 p-3 md:grid-cols-[0.7fr_1.3fr]\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-3 max-md:hidden\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-border bg-background p-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-label-sm text-muted-foreground\">Spaces</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-4 space-y-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{spaces.map((space) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={space.name} className=\"rounded-concentric-xl/10 bg-card p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-title-sm\">{space.name}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm\">{space.value}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-xs text-muted-foreground\">{space.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-border bg-background p-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-title-sm\">Route status</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge tone=\"success\" size=\"sm\" dot>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tHealthy\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-4 space-y-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{routes.map((route) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row key={route.label} justify=\"between\" className=\"rounded-full bg-card px-3 py-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone={route.tone} />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm\">{route.label}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-label-sm text-muted-foreground\">{route.state}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-border bg-background p-4\">\n\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\" className=\"border-b border-border pb-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-label-sm text-muted-foreground\">Signal map</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-heading-4 sm:text-heading-3\">Every approval path in view</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\">Review</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-5 grid gap-4 sm:mt-6 sm:gap-5\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"grid grid-cols-9 items-end gap-1.5 rounded-concentric-xl/10 bg-card p-4 sm:gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{bars.map((bar) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={bar.week} className=\"flex h-36 items-end sm:h-56\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`w-full rounded-full ${bar.current ? \"bg-success\" : \"bg-secondary\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ height: bar.height }}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"grid grid-cols-3 gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{sceneMetrics.map((metric) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={metric.label} className=\"rounded-concentric-xl/10 border border-border bg-card p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"truncate text-label-sm text-muted-foreground\">{metric.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-2 text-heading-4 sm:text-heading-3\">{metric.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-xs text-muted-foreground max-sm:hidden\">{metric.detail}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\n\t\t\t\t<section id=\"platform\" aria-labelledby=\"brand-platform-title\" className=\"border-y border-border bg-muted/40 py-20 lg:py-28\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<Stack gap={6}>\n\t\t\t\t\t\t\t<Stack gap={2} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Why Cove</p>\n\t\t\t\t\t\t\t\t<h2 id=\"brand-platform-title\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\tOne private workspace for the decisions that matter.\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tMost operations work lives in threads, spreadsheets, and memory.\n\t\t\t\t\t\t\t\t\tCove gives it a home: every request routed, every signal visible,\n\t\t\t\t\t\t\t\t\tevery outcome on the record.\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t\t\t<Grid minItemWidth=\"calc(var(--spacing) * 64)\" gap={2}>\n\t\t\t\t\t\t\t\t\t{values.map((value) => (\n\t\t\t\t\t\t\t\t\t\t<Card key={value.title} render={<article />} className=\"h-full\">\n\t\t\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"mb-4 grid size-11 place-items-center rounded-md border border-border sable-well\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Icon name={value.icon} className=\"text-xl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"text-title-lg\">{value.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{value.body}</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" id=\"how-it-works\" aria-labelledby=\"brand-steps-title\" max=\"wide\" className=\"py-20 lg:py-28\">\n\t\t\t\t\t<div className=\"grid gap-12 lg:grid-cols-[1fr_2fr] lg:gap-16\">\n\t\t\t\t\t\t<Stack gap={2} className=\"max-w-md\">\n\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">How it works</p>\n\t\t\t\t\t\t\t<h2 id=\"brand-steps-title\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\tFrom request to sealed record.\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t<p className=\"text-body-md text-muted-foreground\">\n\t\t\t\t\t\t\t\tTeams are usually live within a week. Cove adapts to the routes\n\t\t\t\t\t\t\t\tyou already run instead of asking you to start over.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<div className=\"pt-3\">\n\t\t\t\t\t\t\t\t<Button variant=\"outline\" iconEnd={<Icon name=\"arrow-right\" />} render={<a href=\"/guides/rollout\" />}>\n\t\t\t\t\t\t\t\t\tRead the rollout guide\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<ol className=\"grid gap-10 sm:grid-cols-3 sm:gap-6\">\n\t\t\t\t\t\t\t{steps.map((step) => (\n\t\t\t\t\t\t\t\t<li key={step.number} className=\"flex flex-col gap-2 border-t border-border-strong pt-6\">\n\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-muted-foreground\">{step.number}</span>\n\t\t\t\t\t\t\t\t\t<h3 className=\"mt-3 text-title-lg sm:mt-6\">{step.title}</h3>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{step.body}</p>\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</ol>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\n\t\t\t\t<Region as=\"section\" id=\"customers\" aria-label=\"Customer story\" max=\"wide\" className=\"pb-20 lg:pb-28\">\n\t\t\t\t\t<Card render={<figure />}>\n\t\t\t\t\t\t<div className=\"grid lg:grid-cols-[1.5fr_1fr]\">\n\t\t\t\t\t\t\t<CardContent className=\"flex flex-col justify-between gap-10 sm:p-10\">\n\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Customer story</p>\n\t\t\t\t\t\t\t\t\t<blockquote className=\"text-balance text-heading-3 sm:text-heading-2\">\n\t\t\t\t\t\t\t\t\t\t“We used to chase approvals across four tools. Now every request\n\t\t\t\t\t\t\t\t\t\thas an owner, a deadline, and a record, and our auditors stopped\n\t\t\t\t\t\t\t\t\t\tasking for screenshots.”\n\t\t\t\t\t\t\t\t\t</blockquote>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<figcaption className=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t\t\t<Avatar size=\"lg\" fallback=\"MO\" />\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-title-sm\">Maya Okafor</p>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">Head of operations, Fielddesk</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</figcaption>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t<div className=\"border-border max-lg:border-t lg:border-s\">\n\t\t\t\t\t\t\t\t<CardContent className=\"flex h-full flex-col justify-center gap-6 sm:p-10\">\n\t\t\t\t\t\t\t\t\t{outcomes.map((outcome, index) => (\n\t\t\t\t\t\t\t\t\t\t<Stack key={outcome.label} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t{index > 0 && <Separator />}\n\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-1 sm:text-display-lg\">{outcome.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-sm text-muted-foreground\">{outcome.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Card>\n\t\t\t\t</Region>\n\n\t\t\t\t<Region as=\"section\" aria-labelledby=\"brand-cta-title\" max=\"wide\" className=\"pb-20 lg:pb-28\">\n\t\t\t\t\t<div className=\"dark\">\n\t\t\t\t\t\t<div className=\"rounded-xl border border-border bg-card px-6 py-12 text-card-foreground sm:px-12 sm:py-16\">\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between\">\n\t\t\t\t\t\t\t\t<Stack gap={3} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t\t\t<h2 id=\"brand-cta-title\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\t\tSee your own approval paths in Cove.\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tA 30-minute walkthrough with your routes, your roles, and your\n\t\t\t\t\t\t\t\t\t\trecords. No slides.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<Row gap={2} wrap>\n\t\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/demo\" />}>\n\t\t\t\t\t\t\t\t\t\tBook a walkthrough\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"ghost\" render={<a href=\"/pricing\" />}>\n\t\t\t\t\t\t\t\t\t\tView pricing\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\t\t\t</main>\n\n\t\t\t<footer className=\"border-t border-border bg-muted/35\">\n\t\t\t\t<Region max=\"wide\" className=\"py-12 sm:py-16\">\n\t\t\t\t\t<div className=\"grid gap-10 lg:grid-cols-[1fr_1.35fr_1fr] lg:gap-8\">\n\t\t\t\t\t\t<Stack gap={4} className=\"max-w-sm\">\n\t\t\t\t\t\t\t<a href=\"/\" className=\"flex w-fit items-center gap-2 rounded-full motion-interactive motion-press\">\n\t\t\t\t\t\t\t\t<span className=\"grid size-9 place-items-center rounded-full bg-primary text-primary-foreground\">\n\t\t\t\t\t\t\t\t\t<Icon name=\"sparkles\" className=\"text-sm\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span className=\"text-title-lg\">Cove</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\tA calm operating surface for teams that need trust, speed, and\n\t\t\t\t\t\t\t\tcontext without the noise.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone=\"success\" dot>\n\t\t\t\t\t\t\t\tAll systems normal\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t<nav aria-label=\"Footer\" className=\"grid grid-cols-3 gap-6\">\n\t\t\t\t\t\t\t{footerGroups.map((group) => (\n\t\t\t\t\t\t\t\t<div key={group.title}>\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-label-md\">{group.title}</h2>\n\t\t\t\t\t\t\t\t\t<ul className=\"mt-3 space-y-1\">\n\t\t\t\t\t\t\t\t\t\t{group.links.map((link) => (\n\t\t\t\t\t\t\t\t\t\t\t<li key={link.label}>\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref={link.href}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-9 items-center text-body-sm text-muted-foreground motion-interactive hover:text-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t<Card className=\"h-fit\">\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t{/* CardTitle carries the type; the h2 inside it keeps the outline. */}\n\t\t\t\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t\t\t\t<h2>The Cove digest</h2>\n\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\tOne note a month on trusted workflows and product releases.\n\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<form action=\"/newsletter\" className=\"grid gap-2 sm:grid-cols-[1fr_auto]\">\n\t\t\t\t\t\t\t\t\t<label htmlFor=\"brand-footer-email\" className=\"sr-only\">\n\t\t\t\t\t\t\t\t\t\tWork email\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\t\tid=\"brand-footer-email\"\n\t\t\t\t\t\t\t\t\t\tname=\"email\"\n\t\t\t\t\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\t\t\t\t\tautoComplete=\"email\"\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"you@company.com\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Button type=\"submit\" className=\"w-full sm:w-auto\">\n\t\t\t\t\t\t\t\t\t\tSubscribe\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</form>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"mt-12 flex flex-col gap-3 border-t border-border pt-6 text-caption text-muted-foreground sm:flex-row sm:items-center sm:justify-between\">\n\t\t\t\t\t\t<p>© 2026 Cove. All rights reserved.</p>\n\t\t\t\t\t\t<Row gap={4} wrap>\n\t\t\t\t\t\t\t{legalLinks.map((link) => (\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tkey={link.label}\n\t\t\t\t\t\t\t\t\thref={link.href}\n\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-9 min-w-9 items-center motion-interactive hover:text-foreground\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\t\t\t</footer>\n\t\t</div>\n\t)\n}"
|
|
19001
|
+
"code": "const navItems = [\n\t{ label: \"Platform\", href: \"#platform\" },\n\t{ label: \"How it works\", href: \"#how-it-works\" },\n\t{ label: \"Customers\", href: \"#customers\" },\n\t{ label: \"Pricing\", href: \"/pricing\" },\n]\n\nconst spaces = [\n\t{ name: \"Waypoint\", value: \"98.4%\", label: \"clear routes\" },\n\t{ name: \"Fielddesk\", value: \"12\", label: \"open holds\" },\n\t{ name: \"Archive\", value: \"4.8k\", label: \"sealed records\" },\n]\n\nconst routes = [\n\t{ label: \"Intake\", state: \"Open\", tone: \"info\" as const },\n\t{ label: \"Review\", state: \"Live\", tone: \"edition\" as const },\n\t{ label: \"Archive\", state: \"Safe\", tone: \"success\" as const },\n]\n\n// Weekly approvals: past weeks recede, the current week carries the signal.\nconst bars = [\n\t{ week: \"w1\", height: \"38%\", current: false },\n\t{ week: \"w2\", height: \"56%\", current: false },\n\t{ week: \"w3\", height: \"44%\", current: false },\n\t{ week: \"w4\", height: \"68%\", current: false },\n\t{ week: \"w5\", height: \"52%\", current: false },\n\t{ week: \"w6\", height: \"78%\", current: false },\n\t{ week: \"w7\", height: \"60%\", current: false },\n\t{ week: \"w8\", height: \"72%\", current: false },\n\t{ week: \"w9\", height: \"88%\", current: true },\n]\n\nconst sceneMetrics = [\n\t{ label: \"Cleared\", value: \"1.8M\", detail: \"last 30 days\" },\n\t{ label: \"Review\", value: \"42s\", detail: \"median path\" },\n\t{ label: \"Spaces\", value: \"284\", detail: \"active teams\" },\n]\n\nconst values = [\n\t{\n\t\ticon: \"lock\" as const,\n\t\ttitle: \"Private by default\",\n\t\tbody: \"Roles, approvals, and handoff history stay with the people who need them. No shared inboxes, no forwarded screenshots.\",\n\t},\n\t{\n\t\ticon: \"bell\" as const,\n\t\ttitle: \"Signals before surprises\",\n\t\tbody: \"Cove watches every route and surfaces stalled reviews and expiring approvals while there is still time to act.\",\n\t},\n\t{\n\t\ticon: \"clock\" as const,\n\t\ttitle: \"A trail for every decision\",\n\t\tbody: \"Each approval keeps its context, reviewer, and timestamp, so an audit takes an afternoon instead of a quarter.\",\n\t},\n]\n\nconst steps = [\n\t{\n\t\tnumber: \"01\",\n\t\ttitle: \"Connect your spaces\",\n\t\tbody: \"Bring teams, vendors, and record stores into one workspace, with roles that mirror how you already work.\",\n\t},\n\t{\n\t\tnumber: \"02\",\n\t\ttitle: \"Route every approval\",\n\t\tbody: \"Requests follow the path you define, with reviewers, deadlines, and escalation built in from the start.\",\n\t},\n\t{\n\t\tnumber: \"03\",\n\t\ttitle: \"Seal the record\",\n\t\tbody: \"Approved work is archived with its full context and is ready for review the moment it closes.\",\n\t},\n]\n\nconst outcomes = [\n\t{ value: \"3.2×\", label: \"faster approvals in the first quarter\" },\n\t{ value: \"68%\", label: \"fewer follow-ups on handoffs\" },\n\t{ value: \"1 day\", label: \"to prepare a quarterly audit\" },\n]\n\nconst footerGroups = [\n\t{\n\t\ttitle: \"Platform\",\n\t\tlinks: [\n\t\t\t{ label: \"Overview\", href: \"#platform\" },\n\t\t\t{ label: \"Signals\", href: \"/signals\" },\n\t\t\t{ label: \"Approvals\", href: \"/approvals\" },\n\t\t\t{ label: \"Records\", href: \"/records\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Company\",\n\t\tlinks: [\n\t\t\t{ label: \"About\", href: \"/about\" },\n\t\t\t{ label: \"Security\", href: \"/security\" },\n\t\t\t{ label: \"Customers\", href: \"#customers\" },\n\t\t\t{ label: \"Careers\", href: \"/careers\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Resources\",\n\t\tlinks: [\n\t\t\t{ label: \"Guides\", href: \"/guides\" },\n\t\t\t{ label: \"Status\", href: \"/status\" },\n\t\t\t{ label: \"Changelog\", href: \"/changelog\" },\n\t\t\t{ label: \"Docs\", href: \"/docs\" },\n\t\t],\n\t},\n]\n\nconst legalLinks = [\n\t{ label: \"Privacy\", href: \"/privacy\" },\n\t{ label: \"Terms\", href: \"/terms\" },\n\t{ label: \"Contact\", href: \"/contact\" },\n]\n\nexport function BrandLandingPage() {\n\treturn (\n\t\t<div className=\"min-h-dvh overflow-hidden bg-background text-foreground\">\n\t\t\t<header className=\"relative z-10 pt-4\">\n\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t<nav\n\t\t\t\t\t\taria-label=\"Main\"\n\t\t\t\t\t\tclassName=\"flex min-h-15 items-center justify-between gap-3 rounded-full border border-border bg-card/90 px-3 py-2 shadow-sm backdrop-blur\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<a href=\"/\" className=\"flex items-center gap-2 rounded-full px-2 motion-interactive motion-press\">\n\t\t\t\t\t\t\t<span className=\"grid size-8 place-items-center rounded-full bg-primary text-primary-foreground\">\n\t\t\t\t\t\t\t\t<Icon name=\"sparkles\" className=\"text-sm\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className=\"text-title-lg\">Cove</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<div className=\"hidden lg:block\">\n\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t{navItems.map((item) => (\n\t\t\t\t\t\t\t\t\t<Button key={item.label} variant=\"ghost\" size=\"sm\" render={<a href={item.href} />}>\n\t\t\t\t\t\t\t\t\t\t{item.label}\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" className=\"max-sm:hidden\" render={<a href=\"/login\" />}>\n\t\t\t\t\t\t\t\tSign in\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button size=\"sm\" render={<a href=\"/demo\" />}>\n\t\t\t\t\t\t\t\tRequest access\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</nav>\n\t\t\t\t</Region>\n\t\t\t</header>\n\n\t\t\t<main>\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"pb-20 pt-14 sm:pt-20 lg:pb-28\">\n\t\t\t\t\t<div className=\"grid items-center gap-12 lg:grid-cols-[1fr_1.1fr] lg:gap-10\">\n\t\t\t\t\t\t<Stack gap={3} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t<Badge appearance=\"outline\" icon={<Icon name=\"lock\" className=\"text-sm\" />}>\n\t\t\t\t\t\t\t\tPrivate operations, made legible\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t<TextReveal\n\t\t\t\t\t\t\t\tas=\"h1\"\n\t\t\t\t\t\t\t\ttext=\"A calmer command center for high-trust work\"\n\t\t\t\t\t\t\t\tclassName=\"text-balance text-display-lg sm:text-display-2xl\"\n\t\t\t\t\t\t\t\tonce\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\tCove gives teams a polished front door and a serious operating\n\t\t\t\t\t\t\t\tsurface: approvals, signals, records, and handoffs in one\n\t\t\t\t\t\t\t\tunderstandable flow.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Row gap={2} wrap className=\"pt-3\">\n\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/demo\" />}>\n\t\t\t\t\t\t\t\t\tBook a walkthrough\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\" render={<a href=\"#platform\" />}>\n\t\t\t\t\t\t\t\t\tExplore the platform\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t{/* The product scene is a dark island: `dark` re-scopes every\n\t\t\t\t\t\t semantic token, so its surfaces, muted ink and status tones\n\t\t\t\t\t\t are the theme's own dark values in either page mode. It is a\n\t\t\t\t\t\t picture of the product, so it stays out of the tab order and\n\t\t\t\t\t\t the accessibility tree; the hero copy carries its meaning. */}\n\t\t\t\t\t\t<div className=\"dark\" aria-hidden inert>\n\t\t\t\t\t\t\t<div className=\"overflow-hidden rounded-xl border border-border-floating bg-popover p-3 text-popover-foreground shadow-xl\">\n\t\t\t\t\t\t\t\t{/* Nested surfaces take concentric corners: each level's radius is\n\t\t\t\t\t\t\t\t the shell's xl minus every inset between it and the shell edge. */}\n\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/3 border border-border bg-card\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-between border-b border-border px-4 py-3\">\n\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone=\"warning\" />\n\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone=\"edition\" />\n\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone=\"success\" />\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-label-sm text-muted-foreground\">Live workspace</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"grid gap-3 p-3 md:grid-cols-[0.7fr_1.3fr]\">\n\t\t\t\t\t\t\t\t\t\t<div className=\"space-y-3 max-md:hidden\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-border bg-background p-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-label-sm text-muted-foreground\">Spaces</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-4 space-y-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{spaces.map((space) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={space.name} className=\"rounded-concentric-xl/10 bg-card p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-title-sm\">{space.name}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm\">{space.value}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-xs text-muted-foreground\">{space.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-border bg-background p-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-title-sm\">Route status</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge tone=\"success\" size=\"sm\" dot>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tHealthy\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-4 space-y-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{routes.map((route) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row key={route.label} justify=\"between\" className=\"rounded-full bg-card px-3 py-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<StatusDot tone={route.tone} />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm\">{route.label}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-label-sm text-muted-foreground\">{route.state}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-border bg-background p-4\">\n\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\" className=\"border-b border-border pb-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-label-sm text-muted-foreground\">Signal map</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-heading-4 sm:text-heading-3\">Every approval path in view</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\">Review</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-5 grid gap-4 sm:mt-6 sm:gap-5\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"grid grid-cols-9 items-end gap-1.5 rounded-concentric-xl/10 bg-card p-4 sm:gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{bars.map((bar) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={bar.week} className=\"flex h-36 items-end sm:h-56\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={`w-full rounded-full ${bar.current ? \"bg-success\" : \"bg-secondary\"}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ height: bar.height }}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"grid grid-cols-3 gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{sceneMetrics.map((metric) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={metric.label} className=\"rounded-concentric-xl/10 border border-border bg-card p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"truncate text-label-sm text-muted-foreground\">{metric.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-2 text-heading-4 sm:text-heading-3\">{metric.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-xs text-muted-foreground max-sm:hidden\">{metric.detail}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\n\t\t\t\t<section id=\"platform\" aria-labelledby=\"brand-platform-title\" className=\"border-y border-border bg-muted/40 py-20 lg:py-28\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<Stack gap={6}>\n\t\t\t\t\t\t\t<Stack gap={2} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Why Cove</p>\n\t\t\t\t\t\t\t\t<h2 id=\"brand-platform-title\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\tOne private workspace for the decisions that matter.\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tMost operations work lives in threads, spreadsheets, and memory.\n\t\t\t\t\t\t\t\t\tCove gives it a home: every request routed, every signal visible,\n\t\t\t\t\t\t\t\t\tevery outcome on the record.\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t\t\t<Grid minItemWidth=\"calc(var(--spacing) * 64)\" gap={2}>\n\t\t\t\t\t\t\t\t\t{values.map((value) => (\n\t\t\t\t\t\t\t\t\t\t<Card key={value.title} render={<article />} className=\"h-full\">\n\t\t\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"mb-4 grid size-11 place-items-center rounded-md border border-border sable-well\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Icon name={value.icon} className=\"text-xl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"text-title-lg\">{value.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{value.body}</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" id=\"how-it-works\" aria-labelledby=\"brand-steps-title\" max=\"wide\" className=\"py-20 lg:py-28\">\n\t\t\t\t\t<div className=\"grid gap-12 lg:grid-cols-[1fr_2fr] lg:gap-16\">\n\t\t\t\t\t\t<Stack gap={2} className=\"max-w-md\">\n\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">How it works</p>\n\t\t\t\t\t\t\t<h2 id=\"brand-steps-title\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\tFrom request to sealed record.\n\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t<p className=\"text-body-md text-muted-foreground\">\n\t\t\t\t\t\t\t\tTeams are usually live within a week. Cove adapts to the routes\n\t\t\t\t\t\t\t\tyou already run instead of asking you to start over.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<div className=\"pt-3\">\n\t\t\t\t\t\t\t\t<Button variant=\"outline\" iconEnd={<Icon name=\"arrow-right\" />} render={<a href=\"/guides/rollout\" />}>\n\t\t\t\t\t\t\t\t\tRead the rollout guide\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<ol className=\"grid gap-10 sm:grid-cols-3 sm:gap-6\">\n\t\t\t\t\t\t\t{steps.map((step) => (\n\t\t\t\t\t\t\t\t<li key={step.number} className=\"flex flex-col gap-2 border-t border-border-strong pt-6\">\n\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-muted-foreground\">{step.number}</span>\n\t\t\t\t\t\t\t\t\t<h3 className=\"mt-3 text-title-lg sm:mt-6\">{step.title}</h3>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{step.body}</p>\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</ol>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\n\t\t\t\t<Region as=\"section\" id=\"customers\" aria-label=\"Customer story\" max=\"wide\" className=\"pb-20 lg:pb-28\">\n\t\t\t\t\t<Card render={<figure />}>\n\t\t\t\t\t\t<div className=\"grid lg:grid-cols-[1.5fr_1fr]\">\n\t\t\t\t\t\t\t<CardContent className=\"flex flex-col justify-between gap-10 sm:p-10\">\n\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Customer story</p>\n\t\t\t\t\t\t\t\t\t<blockquote className=\"text-balance text-heading-3 sm:text-heading-2\">\n\t\t\t\t\t\t\t\t\t\t“We used to chase approvals across four tools. Now every request\n\t\t\t\t\t\t\t\t\t\thas an owner, a deadline, and a record, and our auditors stopped\n\t\t\t\t\t\t\t\t\t\tasking for screenshots.”\n\t\t\t\t\t\t\t\t\t</blockquote>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<figcaption className=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t\t\t<Avatar variant=\"art\" size=\"lg\" fallback=\"MO\" />\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-title-sm\">Maya Okafor</p>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">Head of operations, Fielddesk</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</figcaption>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t<div className=\"border-border max-lg:border-t lg:border-s\">\n\t\t\t\t\t\t\t\t<CardContent className=\"flex h-full flex-col justify-center gap-6 sm:p-10\">\n\t\t\t\t\t\t\t\t\t{outcomes.map((outcome, index) => (\n\t\t\t\t\t\t\t\t\t\t<Stack key={outcome.label} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t{index > 0 && <Separator />}\n\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-1 sm:text-display-lg\">{outcome.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-sm text-muted-foreground\">{outcome.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Card>\n\t\t\t\t</Region>\n\n\t\t\t\t<Region as=\"section\" aria-labelledby=\"brand-cta-title\" max=\"wide\" className=\"pb-20 lg:pb-28\">\n\t\t\t\t\t<div className=\"dark\">\n\t\t\t\t\t\t<div className=\"rounded-xl border border-border bg-card px-6 py-12 text-card-foreground sm:px-12 sm:py-16\">\n\t\t\t\t\t\t\t<div className=\"flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between\">\n\t\t\t\t\t\t\t\t<Stack gap={3} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t\t\t<h2 id=\"brand-cta-title\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\t\tSee your own approval paths in Cove.\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tA 30-minute walkthrough with your routes, your roles, and your\n\t\t\t\t\t\t\t\t\t\trecords. No slides.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<Row gap={2} wrap>\n\t\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/demo\" />}>\n\t\t\t\t\t\t\t\t\t\tBook a walkthrough\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"ghost\" render={<a href=\"/pricing\" />}>\n\t\t\t\t\t\t\t\t\t\tView pricing\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\t\t\t</main>\n\n\t\t\t<footer className=\"border-t border-border bg-muted/35\">\n\t\t\t\t<Region max=\"wide\" className=\"py-12 sm:py-16\">\n\t\t\t\t\t<div className=\"grid gap-10 lg:grid-cols-[1fr_1.35fr_1fr] lg:gap-8\">\n\t\t\t\t\t\t<Stack gap={4} className=\"max-w-sm\">\n\t\t\t\t\t\t\t<a href=\"/\" className=\"flex w-fit items-center gap-2 rounded-full motion-interactive motion-press\">\n\t\t\t\t\t\t\t\t<span className=\"grid size-9 place-items-center rounded-full bg-primary text-primary-foreground\">\n\t\t\t\t\t\t\t\t\t<Icon name=\"sparkles\" className=\"text-sm\" />\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span className=\"text-title-lg\">Cove</span>\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\tA calm operating surface for teams that need trust, speed, and\n\t\t\t\t\t\t\t\tcontext without the noise.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone=\"success\" dot>\n\t\t\t\t\t\t\t\tAll systems normal\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t<nav aria-label=\"Footer\" className=\"grid grid-cols-3 gap-6\">\n\t\t\t\t\t\t\t{footerGroups.map((group) => (\n\t\t\t\t\t\t\t\t<div key={group.title}>\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-label-md\">{group.title}</h2>\n\t\t\t\t\t\t\t\t\t<ul className=\"mt-3 space-y-1\">\n\t\t\t\t\t\t\t\t\t\t{group.links.map((link) => (\n\t\t\t\t\t\t\t\t\t\t\t<li key={link.label}>\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref={link.href}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-9 items-center text-body-sm text-muted-foreground motion-interactive hover:text-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</nav>\n\n\t\t\t\t\t\t<Card className=\"h-fit\">\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t{/* CardTitle carries the type; the h2 inside it keeps the outline. */}\n\t\t\t\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t\t\t\t<h2>The Cove digest</h2>\n\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\tOne note a month on trusted workflows and product releases.\n\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<form action=\"/newsletter\" className=\"grid gap-2 sm:grid-cols-[1fr_auto]\">\n\t\t\t\t\t\t\t\t\t<label htmlFor=\"brand-footer-email\" className=\"sr-only\">\n\t\t\t\t\t\t\t\t\t\tWork email\n\t\t\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\t\tid=\"brand-footer-email\"\n\t\t\t\t\t\t\t\t\t\tname=\"email\"\n\t\t\t\t\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\t\t\t\t\tautoComplete=\"email\"\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"you@company.com\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Button type=\"submit\" className=\"w-full sm:w-auto\">\n\t\t\t\t\t\t\t\t\t\tSubscribe\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</form>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"mt-12 flex flex-col gap-3 border-t border-border pt-6 text-caption text-muted-foreground sm:flex-row sm:items-center sm:justify-between\">\n\t\t\t\t\t\t<p>© 2026 Cove. All rights reserved.</p>\n\t\t\t\t\t\t<Row gap={4} wrap>\n\t\t\t\t\t\t\t{legalLinks.map((link) => (\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tkey={link.label}\n\t\t\t\t\t\t\t\t\thref={link.href}\n\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-9 min-w-9 items-center motion-interactive hover:text-foreground\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\t\t\t</footer>\n\t\t</div>\n\t)\n}"
|
|
18765
19002
|
},
|
|
18766
19003
|
{
|
|
18767
19004
|
"name": "ChatPanel",
|
|
@@ -18791,7 +19028,7 @@
|
|
|
18791
19028
|
"The composer is a real <form> whose Enter handling skips IME composition, and every icon-only control in the panel carries an accessible label — the answer actions also carry a tooltip, and the stop control is named for what it stops (`stopLabel=\"Stop answering\"`)."
|
|
18792
19029
|
],
|
|
18793
19030
|
"imports": "import { Attachment, Avatar, Bubble, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, ChatComposer, CodeBlock, Icon, Marker, MenuRadioGroup, MenuRadioItem, Message, MessageScroller, Page, SettingsMenu, SettingsRow, SimpleTooltip, StreamingText, Toolbar, ToolbarButton } from \"@sableui/react\"",
|
|
18794
|
-
"code": "/**\n * The request is in flight. In an app this is your send state — from `useChat`,\n * a fetch, whatever owns the turn — and it is deliberately ONE flag driving two\n * places: the composer's `busy` and the answer's `streaming`. They are the same\n * wait reported at both ends of the panel, and letting them drift is how you get\n * a composer that still says \"sending\" under an answer that already finished.\n */\nconst pending = true\n\n/** The answer so far. Append tokens to this; `StreamingText` assumes it only grows. */\nconst streamed =\n\t\"Yes, appends are already safe: your layout effect measures before paint. The jump is on the prepend path, which reads scrollHeight after the new rows\"\n\n/**\n * The opening exchange. `step` is what the assistant did before answering — a\n * doc it read, a tool it ran — and it rides in that message's header, so it\n * reads as part of the turn rather than as an event between turns.\n */\nconst turns: { id: string; mine: boolean; step?: string; body: string }[] = [\n\t{\n\t\tid: \"t_01\",\n\t\tmine: true,\n\t\tbody: \"Our chat scroll jumps every time older messages load. What are we doing wrong?\",\n\t},\n\t{\n\t\tid: \"t_02\",\n\t\tmine: false,\n\t\tstep: \"Read message-scroller.md\",\n\t\tbody: \"Almost always the anchor. If you preserve the scroll position by measuring the container, prepending history moves everything under the reader.\",\n\t},\n\t{\n\t\tid: \"t_03\",\n\t\tmine: false,\n\t\tbody: \"Anchor on the oldest rendered message instead and add back however far it moved. Appends leave it alone, so the same rule covers both.\",\n\t},\n]\n\nexport function ChatPanel() {\n\t// One identity for every assistant turn. A glyph rather than initials: \"AI\"\n\t// in a circle reads as a person called A. I. The glyph is sized to the\n\t// circle (the fallback's own size is tuned for two letters, and a lone icon\n\t// at that size reads as a speck); `alt` names it for a screen reader, which\n\t// otherwise gets nothing from a fallback-only avatar.\n\tconst assistant = (\n\t\t<Avatar\n\t\t\tsize=\"sm\"\n\t\t\talt=\"Assistant\"\n\t\t\tfallback={<Icon name=\"sparkles\" className=\"text-base\" />}\n\t\t/>\n\t)\n\n\treturn (\n\t\t<Page as=\"main\" max=\"feed\" gap={0} className=\"py-6\">\n\t\t\t{/* h-dvh minus the page's own padding (py-6 = 12 spacing units, in\n\t\t\t spacing units so density moves both together): the panel owns the\n\t\t\t viewport, and the transcript — not the page — is the thing that\n\t\t\t scrolls. The panel is a Card; its parts inset by the card's own\n\t\t\t --card-p, so header, transcript and composer share one edge. */}\n\t\t\t<Card\n\t\t\t\trender={<section aria-labelledby=\"chat-title\" />}\n\t\t\t\tclassName=\"flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden\"\n\t\t\t>\n\t\t\t\t<CardHeader className=\"border-b border-border pb-(--card-p)\">\n\t\t\t\t\t{/* CardTitle carries the type; the h1 inside it is the page heading. */}\n\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t<h1 id=\"chat-title\">Scroll behaviour</h1>\n\t\t\t\t\t</CardTitle>\n\t\t\t\t\t<CardDescription>Assistant · answers from your team's docs</CardDescription>\n\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t{/* Labelled, not a bare plus: the composer already has a plus\n\t\t\t\t\t\t (attach), and two identical glyphs a panel apart that do\n\t\t\t\t\t\t different things is a guessing game. */}\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t<Icon name=\"edit\" />\n\t\t\t\t\t\t\tNew chat\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</CardAction>\n\t\t\t\t</CardHeader>\n\n\t\t\t\t{/* flex-1 min-h-0 is the whole layout: it bounds the scroll region\n\t\t\t\t against the panel instead of letting the transcript grow the page. */}\n\t\t\t\t<MessageScroller className=\"min-h-0 flex-1 p-(--card-p)\">\n\t\t\t\t\t<Marker variant=\"separator\">Today</Marker>\n\n\t\t\t\t\t{turns.map((turn, i) => {\n\t\t\t\t\t\t// Derived, not hand-set: a turn continues a run when the one\n\t\t\t\t\t\t// before it has the same author. A continuation drops its\n\t\t\t\t\t\t// avatar (Message keeps the column) AND its tail (Bubble keeps\n\t\t\t\t\t\t// the ear's space) — together, that makes the run one block.\n\t\t\t\t\t\tconst continues = i > 0 && turns[i - 1].mine === turn.mine\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Message\n\t\t\t\t\t\t\t\tkey={turn.id}\n\t\t\t\t\t\t\t\tmessageId={turn.id}\n\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\tgrouped={continues}\n\t\t\t\t\t\t\t\tavatar={turn.mine ? undefined : assistant}\n\t\t\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t\t\tturn.step ? (\n\t\t\t\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"book\" className=\"justify-start\">\n\t\t\t\t\t\t\t\t\t\t\t{turn.step}\n\t\t\t\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t\t\t\t) : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Bubble\n\t\t\t\t\t\t\t\t\tvariant={turn.mine ? \"primary\" : \"muted\"}\n\t\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\t\ttail={!continues}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{turn.body}\n\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\n\t\t\t\t\t{/* The same run, continued: long-form output with no bubble,\n\t\t\t\t\t because a tinted box around a code block reads as a quote\n\t\t\t\t\t rather than as a turn. Its footer carries what the reader does\n\t\t\t\t\t WITH a settled answer, as a Toolbar: one tab stop, arrow keys\n\t\t\t\t\t between the actions. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_04\"\n\t\t\t\t\t\tgrouped\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t<Toolbar aria-label=\"Answer actions\">\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"copy\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Regenerate\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Regenerate answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"refresh\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t</Toolbar>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{/* max-w-full: a plain Bubble sizes to its content with no\n\t\t\t\t\t\t measure cap, and a code line is wider than a phone. Bounded\n\t\t\t\t\t\t by the column, the CodeBlock scrolls inside itself instead of\n\t\t\t\t\t\t pushing the whole transcript sideways. */}\n\t\t\t\t\t\t<Bubble variant=\"plain\" className=\"max-w-full\">\n\t\t\t\t\t\t\t<p className=\"text-body-md text-foreground\">\n\t\t\t\t\t\t\t\tHere is the shape of it:\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<CodeBlock\n\t\t\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\t\t\t\"const shift = anchor.offsetTop - lastTop\\nif (shift !== 0) viewport.scrollTop += shift\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfilename=\"preserve.ts\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The reader's follow-up is one turn: the question and the file\n\t\t\t\t\t it refers to stack in a single Message, so they share a side, an\n\t\t\t\t\t edge and a timestamp rather than reading as two sends. */}\n\t\t\t\t\t<Message messageId=\"t_05\" align=\"end\">\n\t\t\t\t\t\t<Bubble variant=\"primary\" align=\"end\">\n\t\t\t\t\t\t\tHere's ours. Does the same fix cover new messages arriving?\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t<Attachment\n\t\t\t\t\t\t\tname=\"scroller.tsx\"\n\t\t\t\t\t\t\tmeta=\"4.2 KB · TSX\"\n\t\t\t\t\t\t\tonOpen={() => undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The answer arriving. A `Marker` with a spinner — or ThinkingOrb —\n\t\t\t\t\t is what sits here BEFORE the first token; once tokens land it is\n\t\t\t\t\t replaced rather than kept, because two indicators for one wait\n\t\t\t\t\t read as two things happening. What stays is the step it took,\n\t\t\t\t\t as a finished note in the header.\n\n\t\t\t\t\t No bubble, same as the long-form answer above: a tinted box\n\t\t\t\t\t around a growing paragraph makes the reflow look like the box\n\t\t\t\t\t breathing. And no answer actions yet — there is nothing to copy\n\t\t\t\t\t or regenerate until it settles; Stop lives in the composer.\n\n\t\t\t\t\t MessageScroller needs no help with this — it already re-follows on\n\t\t\t\t\t \"a message grew\" (ResizeObserver), not just on \"a message arrived\",\n\t\t\t\t\t so the newest token stays pinned while the reader can still scroll\n\t\t\t\t\t up without being yanked back. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_06\"\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"file\" className=\"justify-start\">\n\t\t\t\t\t\t\t\tRead scroller.tsx\n\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Bubble variant=\"plain\">\n\t\t\t\t\t\t\t<StreamingText text={streamed} streaming={pending} />\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\t\t\t\t</MessageScroller>\n\n\t\t\t\t<CardFooter className=\"border-t border-border pt-(--card-p)\">\n\t\t\t\t\t<ChatComposer\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tplaceholder=\"Ask a follow-up…\"\n\t\t\t\t\t\t// Same flag as the answer's `streaming`, which is what turns Send\n\t\t\t\t\t\t// into Stop. A composer that stays sendable mid-answer invites a\n\t\t\t\t\t\t// second request on top of the one still running.\n\t\t\t\t\t\tbusy={pending}\n\t\t\t\t\t\tonStop={() => undefined}\n\t\t\t\t\t\tstopLabel=\"Stop answering\"\n\t\t\t\t\t\tonAttach={() => undefined}\n\t\t\t\t\t\tfooter=\"Enter to send · Shift+Enter for a new line\"\n\t\t\t\t\t\t// Model and effort sit in `actions`, at the send end of the row:\n\t\t\t\t\t\t// they qualify the message about to be sent, so they belong beside\n\t\t\t\t\t\t// the button that sends it rather than next to the attach control.\n\t\t\t\t\t\tactions={\n\t\t\t\t\t\t\t<SettingsMenu\n\t\t\t\t\t\t\t\tvariant=\"subtle\"\n\t\t\t\t\t\t\t\tsize=\"xs\"\n\t\t\t\t\t\t\t\tlabel=\"Change model and effort\"\n\t\t\t\t\t\t\t\tsummary={\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-foreground\">5.6 Sol</span>{\" \"}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground\">High</span>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Model\" value=\"5.6 Sol\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"sol\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"sol\">5.6 Sol</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"lite\">5.6 Lite</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Effort\" value=\"High\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"high\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"low\">Low</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"high\">High</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Speed\" value=\"Standard\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"standard\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"priority\">Priority</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t</SettingsMenu>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t</CardFooter>\n\t\t\t</Card>\n\t\t</Page>\n\t)\n}"
|
|
19031
|
+
"code": "/**\n * The request is in flight. In an app this is your send state — from `useChat`,\n * a fetch, whatever owns the turn — and it is deliberately ONE flag driving two\n * places: the composer's `busy` and the answer's `streaming`. They are the same\n * wait reported at both ends of the panel, and letting them drift is how you get\n * a composer that still says \"sending\" under an answer that already finished.\n */\nconst pending = true\n\n/** The answer so far. Append tokens to this; `StreamingText` assumes it only grows. */\nconst streamed =\n\t\"Yes, appends are already safe: your layout effect measures before paint. The jump is on the prepend path, which reads scrollHeight after the new rows\"\n\n/**\n * The opening exchange. `step` is what the assistant did before answering — a\n * doc it read, a tool it ran — and it rides in that message's header, so it\n * reads as part of the turn rather than as an event between turns.\n */\nconst turns: { id: string; mine: boolean; step?: string; body: string }[] = [\n\t{\n\t\tid: \"t_01\",\n\t\tmine: true,\n\t\tbody: \"Our chat scroll jumps every time older messages load. What are we doing wrong?\",\n\t},\n\t{\n\t\tid: \"t_02\",\n\t\tmine: false,\n\t\tstep: \"Read message-scroller.md\",\n\t\tbody: \"Almost always the anchor. If you preserve the scroll position by measuring the container, prepending history moves everything under the reader.\",\n\t},\n\t{\n\t\tid: \"t_03\",\n\t\tmine: false,\n\t\tbody: \"Anchor on the oldest rendered message instead and add back however far it moved. Appends leave it alone, so the same rule covers both.\",\n\t},\n]\n\nexport function ChatPanel() {\n\t// One identity for every assistant turn. A glyph rather than initials: \"AI\"\n\t// in a circle reads as a person called A. I. The glyph is sized to the\n\t// circle (the fallback's own size is tuned for two letters, and a lone icon\n\t// at that size reads as a speck); `alt` names it for a screen reader, which\n\t// otherwise gets nothing from a fallback-only avatar.\n\tconst assistant = (\n\t\t<Avatar variant=\"art\"\n\t\t\tsize=\"sm\"\n\t\t\talt=\"Assistant\"\n\t\t\tfallback={<Icon name=\"sparkles\" className=\"text-base\" />}\n\t\t/>\n\t)\n\n\treturn (\n\t\t<Page as=\"main\" max=\"feed\" gap={0} className=\"py-6\">\n\t\t\t{/* h-dvh minus the page's own padding (py-6 = 12 spacing units, in\n\t\t\t spacing units so density moves both together): the panel owns the\n\t\t\t viewport, and the transcript — not the page — is the thing that\n\t\t\t scrolls. The panel is a Card; its parts inset by the card's own\n\t\t\t --card-p, so header, transcript and composer share one edge. */}\n\t\t\t<Card\n\t\t\t\trender={<section aria-labelledby=\"chat-title\" />}\n\t\t\t\tclassName=\"flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden\"\n\t\t\t>\n\t\t\t\t<CardHeader className=\"border-b border-border pb-(--card-p)\">\n\t\t\t\t\t{/* CardTitle carries the type; the h1 inside it is the page heading. */}\n\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t<h1 id=\"chat-title\">Scroll behaviour</h1>\n\t\t\t\t\t</CardTitle>\n\t\t\t\t\t<CardDescription>Assistant · answers from your team's docs</CardDescription>\n\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t{/* Labelled, not a bare plus: the composer already has a plus\n\t\t\t\t\t\t (attach), and two identical glyphs a panel apart that do\n\t\t\t\t\t\t different things is a guessing game. */}\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t<Icon name=\"edit\" />\n\t\t\t\t\t\t\tNew chat\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</CardAction>\n\t\t\t\t</CardHeader>\n\n\t\t\t\t{/* flex-1 min-h-0 is the whole layout: it bounds the scroll region\n\t\t\t\t against the panel instead of letting the transcript grow the page. */}\n\t\t\t\t<MessageScroller className=\"min-h-0 flex-1 p-(--card-p)\">\n\t\t\t\t\t<Marker variant=\"separator\">Today</Marker>\n\n\t\t\t\t\t{turns.map((turn, i) => {\n\t\t\t\t\t\t// Derived, not hand-set: a turn continues a run when the one\n\t\t\t\t\t\t// before it has the same author. A continuation drops its\n\t\t\t\t\t\t// avatar (Message keeps the column) AND its tail (Bubble keeps\n\t\t\t\t\t\t// the ear's space) — together, that makes the run one block.\n\t\t\t\t\t\tconst continues = i > 0 && turns[i - 1].mine === turn.mine\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Message\n\t\t\t\t\t\t\t\tkey={turn.id}\n\t\t\t\t\t\t\t\tmessageId={turn.id}\n\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\tgrouped={continues}\n\t\t\t\t\t\t\t\tavatar={turn.mine ? undefined : assistant}\n\t\t\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t\t\tturn.step ? (\n\t\t\t\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"book\" className=\"justify-start\">\n\t\t\t\t\t\t\t\t\t\t\t{turn.step}\n\t\t\t\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t\t\t\t) : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Bubble\n\t\t\t\t\t\t\t\t\tvariant={turn.mine ? \"primary\" : \"muted\"}\n\t\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\t\ttail={!continues}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{turn.body}\n\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\n\t\t\t\t\t{/* The same run, continued: long-form output with no bubble,\n\t\t\t\t\t because a tinted box around a code block reads as a quote\n\t\t\t\t\t rather than as a turn. Its footer carries what the reader does\n\t\t\t\t\t WITH a settled answer, as a Toolbar: one tab stop, arrow keys\n\t\t\t\t\t between the actions. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_04\"\n\t\t\t\t\t\tgrouped\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t<Toolbar aria-label=\"Answer actions\">\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Copy answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"copy\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t\t<SimpleTooltip content=\"Regenerate\">\n\t\t\t\t\t\t\t\t\t<ToolbarButton size=\"icon-sm\" aria-label=\"Regenerate answer\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"refresh\" />\n\t\t\t\t\t\t\t\t\t</ToolbarButton>\n\t\t\t\t\t\t\t\t</SimpleTooltip>\n\t\t\t\t\t\t\t</Toolbar>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{/* max-w-full: a plain Bubble sizes to its content with no\n\t\t\t\t\t\t measure cap, and a code line is wider than a phone. Bounded\n\t\t\t\t\t\t by the column, the CodeBlock scrolls inside itself instead of\n\t\t\t\t\t\t pushing the whole transcript sideways. */}\n\t\t\t\t\t\t<Bubble variant=\"plain\" className=\"max-w-full\">\n\t\t\t\t\t\t\t<p className=\"text-body-md text-foreground\">\n\t\t\t\t\t\t\t\tHere is the shape of it:\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<CodeBlock\n\t\t\t\t\t\t\t\tcode={\n\t\t\t\t\t\t\t\t\t\"const shift = anchor.offsetTop - lastTop\\nif (shift !== 0) viewport.scrollTop += shift\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfilename=\"preserve.ts\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The reader's follow-up is one turn: the question and the file\n\t\t\t\t\t it refers to stack in a single Message, so they share a side, an\n\t\t\t\t\t edge and a timestamp rather than reading as two sends. */}\n\t\t\t\t\t<Message messageId=\"t_05\" align=\"end\">\n\t\t\t\t\t\t<Bubble variant=\"primary\" align=\"end\">\n\t\t\t\t\t\t\tHere's ours. Does the same fix cover new messages arriving?\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t<Attachment\n\t\t\t\t\t\t\tname=\"scroller.tsx\"\n\t\t\t\t\t\t\tmeta=\"4.2 KB · TSX\"\n\t\t\t\t\t\t\tonOpen={() => undefined}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t{/* The answer arriving. A `Marker` with a spinner — or ThinkingOrb —\n\t\t\t\t\t is what sits here BEFORE the first token; once tokens land it is\n\t\t\t\t\t replaced rather than kept, because two indicators for one wait\n\t\t\t\t\t read as two things happening. What stays is the step it took,\n\t\t\t\t\t as a finished note in the header.\n\n\t\t\t\t\t No bubble, same as the long-form answer above: a tinted box\n\t\t\t\t\t around a growing paragraph makes the reflow look like the box\n\t\t\t\t\t breathing. And no answer actions yet — there is nothing to copy\n\t\t\t\t\t or regenerate until it settles; Stop lives in the composer.\n\n\t\t\t\t\t MessageScroller needs no help with this — it already re-follows on\n\t\t\t\t\t \"a message grew\" (ResizeObserver), not just on \"a message arrived\",\n\t\t\t\t\t so the newest token stays pinned while the reader can still scroll\n\t\t\t\t\t up without being yanked back. */}\n\t\t\t\t\t<Message\n\t\t\t\t\t\tmessageId=\"t_06\"\n\t\t\t\t\t\tavatar={assistant}\n\t\t\t\t\t\theader={\n\t\t\t\t\t\t\t<Marker variant=\"note\" icon=\"file\" className=\"justify-start\">\n\t\t\t\t\t\t\t\tRead scroller.tsx\n\t\t\t\t\t\t\t</Marker>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Bubble variant=\"plain\">\n\t\t\t\t\t\t\t<StreamingText text={streamed} streaming={pending} />\n\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t</Message>\n\t\t\t\t</MessageScroller>\n\n\t\t\t\t<CardFooter className=\"border-t border-border pt-(--card-p)\">\n\t\t\t\t\t<ChatComposer\n\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\tplaceholder=\"Ask a follow-up…\"\n\t\t\t\t\t\t// Same flag as the answer's `streaming`, which is what turns Send\n\t\t\t\t\t\t// into Stop. A composer that stays sendable mid-answer invites a\n\t\t\t\t\t\t// second request on top of the one still running.\n\t\t\t\t\t\tbusy={pending}\n\t\t\t\t\t\tonStop={() => undefined}\n\t\t\t\t\t\tstopLabel=\"Stop answering\"\n\t\t\t\t\t\tonAttach={() => undefined}\n\t\t\t\t\t\tfooter=\"Enter to send · Shift+Enter for a new line\"\n\t\t\t\t\t\t// Model and effort sit in `actions`, at the send end of the row:\n\t\t\t\t\t\t// they qualify the message about to be sent, so they belong beside\n\t\t\t\t\t\t// the button that sends it rather than next to the attach control.\n\t\t\t\t\t\tactions={\n\t\t\t\t\t\t\t<SettingsMenu\n\t\t\t\t\t\t\t\tvariant=\"subtle\"\n\t\t\t\t\t\t\t\tsize=\"xs\"\n\t\t\t\t\t\t\t\tlabel=\"Change model and effort\"\n\t\t\t\t\t\t\t\tsummary={\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-foreground\">5.6 Sol</span>{\" \"}\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground\">High</span>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Model\" value=\"5.6 Sol\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"sol\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"sol\">5.6 Sol</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"lite\">5.6 Lite</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Effort\" value=\"High\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"high\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"low\">Low</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"high\">High</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t\t<SettingsRow label=\"Speed\" value=\"Standard\">\n\t\t\t\t\t\t\t\t\t<MenuRadioGroup defaultValue=\"standard\">\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"standard\">Standard</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t<MenuRadioItem value=\"priority\">Priority</MenuRadioItem>\n\t\t\t\t\t\t\t\t\t</MenuRadioGroup>\n\t\t\t\t\t\t\t\t</SettingsRow>\n\t\t\t\t\t\t\t</SettingsMenu>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t</CardFooter>\n\t\t\t</Card>\n\t\t</Page>\n\t)\n}"
|
|
18795
19032
|
},
|
|
18796
19033
|
{
|
|
18797
19034
|
"name": "DashboardPage",
|
|
@@ -18819,7 +19056,7 @@
|
|
|
18819
19056
|
"Avatars beside a visible name use an empty alt so the name is not read twice."
|
|
18820
19057
|
],
|
|
18821
19058
|
"imports": "import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, OverflowMenu, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from \"@sableui/react\"",
|
|
18822
|
-
"code": "// Edition status → Badge tone. Sable has no status vocabulary of its own;\n// an app maps ITS states onto tones once, here, and every badge follows.\nconst STATUS_TONE = { live: \"success\", scheduled: \"info\", \"sold-out\": \"edition\", closed: \"neutral\" } as const\nconst STATUS_LABEL = { live: \"Live\", scheduled: \"Scheduled\", \"sold-out\": \"Sold out\", closed: \"Closed\" } as const\ntype EditionStatus = keyof typeof STATUS_TONE\n\nconst MIN = 60 * 1000\nconst HOUR = 60 * MIN\nconst DAY = 24 * HOUR\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 })\nconst usdCents = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\" })\nconst shortDate = (offset: number) =>\n\tnew Date(Date.now() + offset).toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })\n\nconst stats = [\n\t{ label: \"Revenue\", value: \"$18,240\", delta: \"+8.2%\", up: true, note: \"vs. $16,857\" },\n\t{ label: \"Collects\", value: \"1,284\", delta: \"+3.1%\", up: true, note: \"182 first-time\" },\n\t{ label: \"Collectors\", value: \"312\", delta: \"+12.4%\", up: true, note: \"Best month yet\" },\n\t{ label: \"Sell-through\", value: \"64%\", delta: \"−1.8%\", up: false, note: \"6 open editions\" },\n]\n\nconst editions = [\n\t{ id: \"ed_01\", title: \"Gold Rush\", status: \"live\" as const, sold: 184, supply: 250, price: 48, released: -6 * DAY },\n\t{ id: \"ed_02\", title: \"Quiet Harbor\", status: \"sold-out\" as const, sold: 100, supply: 100, price: 120, released: -19 * DAY },\n\t{ id: \"ed_03\", title: \"Dusk, no. 4\", status: \"live\" as const, sold: 61, supply: 150, price: 64, released: -2 * DAY },\n\t{ id: \"ed_04\", title: \"Tidewater\", status: \"live\" as const, sold: 212, supply: 300, price: 36, released: -27 * DAY },\n\t{ id: \"ed_05\", title: \"Salt Study\", status: \"closed\" as const, sold: 38, supply: 75, price: 90, released: -44 * DAY },\n\t{ id: \"ed_06\", title: \"Paper Moon\", status: \"sold-out\" as const, sold: 50, supply: 50, price: 220, released: -58 * DAY },\n\t{ id: \"ed_07\", title: \"Night Signal\", status: \"scheduled\" as const, sold: 0, supply: 200, price: 56, released: 3 * DAY },\n\t{ id: \"ed_08\", title: \"Low Tide, Blue\", status: \"closed\" as const, sold: 72, supply: 120, price: 42, released: -71 * DAY },\n\t{ id: \"ed_09\", title: \"Ember Field\", status: \"sold-out\" as const, sold: 80, supply: 80, price: 140, released: -86 * DAY },\n]\nconst editionCount = 24\n\nconst payout = {\n\tamount: 8214.6,\n\tdate: 5 * DAY,\n\tlines: [\n\t\t{ label: \"Sales\", value: 9127.33 },\n\t\t{ label: \"Platform fee (10%)\", value: -912.73 },\n\t\t{ label: \"Deposits to\", value: \"Checking ••4821\" },\n\t],\n}\n\nconst collectors = [\n\t{ id: \"co_01\", name: \"Nina Simone\", handle: \"@nina\", initials: \"NS\", pieces: 14, spent: 1460 },\n\t{ id: \"co_02\", name: \"Grace Hopper\", handle: \"@grace\", initials: \"GH\", pieces: 11, spent: 1128 },\n\t{ id: \"co_03\", name: \"Frida Kahlo\", handle: \"@frida\", initials: \"FK\", pieces: 9, spent: 942 },\n\t{ id: \"co_04\", name: \"Mary Shelley\", handle: \"@mary\", initials: \"MS\", pieces: 6, spent: 588 },\n]\n\nconst upcoming = [\n\t{ id: \"up_01\", title: \"Night Signal\", detail: \"Edition of 200 · $56\", at: 3 * DAY },\n\t{ id: \"up_02\", title: \"Tidewater, second printing\", detail: \"Edition of 100 · $40\", at: 12 * DAY },\n\t{ id: \"up_03\", title: \"Dusk, no. 5\", detail: \"Edition of 150 · $64\", at: 26 * DAY },\n\t{ id: \"up_04\", title: \"Quiet Harbor, artist proofs\", detail: \"Edition of 10 · $480\", at: 34 * DAY },\n]\n\nconst activity = [\n\t{ id: \"ac_01\", who: \"Nina Simone\", initials: \"NS\", what: \"collected Gold Rush · 185 of 250\", when: 2 * MIN },\n\t{ id: \"ac_02\", who: \"Grace Hopper\", initials: \"GH\", what: \"collected Dusk, no. 4 · 61 of 150\", when: 26 * MIN },\n\t{ id: \"ac_03\", who: \"Frida Kahlo\", initials: \"FK\", what: \"tipped $20 on Tidewater\", when: 1 * HOUR },\n\t{ id: \"ac_04\", who: \"Mary Shelley\", initials: \"MS\", what: \"started following you\", when: 3 * HOUR },\n\t{ id: \"ac_05\", who: \"Nina Simone\", initials: \"NS\", what: \"set a reminder for Night Signal\", when: 6 * HOUR },\n]\n\nexport function DashboardPage() {\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel active render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" showLabel render={<a href=\"/editions\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Collectors\" showLabel render={<a href=\"/collectors\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<Row gap={1} align=\"center\" className=\"px-3\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" active render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" render={<a href=\"/editions\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Collectors\" render={<a href=\"/collectors\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tclassName=\"[--entity-py:0]\"\n\t\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\ttitle=\"Ada Lovelace\"\n\t\t\t\t\t\t\tsubtitle=\"@ada\"\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<OverflowMenu label=\"Account\" size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Overview\"\n\t\t\t\t\tdescription=\"Sales and collector activity for the last 30 days.\"\n\t\t\t\t\tactions={\n\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t<Button variant=\"outline\">\n\t\t\t\t\t\t\t\t<Icon name=\"download\" />\n\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button>\n\t\t\t\t\t\t\t\t<Icon name=\"plus\" />\n\t\t\t\t\t\t\t\tNew edition\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<Grid cols={{ base: 2, xl: 4 }} gap={3}>\n\t\t\t\t\t{stats.map((stat) => (\n\t\t\t\t\t\t<Card key={stat.label}>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>{stat.label}</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{stat.value}</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<Row gap={1} wrap>\n\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\ttone={stat.up ? \"success\" : \"warning\"}\n\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\ticon={<Icon name={stat.up ? \"arrow-up\" : \"arrow-down\"} />}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{stat.delta}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">{stat.note}</span>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t))}\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Editions</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Sales across your releases, newest first.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardContent flush className=\"mt-4 flex-1\">\n\t\t\t\t\t\t\t<DataTable\n\t\t\t\t\t\t\t\tdata={editions}\n\t\t\t\t\t\t\t\trowKey={(row) => row.id}\n\t\t\t\t\t\t\t\tdefaultSort={{ key: \"released\", direction: \"desc\" }}\n\t\t\t\t\t\t\t\tcolumns={[\n\t\t\t\t\t\t\t\t\t{ key: \"title\", header: \"Edition\", sortable: true, className: \"text-label-md whitespace-nowrap\" },\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"status\",\n\t\t\t\t\t\t\t\t\t\theader: \"Status\",\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone={STATUS_TONE[row.status as EditionStatus]} dot>\n\t\t\t\t\t\t\t\t\t\t\t\t{STATUS_LABEL[row.status as EditionStatus]}\n\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"sold\",\n\t\t\t\t\t\t\t\t\t\theader: \"Sold\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.sold / row.supply,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={3} align=\"center\" className=\"min-w-40\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Meter\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={row.sold}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmax={row.supply}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttone={row.sold === row.supply ? \"success\" : \"primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={`${row.title} sold`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tgetAriaValueText={(_, v) => `${v} of ${row.supply} sold`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-caption whitespace-nowrap text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.sold}/{row.supply}\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"revenue\",\n\t\t\t\t\t\t\t\t\t\theader: \"Revenue\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.sold * row.price,\n\t\t\t\t\t\t\t\t\t\tcell: (row) =>\n\t\t\t\t\t\t\t\t\t\t\trow.sold > 0 ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm\">{usd.format(row.sold * row.price)}</span>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"released\",\n\t\t\t\t\t\t\t\t\t\theader: \"Release\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm whitespace-nowrap text-muted-foreground\">{shortDate(row.released)}</span>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t<CardFooter className=\"justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\tShowing {editions.length} of {editionCount} editions\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/editions\" />}>\n\t\t\t\t\t\t\t\tView all editions\n\t\t\t\t\t\t\t\t<Icon name=\"arrow-right\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>Next payout</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{usdCents.format(payout.amount)}</CardTitle>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Badge tone=\"info\" size=\"sm\" icon={<Icon name=\"calendar\" />}>\n\t\t\t\t\t\t\t\t\t\t{shortDate(payout.date)}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl text-body-sm\">\n\t\t\t\t\t\t\t\t\t{payout.lines.map((line) => (\n\t\t\t\t\t\t\t\t\t\t<div key={line.label} className=\"contents\">\n\t\t\t\t\t\t\t\t\t\t\t<dt>{line.label}</dt>\n\t\t\t\t\t\t\t\t\t\t\t<dd className={typeof line.value === \"number\" ? \"text-end text-mono-sm\" : \"text-end\"}>\n\t\t\t\t\t\t\t\t\t\t\t\t{typeof line.value === \"number\" ? usdCents.format(line.value) : line.value}\n\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle>Top collectors</CardTitle>\n\t\t\t\t\t\t\t\t<CardDescription>By spend this month.</CardDescription>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/collectors\" />}>\n\t\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{collectors.map((person) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={person.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback={person.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\t\ttitle={person.name}\n\t\t\t\t\t\t\t\t\t\tsubtitle={`${person.handle} · ${person.pieces} pieces`}\n\t\t\t\t\t\t\t\t\t\taction={<span className=\"text-mono-sm\">{usd.format(person.spent)}</span>}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Upcoming releases</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Scheduled to open to collectors.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{upcoming.map((release) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={release.id}\n\t\t\t\t\t\t\t\t\ttitle={release.title}\n\t\t\t\t\t\t\t\t\tsubtitle={release.detail}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() + release.at} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t<CardFooter className=\"mt-auto\">\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/editions/new\" />}>\n\t\t\t\t\t\t\t\t<Icon name=\"calendar\" />\n\t\t\t\t\t\t\t\tSchedule a release\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Card className=\"xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Activity</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Collects, tips, and follows.</CardDescription>\n\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/activity\" />}>\n\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{activity.map((item) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback={item.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\ttitle={item.who}\n\t\t\t\t\t\t\t\t\tsubtitle={item.what}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() - item.when} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t</Card>\n\t\t\t\t</Grid>\n\t\t\t</Page>\n\t\t</AppShell>\n\t)\n}"
|
|
19059
|
+
"code": "// Edition status → Badge tone. Sable has no status vocabulary of its own;\n// an app maps ITS states onto tones once, here, and every badge follows.\nconst STATUS_TONE = { live: \"success\", scheduled: \"info\", \"sold-out\": \"edition\", closed: \"neutral\" } as const\nconst STATUS_LABEL = { live: \"Live\", scheduled: \"Scheduled\", \"sold-out\": \"Sold out\", closed: \"Closed\" } as const\ntype EditionStatus = keyof typeof STATUS_TONE\n\nconst MIN = 60 * 1000\nconst HOUR = 60 * MIN\nconst DAY = 24 * HOUR\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 })\nconst usdCents = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\" })\nconst shortDate = (offset: number) =>\n\tnew Date(Date.now() + offset).toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" })\n\nconst stats = [\n\t{ label: \"Revenue\", value: \"$18,240\", delta: \"+8.2%\", up: true, note: \"vs. $16,857\" },\n\t{ label: \"Collects\", value: \"1,284\", delta: \"+3.1%\", up: true, note: \"182 first-time\" },\n\t{ label: \"Collectors\", value: \"312\", delta: \"+12.4%\", up: true, note: \"Best month yet\" },\n\t{ label: \"Sell-through\", value: \"64%\", delta: \"−1.8%\", up: false, note: \"6 open editions\" },\n]\n\nconst editions = [\n\t{ id: \"ed_01\", title: \"Gold Rush\", status: \"live\" as const, sold: 184, supply: 250, price: 48, released: -6 * DAY },\n\t{ id: \"ed_02\", title: \"Quiet Harbor\", status: \"sold-out\" as const, sold: 100, supply: 100, price: 120, released: -19 * DAY },\n\t{ id: \"ed_03\", title: \"Dusk, no. 4\", status: \"live\" as const, sold: 61, supply: 150, price: 64, released: -2 * DAY },\n\t{ id: \"ed_04\", title: \"Tidewater\", status: \"live\" as const, sold: 212, supply: 300, price: 36, released: -27 * DAY },\n\t{ id: \"ed_05\", title: \"Salt Study\", status: \"closed\" as const, sold: 38, supply: 75, price: 90, released: -44 * DAY },\n\t{ id: \"ed_06\", title: \"Paper Moon\", status: \"sold-out\" as const, sold: 50, supply: 50, price: 220, released: -58 * DAY },\n\t{ id: \"ed_07\", title: \"Night Signal\", status: \"scheduled\" as const, sold: 0, supply: 200, price: 56, released: 3 * DAY },\n\t{ id: \"ed_08\", title: \"Low Tide, Blue\", status: \"closed\" as const, sold: 72, supply: 120, price: 42, released: -71 * DAY },\n\t{ id: \"ed_09\", title: \"Ember Field\", status: \"sold-out\" as const, sold: 80, supply: 80, price: 140, released: -86 * DAY },\n]\nconst editionCount = 24\n\nconst payout = {\n\tamount: 8214.6,\n\tdate: 5 * DAY,\n\tlines: [\n\t\t{ label: \"Sales\", value: 9127.33 },\n\t\t{ label: \"Platform fee (10%)\", value: -912.73 },\n\t\t{ label: \"Deposits to\", value: \"Checking ••4821\" },\n\t],\n}\n\nconst collectors = [\n\t{ id: \"co_01\", name: \"Nina Simone\", handle: \"@nina\", initials: \"NS\", pieces: 14, spent: 1460 },\n\t{ id: \"co_02\", name: \"Grace Hopper\", handle: \"@grace\", initials: \"GH\", pieces: 11, spent: 1128 },\n\t{ id: \"co_03\", name: \"Frida Kahlo\", handle: \"@frida\", initials: \"FK\", pieces: 9, spent: 942 },\n\t{ id: \"co_04\", name: \"Mary Shelley\", handle: \"@mary\", initials: \"MS\", pieces: 6, spent: 588 },\n]\n\nconst upcoming = [\n\t{ id: \"up_01\", title: \"Night Signal\", detail: \"Edition of 200 · $56\", at: 3 * DAY },\n\t{ id: \"up_02\", title: \"Tidewater, second printing\", detail: \"Edition of 100 · $40\", at: 12 * DAY },\n\t{ id: \"up_03\", title: \"Dusk, no. 5\", detail: \"Edition of 150 · $64\", at: 26 * DAY },\n\t{ id: \"up_04\", title: \"Quiet Harbor, artist proofs\", detail: \"Edition of 10 · $480\", at: 34 * DAY },\n]\n\nconst activity = [\n\t{ id: \"ac_01\", who: \"Nina Simone\", initials: \"NS\", what: \"collected Gold Rush · 185 of 250\", when: 2 * MIN },\n\t{ id: \"ac_02\", who: \"Grace Hopper\", initials: \"GH\", what: \"collected Dusk, no. 4 · 61 of 150\", when: 26 * MIN },\n\t{ id: \"ac_03\", who: \"Frida Kahlo\", initials: \"FK\", what: \"tipped $20 on Tidewater\", when: 1 * HOUR },\n\t{ id: \"ac_04\", who: \"Mary Shelley\", initials: \"MS\", what: \"started following you\", when: 3 * HOUR },\n\t{ id: \"ac_05\", who: \"Nina Simone\", initials: \"NS\", what: \"set a reminder for Night Signal\", when: 6 * HOUR },\n]\n\nexport function DashboardPage() {\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel active render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" showLabel render={<a href=\"/editions\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Collectors\" showLabel render={<a href=\"/collectors\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<Row gap={1} align=\"center\" className=\"px-3\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" active render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" render={<a href=\"/editions\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Collectors\" render={<a href=\"/collectors\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tclassName=\"[--entity-py:0]\"\n\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\ttitle=\"Ada Lovelace\"\n\t\t\t\t\t\t\tsubtitle=\"@ada\"\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<OverflowMenu label=\"Account\" size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Overview\"\n\t\t\t\t\tdescription=\"Sales and collector activity for the last 30 days.\"\n\t\t\t\t\tactions={\n\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t<Button variant=\"outline\">\n\t\t\t\t\t\t\t\t<Icon name=\"download\" />\n\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button>\n\t\t\t\t\t\t\t\t<Icon name=\"plus\" />\n\t\t\t\t\t\t\t\tNew edition\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<Grid cols={{ base: 2, xl: 4 }} gap={3}>\n\t\t\t\t\t{stats.map((stat) => (\n\t\t\t\t\t\t<Card key={stat.label}>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>{stat.label}</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{stat.value}</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<Row gap={1} wrap>\n\t\t\t\t\t\t\t\t\t<Badge\n\t\t\t\t\t\t\t\t\t\ttone={stat.up ? \"success\" : \"warning\"}\n\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\ticon={<Icon name={stat.up ? \"arrow-up\" : \"arrow-down\"} />}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{stat.delta}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">{stat.note}</span>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t))}\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Editions</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Sales across your releases, newest first.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardContent flush className=\"mt-4 flex-1\">\n\t\t\t\t\t\t\t<DataTable\n\t\t\t\t\t\t\t\tdata={editions}\n\t\t\t\t\t\t\t\trowKey={(row) => row.id}\n\t\t\t\t\t\t\t\tdefaultSort={{ key: \"released\", direction: \"desc\" }}\n\t\t\t\t\t\t\t\tcolumns={[\n\t\t\t\t\t\t\t\t\t{ key: \"title\", header: \"Edition\", sortable: true, className: \"text-label-md whitespace-nowrap\" },\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"status\",\n\t\t\t\t\t\t\t\t\t\theader: \"Status\",\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone={STATUS_TONE[row.status as EditionStatus]} dot>\n\t\t\t\t\t\t\t\t\t\t\t\t{STATUS_LABEL[row.status as EditionStatus]}\n\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"sold\",\n\t\t\t\t\t\t\t\t\t\theader: \"Sold\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.sold / row.supply,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={3} align=\"center\" className=\"min-w-40\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Meter\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={row.sold}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmax={row.supply}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\ttone={row.sold === row.supply ? \"success\" : \"primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={`${row.title} sold`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tgetAriaValueText={(_, v) => `${v} of ${row.supply} sold`}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-caption whitespace-nowrap text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{row.sold}/{row.supply}\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"revenue\",\n\t\t\t\t\t\t\t\t\t\theader: \"Revenue\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tsortValue: (row) => row.sold * row.price,\n\t\t\t\t\t\t\t\t\t\tcell: (row) =>\n\t\t\t\t\t\t\t\t\t\t\trow.sold > 0 ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm\">{usd.format(row.sold * row.price)}</span>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-muted-foreground\">—</span>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkey: \"released\",\n\t\t\t\t\t\t\t\t\t\theader: \"Release\",\n\t\t\t\t\t\t\t\t\t\talign: \"end\",\n\t\t\t\t\t\t\t\t\t\tsortable: true,\n\t\t\t\t\t\t\t\t\t\tcell: (row) => (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm whitespace-nowrap text-muted-foreground\">{shortDate(row.released)}</span>\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t<CardFooter className=\"justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\tShowing {editions.length} of {editionCount} editions\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/editions\" />}>\n\t\t\t\t\t\t\t\tView all editions\n\t\t\t\t\t\t\t\t<Icon name=\"arrow-right\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardDescription>Next payout</CardDescription>\n\t\t\t\t\t\t\t\t<CardTitle className=\"text-mono-xl\">{usdCents.format(payout.amount)}</CardTitle>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Badge tone=\"info\" size=\"sm\" icon={<Icon name=\"calendar\" />}>\n\t\t\t\t\t\t\t\t\t\t{shortDate(payout.date)}\n\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl text-body-sm\">\n\t\t\t\t\t\t\t\t\t{payout.lines.map((line) => (\n\t\t\t\t\t\t\t\t\t\t<div key={line.label} className=\"contents\">\n\t\t\t\t\t\t\t\t\t\t\t<dt>{line.label}</dt>\n\t\t\t\t\t\t\t\t\t\t\t<dd className={typeof line.value === \"number\" ? \"text-end text-mono-sm\" : \"text-end\"}>\n\t\t\t\t\t\t\t\t\t\t\t\t{typeof line.value === \"number\" ? usdCents.format(line.value) : line.value}\n\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle>Top collectors</CardTitle>\n\t\t\t\t\t\t\t\t<CardDescription>By spend this month.</CardDescription>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/collectors\" />}>\n\t\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{collectors.map((person) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={person.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={person.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\t\ttitle={person.name}\n\t\t\t\t\t\t\t\t\t\tsubtitle={`${person.handle} · ${person.pieces} pieces`}\n\t\t\t\t\t\t\t\t\t\taction={<span className=\"text-mono-sm\">{usd.format(person.spent)}</span>}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Grid>\n\t\t\t\t<Grid cols={{ base: 1, xl: 3 }} gap={3}>\n\t\t\t\t\t<Card className=\"flex flex-col\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Upcoming releases</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Scheduled to open to collectors.</CardDescription>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{upcoming.map((release) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={release.id}\n\t\t\t\t\t\t\t\t\ttitle={release.title}\n\t\t\t\t\t\t\t\t\tsubtitle={release.detail}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() + release.at} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t<CardFooter className=\"mt-auto\">\n\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/editions/new\" />}>\n\t\t\t\t\t\t\t\t<Icon name=\"calendar\" />\n\t\t\t\t\t\t\t\tSchedule a release\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Card className=\"xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Activity</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Collects, tips, and follows.</CardDescription>\n\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/activity\" />}>\n\t\t\t\t\t\t\t\t\tView all\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t{activity.map((item) => (\n\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={item.initials} alt=\"\" />}\n\t\t\t\t\t\t\t\t\ttitle={item.who}\n\t\t\t\t\t\t\t\t\tsubtitle={item.what}\n\t\t\t\t\t\t\t\t\taction={<RelativeTime date={Date.now() - item.when} className=\"text-caption text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardList>\n\t\t\t\t\t</Card>\n\t\t\t\t</Grid>\n\t\t\t</Page>\n\t\t</AppShell>\n\t)\n}"
|
|
18823
19060
|
},
|
|
18824
19061
|
{
|
|
18825
19062
|
"name": "FeedPage",
|
|
@@ -18843,7 +19080,7 @@
|
|
|
18843
19080
|
"The Skeleton card is aria-busy; the loading Button keeps its label (\"Loading posts\")."
|
|
18844
19081
|
],
|
|
18845
19082
|
"imports": "import { AspectBox, Avatar, Badge, Button, Card, CardContent, CardHeader, CardList, CardTitle, Entity, Icon, MenuItem, MenuSeparator, Meter, MeterLabel, MeterTrack, MeterValue, OverflowMenu, Page, PageHeader, Row, Skeleton, Stack, Tabs, TabsList, TabsPanel, TabsTrigger, Textarea } from \"@sableui/react\"",
|
|
18846
|
-
"code": "type Post = {\n\tid: string\n\tauthor: string\n\thandle: string\n\tinitials: string\n\ttime: string\n\tfollowing: boolean\n\tbody: string\n\tmedia?: { src: string; alt: string }\n\tdrop?: { title: string; price: string; collected: number; supply: number }\n\tlikes: number\n\tcomments: number\n}\n\nconst posts: Post[] = [\n\t{\n\t\tid: \"p_01\",\n\t\tauthor: \"Ada Lovelace\",\n\t\thandle: \"@ada\",\n\t\tinitials: \"AL\",\n\t\ttime: \"2h\",\n\t\tfollowing: true,\n\t\tbody: \"Gold Rush is live. 100 signed prints, each paired with the on-chain original. Thank you for waiting.\",\n\t\tmedia: { src: \"/media/edition-cover.svg\", alt: \"Gold Rush, a gold-leaf study in overlapping arcs\" },\n\t\tdrop: { title: \"Gold Rush\", price: \"$120\", collected: 62, supply: 100 },\n\t\tlikes: 128,\n\t\tcomments: 24,\n\t},\n\t{\n\t\tid: \"p_02\",\n\t\tauthor: \"Grace Hopper\",\n\t\thandle: \"@grace\",\n\t\tinitials: \"GH\",\n\t\ttime: \"5h\",\n\t\tfollowing: true,\n\t\tbody: \"Process shot from this week. Compilers and copper ink — the plotter ran for eleven hours on this one.\",\n\t\tmedia: { src: \"/media/process-shot.svg\", alt: \"Work-in-progress plotter drawing in copper ink\" },\n\t\tlikes: 86,\n\t\tcomments: 9,\n\t},\n\t{\n\t\tid: \"p_03\",\n\t\tauthor: \"Nina Simone\",\n\t\thandle: \"@nina\",\n\t\tinitials: \"NS\",\n\t\ttime: \"8h\",\n\t\tfollowing: false,\n\t\tbody: \"Collectors of Dusk, no. 3 — your unlockable score is now in your library. Headphones on for this one.\",\n\t\tlikes: 214,\n\t\tcomments: 31,\n\t},\n]\n\nconst suggestions = [\n\t{ id: \"u_01\", name: \"Mary Shelley\", handle: \"@mary\", initials: \"MS\" },\n\t{ id: \"u_02\", name: \"Nina Simone\", handle: \"@nina\", initials: \"NS\" },\n]\n\nconst upcoming = [\n\t{ id: \"d_01\", title: \"Quiet Harbor\", artist: \"Mary Shelley\", initials: \"MS\", date: \"Sat, Oct 12\" },\n\t{ id: \"d_02\", title: \"Dusk, no. 4\", artist: \"Nina Simone\", initials: \"NS\", date: \"Tue, Oct 15\" },\n]\n\nconst renderPost = (post: Post) => (\n\t<article key={post.id}>\n\t\t<Card>\n\t\t\t<CardList>\n\t\t\t\t<Entity\n\t\t\t\t\tleading={<Avatar fallback={post.initials} alt={post.author} />}\n\t\t\t\t\ttitle={post.author}\n\t\t\t\t\tsubtitle={`${post.handle} · ${post.time}`}\n\t\t\t\t\taction={\n\t\t\t\t\t\t<OverflowMenu label={`Post options for ${post.author}`} size=\"icon-sm\">\n\t\t\t\t\t\t\t<MenuItem>Copy link</MenuItem>\n\t\t\t\t\t\t\t<MenuItem>Mute {post.handle}</MenuItem>\n\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t<MenuItem variant=\"destructive\">Report post</MenuItem>\n\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</CardList>\n\t\t\t<CardContent className=\"flex flex-col gap-4 pt-0\">\n\t\t\t\t<p className=\"text-body-md text-foreground\">{post.body}</p>\n\t\t\t\t{post.media ? (\n\t\t\t\t\t<AspectBox ratio={16 / 9} className=\"overflow-hidden rounded-concentric-lg/6 bg-muted\">\n\t\t\t\t\t\t<img src={post.media.src} alt={post.media.alt} className=\"size-full object-cover\" />\n\t\t\t\t\t\t{post.drop ? (\n\t\t\t\t\t\t\t<div className=\"absolute start-3 top-3\">\n\t\t\t\t\t\t\t\t<Badge appearance=\"overlay\" size=\"sm\" dot>\n\t\t\t\t\t\t\t\t\tLive drop\n\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</AspectBox>\n\t\t\t\t) : null}\n\t\t\t\t{post.drop ? (\n\t\t\t\t\t<Stack gap={1.5}>\n\t\t\t\t\t\t<Row gap={2} align=\"center\" justify=\"between\">\n\t\t\t\t\t\t\t<div className=\"min-w-0\">\n\t\t\t\t\t\t\t\t<p className=\"text-title-md text-foreground\">{post.drop.title}</p>\n\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{`${post.drop.price} · Edition of ${post.drop.supply}`}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Button>{`Collect for ${post.drop.price}`}</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Meter\n\t\t\t\t\t\t\tvalue={post.drop.collected}\n\t\t\t\t\t\t\tmax={post.drop.supply}\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tgetAriaValueText={(_, value) => `${value} of ${post.drop?.supply} collected`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Row gap={2} align=\"center\" justify=\"between\" className=\"mb-2\">\n\t\t\t\t\t\t\t\t<MeterLabel>{`${post.drop.collected} collected`}</MeterLabel>\n\t\t\t\t\t\t\t\t<MeterValue>{(_, value) => `${(post.drop?.supply ?? 0) - value} left`}</MeterValue>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t<MeterTrack />\n\t\t\t\t\t\t</Meter>\n\t\t\t\t\t</Stack>\n\t\t\t\t) : null}\n\t\t\t\t{/* Pulled out by exactly a sm Button's own padding (px-3), so the\n\t\t\t\t first icon lands on the text edge above it. */}\n\t\t\t\t<Row gap={0.5} className=\"-ms-3\">\n\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" aria-label={`Like, ${post.likes} likes`}>\n\t\t\t\t\t\t<Icon name=\"heart\" />\n\t\t\t\t\t\t{post.likes}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" aria-label={`Comment, ${post.comments} comments`}>\n\t\t\t\t\t\t<Icon name=\"comment\" />\n\t\t\t\t\t\t{post.comments}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" aria-label=\"Share\">\n\t\t\t\t\t\t<Icon name=\"share\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</Row>\n\t\t\t</CardContent>\n\t\t</Card>\n\t</article>\n)\n\nexport function FeedPage() {\n\treturn (\n\t\t<Page as=\"main\" max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t<Row gap={4} align=\"start\" justify=\"center\">\n\t\t\t\t<Stack gap={3} className=\"min-w-0 max-w-(--region-feed) flex-1\">\n\t\t\t\t\t<PageHeader title=\"Home\" />\n\t\t\t\t\t<Card>\n\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t<Row gap={1.5} align=\"start\">\n\t\t\t\t\t\t\t\t<Avatar fallback=\"AL\" alt=\"Ada Lovelace\" />\n\t\t\t\t\t\t\t\t<Stack gap={1.5} className=\"min-w-0 flex-1\">\n\t\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\t\tautoResize\n\t\t\t\t\t\t\t\t\t\trows={2}\n\t\t\t\t\t\t\t\t\t\taria-label=\"New post\"\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"Share a process shot or an upcoming drop\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Row gap={1} align=\"center\" justify=\"between\">\n\t\t\t\t\t\t\t\t\t\t<Row gap={0.5}>\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Add images\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Icon name=\"image-stack\" />\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Attach an edition\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Icon name=\"gem\" />\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\tPost\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Tabs defaultValue=\"for-you\">\n\t\t\t\t\t\t<TabsList className=\"flex\">\n\t\t\t\t\t\t\t<TabsTrigger value=\"for-you\">For you</TabsTrigger>\n\t\t\t\t\t\t\t<TabsTrigger value=\"following\">Following</TabsTrigger>\n\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t\t<TabsPanel value=\"for-you\">\n\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t{posts.map(renderPost)}\n\t\t\t\t\t\t\t\t{/* The next page, in the shape it will land in, while it fetches. */}\n\t\t\t\t\t\t\t\t<Card aria-busy=\"true\">\n\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t\t<Row gap={1.5} align=\"center\">\n\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"size-10 rounded-full\" />\n\t\t\t\t\t\t\t\t\t\t\t<Stack gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-32\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-20\" />\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-full\" />\n\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-2/3\" />\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t<Button variant=\"secondary\" loading loadingLabel=\"Loading posts\">\n\t\t\t\t\t\t\t\t\tLoad more posts\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t<TabsPanel value=\"following\">\n\t\t\t\t\t\t\t<Stack gap={3}>{posts.filter((post) => post.following).map(renderPost)}</Stack>\n\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t</Tabs>\n\t\t\t\t</Stack>\n\t\t\t\t<aside aria-label=\"Suggestions\" className=\"sticky top-6 hidden w-80 shrink-0 lg:block\">\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Who to follow</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{suggestions.map((person) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={person.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar fallback={person.initials} alt={person.name} />}\n\t\t\t\t\t\t\t\t\t\ttitle={person.name}\n\t\t\t\t\t\t\t\t\t\tsubtitle={person.handle}\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\" aria-label={`Follow ${person.name}`}>\n\t\t\t\t\t\t\t\t\t\t\t\tFollow\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Upcoming drops</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{upcoming.map((drop) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={drop.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar fallback={drop.initials} alt={drop.artist} />}\n\t\t\t\t\t\t\t\t\t\ttitle={drop.title}\n\t\t\t\t\t\t\t\t\t\tsubtitle={`${drop.artist} · ${drop.date}`}\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label={`Remind me about ${drop.title}`}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Icon name=\"bell\" />\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</aside>\n\t\t\t</Row>\n\t\t</Page>\n\t)\n}"
|
|
19083
|
+
"code": "type Post = {\n\tid: string\n\tauthor: string\n\thandle: string\n\tinitials: string\n\ttime: string\n\tfollowing: boolean\n\tbody: string\n\tmedia?: { src: string; alt: string }\n\tdrop?: { title: string; price: string; collected: number; supply: number }\n\tlikes: number\n\tcomments: number\n}\n\nconst posts: Post[] = [\n\t{\n\t\tid: \"p_01\",\n\t\tauthor: \"Ada Lovelace\",\n\t\thandle: \"@ada\",\n\t\tinitials: \"AL\",\n\t\ttime: \"2h\",\n\t\tfollowing: true,\n\t\tbody: \"Gold Rush is live. 100 signed prints, each paired with the on-chain original. Thank you for waiting.\",\n\t\tmedia: { src: \"/media/edition-cover.svg\", alt: \"Gold Rush, a gold-leaf study in overlapping arcs\" },\n\t\tdrop: { title: \"Gold Rush\", price: \"$120\", collected: 62, supply: 100 },\n\t\tlikes: 128,\n\t\tcomments: 24,\n\t},\n\t{\n\t\tid: \"p_02\",\n\t\tauthor: \"Grace Hopper\",\n\t\thandle: \"@grace\",\n\t\tinitials: \"GH\",\n\t\ttime: \"5h\",\n\t\tfollowing: true,\n\t\tbody: \"Process shot from this week. Compilers and copper ink — the plotter ran for eleven hours on this one.\",\n\t\tmedia: { src: \"/media/process-shot.svg\", alt: \"Work-in-progress plotter drawing in copper ink\" },\n\t\tlikes: 86,\n\t\tcomments: 9,\n\t},\n\t{\n\t\tid: \"p_03\",\n\t\tauthor: \"Nina Simone\",\n\t\thandle: \"@nina\",\n\t\tinitials: \"NS\",\n\t\ttime: \"8h\",\n\t\tfollowing: false,\n\t\tbody: \"Collectors of Dusk, no. 3 — your unlockable score is now in your library. Headphones on for this one.\",\n\t\tlikes: 214,\n\t\tcomments: 31,\n\t},\n]\n\nconst suggestions = [\n\t{ id: \"u_01\", name: \"Mary Shelley\", handle: \"@mary\", initials: \"MS\" },\n\t{ id: \"u_02\", name: \"Nina Simone\", handle: \"@nina\", initials: \"NS\" },\n]\n\nconst upcoming = [\n\t{ id: \"d_01\", title: \"Quiet Harbor\", artist: \"Mary Shelley\", initials: \"MS\", date: \"Sat, Oct 12\" },\n\t{ id: \"d_02\", title: \"Dusk, no. 4\", artist: \"Nina Simone\", initials: \"NS\", date: \"Tue, Oct 15\" },\n]\n\nconst renderPost = (post: Post) => (\n\t<article key={post.id}>\n\t\t<Card>\n\t\t\t<CardList>\n\t\t\t\t<Entity\n\t\t\t\t\tleading={<Avatar variant=\"art\" fallback={post.initials} alt={post.author} />}\n\t\t\t\t\ttitle={post.author}\n\t\t\t\t\tsubtitle={`${post.handle} · ${post.time}`}\n\t\t\t\t\taction={\n\t\t\t\t\t\t<OverflowMenu label={`Post options for ${post.author}`} size=\"icon-sm\">\n\t\t\t\t\t\t\t<MenuItem>Copy link</MenuItem>\n\t\t\t\t\t\t\t<MenuItem>Mute {post.handle}</MenuItem>\n\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t<MenuItem variant=\"destructive\">Report post</MenuItem>\n\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</CardList>\n\t\t\t<CardContent className=\"flex flex-col gap-4 pt-0\">\n\t\t\t\t<p className=\"text-body-md text-foreground\">{post.body}</p>\n\t\t\t\t{post.media ? (\n\t\t\t\t\t<AspectBox ratio={16 / 9} className=\"overflow-hidden rounded-concentric-lg/6 bg-muted\">\n\t\t\t\t\t\t<img src={post.media.src} alt={post.media.alt} className=\"size-full object-cover\" />\n\t\t\t\t\t\t{post.drop ? (\n\t\t\t\t\t\t\t<div className=\"absolute start-3 top-3\">\n\t\t\t\t\t\t\t\t<Badge appearance=\"overlay\" size=\"sm\" dot>\n\t\t\t\t\t\t\t\t\tLive drop\n\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</AspectBox>\n\t\t\t\t) : null}\n\t\t\t\t{post.drop ? (\n\t\t\t\t\t<Stack gap={1.5}>\n\t\t\t\t\t\t<Row gap={2} align=\"center\" justify=\"between\">\n\t\t\t\t\t\t\t<div className=\"min-w-0\">\n\t\t\t\t\t\t\t\t<p className=\"text-title-md text-foreground\">{post.drop.title}</p>\n\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{`${post.drop.price} · Edition of ${post.drop.supply}`}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Button>{`Collect for ${post.drop.price}`}</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Meter\n\t\t\t\t\t\t\tvalue={post.drop.collected}\n\t\t\t\t\t\t\tmax={post.drop.supply}\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tgetAriaValueText={(_, value) => `${value} of ${post.drop?.supply} collected`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Row gap={2} align=\"center\" justify=\"between\" className=\"mb-2\">\n\t\t\t\t\t\t\t\t<MeterLabel>{`${post.drop.collected} collected`}</MeterLabel>\n\t\t\t\t\t\t\t\t<MeterValue>{(_, value) => `${(post.drop?.supply ?? 0) - value} left`}</MeterValue>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t<MeterTrack />\n\t\t\t\t\t\t</Meter>\n\t\t\t\t\t</Stack>\n\t\t\t\t) : null}\n\t\t\t\t{/* Pulled out by exactly a sm Button's own padding (px-3), so the\n\t\t\t\t first icon lands on the text edge above it. */}\n\t\t\t\t<Row gap={0.5} className=\"-ms-3\">\n\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" aria-label={`Like, ${post.likes} likes`}>\n\t\t\t\t\t\t<Icon name=\"heart\" />\n\t\t\t\t\t\t{post.likes}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" aria-label={`Comment, ${post.comments} comments`}>\n\t\t\t\t\t\t<Icon name=\"comment\" />\n\t\t\t\t\t\t{post.comments}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" aria-label=\"Share\">\n\t\t\t\t\t\t<Icon name=\"share\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</Row>\n\t\t\t</CardContent>\n\t\t</Card>\n\t</article>\n)\n\nexport function FeedPage() {\n\treturn (\n\t\t<Page as=\"main\" max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t<Row gap={4} align=\"start\" justify=\"center\">\n\t\t\t\t<Stack gap={3} className=\"min-w-0 max-w-(--region-feed) flex-1\">\n\t\t\t\t\t<PageHeader title=\"Home\" />\n\t\t\t\t\t<Card>\n\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t<Row gap={1.5} align=\"start\">\n\t\t\t\t\t\t\t\t<Avatar variant=\"art\" fallback=\"AL\" alt=\"Ada Lovelace\" />\n\t\t\t\t\t\t\t\t<Stack gap={1.5} className=\"min-w-0 flex-1\">\n\t\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\t\tautoResize\n\t\t\t\t\t\t\t\t\t\trows={2}\n\t\t\t\t\t\t\t\t\t\taria-label=\"New post\"\n\t\t\t\t\t\t\t\t\t\tplaceholder=\"Share a process shot or an upcoming drop\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Row gap={1} align=\"center\" justify=\"between\">\n\t\t\t\t\t\t\t\t\t\t<Row gap={0.5}>\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Add images\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Icon name=\"image-stack\" />\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Attach an edition\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Icon name=\"gem\" />\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\tPost\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Tabs defaultValue=\"for-you\">\n\t\t\t\t\t\t<TabsList className=\"flex\">\n\t\t\t\t\t\t\t<TabsTrigger value=\"for-you\">For you</TabsTrigger>\n\t\t\t\t\t\t\t<TabsTrigger value=\"following\">Following</TabsTrigger>\n\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t\t<TabsPanel value=\"for-you\">\n\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t{posts.map(renderPost)}\n\t\t\t\t\t\t\t\t{/* The next page, in the shape it will land in, while it fetches. */}\n\t\t\t\t\t\t\t\t<Card aria-busy=\"true\">\n\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t\t<Row gap={1.5} align=\"center\">\n\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"size-10 rounded-full\" />\n\t\t\t\t\t\t\t\t\t\t\t<Stack gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-32\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-20\" />\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-full\" />\n\t\t\t\t\t\t\t\t\t\t<Skeleton className=\"h-3 w-2/3\" />\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t<Button variant=\"secondary\" loading loadingLabel=\"Loading posts\">\n\t\t\t\t\t\t\t\t\tLoad more posts\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t<TabsPanel value=\"following\">\n\t\t\t\t\t\t\t<Stack gap={3}>{posts.filter((post) => post.following).map(renderPost)}</Stack>\n\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t</Tabs>\n\t\t\t\t</Stack>\n\t\t\t\t<aside aria-label=\"Suggestions\" className=\"sticky top-6 hidden w-80 shrink-0 lg:block\">\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Who to follow</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{suggestions.map((person) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={person.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" fallback={person.initials} alt={person.name} />}\n\t\t\t\t\t\t\t\t\t\ttitle={person.name}\n\t\t\t\t\t\t\t\t\t\tsubtitle={person.handle}\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\" aria-label={`Follow ${person.name}`}>\n\t\t\t\t\t\t\t\t\t\t\t\tFollow\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Upcoming drops</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t{upcoming.map((drop) => (\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tkey={drop.id}\n\t\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" fallback={drop.initials} alt={drop.artist} />}\n\t\t\t\t\t\t\t\t\t\ttitle={drop.title}\n\t\t\t\t\t\t\t\t\t\tsubtitle={`${drop.artist} · ${drop.date}`}\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label={`Remind me about ${drop.title}`}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Icon name=\"bell\" />\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</aside>\n\t\t\t</Row>\n\t\t</Page>\n\t)\n}"
|
|
18847
19084
|
},
|
|
18848
19085
|
{
|
|
18849
19086
|
"name": "LandingPage",
|
|
@@ -18868,7 +19105,7 @@
|
|
|
18868
19105
|
"CTA buttons are real links with visible text, with no icon-only CTAs. Footer links keep a 36px minimum row height for touch."
|
|
18869
19106
|
],
|
|
18870
19107
|
"imports": "import { Avatar, Badge, Browser, Bubble, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardList, CardTitle, ChatComposer, Code, CodeBlock, Entity, Grid, Icon, Message, Meter, MeterLabel, MeterTrack, MeterValue, PageHeader, Region, Reveal, Row, Stack, Tabs, TabsList, TabsPanel, TabsTrigger } from \"@sableui/react\"",
|
|
18871
|
-
"code": "const navLinks = [\n\t{ label: \"Docs\", href: \"/docs\" },\n\t{ label: \"Components\", href: \"/components\" },\n\t{ label: \"Patterns\", href: \"/patterns\" },\n\t{ label: \"Themes\", href: \"/themes\" },\n]\n\nconst deploys = [\n\t{\n\t\tinitials: \"MO\",\n\t\tname: \"Maya Ortiz\",\n\t\ttitle: \"Add usage-based billing\",\n\t\tmeta: \"main · a3f9c21 · 2m ago\",\n\t\tstatus: \"Building\",\n\t\ttone: \"info\" as const,\n\t\tpulse: true,\n\t},\n\t{\n\t\tinitials: \"SR\",\n\t\tname: \"Sam Reyes\",\n\t\ttitle: \"Fix search ranking for long queries\",\n\t\tmeta: \"main · 81be04d · 18m ago\",\n\t\tstatus: \"Ready\",\n\t\ttone: \"success\" as const,\n\t\tpulse: false,\n\t},\n\t{\n\t\tinitials: \"JL\",\n\t\tname: \"Jun Lee\",\n\t\ttitle: \"Rotate webhook signing keys\",\n\t\tmeta: \"main · 5c7e9aa · 1h ago\",\n\t\tstatus: \"Ready\",\n\t\ttone: \"success\" as const,\n\t\tpulse: false,\n\t},\n]\n\nconst features = [\n\t{\n\t\ticon: \"component\" as const,\n\t\ttitle: \"Behavior built in\",\n\t\tbody: \"Focus management, keyboard navigation, and ARIA come from Base UI primitives under every control.\",\n\t},\n\t{\n\t\ticon: \"eyedropper\" as const,\n\t\ttitle: \"Semantic tokens end to end\",\n\t\tbody: \"Re-theme the whole product, light and dark, from one preset without touching a component.\",\n\t},\n\t{\n\t\ticon: \"layout\" as const,\n\t\ttitle: \"Pages, not just parts\",\n\t\tbody: \"Landing, dashboard, settings, auth, and list-detail shells to copy and adapt.\",\n\t},\n\t{\n\t\ticon: \"book\" as const,\n\t\ttitle: \"A contract agents can read\",\n\t\tbody: \"registry.json, llms.txt, and a CLI hand agents the exact props, examples, and rules.\",\n\t},\n\t{\n\t\ticon: \"circle-check\" as const,\n\t\ttitle: \"Lint that knows the design\",\n\t\tbody: \"sable-lint flags raw colors, hand-built controls, and motion that ignores reduced motion.\",\n\t},\n\t{\n\t\ticon: \"play\" as const,\n\t\ttitle: \"Motion with an off switch\",\n\t\tbody: \"Named recipes run on one scale variable, so reduced motion stops every transition at once.\",\n\t},\n]\n\nconst steps = [\n\t{\n\t\ttitle: \"Install the package\",\n\t\tbody: \"One dependency. React 19 and Tailwind v4 are the only peers.\",\n\t},\n\t{\n\t\ttitle: \"Import the styles\",\n\t\tbody: \"Tailwind first, then Sable's tokens, then a source glob over the package.\",\n\t},\n\t{\n\t\ttitle: \"Compose your screens\",\n\t\tbody: \"Build from components and layout primitives. Your theme does the rest.\",\n\t},\n]\n\nconst stylesCode = `@import \"tailwindcss\";\n@import \"@sableui/react/styles.css\";\n@source \"../node_modules/@sableui/react/dist/**/*.{js,mjs}\";`\n\n// design-lint-disable-next-line self-import -- a consumer code sample displayed on the page, not an import\nconst composeCode = `import { Button, Card, CardContent, CardHeader, CardTitle } from \"@sableui/react\"\n\nexport function DeployCard() {\n return (\n <Card>\n <CardHeader>\n <CardTitle>Production</CardTitle>\n </CardHeader>\n <CardContent>\n <Button>Deploy</Button>\n </CardContent>\n </Card>\n )\n}`\n\nconst terminalCode = `$ npx sable search \"confirm before deleting\"\ncomponent DestructiveActionModal\ncomponent Dialog\n\n$ npx sable component DestructiveActionModal --props\nprops, a11y contract, composed examples\n\n$ npx sable lint src\n0 findings in 42 files`\n\nconst agentPoints = [\n\t\"Search ranks components, patterns, tokens, and docs for a plain-language need.\",\n\t\"Every component ships its props, accessibility contract, and composed examples.\",\n\t\"The same lint runs in the editor, in CI, and inside the agent's own loop.\",\n]\n\nconst footerGroups = [\n\t{\n\t\ttitle: \"Product\",\n\t\tlinks: [\n\t\t\t{ label: \"Components\", href: \"/components\" },\n\t\t\t{ label: \"Patterns\", href: \"/patterns\" },\n\t\t\t{ label: \"Themes\", href: \"/themes\" },\n\t\t\t{ label: \"Canvas\", href: \"/canvas\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Resources\",\n\t\tlinks: [\n\t\t\t{ label: \"Documentation\", href: \"/docs\" },\n\t\t\t{ label: \"Changelog\", href: \"/changelog\" },\n\t\t\t{ label: \"llms.txt\", href: \"/llms.txt\" },\n\t\t\t{ label: \"registry.json\", href: \"/registry.json\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Community\",\n\t\tlinks: [\n\t\t\t{ label: \"GitHub\", href: \"/github\" },\n\t\t\t{ label: \"npm\", href: \"/npm\" },\n\t\t\t{ label: \"Discussions\", href: \"/discussions\" },\n\t\t],\n\t},\n]\n\nexport function LandingPage() {\n\treturn (\n\t\t<div className=\"min-h-dvh bg-background text-foreground\">\n\t\t\t<header className=\"border-b border-border\">\n\t\t\t\t<Region as=\"nav\" aria-label=\"Main\" max=\"wide\" className=\"grid h-16 grid-cols-[1fr_auto_1fr] items-center\">\n\t\t\t\t\t<a href=\"/\" className=\"justify-self-start text-title-lg motion-interactive hover:text-muted-foreground\">\n\t\t\t\t\t\tSable\n\t\t\t\t\t</a>\n\t\t\t\t\t<div className=\"justify-self-center max-md:hidden\">\n\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t{navLinks.map((link) => (\n\t\t\t\t\t\t\t\t<Button key={link.label} variant=\"ghost\" size=\"sm\" render={<a href={link.href} />}>\n\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Row gap={1} className=\"col-start-3 justify-self-end\">\n\t\t\t\t\t\t<div className=\"max-sm:hidden\">\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/github\" />}>\n\t\t\t\t\t\t\t\tGitHub\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Button size=\"sm\" render={<a href=\"/docs/getting-started\" />}>\n\t\t\t\t\t\t\tGet started\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Row>\n\t\t\t\t</Region>\n\t\t\t</header>\n\n\t\t\t<main>\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"pb-16 pt-16 sm:pt-24\">\n\t\t\t\t\t<Stack gap={8} align=\"center\">\n\t\t\t\t\t\t<Stack gap={4} align=\"center\" className=\"text-center\">\n\t\t\t\t\t\t\t<Badge appearance=\"outline\" size=\"sm\" icon={<Icon name=\"sparkles\" />}>\n\t\t\t\t\t\t\t\tOpen source · MIT licensed\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t<h1 className=\"max-w-4xl text-balance text-display-lg sm:text-display-2xl\">\n\t\t\t\t\t\t\t\tProduct UI your agents can't get wrong\n\t\t\t\t\t\t\t</h1>\n\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-pretty text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\tSable is a themeable React component library with enforced tokens,\n\t\t\t\t\t\t\t\ttypography, and motion, documented for coding agents as carefully\n\t\t\t\t\t\t\t\tas for people.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Row gap={2} justify=\"center\" wrap className=\"pt-2\">\n\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/docs/getting-started\" />}>\n\t\t\t\t\t\t\t\t\tGet started\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\" render={<a href=\"/components\" />}>\n\t\t\t\t\t\t\t\t\tBrowse components\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<div className=\"w-full max-w-sm\">\n\t\t\t\t\t\t\t<CodeBlock prompt=\"$\" code=\"pnpm add @sableui/react\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Region>\n\n\t\t\t\t<Region as=\"section\" aria-label=\"Product preview\" max=\"wide\" className=\"pb-20 sm:pb-28\">\n\t\t\t\t\t<figure>\n\t\t\t\t\t\t<Browser url=\"atlas.example/deploys\" className=\"shadow-xl\">\n\t\t\t\t\t\t\t{/* A picture of software, not live UI: inert keeps its controls\n\t\t\t\t\t\t\t out of the tab order and the accessibility tree. */}\n\t\t\t\t\t\t\t<div inert className=\"grid gap-4 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n\t\t\t\t\t\t\t\t<Stack gap={4} className=\"min-w-0\">\n\t\t\t\t\t\t\t\t\t<PageHeader\n\t\t\t\t\t\t\t\t\t\ttitleAs=\"h2\"\n\t\t\t\t\t\t\t\t\t\ttitle=\"Deployments\"\n\t\t\t\t\t\t\t\t\t\tdescription=\"atlas-web · production\"\n\t\t\t\t\t\t\t\t\t\tactions={\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\tView logs\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button size=\"sm\" iconStart={<Icon name=\"upload\" />}>\n\t\t\t\t\t\t\t\t\t\t\t\t\tDeploy\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 2, md: 3 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t<Card density=\"compact\">\n\t\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle>Uptime</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Last 30 days</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-3\">99.98%</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t\t<Card density=\"compact\">\n\t\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle>p95 latency</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Edge, all regions</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-3\">182 ms</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t\t<Card density=\"compact\" className=\"max-md:hidden\">\n\t\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle>Build minutes</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Team plan</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<Meter value={64} tone=\"standard\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\" wrap className=\"mb-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<MeterLabel>Used</MeterLabel>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<MeterValue>{(_, value) => `${(value * 30).toLocaleString(\"en-US\")} of 3,000`}</MeterValue>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<MeterTrack />\n\t\t\t\t\t\t\t\t\t\t\t\t</Meter>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardTitle>Recent deploys</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Production branch</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone=\"success\" dot>\n\t\t\t\t\t\t\t\t\t\t\t\t\tHealthy\n\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t\t\t{deploys.map((deploy) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={deploy.meta}\n\t\t\t\t\t\t\t\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback={deploy.initials} alt={deploy.name} />}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={deploy.title}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsubtitle={deploy.meta}\n\t\t\t\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone={deploy.tone} dot pulse={deploy.pulse}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{deploy.status}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<Card className=\"max-lg:hidden\">\n\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t<CardTitle>Release assistant</CardTitle>\n\t\t\t\t\t\t\t\t\t\t<CardDescription>Summaries from today's deploys</CardDescription>\n\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t\t<Message align=\"end\">\n\t\t\t\t\t\t\t\t\t\t\t<Bubble variant=\"primary\" align=\"end\">\n\t\t\t\t\t\t\t\t\t\t\t\tWhat shipped to production today?\n\t\t\t\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t\t\t\t\t<Message>\n\t\t\t\t\t\t\t\t\t\t\t<Bubble>\n\t\t\t\t\t\t\t\t\t\t\t\tThree deploys. Search ranking is fixed, webhook keys are\n\t\t\t\t\t\t\t\t\t\t\t\trotated, and usage-based billing is building now.\n\t\t\t\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t\t\t\t\t<ChatComposer placeholder=\"Ask about a deploy\" />\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Browser>\n\t\t\t\t\t\t<figcaption className=\"mt-4 text-center text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\tEvery surface in this preview is a Sable component, styled by the active theme.\n\t\t\t\t\t\t</figcaption>\n\t\t\t\t\t</figure>\n\t\t\t\t</Region>\n\n\t\t\t\t<section aria-labelledby=\"landing-features\" className=\"border-y border-border bg-muted/40 py-20 sm:py-28\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<Stack gap={10}>\n\t\t\t\t\t\t\t<Stack gap={3} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Why Sable</p>\n\t\t\t\t\t\t\t\t<h2 id=\"landing-features\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\tA design system with the rules written down\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tThe decisions a senior designer would make are encoded once, so every\n\t\t\t\t\t\t\t\t\tscreen your team or your agent builds starts from them.\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2, lg: 3 }} gap={4}>\n\t\t\t\t\t\t\t\t\t{features.map((feature) => (\n\t\t\t\t\t\t\t\t\t\t<Card key={feature.title} render={<article />} className=\"h-full\">\n\t\t\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"grid size-10 place-items-center rounded-md sable-well\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Icon name={feature.icon} className=\"text-xl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"pt-2 text-title-lg\">{feature.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{feature.body}</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" aria-labelledby=\"landing-setup\" max=\"wide\" className=\"py-20 sm:py-28\">\n\t\t\t\t\t<div className=\"grid items-start gap-10 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-16\">\n\t\t\t\t\t\t<Stack gap={8}>\n\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Setup</p>\n\t\t\t\t\t\t\t\t<h2 id=\"landing-setup\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\tFrom install to first screen in three steps\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<ol className=\"flex flex-col gap-6\">\n\t\t\t\t\t\t\t\t{steps.map((step, index) => (\n\t\t\t\t\t\t\t\t\t<li key={step.title} className=\"grid grid-cols-[auto_1fr] gap-4\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"pt-0.5 text-mono-md text-muted-foreground\">0{index + 1}</span>\n\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"text-title-lg\">{step.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-md text-muted-foreground\">{step.body}</p>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</ol>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<Reveal className=\"min-w-0\">\n\t\t\t\t\t\t\t<Tabs defaultValue=\"compose\">\n\t\t\t\t\t\t\t\t<TabsList>\n\t\t\t\t\t\t\t\t\t<TabsTrigger value=\"install\">Install</TabsTrigger>\n\t\t\t\t\t\t\t\t\t<TabsTrigger value=\"styles\">Styles</TabsTrigger>\n\t\t\t\t\t\t\t\t\t<TabsTrigger value=\"compose\">Compose</TabsTrigger>\n\t\t\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t\t\t\t<TabsPanel value=\"install\" className=\"pt-4\">\n\t\t\t\t\t\t\t\t\t<CodeBlock prompt=\"$\" code=\"pnpm add @sableui/react\" />\n\t\t\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t\t\t<TabsPanel value=\"styles\" className=\"pt-4\">\n\t\t\t\t\t\t\t\t\t<CodeBlock filename=\"app.css\" code={stylesCode} />\n\t\t\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t\t\t<TabsPanel value=\"compose\" className=\"pt-4\">\n\t\t\t\t\t\t\t\t\t<CodeBlock filename=\"deploy-card.tsx\" code={composeCode} />\n\t\t\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t\t</Tabs>\n\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\n\t\t\t\t<section aria-labelledby=\"landing-agents\" className=\"border-t border-border py-20 sm:py-28\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<div className=\"grid items-center gap-10 lg:grid-cols-2 lg:gap-16\">\n\t\t\t\t\t\t\t<Stack gap={5}>\n\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Built for agents</p>\n\t\t\t\t\t\t\t\t\t<h2 id=\"landing-agents\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\t\tYour agent reads the same contract you do\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tPoint Claude Code, Codex, or Cursor at the installed package. The{\" \"}\n\t\t\t\t\t\t\t\t\t\t<Code>sable</Code> CLI answers what to use, how to use it, and whether\n\t\t\t\t\t\t\t\t\t\tthe result holds up.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<ul className=\"sable-list\">\n\t\t\t\t\t\t\t\t\t{agentPoints.map((point) => (\n\t\t\t\t\t\t\t\t\t\t<li key={point}>{point}</li>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Reveal className=\"min-w-0\">\n\t\t\t\t\t\t\t\t<CodeBlock filename=\"Terminal\" code={terminalCode} highlight={false} />\n\t\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" aria-labelledby=\"landing-cta\" max=\"wide\" className=\"pb-20 sm:pb-28\">\n\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<Stack gap={4} align=\"center\" className=\"py-8 text-center sm:py-12\">\n\t\t\t\t\t\t\t\t\t<h2 id=\"landing-cta\" className=\"max-w-2xl text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\t\tBuild the product, not another component library\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tInstall Sable, pick a preset, and ship the first screen today.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<Row gap={2} justify=\"center\" wrap className=\"pt-2\">\n\t\t\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/docs/getting-started\" />}>\n\t\t\t\t\t\t\t\t\t\t\tGet started\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\" render={<a href=\"/github\" />}>\n\t\t\t\t\t\t\t\t\t\t\tStar on GitHub\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Reveal>\n\t\t\t\t</Region>\n\t\t\t</main>\n\n\t\t\t<footer className=\"border-t border-border\">\n\t\t\t\t<Region max=\"wide\" className=\"py-12\">\n\t\t\t\t\t<div className=\"grid gap-10 md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]\">\n\t\t\t\t\t\t<Stack gap={3} className=\"max-w-xs\">\n\t\t\t\t\t\t\t<a href=\"/\" className=\"w-fit text-title-lg motion-interactive hover:text-muted-foreground\">\n\t\t\t\t\t\t\t\tSable\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\tReact components, patterns, and tokens for products built with AI agents.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<nav aria-label=\"Footer\" className=\"grid grid-cols-2 gap-8 sm:grid-cols-3\">\n\t\t\t\t\t\t\t{footerGroups.map((group) => (\n\t\t\t\t\t\t\t\t<div key={group.title}>\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-label-md text-muted-foreground\">{group.title}</h2>\n\t\t\t\t\t\t\t\t\t<ul className=\"mt-3 flex flex-col gap-1\">\n\t\t\t\t\t\t\t\t\t\t{group.links.map((link) => (\n\t\t\t\t\t\t\t\t\t\t\t<li key={link.label}>\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref={link.href}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-9 items-center text-body-sm motion-interactive hover:text-muted-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"mt-12 border-t border-border pt-6\">\n\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">© 2026 Sable. Released under the MIT license.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\t\t\t</footer>\n\t\t</div>\n\t)\n}"
|
|
19108
|
+
"code": "const navLinks = [\n\t{ label: \"Docs\", href: \"/docs\" },\n\t{ label: \"Components\", href: \"/components\" },\n\t{ label: \"Patterns\", href: \"/patterns\" },\n\t{ label: \"Themes\", href: \"/themes\" },\n]\n\nconst deploys = [\n\t{\n\t\tinitials: \"MO\",\n\t\tname: \"Maya Ortiz\",\n\t\ttitle: \"Add usage-based billing\",\n\t\tmeta: \"main · a3f9c21 · 2m ago\",\n\t\tstatus: \"Building\",\n\t\ttone: \"info\" as const,\n\t\tpulse: true,\n\t},\n\t{\n\t\tinitials: \"SR\",\n\t\tname: \"Sam Reyes\",\n\t\ttitle: \"Fix search ranking for long queries\",\n\t\tmeta: \"main · 81be04d · 18m ago\",\n\t\tstatus: \"Ready\",\n\t\ttone: \"success\" as const,\n\t\tpulse: false,\n\t},\n\t{\n\t\tinitials: \"JL\",\n\t\tname: \"Jun Lee\",\n\t\ttitle: \"Rotate webhook signing keys\",\n\t\tmeta: \"main · 5c7e9aa · 1h ago\",\n\t\tstatus: \"Ready\",\n\t\ttone: \"success\" as const,\n\t\tpulse: false,\n\t},\n]\n\nconst features = [\n\t{\n\t\ticon: \"component\" as const,\n\t\ttitle: \"Behavior built in\",\n\t\tbody: \"Focus management, keyboard navigation, and ARIA come from Base UI primitives under every control.\",\n\t},\n\t{\n\t\ticon: \"eyedropper\" as const,\n\t\ttitle: \"Semantic tokens end to end\",\n\t\tbody: \"Re-theme the whole product, light and dark, from one preset without touching a component.\",\n\t},\n\t{\n\t\ticon: \"layout\" as const,\n\t\ttitle: \"Pages, not just parts\",\n\t\tbody: \"Landing, dashboard, settings, auth, and list-detail shells to copy and adapt.\",\n\t},\n\t{\n\t\ticon: \"book\" as const,\n\t\ttitle: \"A contract agents can read\",\n\t\tbody: \"registry.json, llms.txt, and a CLI hand agents the exact props, examples, and rules.\",\n\t},\n\t{\n\t\ticon: \"circle-check\" as const,\n\t\ttitle: \"Lint that knows the design\",\n\t\tbody: \"sable-lint flags raw colors, hand-built controls, and motion that ignores reduced motion.\",\n\t},\n\t{\n\t\ticon: \"play\" as const,\n\t\ttitle: \"Motion with an off switch\",\n\t\tbody: \"Named recipes run on one scale variable, so reduced motion stops every transition at once.\",\n\t},\n]\n\nconst steps = [\n\t{\n\t\ttitle: \"Install the package\",\n\t\tbody: \"One dependency. React 19 and Tailwind v4 are the only peers.\",\n\t},\n\t{\n\t\ttitle: \"Import the styles\",\n\t\tbody: \"Tailwind first, then Sable's tokens, then a source glob over the package.\",\n\t},\n\t{\n\t\ttitle: \"Compose your screens\",\n\t\tbody: \"Build from components and layout primitives. Your theme does the rest.\",\n\t},\n]\n\nconst stylesCode = `@import \"tailwindcss\";\n@import \"@sableui/react/styles.css\";\n@source \"../node_modules/@sableui/react/dist/**/*.{js,mjs}\";`\n\n// design-lint-disable-next-line self-import -- a consumer code sample displayed on the page, not an import\nconst composeCode = `import { Button, Card, CardContent, CardHeader, CardTitle } from \"@sableui/react\"\n\nexport function DeployCard() {\n return (\n <Card>\n <CardHeader>\n <CardTitle>Production</CardTitle>\n </CardHeader>\n <CardContent>\n <Button>Deploy</Button>\n </CardContent>\n </Card>\n )\n}`\n\nconst terminalCode = `$ npx sable search \"confirm before deleting\"\ncomponent DestructiveActionModal\ncomponent Dialog\n\n$ npx sable component DestructiveActionModal --props\nprops, a11y contract, composed examples\n\n$ npx sable lint src\n0 findings in 42 files`\n\nconst agentPoints = [\n\t\"Search ranks components, patterns, tokens, and docs for a plain-language need.\",\n\t\"Every component ships its props, accessibility contract, and composed examples.\",\n\t\"The same lint runs in the editor, in CI, and inside the agent's own loop.\",\n]\n\nconst footerGroups = [\n\t{\n\t\ttitle: \"Product\",\n\t\tlinks: [\n\t\t\t{ label: \"Components\", href: \"/components\" },\n\t\t\t{ label: \"Patterns\", href: \"/patterns\" },\n\t\t\t{ label: \"Themes\", href: \"/themes\" },\n\t\t\t{ label: \"Canvas\", href: \"/canvas\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Resources\",\n\t\tlinks: [\n\t\t\t{ label: \"Documentation\", href: \"/docs\" },\n\t\t\t{ label: \"Changelog\", href: \"/changelog\" },\n\t\t\t{ label: \"llms.txt\", href: \"/llms.txt\" },\n\t\t\t{ label: \"registry.json\", href: \"/registry.json\" },\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Community\",\n\t\tlinks: [\n\t\t\t{ label: \"GitHub\", href: \"/github\" },\n\t\t\t{ label: \"npm\", href: \"/npm\" },\n\t\t\t{ label: \"Discussions\", href: \"/discussions\" },\n\t\t],\n\t},\n]\n\nexport function LandingPage() {\n\treturn (\n\t\t<div className=\"min-h-dvh bg-background text-foreground\">\n\t\t\t<header className=\"border-b border-border\">\n\t\t\t\t<Region as=\"nav\" aria-label=\"Main\" max=\"wide\" className=\"grid h-16 grid-cols-[1fr_auto_1fr] items-center\">\n\t\t\t\t\t<a href=\"/\" className=\"justify-self-start text-title-lg motion-interactive hover:text-muted-foreground\">\n\t\t\t\t\t\tSable\n\t\t\t\t\t</a>\n\t\t\t\t\t<div className=\"justify-self-center max-md:hidden\">\n\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t{navLinks.map((link) => (\n\t\t\t\t\t\t\t\t<Button key={link.label} variant=\"ghost\" size=\"sm\" render={<a href={link.href} />}>\n\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Row gap={1} className=\"col-start-3 justify-self-end\">\n\t\t\t\t\t\t<div className=\"max-sm:hidden\">\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/github\" />}>\n\t\t\t\t\t\t\t\tGitHub\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Button size=\"sm\" render={<a href=\"/docs/getting-started\" />}>\n\t\t\t\t\t\t\tGet started\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Row>\n\t\t\t\t</Region>\n\t\t\t</header>\n\n\t\t\t<main>\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"pb-16 pt-16 sm:pt-24\">\n\t\t\t\t\t<Stack gap={8} align=\"center\">\n\t\t\t\t\t\t<Stack gap={4} align=\"center\" className=\"text-center\">\n\t\t\t\t\t\t\t<Badge appearance=\"outline\" size=\"sm\" icon={<Icon name=\"sparkles\" />}>\n\t\t\t\t\t\t\t\tOpen source · MIT licensed\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t<h1 className=\"max-w-4xl text-balance text-display-lg sm:text-display-2xl\">\n\t\t\t\t\t\t\t\tProduct UI your agents can't get wrong\n\t\t\t\t\t\t\t</h1>\n\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-pretty text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\tSable is a themeable React component library with enforced tokens,\n\t\t\t\t\t\t\t\ttypography, and motion, documented for coding agents as carefully\n\t\t\t\t\t\t\t\tas for people.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<Row gap={2} justify=\"center\" wrap className=\"pt-2\">\n\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/docs/getting-started\" />}>\n\t\t\t\t\t\t\t\t\tGet started\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\" render={<a href=\"/components\" />}>\n\t\t\t\t\t\t\t\t\tBrowse components\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<div className=\"w-full max-w-sm\">\n\t\t\t\t\t\t\t<CodeBlock prompt=\"$\" code=\"pnpm add @sableui/react\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Region>\n\n\t\t\t\t<Region as=\"section\" aria-label=\"Product preview\" max=\"wide\" className=\"pb-20 sm:pb-28\">\n\t\t\t\t\t<figure>\n\t\t\t\t\t\t<Browser url=\"atlas.example/deploys\" className=\"shadow-xl\">\n\t\t\t\t\t\t\t{/* A picture of software, not live UI: inert keeps its controls\n\t\t\t\t\t\t\t out of the tab order and the accessibility tree. */}\n\t\t\t\t\t\t\t<div inert className=\"grid gap-4 p-4 sm:p-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n\t\t\t\t\t\t\t\t<Stack gap={4} className=\"min-w-0\">\n\t\t\t\t\t\t\t\t\t<PageHeader\n\t\t\t\t\t\t\t\t\t\ttitleAs=\"h2\"\n\t\t\t\t\t\t\t\t\t\ttitle=\"Deployments\"\n\t\t\t\t\t\t\t\t\t\tdescription=\"atlas-web · production\"\n\t\t\t\t\t\t\t\t\t\tactions={\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\tView logs\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button size=\"sm\" iconStart={<Icon name=\"upload\" />}>\n\t\t\t\t\t\t\t\t\t\t\t\t\tDeploy\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 2, md: 3 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t<Card density=\"compact\">\n\t\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle>Uptime</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Last 30 days</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-3\">99.98%</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t\t<Card density=\"compact\">\n\t\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle>p95 latency</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Edge, all regions</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-3\">182 ms</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t\t<Card density=\"compact\" className=\"max-md:hidden\">\n\t\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle>Build minutes</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Team plan</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t\t<Meter value={64} tone=\"standard\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\" wrap className=\"mb-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<MeterLabel>Used</MeterLabel>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<MeterValue>{(_, value) => `${(value * 30).toLocaleString(\"en-US\")} of 3,000`}</MeterValue>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<MeterTrack />\n\t\t\t\t\t\t\t\t\t\t\t\t</Meter>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardTitle>Recent deploys</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t<CardDescription>Production branch</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone=\"success\" dot>\n\t\t\t\t\t\t\t\t\t\t\t\t\tHealthy\n\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t\t\t{deploys.map((deploy) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={deploy.meta}\n\t\t\t\t\t\t\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={deploy.initials} alt={deploy.name} />}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={deploy.title}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsubtitle={deploy.meta}\n\t\t\t\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge appearance=\"plain\" tone={deploy.tone} dot pulse={deploy.pulse}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{deploy.status}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<Card className=\"max-lg:hidden\">\n\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t<CardTitle>Release assistant</CardTitle>\n\t\t\t\t\t\t\t\t\t\t<CardDescription>Summaries from today's deploys</CardDescription>\n\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t\t\t\t<Message align=\"end\">\n\t\t\t\t\t\t\t\t\t\t\t<Bubble variant=\"primary\" align=\"end\">\n\t\t\t\t\t\t\t\t\t\t\t\tWhat shipped to production today?\n\t\t\t\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t\t\t\t\t<Message>\n\t\t\t\t\t\t\t\t\t\t\t<Bubble>\n\t\t\t\t\t\t\t\t\t\t\t\tThree deploys. Search ranking is fixed, webhook keys are\n\t\t\t\t\t\t\t\t\t\t\t\trotated, and usage-based billing is building now.\n\t\t\t\t\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t\t\t\t\t</Message>\n\t\t\t\t\t\t\t\t\t\t<ChatComposer placeholder=\"Ask about a deploy\" />\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</Browser>\n\t\t\t\t\t\t<figcaption className=\"mt-4 text-center text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\tEvery surface in this preview is a Sable component, styled by the active theme.\n\t\t\t\t\t\t</figcaption>\n\t\t\t\t\t</figure>\n\t\t\t\t</Region>\n\n\t\t\t\t<section aria-labelledby=\"landing-features\" className=\"border-y border-border bg-muted/40 py-20 sm:py-28\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<Stack gap={10}>\n\t\t\t\t\t\t\t<Stack gap={3} className=\"max-w-2xl\">\n\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Why Sable</p>\n\t\t\t\t\t\t\t\t<h2 id=\"landing-features\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\tA design system with the rules written down\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tThe decisions a senior designer would make are encoded once, so every\n\t\t\t\t\t\t\t\t\tscreen your team or your agent builds starts from them.\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2, lg: 3 }} gap={4}>\n\t\t\t\t\t\t\t\t\t{features.map((feature) => (\n\t\t\t\t\t\t\t\t\t\t<Card key={feature.title} render={<article />} className=\"h-full\">\n\t\t\t\t\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"grid size-10 place-items-center rounded-md sable-well\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Icon name={feature.icon} className=\"text-xl\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"pt-2 text-title-lg\">{feature.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{feature.body}</p>\n\t\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" aria-labelledby=\"landing-setup\" max=\"wide\" className=\"py-20 sm:py-28\">\n\t\t\t\t\t<div className=\"grid items-start gap-10 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-16\">\n\t\t\t\t\t\t<Stack gap={8}>\n\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Setup</p>\n\t\t\t\t\t\t\t\t<h2 id=\"landing-setup\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\tFrom install to first screen in three steps\n\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<ol className=\"flex flex-col gap-6\">\n\t\t\t\t\t\t\t\t{steps.map((step, index) => (\n\t\t\t\t\t\t\t\t\t<li key={step.title} className=\"grid grid-cols-[auto_1fr] gap-4\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"pt-0.5 text-mono-md text-muted-foreground\">0{index + 1}</span>\n\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"text-title-lg\">{step.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-md text-muted-foreground\">{step.body}</p>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</ol>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<Reveal className=\"min-w-0\">\n\t\t\t\t\t\t\t<Tabs defaultValue=\"compose\">\n\t\t\t\t\t\t\t\t<TabsList>\n\t\t\t\t\t\t\t\t\t<TabsTrigger value=\"install\">Install</TabsTrigger>\n\t\t\t\t\t\t\t\t\t<TabsTrigger value=\"styles\">Styles</TabsTrigger>\n\t\t\t\t\t\t\t\t\t<TabsTrigger value=\"compose\">Compose</TabsTrigger>\n\t\t\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t\t\t\t<TabsPanel value=\"install\" className=\"pt-4\">\n\t\t\t\t\t\t\t\t\t<CodeBlock prompt=\"$\" code=\"pnpm add @sableui/react\" />\n\t\t\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t\t\t<TabsPanel value=\"styles\" className=\"pt-4\">\n\t\t\t\t\t\t\t\t\t<CodeBlock filename=\"app.css\" code={stylesCode} />\n\t\t\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t\t\t<TabsPanel value=\"compose\" className=\"pt-4\">\n\t\t\t\t\t\t\t\t\t<CodeBlock filename=\"deploy-card.tsx\" code={composeCode} />\n\t\t\t\t\t\t\t\t</TabsPanel>\n\t\t\t\t\t\t\t</Tabs>\n\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\n\t\t\t\t<section aria-labelledby=\"landing-agents\" className=\"border-t border-border py-20 sm:py-28\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<div className=\"grid items-center gap-10 lg:grid-cols-2 lg:gap-16\">\n\t\t\t\t\t\t\t<Stack gap={5}>\n\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Built for agents</p>\n\t\t\t\t\t\t\t\t\t<h2 id=\"landing-agents\" className=\"text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\t\tYour agent reads the same contract you do\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tPoint Claude Code, Codex, or Cursor at the installed package. The{\" \"}\n\t\t\t\t\t\t\t\t\t\t<Code>sable</Code> CLI answers what to use, how to use it, and whether\n\t\t\t\t\t\t\t\t\t\tthe result holds up.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t<ul className=\"sable-list\">\n\t\t\t\t\t\t\t\t\t{agentPoints.map((point) => (\n\t\t\t\t\t\t\t\t\t\t<li key={point}>{point}</li>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t<Reveal className=\"min-w-0\">\n\t\t\t\t\t\t\t\t<CodeBlock filename=\"Terminal\" code={terminalCode} highlight={false} />\n\t\t\t\t\t\t\t</Reveal>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" aria-labelledby=\"landing-cta\" max=\"wide\" className=\"pb-20 sm:pb-28\">\n\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t<Stack gap={4} align=\"center\" className=\"py-8 text-center sm:py-12\">\n\t\t\t\t\t\t\t\t\t<h2 id=\"landing-cta\" className=\"max-w-2xl text-balance text-heading-1\">\n\t\t\t\t\t\t\t\t\t\tBuild the product, not another component library\n\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-lg text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\tInstall Sable, pick a preset, and ship the first screen today.\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t<Row gap={2} justify=\"center\" wrap className=\"pt-2\">\n\t\t\t\t\t\t\t\t\t\t<Button size=\"cta\" render={<a href=\"/docs/getting-started\" />}>\n\t\t\t\t\t\t\t\t\t\t\tGet started\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\" render={<a href=\"/github\" />}>\n\t\t\t\t\t\t\t\t\t\t\tStar on GitHub\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Reveal>\n\t\t\t\t</Region>\n\t\t\t</main>\n\n\t\t\t<footer className=\"border-t border-border\">\n\t\t\t\t<Region max=\"wide\" className=\"py-12\">\n\t\t\t\t\t<div className=\"grid gap-10 md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]\">\n\t\t\t\t\t\t<Stack gap={3} className=\"max-w-xs\">\n\t\t\t\t\t\t\t<a href=\"/\" className=\"w-fit text-title-lg motion-interactive hover:text-muted-foreground\">\n\t\t\t\t\t\t\t\tSable\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\tReact components, patterns, and tokens for products built with AI agents.\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<nav aria-label=\"Footer\" className=\"grid grid-cols-2 gap-8 sm:grid-cols-3\">\n\t\t\t\t\t\t\t{footerGroups.map((group) => (\n\t\t\t\t\t\t\t\t<div key={group.title}>\n\t\t\t\t\t\t\t\t\t<h2 className=\"text-label-md text-muted-foreground\">{group.title}</h2>\n\t\t\t\t\t\t\t\t\t<ul className=\"mt-3 flex flex-col gap-1\">\n\t\t\t\t\t\t\t\t\t\t{group.links.map((link) => (\n\t\t\t\t\t\t\t\t\t\t\t<li key={link.label}>\n\t\t\t\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\t\t\t\thref={link.href}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex min-h-9 items-center text-body-sm motion-interactive hover:text-muted-foreground\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{link.label}\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</nav>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"mt-12 border-t border-border pt-6\">\n\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">© 2026 Sable. Released under the MIT license.</p>\n\t\t\t\t\t</div>\n\t\t\t\t</Region>\n\t\t\t</footer>\n\t\t</div>\n\t)\n}"
|
|
18872
19109
|
},
|
|
18873
19110
|
{
|
|
18874
19111
|
"name": "ListDetailPage",
|
|
@@ -18891,7 +19128,7 @@
|
|
|
18891
19128
|
"On a phone, drilling in moves focus to the back button; going back returns focus to the row that was open. After a remove, focus lands on the newly selected row; after Clear search, on the search field."
|
|
18892
19129
|
],
|
|
18893
19130
|
"imports": "import { useState, useEffect, useRef } from \"react\"\nimport { Avatar, Badge, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, EmptyState, Entity, Field, Icon, Page, PageHeader, Row, SearchInput, Select, SelectContent, SelectItem, SelectTrigger, Stack, StatusDot } from \"@sableui/react\"",
|
|
18894
|
-
"code": "type Role = \"owner\" | \"admin\" | \"editor\" | \"viewer\"\n\nconst roles: { value: Role; label: string }[] = [\n\t{ value: \"owner\", label: \"Owner\" },\n\t{ value: \"admin\", label: \"Admin\" },\n\t{ value: \"editor\", label: \"Editor\" },\n\t{ value: \"viewer\", label: \"Viewer\" },\n]\n\nconst roleLabel: Record<Role, string> = {\n\towner: \"Owner\",\n\tadmin: \"Admin\",\n\teditor: \"Editor\",\n\tviewer: \"Viewer\",\n}\n\nconst roleSummary: Record<Role, string> = {\n\towner: \"Full access, including payouts and billing. Transfer ownership in studio settings.\",\n\tadmin: \"Manages members, editions and payouts.\",\n\teditor: \"Drafts, prices and releases editions.\",\n\tviewer: \"Sees editions and sales, but can't change them.\",\n}\n\ntype Activity = { id: string; icon: string; text: string; when: string }\n\ntype Member = {\n\tid: string\n\tname: string\n\temail: string\n\tinitials: string\n\trole: Role\n\tpending: boolean\n\thandle?: string\n\tjoined?: string\n\tlastActive?: string\n\teditions?: number\n\tinvitedBy?: string\n\tsent?: string\n\texpires?: string\n\tresent?: boolean\n\tactivity: Activity[]\n}\n\nconst seats = 10\n\nconst initialMembers: Member[] = [\n\t{\n\t\tid: \"u_01\", name: \"Ada Lovelace\", handle: \"@ada\", email: \"ada@atelier.art\", initials: \"AL\", role: \"owner\", pending: false,\n\t\tjoined: \"Mar 4, 2024\", lastActive: \"Online now\", editions: 12,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"gem\", text: \"Released Analytical Engine No. 3 — edition of 50\", when: \"2 hours ago\" },\n\t\t\t{ id: \"a2\", icon: \"mail\", text: \"Invited louise@bourgeois.studio as Editor\", when: \"2 days ago\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_02\", name: \"Grace Hopper\", handle: \"@grace\", email: \"grace@atelier.art\", initials: \"GH\", role: \"admin\", pending: false,\n\t\tjoined: \"Mar 9, 2024\", lastActive: \"20 minutes ago\", editions: 4,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"user\", text: \"Changed Frida Kahlo's role to Viewer\", when: \"Yesterday\" },\n\t\t\t{ id: \"a2\", icon: \"edit\", text: \"Updated pricing on First Compiler — edition of 10\", when: \"3 days ago\" },\n\t\t\t{ id: \"a3\", icon: \"gem\", text: \"Released First Compiler — edition of 10\", when: \"Sep 28\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_03\", name: \"Mary Shelley\", handle: \"@mary\", email: \"mary@atelier.art\", initials: \"MS\", role: \"editor\", pending: false,\n\t\tjoined: \"Jun 18, 2024\", lastActive: \"3 hours ago\", editions: 7,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"calendar\", text: \"Scheduled Frankenstein Studies for Oct 14\", when: \"3 hours ago\" },\n\t\t\t{ id: \"a2\", icon: \"gem\", text: \"Released The Last Man — edition of 25\", when: \"Sep 21\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_04\", name: \"Nina Simone\", handle: \"@nina\", email: \"nina@atelier.art\", initials: \"NS\", role: \"editor\", pending: false,\n\t\tjoined: \"Jan 12, 2025\", lastActive: \"Yesterday\", editions: 3,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"upload\", text: \"Uploaded artwork for Little Girl Blue\", when: \"Yesterday\" },\n\t\t\t{ id: \"a2\", icon: \"gem\", text: \"Released Feeling Good — edition of 100\", when: \"Aug 30\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_05\", name: \"Frida Kahlo\", handle: \"@frida\", email: \"frida@atelier.art\", initials: \"FK\", role: \"viewer\", pending: false,\n\t\tjoined: \"Jul 2, 2025\", lastActive: \"4 days ago\", editions: 0,\n\t\tactivity: [{ id: \"a1\", icon: \"comment\", text: \"Commented on the Las Dos Fridas draft\", when: \"4 days ago\" }],\n\t},\n\t{\n\t\tid: \"i_01\", name: \"louise@bourgeois.studio\", email: \"louise@bourgeois.studio\", initials: \"\", role: \"editor\", pending: true,\n\t\tinvitedBy: \"Ada Lovelace\", sent: \"2 days ago\", expires: \"In 5 days\",\n\t\tactivity: [],\n\t},\n]\n\nexport function ListDetailPage() {\n\tconst [members, setMembers] = useState(initialMembers)\n\tconst [selectedId, setSelectedId] = useState<string | null>(\"u_02\")\n\tconst [query, setQuery] = useState(\"\")\n\t// Below lg the list and the detail share one column: selecting drills in.\n\tconst [showDetail, setShowDetail] = useState(false)\n\tconst [confirming, setConfirming] = useState(false)\n\tconst listRef = useRef<HTMLDivElement>(null)\n\tconst backRef = useRef<HTMLButtonElement>(null)\n\tconst searchRef = useRef<HTMLInputElement>(null)\n\n\tconst needle = query.trim().toLowerCase()\n\tconst visible = members.filter((m) =>\n\t\t[m.name, m.email, m.handle ?? \"\", roleLabel[m.role]].some((v) => v.toLowerCase().includes(needle)),\n\t)\n\tconst selected = members.find((m) => m.id === selectedId) ?? null\n\tconst active = members.filter((m) => !m.pending).length\n\tconst pending = members.length - active\n\n\t// Drilling in on a phone moves focus to the detail's back control; the\n\t// control is display:none at lg, so desktop focus stays on the list.\n\tuseEffect(() => {\n\t\tif (showDetail && backRef.current?.offsetParent) backRef.current.focus()\n\t}, [showDetail, selectedId])\n\n\tfunction select(id: string) {\n\t\tsetSelectedId(id)\n\t\tsetShowDetail(true)\n\t}\n\n\tfunction focusRow(id: string | undefined) {\n\t\trequestAnimationFrame(() => {\n\t\t\tlistRef.current?.querySelector<HTMLButtonElement>(`[data-member-id=\"${id}\"] button`)?.focus()\n\t\t})\n\t}\n\n\tfunction backToList() {\n\t\tsetShowDetail(false)\n\t\tfocusRow(selectedId ?? undefined)\n\t}\n\n\tfunction setRole(id: string, role: Role) {\n\t\tsetMembers((list) => list.map((m) => (m.id === id ? { ...m, role } : m)))\n\t}\n\n\tfunction resend(id: string) {\n\t\tsetMembers((list) =>\n\t\t\tlist.map((m) => (m.id === id ? { ...m, sent: \"Just now\", expires: \"In 7 days\", resent: true } : m)),\n\t\t)\n\t}\n\n\tfunction remove(id: string) {\n\t\tconst index = members.findIndex((m) => m.id === id)\n\t\tconst rest = members.filter((m) => m.id !== id)\n\t\tsetMembers(rest)\n\t\tconst next = rest[Math.min(index, rest.length - 1)]?.id\n\t\tsetSelectedId(next ?? null)\n\t\tsetShowDetail(false)\n\t\t// The control that opened the confirmation is gone; land on the next row.\n\t\tfocusRow(next)\n\t}\n\n\treturn (\n\t\t<Page as=\"main\" max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t<PageHeader\n\t\t\t\ttitle=\"Members\"\n\t\t\t\tdescription={`${active} members · ${pending} pending ${pending === 1 ? \"invite\" : \"invites\"} · ${members.length} of ${seats} seats used`}\n\t\t\t\tactions={<Button iconStart={<Icon name=\"plus\" />}>Invite member</Button>}\n\t\t\t/>\n\t\t\t<Columns count={{ base: 1, lg: 12 }}>\n\t\t\t\t<Col span={{ base: 1, lg: 5 }} className={showDetail ? \"max-lg:hidden\" : undefined}>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<SearchInput\n\t\t\t\t\t\t\tref={searchRef}\n\t\t\t\t\t\t\tvalue={query}\n\t\t\t\t\t\t\tonValueChange={setQuery}\n\t\t\t\t\t\t\tplaceholder=\"Search name, email or role\"\n\t\t\t\t\t\t\taria-label=\"Search members\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{/* sable-surface derives the rows' selected step from the card (12% ink),\n\t\t\t\t\t\t so a selected row stays distinct from its muted avatar. */}\n\t\t\t\t\t\t<Card ref={listRef} className=\"sable-surface\">\n\t\t\t\t\t\t\t{visible.length > 0 ? (\n\t\t\t\t\t\t\t\t<CardList aria-label=\"Members\">\n\t\t\t\t\t\t\t\t\t{visible.map((m) => {\n\t\t\t\t\t\t\t\t\t\tconst isSelected = m.id === selectedId\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\t\tkey={m.id}\n\t\t\t\t\t\t\t\t\t\t\t\tdata-member-id={m.id}\n\t\t\t\t\t\t\t\t\t\t\t\tinteractive\n\t\t\t\t\t\t\t\t\t\t\t\tselected={isSelected}\n\t\t\t\t\t\t\t\t\t\t\t\tonSelect={() => select(m.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tleading={\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Avatar\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfallback={m.pending ? <Icon name=\"mail\" className=\"size-5\" /> : m.initials}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstatus={m.lastActive === \"Online now\" ? <StatusDot tone=\"success\" /> : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// The row's title already names it: keep the initials out of the row's name.\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-hidden\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={m.name}\n\t\t\t\t\t\t\t\t\t\t\t\tsubtitle={\n\t\t\t\t\t\t\t\t\t\t\t\t\tm.pending\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `${roleLabel[m.role]} · invite sent ${m.sent?.toLowerCase()}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: `${roleLabel[m.role]} · ${m.handle}`\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\taction={m.pending ? <Badge tone=\"info\">Invited</Badge> : undefined}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t<EmptyState\n\t\t\t\t\t\t\t\t\t\ticon={<Icon name=\"search\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"No members found\"\n\t\t\t\t\t\t\t\t\t\tdescription={`Nobody matches “${query.trim()}”. Try a name, email or role.`}\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\" onClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tsetQuery(\"\")\n\t\t\t\t\t\t\t\t\t\t\t\tsearchRef.current?.focus()\n\t\t\t\t\t\t\t\t\t\t\t}}>\n\t\t\t\t\t\t\t\t\t\t\t\tClear search\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Col>\n\t\t\t\t<Col span={{ base: 1, lg: 7 }} className={showDetail ? undefined : \"max-lg:hidden\"}>\n\t\t\t\t\t<Stack role=\"region\" aria-label=\"Member details\" gap={3} className=\"lg:sticky lg:top-6\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tref={backRef}\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\ticonStart={<Icon name=\"chevron-left\" />}\n\t\t\t\t\t\t\tonClick={backToList}\n\t\t\t\t\t\t\tclassName=\"self-start lg:hidden\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tAll members\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t{selected === null ? (\n\t\t\t\t\t\t\t<EmptyState\n\t\t\t\t\t\t\t\tsurface=\"card\"\n\t\t\t\t\t\t\t\ticon={<Icon name=\"user\" />}\n\t\t\t\t\t\t\t\ttitle=\"No member selected\"\n\t\t\t\t\t\t\t\tdescription=\"Choose someone from the list to see their role and activity.\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t<Avatar\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"lg\"\n\t\t\t\t\t\t\t\t\t\t\t\tfallback={selected.pending ? <Icon name=\"mail\" className=\"size-6\" /> : selected.initials}\n\t\t\t\t\t\t\t\t\t\t\t\tstatus={selected.lastActive === \"Online now\" ? <StatusDot tone=\"success\" /> : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\talt={selected.pending ? \"Pending invite\" : selected.name}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<Stack gap={0} className=\"min-w-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />} className=\"truncate\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.name}\n\t\t\t\t\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription className=\"truncate\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? \"Hasn't joined yet\" : `${selected.handle} · ${selected.email}`}\n\t\t\t\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t{selected.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t\t\t<Badge tone=\"info\">Invited</Badge>\n\t\t\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t<Stack gap={5}>\n\t\t\t\t\t\t\t\t\t\t\t<Field\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={selected.pending ? \"Role on joining\" : \"Role\"}\n\t\t\t\t\t\t\t\t\t\t\t\tdescription={roleSummary[selected.role]}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\t\t\t\t\t\t\titems={roles}\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={selected.role}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonValueChange={(value) => setRole(selected.id, value as Role)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={selected.role === \"owner\"}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectTrigger className=\"max-w-72\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{roles\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.filter((r) => r.value !== \"owner\" || selected.role === \"owner\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.map((r) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={r.value} value={r.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{r.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"@container\">\n\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl sable-list-dl-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Sent</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.sent}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Expires</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.expires}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Invited by</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.invitedBy}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl sable-list-dl-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Email</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.email}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Joined</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.joined}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Last active</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd className={selected.lastActive === \"Online now\" ? \"text-success\" : undefined}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.lastActive}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Editions released</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.editions}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t{selected.role !== \"owner\" ? (\n\t\t\t\t\t\t\t\t\t\t<CardFooter>\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2} wrap>\n\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ticonStart={<Icon name={selected.resent ? \"check\" : \"send\"} />}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={selected.resent}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => resend(selected.id)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.resent ? \"Invite resent\" : \"Resend invite\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={() => setConfirming(true)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? \"Revoke invite\" : \"Remove from studio\"}\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t{selected.activity.length > 0 ? (\n\t\t\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardTitle render={<h3 />}>\n\t\t\t\t\t\t\t\t\t\t\t\tRecent activity\n\t\t\t\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t\t\t{selected.activity.map((a) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\tleading={<Icon name={a.icon} className=\"text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={a.text}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsubtitle={a.when}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<DestructiveActionModal\n\t\t\t\t\t\t\t\t\topen={confirming}\n\t\t\t\t\t\t\t\t\tonOpenChange={setConfirming}\n\t\t\t\t\t\t\t\t\ttitle={selected.pending ? \"Revoke this invite?\" : `Remove ${selected.name}?`}\n\t\t\t\t\t\t\t\t\tdescription={\n\t\t\t\t\t\t\t\t\t\tselected.pending\n\t\t\t\t\t\t\t\t\t\t\t? `${selected.email} won't be able to join with the link they were sent.`\n\t\t\t\t\t\t\t\t\t\t\t: \"They lose access to the studio right away. Editions they released stay on sale.\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tconfirmLabel={selected.pending ? \"Revoke invite\" : \"Remove member\"}\n\t\t\t\t\t\t\t\t\tonConfirm={() => remove(selected.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Stack>\n\t\t\t\t</Col>\n\t\t\t</Columns>\n\t\t</Page>\n\t)\n}"
|
|
19131
|
+
"code": "type Role = \"owner\" | \"admin\" | \"editor\" | \"viewer\"\n\nconst roles: { value: Role; label: string }[] = [\n\t{ value: \"owner\", label: \"Owner\" },\n\t{ value: \"admin\", label: \"Admin\" },\n\t{ value: \"editor\", label: \"Editor\" },\n\t{ value: \"viewer\", label: \"Viewer\" },\n]\n\nconst roleLabel: Record<Role, string> = {\n\towner: \"Owner\",\n\tadmin: \"Admin\",\n\teditor: \"Editor\",\n\tviewer: \"Viewer\",\n}\n\nconst roleSummary: Record<Role, string> = {\n\towner: \"Full access, including payouts and billing. Transfer ownership in studio settings.\",\n\tadmin: \"Manages members, editions and payouts.\",\n\teditor: \"Drafts, prices and releases editions.\",\n\tviewer: \"Sees editions and sales, but can't change them.\",\n}\n\ntype Activity = { id: string; icon: string; text: string; when: string }\n\ntype Member = {\n\tid: string\n\tname: string\n\temail: string\n\tinitials: string\n\trole: Role\n\tpending: boolean\n\thandle?: string\n\tjoined?: string\n\tlastActive?: string\n\teditions?: number\n\tinvitedBy?: string\n\tsent?: string\n\texpires?: string\n\tresent?: boolean\n\tactivity: Activity[]\n}\n\nconst seats = 10\n\nconst initialMembers: Member[] = [\n\t{\n\t\tid: \"u_01\", name: \"Ada Lovelace\", handle: \"@ada\", email: \"ada@atelier.art\", initials: \"AL\", role: \"owner\", pending: false,\n\t\tjoined: \"Mar 4, 2024\", lastActive: \"Online now\", editions: 12,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"gem\", text: \"Released Analytical Engine No. 3 — edition of 50\", when: \"2 hours ago\" },\n\t\t\t{ id: \"a2\", icon: \"mail\", text: \"Invited louise@bourgeois.studio as Editor\", when: \"2 days ago\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_02\", name: \"Grace Hopper\", handle: \"@grace\", email: \"grace@atelier.art\", initials: \"GH\", role: \"admin\", pending: false,\n\t\tjoined: \"Mar 9, 2024\", lastActive: \"20 minutes ago\", editions: 4,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"user\", text: \"Changed Frida Kahlo's role to Viewer\", when: \"Yesterday\" },\n\t\t\t{ id: \"a2\", icon: \"edit\", text: \"Updated pricing on First Compiler — edition of 10\", when: \"3 days ago\" },\n\t\t\t{ id: \"a3\", icon: \"gem\", text: \"Released First Compiler — edition of 10\", when: \"Sep 28\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_03\", name: \"Mary Shelley\", handle: \"@mary\", email: \"mary@atelier.art\", initials: \"MS\", role: \"editor\", pending: false,\n\t\tjoined: \"Jun 18, 2024\", lastActive: \"3 hours ago\", editions: 7,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"calendar\", text: \"Scheduled Frankenstein Studies for Oct 14\", when: \"3 hours ago\" },\n\t\t\t{ id: \"a2\", icon: \"gem\", text: \"Released The Last Man — edition of 25\", when: \"Sep 21\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_04\", name: \"Nina Simone\", handle: \"@nina\", email: \"nina@atelier.art\", initials: \"NS\", role: \"editor\", pending: false,\n\t\tjoined: \"Jan 12, 2025\", lastActive: \"Yesterday\", editions: 3,\n\t\tactivity: [\n\t\t\t{ id: \"a1\", icon: \"upload\", text: \"Uploaded artwork for Little Girl Blue\", when: \"Yesterday\" },\n\t\t\t{ id: \"a2\", icon: \"gem\", text: \"Released Feeling Good — edition of 100\", when: \"Aug 30\" },\n\t\t],\n\t},\n\t{\n\t\tid: \"u_05\", name: \"Frida Kahlo\", handle: \"@frida\", email: \"frida@atelier.art\", initials: \"FK\", role: \"viewer\", pending: false,\n\t\tjoined: \"Jul 2, 2025\", lastActive: \"4 days ago\", editions: 0,\n\t\tactivity: [{ id: \"a1\", icon: \"comment\", text: \"Commented on the Las Dos Fridas draft\", when: \"4 days ago\" }],\n\t},\n\t{\n\t\tid: \"i_01\", name: \"louise@bourgeois.studio\", email: \"louise@bourgeois.studio\", initials: \"\", role: \"editor\", pending: true,\n\t\tinvitedBy: \"Ada Lovelace\", sent: \"2 days ago\", expires: \"In 5 days\",\n\t\tactivity: [],\n\t},\n]\n\nexport function ListDetailPage() {\n\tconst [members, setMembers] = useState(initialMembers)\n\tconst [selectedId, setSelectedId] = useState<string | null>(\"u_02\")\n\tconst [query, setQuery] = useState(\"\")\n\t// Below lg the list and the detail share one column: selecting drills in.\n\tconst [showDetail, setShowDetail] = useState(false)\n\tconst [confirming, setConfirming] = useState(false)\n\tconst listRef = useRef<HTMLDivElement>(null)\n\tconst backRef = useRef<HTMLButtonElement>(null)\n\tconst searchRef = useRef<HTMLInputElement>(null)\n\n\tconst needle = query.trim().toLowerCase()\n\tconst visible = members.filter((m) =>\n\t\t[m.name, m.email, m.handle ?? \"\", roleLabel[m.role]].some((v) => v.toLowerCase().includes(needle)),\n\t)\n\tconst selected = members.find((m) => m.id === selectedId) ?? null\n\tconst active = members.filter((m) => !m.pending).length\n\tconst pending = members.length - active\n\n\t// Drilling in on a phone moves focus to the detail's back control; the\n\t// control is display:none at lg, so desktop focus stays on the list.\n\tuseEffect(() => {\n\t\tif (showDetail && backRef.current?.offsetParent) backRef.current.focus()\n\t}, [showDetail, selectedId])\n\n\tfunction select(id: string) {\n\t\tsetSelectedId(id)\n\t\tsetShowDetail(true)\n\t}\n\n\tfunction focusRow(id: string | undefined) {\n\t\trequestAnimationFrame(() => {\n\t\t\tlistRef.current?.querySelector<HTMLButtonElement>(`[data-member-id=\"${id}\"] button`)?.focus()\n\t\t})\n\t}\n\n\tfunction backToList() {\n\t\tsetShowDetail(false)\n\t\tfocusRow(selectedId ?? undefined)\n\t}\n\n\tfunction setRole(id: string, role: Role) {\n\t\tsetMembers((list) => list.map((m) => (m.id === id ? { ...m, role } : m)))\n\t}\n\n\tfunction resend(id: string) {\n\t\tsetMembers((list) =>\n\t\t\tlist.map((m) => (m.id === id ? { ...m, sent: \"Just now\", expires: \"In 7 days\", resent: true } : m)),\n\t\t)\n\t}\n\n\tfunction remove(id: string) {\n\t\tconst index = members.findIndex((m) => m.id === id)\n\t\tconst rest = members.filter((m) => m.id !== id)\n\t\tsetMembers(rest)\n\t\tconst next = rest[Math.min(index, rest.length - 1)]?.id\n\t\tsetSelectedId(next ?? null)\n\t\tsetShowDetail(false)\n\t\t// The control that opened the confirmation is gone; land on the next row.\n\t\tfocusRow(next)\n\t}\n\n\treturn (\n\t\t<Page as=\"main\" max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t<PageHeader\n\t\t\t\ttitle=\"Members\"\n\t\t\t\tdescription={`${active} members · ${pending} pending ${pending === 1 ? \"invite\" : \"invites\"} · ${members.length} of ${seats} seats used`}\n\t\t\t\tactions={<Button iconStart={<Icon name=\"plus\" />}>Invite member</Button>}\n\t\t\t/>\n\t\t\t<Columns count={{ base: 1, lg: 12 }}>\n\t\t\t\t<Col span={{ base: 1, lg: 5 }} className={showDetail ? \"max-lg:hidden\" : undefined}>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<SearchInput\n\t\t\t\t\t\t\tref={searchRef}\n\t\t\t\t\t\t\tvalue={query}\n\t\t\t\t\t\t\tonValueChange={setQuery}\n\t\t\t\t\t\t\tplaceholder=\"Search name, email or role\"\n\t\t\t\t\t\t\taria-label=\"Search members\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{/* sable-surface derives the rows' selected step from the card (12% ink),\n\t\t\t\t\t\t so a selected row stays distinct from its muted avatar. */}\n\t\t\t\t\t\t<Card ref={listRef} className=\"sable-surface\">\n\t\t\t\t\t\t\t{visible.length > 0 ? (\n\t\t\t\t\t\t\t\t<CardList aria-label=\"Members\">\n\t\t\t\t\t\t\t\t\t{visible.map((m) => {\n\t\t\t\t\t\t\t\t\t\tconst isSelected = m.id === selectedId\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\t\tkey={m.id}\n\t\t\t\t\t\t\t\t\t\t\t\tdata-member-id={m.id}\n\t\t\t\t\t\t\t\t\t\t\t\tinteractive\n\t\t\t\t\t\t\t\t\t\t\t\tselected={isSelected}\n\t\t\t\t\t\t\t\t\t\t\t\tonSelect={() => select(m.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tleading={\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Avatar variant=\"art\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tseed={m.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfallback={m.pending ? <Icon name=\"mail\" className=\"size-5\" /> : m.initials}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstatus={m.lastActive === \"Online now\" ? <StatusDot tone=\"success\" /> : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t// The row's title already names it: keep the initials out of the row's name.\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-hidden\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={m.name}\n\t\t\t\t\t\t\t\t\t\t\t\tsubtitle={\n\t\t\t\t\t\t\t\t\t\t\t\t\tm.pending\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `${roleLabel[m.role]} · invite sent ${m.sent?.toLowerCase()}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: `${roleLabel[m.role]} · ${m.handle}`\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\taction={m.pending ? <Badge tone=\"info\">Invited</Badge> : undefined}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t<EmptyState\n\t\t\t\t\t\t\t\t\t\ticon={<Icon name=\"search\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"No members found\"\n\t\t\t\t\t\t\t\t\t\tdescription={`Nobody matches “${query.trim()}”. Try a name, email or role.`}\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"secondary\" size=\"sm\" onClick={() => {\n\t\t\t\t\t\t\t\t\t\t\t\tsetQuery(\"\")\n\t\t\t\t\t\t\t\t\t\t\t\tsearchRef.current?.focus()\n\t\t\t\t\t\t\t\t\t\t\t}}>\n\t\t\t\t\t\t\t\t\t\t\t\tClear search\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Col>\n\t\t\t\t<Col span={{ base: 1, lg: 7 }} className={showDetail ? undefined : \"max-lg:hidden\"}>\n\t\t\t\t\t<Stack role=\"region\" aria-label=\"Member details\" gap={3} className=\"lg:sticky lg:top-6\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tref={backRef}\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\ticonStart={<Icon name=\"chevron-left\" />}\n\t\t\t\t\t\t\tonClick={backToList}\n\t\t\t\t\t\t\tclassName=\"self-start lg:hidden\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tAll members\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t{selected === null ? (\n\t\t\t\t\t\t\t<EmptyState\n\t\t\t\t\t\t\t\tsurface=\"card\"\n\t\t\t\t\t\t\t\ticon={<Icon name=\"user\" />}\n\t\t\t\t\t\t\t\ttitle=\"No member selected\"\n\t\t\t\t\t\t\t\tdescription=\"Choose someone from the list to see their role and activity.\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t<Avatar variant=\"art\"\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"lg\"\n\t\t\t\t\t\t\t\t\t\t\t\tfallback={selected.pending ? <Icon name=\"mail\" className=\"size-6\" /> : selected.initials}\n\t\t\t\t\t\t\t\t\t\t\t\tstatus={selected.lastActive === \"Online now\" ? <StatusDot tone=\"success\" /> : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\talt={selected.pending ? \"Pending invite\" : selected.name}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<Stack gap={0} className=\"min-w-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />} className=\"truncate\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.name}\n\t\t\t\t\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t\t\t\t\t<CardDescription className=\"truncate\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? \"Hasn't joined yet\" : `${selected.handle} · ${selected.email}`}\n\t\t\t\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t{selected.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t\t\t<Badge tone=\"info\">Invited</Badge>\n\t\t\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t<Stack gap={5}>\n\t\t\t\t\t\t\t\t\t\t\t<Field\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={selected.pending ? \"Role on joining\" : \"Role\"}\n\t\t\t\t\t\t\t\t\t\t\t\tdescription={roleSummary[selected.role]}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\t\t\t\t\t\t\titems={roles}\n\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={selected.role}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonValueChange={(value) => setRole(selected.id, value as Role)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={selected.role === \"owner\"}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectTrigger className=\"max-w-72\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{roles\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.filter((r) => r.value !== \"owner\" || selected.role === \"owner\")\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t.map((r) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={r.value} value={r.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{r.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"@container\">\n\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl sable-list-dl-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Sent</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.sent}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Expires</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.expires}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Invited by</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.invitedBy}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<dl className=\"sable-list-dl sable-list-dl-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Email</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.email}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Joined</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.joined}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Last active</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd className={selected.lastActive === \"Online now\" ? \"text-success\" : undefined}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.lastActive}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dt>Editions released</dt>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<dd>{selected.editions}</dd>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</dl>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t{selected.role !== \"owner\" ? (\n\t\t\t\t\t\t\t\t\t\t<CardFooter>\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2} wrap>\n\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ticonStart={<Icon name={selected.resent ? \"check\" : \"send\"} />}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={selected.resent}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => resend(selected.id)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.resent ? \"Invite resent\" : \"Resend invite\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={() => setConfirming(true)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{selected.pending ? \"Revoke invite\" : \"Remove from studio\"}\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t{selected.activity.length > 0 ? (\n\t\t\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t\t\t<CardTitle render={<h3 />}>\n\t\t\t\t\t\t\t\t\t\t\t\tRecent activity\n\t\t\t\t\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t\t\t{selected.activity.map((a) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={a.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\tleading={<Icon name={a.icon} className=\"text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttitle={a.text}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsubtitle={a.when}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t<DestructiveActionModal\n\t\t\t\t\t\t\t\t\topen={confirming}\n\t\t\t\t\t\t\t\t\tonOpenChange={setConfirming}\n\t\t\t\t\t\t\t\t\ttitle={selected.pending ? \"Revoke this invite?\" : `Remove ${selected.name}?`}\n\t\t\t\t\t\t\t\t\tdescription={\n\t\t\t\t\t\t\t\t\t\tselected.pending\n\t\t\t\t\t\t\t\t\t\t\t? `${selected.email} won't be able to join with the link they were sent.`\n\t\t\t\t\t\t\t\t\t\t\t: \"They lose access to the studio right away. Editions they released stay on sale.\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tconfirmLabel={selected.pending ? \"Revoke invite\" : \"Remove member\"}\n\t\t\t\t\t\t\t\t\tonConfirm={() => remove(selected.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Stack>\n\t\t\t\t</Col>\n\t\t\t</Columns>\n\t\t</Page>\n\t)\n}"
|
|
18895
19132
|
},
|
|
18896
19133
|
{
|
|
18897
19134
|
"name": "SettingsPage",
|
|
@@ -18917,7 +19154,7 @@
|
|
|
18917
19154
|
"DestructiveActionModal is an alertdialog: focus opens on Cancel, the backdrop does not dismiss, and the confirm stays disabled until the handle is typed."
|
|
18918
19155
|
],
|
|
18919
19156
|
"imports": "import { useState } from \"react\"\nimport { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, OverflowMenu, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from \"@sableui/react\"",
|
|
18920
|
-
"code": "const sections = [\n\t{ id: \"profile\", label: \"Profile\" },\n\t{ id: \"notifications\", label: \"Notifications\" },\n\t{ id: \"payouts\", label: \"Payouts\" },\n\t{ id: \"account\", label: \"Account\" },\n]\n\nconst timezones = [\n\t{ value: \"europe-london\", label: \"London (GMT+1)\" },\n\t{ value: \"europe-paris\", label: \"Paris (GMT+2)\" },\n\t{ value: \"america-new-york\", label: \"New York (GMT−4)\" },\n\t{ value: \"america-los-angeles\", label: \"Los Angeles (GMT−7)\" },\n\t{ value: \"asia-tokyo\", label: \"Tokyo (GMT+9)\" },\n]\n\nconst notifications = [\n\t{ name: \"notifyCollects\", title: \"New collects\", description: \"When someone collects your work\", on: true },\n\t{ name: \"notifySoldOut\", title: \"Sold out\", description: \"When an edition sells its last copy\", on: true },\n\t{ name: \"notifyFollowers\", title: \"New followers\", description: \"When a collector follows you\", on: false },\n\t{ name: \"notifyDigest\", title: \"Weekly digest\", description: \"Sales and collectors, every Monday\", on: true },\n\t{ name: \"notifyProduct\", title: \"Product updates\", description: \"New Atelier features\", on: false },\n]\n\nconst payouts = [\n\t{ label: \"Next payout\", value: \"$1,284.60\", note: \"Friday, Oct 16\" },\n\t{ label: \"Pending\", value: \"$412.00\", note: \"Clears in 7 days\" },\n\t{ label: \"Paid this year\", value: \"$18,920.00\", note: \"Across 214 collects\" },\n]\n\nexport function SettingsPage() {\n\tconst [activeSection, setActiveSection] = useState(\"profile\")\n\tconst [deleteOpen, setDeleteOpen] = useState(false)\n\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" showLabel render={<a href=\"/editions\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Collectors\" showLabel render={<a href=\"/collectors\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel active render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<Row gap={1} align=\"center\" className=\"px-3\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" render={<a href=\"/editions\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Collectors\" render={<a href=\"/collectors\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" active render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tclassName=\"[--entity-py:0]\"\n\t\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\ttitle=\"Ada Lovelace\"\n\t\t\t\t\t\t\tsubtitle=\"@ada\"\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<OverflowMenu label=\"Account\" size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t{/* No `as=\"main\"`: AppShell already renders <main id=\"main-content\">. */}\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\tdescription=\"Manage your profile, notifications, and payouts.\"\n\t\t\t\t/>\n\t\t\t\t<Columns gap={4}>\n\t\t\t\t\t{/* The section rail is a desktop aid; on mobile the sections simply stack. */}\n\t\t\t\t\t<Col span={{ base: 12, lg: 3 }} className=\"hidden lg:block\">\n\t\t\t\t\t\t<SideNav aria-label=\"Settings sections\" className=\"sticky top-6\">\n\t\t\t\t\t\t\t{sections.map((section) => (\n\t\t\t\t\t\t\t\t<SideNavItem\n\t\t\t\t\t\t\t\t\tkey={section.id}\n\t\t\t\t\t\t\t\t\tlabel={section.label}\n\t\t\t\t\t\t\t\t\tactive={activeSection === section.id}\n\t\t\t\t\t\t\t\t\tonClick={() => setActiveSection(section.id)}\n\t\t\t\t\t\t\t\t\trender={<a href={`#${section.id}`} />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</SideNav>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col span={{ base: 12, lg: 9 }}>\n\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t<Card id=\"profile\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Profile</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>How collectors see you on Atelier.</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t{/* The one explicit save on the page: profile text is drafted, then\n\t\t\t\t\t\t\t\t committed. Toggles below apply the moment they change. */}\n\t\t\t\t\t\t\t\t<Form onSubmit={(event) => event.preventDefault()} className=\"gap-0\">\n\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Avatar size=\"lg\" fallback=\"AL\" alt=\"Ada Lovelace\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Stack gap={0.5} align=\"start\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tUpload photo\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tSquare PNG or JPG, at least 400 px.\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Display name\" name=\"displayName\" required>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input defaultValue=\"Ada Lovelace\" autoComplete=\"name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Handle\" name=\"handle\" description=\"atelier.com/@ada\" required>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input defaultValue=\"ada\" autoComplete=\"username\" spellCheck={false} />\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Bio\" name=\"bio\" description=\"Shown on your profile and edition pages.\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\t\t\t\t\trows={3}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefaultValue=\"Generative artist working in plotted ink and code. New editions of 25 to 100 every month.\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Timezone\" name=\"timezone\" description=\"Release times use this zone.\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Select defaultValue=\"europe-london\" items={timezones}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectTrigger />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{timezones.map((zone) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={zone.value} value={zone.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{zone.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t\t\t\t\t\t<Button type=\"submit\">Save profile</Button>\n\t\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"notifications\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Notifications</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\t\tEmail updates about your editions. Changes save automatically.\n\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t{notifications.map((item) => (\n\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\tkey={item.name}\n\t\t\t\t\t\t\t\t\t\t\ttitle={item.title}\n\t\t\t\t\t\t\t\t\t\t\tsubtitle={item.description}\n\t\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\t\t\t\t\tname={item.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefaultChecked={item.on}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={item.title}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"payouts\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Payouts</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>Paid in USD every Friday to Chase checking ••4821.</CardDescription>\n\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/settings/payouts\" />}>\n\t\t\t\t\t\t\t\t\t\t\tManage payouts\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 3 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t{payouts.map((stat) => (\n\t\t\t\t\t\t\t\t\t\t\t<Stack key={stat.label} gap={0.5}>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{stat.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-mono-lg text-foreground\">{stat.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">{stat.note}</p>\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"account\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Account</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>Sign-in details and your data.</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tleading={<Icon name=\"mail\" className=\"text-lg text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"Email\"\n\t\t\t\t\t\t\t\t\t\tsubtitle=\"ada@atelier.art\"\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\tChange\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tleading={<Icon name=\"download\" className=\"text-lg text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"Export data\"\n\t\t\t\t\t\t\t\t\t\tsubtitle=\"Editions, sales, and collectors\"\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Delete account</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\t\tPermanently remove your profile, editions, and sales history. Collectors keep\n\t\t\t\t\t\t\t\t\t\tthe editions they own.\n\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t\t\t\t\t<Button variant=\"destructive\" onClick={() => setDeleteOpen(true)}>\n\t\t\t\t\t\t\t\t\t\tDelete account\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Col>\n\t\t\t\t</Columns>\n\t\t\t</Page>\n\t\t\t<DestructiveActionModal\n\t\t\t\topen={deleteOpen}\n\t\t\t\tonOpenChange={setDeleteOpen}\n\t\t\t\ttitle=\"Delete your account?\"\n\t\t\t\tdescription=\"This permanently removes your profile, 12 editions, and sales history. Pending payouts are still sent. This can't be undone.\"\n\t\t\t\tconfirmLabel=\"Delete account\"\n\t\t\t\tconfirmingLabel=\"Deleting…\"\n\t\t\t\tconfirmText=\"ada\"\n\t\t\t\tonConfirm={() => setDeleteOpen(false)}\n\t\t\t/>\n\t\t</AppShell>\n\t)\n}"
|
|
19157
|
+
"code": "const sections = [\n\t{ id: \"profile\", label: \"Profile\" },\n\t{ id: \"notifications\", label: \"Notifications\" },\n\t{ id: \"payouts\", label: \"Payouts\" },\n\t{ id: \"account\", label: \"Account\" },\n]\n\nconst timezones = [\n\t{ value: \"europe-london\", label: \"London (GMT+1)\" },\n\t{ value: \"europe-paris\", label: \"Paris (GMT+2)\" },\n\t{ value: \"america-new-york\", label: \"New York (GMT−4)\" },\n\t{ value: \"america-los-angeles\", label: \"Los Angeles (GMT−7)\" },\n\t{ value: \"asia-tokyo\", label: \"Tokyo (GMT+9)\" },\n]\n\nconst notifications = [\n\t{ name: \"notifyCollects\", title: \"New collects\", description: \"When someone collects your work\", on: true },\n\t{ name: \"notifySoldOut\", title: \"Sold out\", description: \"When an edition sells its last copy\", on: true },\n\t{ name: \"notifyFollowers\", title: \"New followers\", description: \"When a collector follows you\", on: false },\n\t{ name: \"notifyDigest\", title: \"Weekly digest\", description: \"Sales and collectors, every Monday\", on: true },\n\t{ name: \"notifyProduct\", title: \"Product updates\", description: \"New Atelier features\", on: false },\n]\n\nconst payouts = [\n\t{ label: \"Next payout\", value: \"$1,284.60\", note: \"Friday, Oct 16\" },\n\t{ label: \"Pending\", value: \"$412.00\", note: \"Clears in 7 days\" },\n\t{ label: \"Paid this year\", value: \"$18,920.00\", note: \"Across 214 collects\" },\n]\n\nexport function SettingsPage() {\n\tconst [activeSection, setActiveSection] = useState(\"profile\")\n\tconst [deleteOpen, setDeleteOpen] = useState(false)\n\n\treturn (\n\t\t<AppShell\n\t\t\t// Sidebar hides below lg, so the same destinations ride in the shell's\n\t\t\t// mobile slots — without them the route has no navigation under 1024px.\n\t\t\theader={\n\t\t\t\t<AppHeader\n\t\t\t\t\ttitle={\n\t\t\t\t\t\t<Row gap={1} align=\"center\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t}\n\t\t\t\t\tactions={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"Ada Lovelace\" />}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tbottomNav={\n\t\t\t\t<BottomNav aria-label=\"Primary\">\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"grid\" />} label=\"Overview\" showLabel render={<a href=\"/\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" showLabel render={<a href=\"/editions\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"user\" />} label=\"Collectors\" showLabel render={<a href=\"/collectors\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"bell\" />} label=\"Activity\" showLabel render={<a href=\"/activity\" />} />\n\t\t\t\t\t<BottomNavItem icon={<Icon name=\"gear\" />} label=\"Settings\" showLabel active render={<a href=\"/settings\" />} />\n\t\t\t\t</BottomNav>\n\t\t\t}\n\t\t\tsidebar={\n\t\t\t\t<Sidebar>\n\t\t\t\t\t<SidebarHeader>\n\t\t\t\t\t\t<Row gap={1} align=\"center\" className=\"px-3\">\n\t\t\t\t\t\t\t<Icon name=\"gem\" variant=\"solid\" className=\"text-xl\" />\n\t\t\t\t\t\t\t<span className=\"text-title-sm\">Atelier</span>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</SidebarHeader>\n\t\t\t\t\t<SidebarNav>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"grid\" />} label=\"Overview\" render={<a href=\"/\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"image-stack\" />} label=\"Editions\" render={<a href=\"/editions\" />} />\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"user\" />} label=\"Collectors\" render={<a href=\"/collectors\" />} />\n\t\t\t\t\t\t<SidebarItem\n\t\t\t\t\t\t\ticon={<Icon name=\"bell\" />}\n\t\t\t\t\t\t\tlabel=\"Activity\"\n\t\t\t\t\t\t\tbadge={<NotificationBadge count={4} tone=\"neutral\" />}\n\t\t\t\t\t\t\trender={<a href=\"/activity\" />}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" active render={<a href=\"/settings\" />} />\n\t\t\t\t\t</SidebarNav>\n\t\t\t\t\t<SidebarFooter>\n\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\tclassName=\"[--entity-py:0]\"\n\t\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback=\"AL\" alt=\"\" />}\n\t\t\t\t\t\t\ttitle=\"Ada Lovelace\"\n\t\t\t\t\t\t\tsubtitle=\"@ada\"\n\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t<OverflowMenu label=\"Account\" size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/@ada\" />}>View profile</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuItem render={<a href=\"/settings\" />}>Settings</MenuItem>\n\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t<MenuItem>Sign out</MenuItem>\n\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</SidebarFooter>\n\t\t\t\t</Sidebar>\n\t\t\t}\n\t\t>\n\t\t\t{/* No `as=\"main\"`: AppShell already renders <main id=\"main-content\">. */}\n\t\t\t<Page max=\"wide\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\tdescription=\"Manage your profile, notifications, and payouts.\"\n\t\t\t\t/>\n\t\t\t\t<Columns gap={4}>\n\t\t\t\t\t{/* The section rail is a desktop aid; on mobile the sections simply stack. */}\n\t\t\t\t\t<Col span={{ base: 12, lg: 3 }} className=\"hidden lg:block\">\n\t\t\t\t\t\t<SideNav aria-label=\"Settings sections\" className=\"sticky top-6\">\n\t\t\t\t\t\t\t{sections.map((section) => (\n\t\t\t\t\t\t\t\t<SideNavItem\n\t\t\t\t\t\t\t\t\tkey={section.id}\n\t\t\t\t\t\t\t\t\tlabel={section.label}\n\t\t\t\t\t\t\t\t\tactive={activeSection === section.id}\n\t\t\t\t\t\t\t\t\tonClick={() => setActiveSection(section.id)}\n\t\t\t\t\t\t\t\t\trender={<a href={`#${section.id}`} />}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</SideNav>\n\t\t\t\t\t</Col>\n\t\t\t\t\t<Col span={{ base: 12, lg: 9 }}>\n\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t<Card id=\"profile\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Profile</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>How collectors see you on Atelier.</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t{/* The one explicit save on the page: profile text is drafted, then\n\t\t\t\t\t\t\t\t committed. Toggles below apply the moment they change. */}\n\t\t\t\t\t\t\t\t<Form onSubmit={(event) => event.preventDefault()} className=\"gap-0\">\n\t\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Avatar variant=\"art\" size=\"lg\" fallback=\"AL\" alt=\"Ada Lovelace\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Stack gap={0.5} align=\"start\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tUpload photo\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tSquare PNG or JPG, at least 400 px.\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Display name\" name=\"displayName\" required>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input defaultValue=\"Ada Lovelace\" autoComplete=\"name\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Handle\" name=\"handle\" description=\"atelier.com/@ada\" required>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input defaultValue=\"ada\" autoComplete=\"username\" spellCheck={false} />\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Bio\" name=\"bio\" description=\"Shown on your profile and edition pages.\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\t\t\t\t\t\trows={3}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefaultValue=\"Generative artist working in plotted ink and code. New editions of 25 to 100 every month.\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Field label=\"Timezone\" name=\"timezone\" description=\"Release times use this zone.\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Select defaultValue=\"europe-london\" items={timezones}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectTrigger />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{timezones.map((zone) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={zone.value} value={zone.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{zone.label}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t\t\t\t\t\t<Button type=\"submit\">Save profile</Button>\n\t\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t\t</Form>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"notifications\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Notifications</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\t\tEmail updates about your editions. Changes save automatically.\n\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t{notifications.map((item) => (\n\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\tkey={item.name}\n\t\t\t\t\t\t\t\t\t\t\ttitle={item.title}\n\t\t\t\t\t\t\t\t\t\t\tsubtitle={item.description}\n\t\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\t\t\t\t\tname={item.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefaultChecked={item.on}\n\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={item.title}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"payouts\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Payouts</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>Paid in USD every Friday to Chase checking ••4821.</CardDescription>\n\t\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" render={<a href=\"/settings/payouts\" />}>\n\t\t\t\t\t\t\t\t\t\t\tManage payouts\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t</CardAction>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 3 }} gap={3}>\n\t\t\t\t\t\t\t\t\t\t{payouts.map((stat) => (\n\t\t\t\t\t\t\t\t\t\t\t<Stack key={stat.label} gap={0.5}>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{stat.label}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-mono-lg text-foreground\">{stat.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">{stat.note}</p>\n\t\t\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card id=\"account\" className=\"scroll-mt-6\">\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Account</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>Sign-in details and your data.</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tleading={<Icon name=\"mail\" className=\"text-lg text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"Email\"\n\t\t\t\t\t\t\t\t\t\tsubtitle=\"ada@atelier.art\"\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\tChange\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\tleading={<Icon name=\"download\" className=\"text-lg text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\ttitle=\"Export data\"\n\t\t\t\t\t\t\t\t\t\tsubtitle=\"Editions, sales, and collectors\"\n\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t\t\tExport\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</CardList>\n\t\t\t\t\t\t\t</Card>\n\n\t\t\t\t\t\t\t<Card>\n\t\t\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t\t\t<CardTitle render={<h2 />}>Delete account</CardTitle>\n\t\t\t\t\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\t\t\t\t\tPermanently remove your profile, editions, and sales history. Collectors keep\n\t\t\t\t\t\t\t\t\t\tthe editions they own.\n\t\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t\t\t\t\t<Button variant=\"destructive\" onClick={() => setDeleteOpen(true)}>\n\t\t\t\t\t\t\t\t\t\tDelete account\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</CardFooter>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Col>\n\t\t\t\t</Columns>\n\t\t\t</Page>\n\t\t\t<DestructiveActionModal\n\t\t\t\topen={deleteOpen}\n\t\t\t\tonOpenChange={setDeleteOpen}\n\t\t\t\ttitle=\"Delete your account?\"\n\t\t\t\tdescription=\"This permanently removes your profile, 12 editions, and sales history. Pending payouts are still sent. This can't be undone.\"\n\t\t\t\tconfirmLabel=\"Delete account\"\n\t\t\t\tconfirmingLabel=\"Deleting…\"\n\t\t\t\tconfirmText=\"ada\"\n\t\t\t\tonConfirm={() => setDeleteOpen(false)}\n\t\t\t/>\n\t\t</AppShell>\n\t)\n}"
|
|
18921
19158
|
}
|
|
18922
19159
|
]
|
|
18923
19160
|
},
|
|
@@ -19033,7 +19270,7 @@
|
|
|
19033
19270
|
"The CardTitle is an h2, so the section is reachable by heading."
|
|
19034
19271
|
],
|
|
19035
19272
|
"imports": "import { useState } from \"react\"\nimport { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, cn, DestructiveActionModal } from \"@sableui/react\"",
|
|
19036
|
-
"code": "const account = { handle: \"ada\", editions: 12 }\n\nexport interface DangerZoneBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called once the person has typed the handle and confirmed. */\n\tonDelete?: () => void\n}\n\nexport function DangerZoneBlock({ className, framed = true, onDelete }: DangerZoneBlockProps) {\n\tconst [open, setOpen] = useState(false)\n\treturn (\n\t\t<>\n\t\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle render={<h2 />}>Delete account</CardTitle>\n\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\tPermanently remove your profile, editions, and sales history. Collectors keep the editions they own.\n\t\t\t\t\t</CardDescription>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t<Button variant=\"destructive\" onClick={() => setOpen(true)}>\n\t\t\t\t\t\tDelete account\n\t\t\t\t\t</Button>\n\t\t\t\t</CardFooter>\n\t\t\t</Card>\n\t\t\t<DestructiveActionModal\n\t\t\t\topen={open}\n\t\t\t\tonOpenChange={setOpen}\n\t\t\t\ttitle=\"Delete your account?\"\n\t\t\t\tdescription={`This permanently removes your profile, ${account.editions} editions, and sales history. Pending payouts are still sent. This can't be undone.`}\n\t\t\t\tconfirmLabel=\"Delete account\"\n\t\t\t\tconfirmingLabel=\"Deleting…\"\n\t\t\t\tconfirmText={account.handle}\n\t\t\t\tonConfirm={() => {\n\t\t\t\t\tsetOpen(false)\n\t\t\t\t\tonDelete?.()\n\t\t\t\t}}\n\t\t\t/>\n\t\t</>\n\t)\n}"
|
|
19273
|
+
"code": "const account = { handle: \"ada\", editions: 12 }\n\nexport interface DangerZoneBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called once the person has typed the handle and confirmed. */\n\tonDelete?: () => void\n}\n\nexport function DangerZoneBlock({ className, framed = true, onDelete }: DangerZoneBlockProps) {\n\tconst [open, setOpen] = useState(false)\n\treturn (\n\t\t<>\n\t\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t\t<CardHeader>\n\t\t\t\t\t<CardTitle render={<h2 />}>Delete account</CardTitle>\n\t\t\t\t\t<CardDescription>\n\t\t\t\t\t\tPermanently remove your profile, editions, and sales history. Collectors keep the editions they own.\n\t\t\t\t\t</CardDescription>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardFooter actions className=\"justify-end\">\n\t\t\t\t\t<Button variant=\"destructive\" onClick={() => setOpen(true)}>\n\t\t\t\t\t\tDelete account\n\t\t\t\t\t</Button>\n\t\t\t\t</CardFooter>\n\t\t\t</Card>\n\t\t\t<DestructiveActionModal\n\t\t\t\topen={open}\n\t\t\t\tonOpenChange={setOpen}\n\t\t\t\ttitle=\"Delete your account?\"\n\t\t\t\tdescription={`This permanently removes your profile, ${account.editions} editions, and sales history. Pending payouts are still sent. This can't be undone.`}\n\t\t\t\tconfirmLabel=\"Delete account\"\n\t\t\t\tconfirmingLabel=\"Deleting…\"\n\t\t\t\tconfirmText={account.handle}\n\t\t\t\tonConfirm={() => {\n\t\t\t\t\tsetOpen(false)\n\t\t\t\t\tonDelete?.()\n\t\t\t\t}}\n\t\t\t/>\n\t\t</>\n\t)\n}"
|
|
19037
19274
|
},
|
|
19038
19275
|
{
|
|
19039
19276
|
"name": "DeviceComparisonBlock",
|
|
@@ -19079,7 +19316,7 @@
|
|
|
19079
19316
|
"The AvatarGroup's aria-label states the total attendee count."
|
|
19080
19317
|
],
|
|
19081
19318
|
"imports": "import { Avatar, AvatarGroup, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardTitle, cn, DateTile } from \"@sableui/react\"",
|
|
19082
|
-
"code": "const event = {\n\ttitle: \"Design review\",\n\tdate: new Date(2026, 9, 14),\n\twhen: \"Tuesday, 2:00 – 2:45 PM\",\n\tstarts: \"In 10 min\",\n\tattendees: [\"MO\", \"SR\", \"JL\"],\n\ttotal: 7,\n}\n\nexport interface EventCardBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the person's answer. */\n\tonRespond?: (answer: \"join\" | \"decline\") => void\n}\n\nexport function EventCardBlock({ className, framed = true, onRespond }: EventCardBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardContent className=\"flex gap-4\">\n\t\t\t\t<DateTile date={event.date} />\n\t\t\t\t<div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n\t\t\t\t\t{/* The row carries the title's type, so the badge's box is exactly\n\t\t\t\t\t one title line (`h-[1lh]`): it centres on the title and never\n\t\t\t\t\t makes the row taller than the title alone. When a bolder or\n\t\t\t\t\t roomier look leaves no space beside the title, the badge wraps\n\t\t\t\t\t to its own line instead of squeezing the title onto two. */}\n\t\t\t\t\t<div className=\"flex flex-wrap items-start justify-between gap-x-2 text-title-lg\">\n\t\t\t\t\t\t<CardTitle>{event.title}</CardTitle>\n\t\t\t\t\t\t<span className=\"flex h-[1lh] shrink-0 items-center\">\n\t\t\t\t\t\t\t<Badge tone=\"info\" size=\"sm\">\n\t\t\t\t\t\t\t\t{event.starts}\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<CardDescription>{event.when}</CardDescription>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t\t{/* `fit`: when a roomy look tightens the row, the stack gives up\n\t\t\t avatars to the count instead of pushing the actions to a new line. */}\n\t\t\t<CardFooter className=\"flex-wrap justify-between gap-3\">\n\t\t\t\t<AvatarGroup aria-label={`${event.total} attendees`} total={event.total} size=\"sm\" surface={framed ? \"card\" : undefined} fit>\n\t\t\t\t\t{event.attendees.map((initials) => (\n\t\t\t\t\t\t<Avatar key={initials} fallback={initials} alt={initials} />\n\t\t\t\t\t))}\n\t\t\t\t</AvatarGroup>\n\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={() => onRespond?.(\"decline\")}>\n\t\t\t\t\t\tDecline\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button size=\"sm\" onClick={() => onRespond?.(\"join\")}>\n\t\t\t\t\t\tJoin\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
19319
|
+
"code": "const event = {\n\ttitle: \"Design review\",\n\tdate: new Date(2026, 9, 14),\n\twhen: \"Tuesday, 2:00 – 2:45 PM\",\n\tstarts: \"In 10 min\",\n\tattendees: [\"MO\", \"SR\", \"JL\"],\n\ttotal: 7,\n}\n\nexport interface EventCardBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the person's answer. */\n\tonRespond?: (answer: \"join\" | \"decline\") => void\n}\n\nexport function EventCardBlock({ className, framed = true, onRespond }: EventCardBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardContent className=\"flex gap-4\">\n\t\t\t\t<DateTile date={event.date} />\n\t\t\t\t<div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n\t\t\t\t\t{/* The row carries the title's type, so the badge's box is exactly\n\t\t\t\t\t one title line (`h-[1lh]`): it centres on the title and never\n\t\t\t\t\t makes the row taller than the title alone. When a bolder or\n\t\t\t\t\t roomier look leaves no space beside the title, the badge wraps\n\t\t\t\t\t to its own line instead of squeezing the title onto two. */}\n\t\t\t\t\t<div className=\"flex flex-wrap items-start justify-between gap-x-2 text-title-lg\">\n\t\t\t\t\t\t<CardTitle>{event.title}</CardTitle>\n\t\t\t\t\t\t<span className=\"flex h-[1lh] shrink-0 items-center\">\n\t\t\t\t\t\t\t<Badge tone=\"info\" size=\"sm\">\n\t\t\t\t\t\t\t\t{event.starts}\n\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<CardDescription>{event.when}</CardDescription>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t\t{/* `fit`: when a roomy look tightens the row, the stack gives up\n\t\t\t avatars to the count instead of pushing the actions to a new line. */}\n\t\t\t<CardFooter className=\"flex-wrap justify-between gap-3\">\n\t\t\t\t<AvatarGroup aria-label={`${event.total} attendees`} total={event.total} size=\"sm\" surface={framed ? \"card\" : undefined} fit>\n\t\t\t\t\t{event.attendees.map((initials) => (\n\t\t\t\t\t\t<Avatar variant=\"art\" key={initials} fallback={initials} alt={initials} />\n\t\t\t\t\t))}\n\t\t\t\t</AvatarGroup>\n\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" onClick={() => onRespond?.(\"decline\")}>\n\t\t\t\t\t\tDecline\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button size=\"sm\" onClick={() => onRespond?.(\"join\")}>\n\t\t\t\t\t\tJoin\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
19083
19320
|
},
|
|
19084
19321
|
{
|
|
19085
19322
|
"name": "FaqBlock",
|
|
@@ -19141,7 +19378,7 @@
|
|
|
19141
19378
|
"The Textarea carries aria-label=\"Notes\"; its placeholder is a prompt, not the label."
|
|
19142
19379
|
],
|
|
19143
19380
|
"imports": "import { useState } from \"react\"\nimport { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, Textarea, Toggle, ToggleGroup } from \"@sableui/react\"",
|
|
19144
|
-
"code": "const scale = [\"1\", \"2\", \"3\", \"4\", \"5\"]\n\nexport interface FeedbackFormBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or Dialog that already frames it. */\n\tframed?: boolean\n\t/** Skip calls it with `sent: false`; Send with `sent: true` and the answers. */\n\tonDone?: (sent: boolean, feedback?: { score: number; notes: string }) => void\n}\n\nexport function FeedbackFormBlock({ className, framed = true, onDone }: FeedbackFormBlockProps) {\n\tconst [score, setScore] = useState(\"4\")\n\tconst [notes, setNotes] = useState(\"\")\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>How was the session?</CardTitle>\n\t\t\t\t<CardDescription>One tap is enough. Notes help us more.</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t<ToggleGroup value={[score]} onValueChange={(v: unknown[]) => v[0] && setScore(String(v[0]))} aria-label=\"Rating\" className=\"w-full\">\n\t\t\t\t\t{scale.map((n) => (\n\t\t\t\t\t\t<Toggle key={n} value={n} className=\"flex-1\">\n\t\t\t\t\t\t\t{n}\n\t\t\t\t\t\t</Toggle>\n\t\t\t\t\t))}\n\t\t\t\t</ToggleGroup>\n\t\t\t\t<Textarea placeholder=\"What worked, what didn’t…\" aria-label=\"Notes\" rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} />\n\t\t\t</CardContent>\n\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" onClick={() => onDone?.(false)}>\n\t\t\t\t\tSkip\n\t\t\t\t</Button>\n\t\t\t\t<Button size=\"sm\" onClick={() => onDone?.(true, { score: Number(score), notes })}>\n\t\t\t\t\tSend feedback\n\t\t\t\t</Button>\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
19381
|
+
"code": "const scale = [\"1\", \"2\", \"3\", \"4\", \"5\"]\n\nexport interface FeedbackFormBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or Dialog that already frames it. */\n\tframed?: boolean\n\t/** Skip calls it with `sent: false`; Send with `sent: true` and the answers. */\n\tonDone?: (sent: boolean, feedback?: { score: number; notes: string }) => void\n}\n\nexport function FeedbackFormBlock({ className, framed = true, onDone }: FeedbackFormBlockProps) {\n\tconst [score, setScore] = useState(\"4\")\n\tconst [notes, setNotes] = useState(\"\")\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>How was the session?</CardTitle>\n\t\t\t\t<CardDescription>One tap is enough. Notes help us more.</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t<ToggleGroup value={[score]} onValueChange={(v: unknown[]) => v[0] && setScore(String(v[0]))} aria-label=\"Rating\" className=\"w-full\">\n\t\t\t\t\t{scale.map((n) => (\n\t\t\t\t\t\t<Toggle key={n} value={n} className=\"flex-1\">\n\t\t\t\t\t\t\t{n}\n\t\t\t\t\t\t</Toggle>\n\t\t\t\t\t))}\n\t\t\t\t</ToggleGroup>\n\t\t\t\t<Textarea placeholder=\"What worked, what didn’t…\" aria-label=\"Notes\" rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} />\n\t\t\t</CardContent>\n\t\t\t<CardFooter actions className=\"justify-end\">\n\t\t\t\t<Button variant=\"ghost\" size=\"sm\" onClick={() => onDone?.(false)}>\n\t\t\t\t\tSkip\n\t\t\t\t</Button>\n\t\t\t\t<Button size=\"sm\" onClick={() => onDone?.(true, { score: Number(score), notes })}>\n\t\t\t\t\tSend feedback\n\t\t\t\t</Button>\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
19145
19382
|
},
|
|
19146
19383
|
{
|
|
19147
19384
|
"name": "GoalProgressBlock",
|
|
@@ -19206,7 +19443,7 @@
|
|
|
19206
19443
|
"The email Input carries aria-label; there is no visible label in this compact layout."
|
|
19207
19444
|
],
|
|
19208
19445
|
"imports": "import { Avatar, AvatarGroup, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Input } from \"@sableui/react\"",
|
|
19209
|
-
"code": "const collaborators = [\"MO\", \"SR\", \"JL\", \"AK\"]\n\nexport interface InviteTeamBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet, Dialog or muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the address typed, on Send or Enter. */\n\tonSend?: (email: string) => void\n}\n\nexport function InviteTeamBlock({ className, framed = true, onSend }: InviteTeamBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>Invite your team</CardTitle>\n\t\t\t\t<CardDescription>Collaborators can edit every project.</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t<AvatarGroup aria-label={`${collaborators.length} collaborators`} size=\"sm\" surface={framed ? \"card\" : undefined}>\n\t\t\t\t\t{collaborators.map((initials) => (\n\t\t\t\t\t\t<Avatar key={initials} fallback={initials} alt={initials} />\n\t\t\t\t\t))}\n\t\t\t\t</AvatarGroup>\n\t\t\t\t<form\n\t\t\t\t\tclassName=\"flex gap-2\"\n\t\t\t\t\tonSubmit={(event) => {\n\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\tonSend?.(String(new FormData(event.currentTarget).get(\"email\") ?? \"\"))\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Input name=\"email\" type=\"email\" placeholder=\"name@company.com\" aria-label=\"Email address\" />\n\t\t\t\t\t<Button type=\"submit\">Send</Button>\n\t\t\t\t</form>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
|
|
19446
|
+
"code": "const collaborators = [\"MO\", \"SR\", \"JL\", \"AK\"]\n\nexport interface InviteTeamBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet, Dialog or muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the address typed, on Send or Enter. */\n\tonSend?: (email: string) => void\n}\n\nexport function InviteTeamBlock({ className, framed = true, onSend }: InviteTeamBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>Invite your team</CardTitle>\n\t\t\t\t<CardDescription>Collaborators can edit every project.</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t<AvatarGroup aria-label={`${collaborators.length} collaborators`} size=\"sm\" surface={framed ? \"card\" : undefined}>\n\t\t\t\t\t{collaborators.map((initials) => (\n\t\t\t\t\t\t<Avatar variant=\"art\" key={initials} fallback={initials} alt={initials} />\n\t\t\t\t\t))}\n\t\t\t\t</AvatarGroup>\n\t\t\t\t<form\n\t\t\t\t\tclassName=\"flex gap-2\"\n\t\t\t\t\tonSubmit={(event) => {\n\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\tonSend?.(String(new FormData(event.currentTarget).get(\"email\") ?? \"\"))\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Input name=\"email\" type=\"email\" placeholder=\"name@company.com\" aria-label=\"Email address\" />\n\t\t\t\t\t<Button type=\"submit\">Send</Button>\n\t\t\t\t</form>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
|
|
19210
19447
|
},
|
|
19211
19448
|
{
|
|
19212
19449
|
"name": "InvoiceHistoryBlock",
|
|
@@ -19270,8 +19507,8 @@
|
|
|
19270
19507
|
"Every icon-only Button carries aria-label, and play/pause's label follows its state.",
|
|
19271
19508
|
"The Slider carries aria-label=\"Position\". Give it aria-valuetext in your player (\"1:24 of 3:40\") so the position is read as time, not a percentage."
|
|
19272
19509
|
],
|
|
19273
|
-
"imports": "import { useState } from \"react\"\nimport { Button, Card, CardContent, cn, Entity, Icon, Slider } from \"@sableui/react\"",
|
|
19274
|
-
"code": "const track = { title: \"Low tide, high noon\", artist: \"Juno Avery\", position: 38, elapsed: \"1:24\", remaining: \"-2:16\" }\n\n
|
|
19510
|
+
"imports": "import { useState } from \"react\"\nimport { Button, Card, CardContent, cn, Entity, GradientArt, Icon, Slider } from \"@sableui/react\"",
|
|
19511
|
+
"code": "const track = { title: \"Low tide, high noon\", artist: \"Juno Avery\", position: 38, elapsed: \"1:24\", remaining: \"-2:16\" }\n\n\nexport interface MediaPlayerBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Sheet or muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the new playing state. */\n\tonPlayingChange?: (playing: boolean) => void\n\tonPrevious?: () => void\n\tonNext?: () => void\n}\n\nexport function MediaPlayerBlock({ className, framed = true, onPlayingChange, onPrevious, onNext }: MediaPlayerBlockProps) {\n\tconst [playing, setPlaying] = useState(true)\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs\", className)}>\n\t\t\t<CardContent className=\"flex flex-col gap-4\">\n\t\t\t\t{/* Entity's own row padding is for lists; inside a card's content the\n\t\t\t\t card already pads it. */}\n\t\t\t\t<Entity\n\t\t\t\t\tclassName=\"p-0\"\n\t\t\t\t\t// Placeholder cover painted from the theme. Replace with the cover image.\n\t\t\t\t\tleading={<GradientArt aria-hidden seed={track.title} className=\"size-14 rounded-md\" />}\n\t\t\t\t\ttitle={track.title}\n\t\t\t\t\tsubtitle={track.artist}\n\t\t\t\t\taction={\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Like\">\n\t\t\t\t\t\t\t<Icon name=\"heart\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t\t<Slider defaultValue={track.position} aria-label=\"Position\" />\n\t\t\t\t\t<div className=\"flex justify-between text-mono-caption text-muted-foreground\">\n\t\t\t\t\t\t<span>{track.elapsed}</span>\n\t\t\t\t\t\t<span>{track.remaining}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex items-center justify-center gap-3\">\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label=\"Previous\" onClick={onPrevious}>\n\t\t\t\t\t\t<Icon name=\"previous\" variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\taria-label={playing ? \"Pause\" : \"Play\"}\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tsetPlaying(!playing)\n\t\t\t\t\t\t\tonPlayingChange?.(!playing)\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon name={playing ? \"pause\" : \"play\"} variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label=\"Next\" onClick={onNext}>\n\t\t\t\t\t\t<Icon name=\"next\" variant=\"solid\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
|
|
19275
19512
|
},
|
|
19276
19513
|
{
|
|
19277
19514
|
"name": "NewsletterBlock",
|
|
@@ -19328,7 +19565,7 @@
|
|
|
19328
19565
|
},
|
|
19329
19566
|
"notes": [
|
|
19330
19567
|
"Rows are Card + CardList + Entity, inset from the card's padding at any density.",
|
|
19331
|
-
"Unread is a quiet info dot
|
|
19568
|
+
"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.",
|
|
19332
19569
|
"framed={false} drops the surface for a Popover, which already is one."
|
|
19333
19570
|
],
|
|
19334
19571
|
"a11y": [
|
|
@@ -19336,7 +19573,7 @@
|
|
|
19336
19573
|
"Each Avatar's alt is the actor's name."
|
|
19337
19574
|
],
|
|
19338
19575
|
"imports": "import { Avatar, Card, CardList, cn, Entity, StatusDot } from \"@sableui/react\"",
|
|
19339
|
-
"code": "const notifications = [\n\t{ initials: \"MO\", name: \"Mara Okafor\", what: \"collected your edition\", when: \"2m\", unread: true },\n\t{ initials: \"SR\", name: \"Sam Rivera\", what: \"left a comment on Dusk\", when: \"1h\", unread: false },\n]\n\nexport interface NotificationListBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Popover or Sheet that already frames it. */\n\tframed?: boolean\n}\n\nexport function NotificationListBlock({ className, framed = true }: NotificationListBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs overflow-hidden\", className)}>\n\t\t\t<CardList>\n\t\t\t\t{notifications.map((n) => (\n\t\t\t\t\t<Entity\n\t\t\t\t\t\tkey={n.name}\n\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback={n.initials} alt={n.name} />}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t{n.name}\n\t\t\t\t\t\t\t\t{n.unread ? <span className=\"sr-only\"> (unread)</span> : null}\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsubtitle={n.what}\n\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t<span className=\"flex items-center gap-1.5 text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\t{n.
|
|
19576
|
+
"code": "const notifications = [\n\t{ initials: \"MO\", name: \"Mara Okafor\", what: \"collected your edition\", when: \"2m\", unread: true },\n\t{ initials: \"SR\", name: \"Sam Rivera\", what: \"left a comment on Dusk\", when: \"1h\", unread: false },\n]\n\nexport interface NotificationListBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a Popover or Sheet that already frames it. */\n\tframed?: boolean\n}\n\nexport function NotificationListBlock({ className, framed = true }: NotificationListBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs overflow-hidden\", className)}>\n\t\t\t<CardList>\n\t\t\t\t{notifications.map((n) => (\n\t\t\t\t\t<Entity\n\t\t\t\t\t\tkey={n.name}\n\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={n.initials} alt={n.name} />}\n\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t{n.name}\n\t\t\t\t\t\t\t\t{n.unread ? <span className=\"sr-only\"> (unread)</span> : null}\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsubtitle={n.what}\n\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t<span className=\"flex items-center gap-1.5 text-caption text-muted-foreground\">\n\t\t\t\t\t\t\t\t{n.unread ? <StatusDot tone=\"info\" /> : null}\n\t\t\t\t\t\t\t\t{n.when}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</CardList>\n\t\t</Card>\n\t)\n}"
|
|
19340
19577
|
},
|
|
19341
19578
|
{
|
|
19342
19579
|
"name": "NotificationSettingsBlock",
|
|
@@ -19444,8 +19681,8 @@
|
|
|
19444
19681
|
"The placeholder art is aria-hidden. A real product image needs alt text naming the product.",
|
|
19445
19682
|
"The card is not a link: it holds its own button. To make the whole card navigate, move the action to the detail page and render the Card as an <a>."
|
|
19446
19683
|
],
|
|
19447
|
-
"imports": "import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn } from \"@sableui/react\"",
|
|
19448
|
-
"code": "const product = { title: \"Untitled (gold rush)\", maker: \"Mara Okafor\", edition: \"12 of 50\", price: \"0.42 SOL\" }\n\n
|
|
19684
|
+
"imports": "import { Badge, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, cn, GradientArt } from \"@sableui/react\"",
|
|
19685
|
+
"code": "const product = { title: \"Untitled (gold rush)\", maker: \"Mara Okafor\", edition: \"12 of 50\", price: \"0.42 SOL\" }\n\n\nexport interface ProductCardBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** The buy action. */\n\tonCollect?: () => void\n}\n\nexport function ProductCardBlock({ className, framed = true, onCollect }: ProductCardBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs overflow-hidden\", className)}>\n\t\t\t{/* Placeholder art painted from the theme, so it follows every look. Replace with the product image. */}\n\t\t\t<GradientArt aria-hidden seed={product.title} className=\"aspect-[4/3]\" />\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle>{product.title}</CardTitle>\n\t\t\t\t<CardDescription>{product.maker}</CardDescription>\n\t\t\t\t<CardAction>\n\t\t\t\t\t<Badge tone=\"edition\">{product.edition}</Badge>\n\t\t\t\t</CardAction>\n\t\t\t</CardHeader>\n\t\t\t<CardFooter className=\"justify-between pt-4\">\n\t\t\t\t<span className=\"text-mono-lg\">{product.price}</span>\n\t\t\t\t<Button onClick={onCollect}>Collect</Button>\n\t\t\t</CardFooter>\n\t\t</Card>\n\t)\n}"
|
|
19449
19686
|
},
|
|
19450
19687
|
{
|
|
19451
19688
|
"name": "ProfileCardBlock",
|
|
@@ -19467,8 +19704,8 @@
|
|
|
19467
19704
|
"The Share button carries aria-label; Follow's visible text changes with its state.",
|
|
19468
19705
|
"The banner is decorative (aria-hidden); the Avatar's alt is the person's name."
|
|
19469
19706
|
],
|
|
19470
|
-
"imports": "import { useState } from \"react\"\nimport { Avatar, Button, Card, CardContent, CardDescription, CardTitle, cn, Icon } from \"@sableui/react\"",
|
|
19471
|
-
"code": "const profile = {\n\tname: \"Juno Avery\",\n\tinitials: \"JA\",\n\thandle: \"@juno · Sound and light\",\n\tbio: \"Field recordings, slowed down. New edition every full moon.\",\n\tstats: [\n\t\t{ value: \"128\", label: \"posts\" },\n\t\t{ value: \"4.2k\", label: \"followers\" },\n\t],\n}\n\n
|
|
19707
|
+
"imports": "import { useState } from \"react\"\nimport { Avatar, Button, Card, CardContent, CardDescription, CardTitle, cn, GradientArt, Icon } from \"@sableui/react\"",
|
|
19708
|
+
"code": "const profile = {\n\tname: \"Juno Avery\",\n\tinitials: \"JA\",\n\thandle: \"@juno · Sound and light\",\n\tbio: \"Field recordings, slowed down. New edition every full moon.\",\n\tstats: [\n\t\t{ value: \"128\", label: \"posts\" },\n\t\t{ value: \"4.2k\", label: \"followers\" },\n\t],\n}\n\n\nexport interface ProfileCardBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the new follow state. */\n\tonFollowChange?: (following: boolean) => void\n\tonShare?: () => void\n}\n\nexport function ProfileCardBlock({ className, framed = true, onFollowChange, onShare }: ProfileCardBlockProps) {\n\tconst [following, setFollowing] = useState(false)\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-xs overflow-hidden\", className)}>\n\t\t\t{/* Placeholder banner painted from the theme, so it follows every look. Replace with the person's cover image. */}\n\t\t\t<GradientArt aria-hidden seed={profile.handle} className=\"h-20\" />\n\t\t\t{/* pt-0: the avatar rises into the banner above, so the content\n\t\t\t starts at the banner's edge rather than a full pad below it. */}\n\t\t\t<CardContent className=\"flex flex-col gap-3 pt-0\">\n\t\t\t\t<div className=\"flex items-start justify-between\">\n\t\t\t\t\t<Avatar variant=\"art\" size=\"lg\" fallback={profile.initials} alt={profile.name} className=\"-mt-6 ring-4 ring-card\" />\n\t\t\t\t\t<div className=\"flex gap-1.5 pt-3\">\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Share profile\" onClick={onShare}>\n\t\t\t\t\t\t\t<Icon name=\"share\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tvariant={following ? \"secondary\" : \"primary\"}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tsetFollowing(!following)\n\t\t\t\t\t\t\t\tonFollowChange?.(!following)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{following ? \"Following\" : \"Follow\"}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex flex-col gap-0.5\">\n\t\t\t\t\t<CardTitle>{profile.name}</CardTitle>\n\t\t\t\t\t<CardDescription>{profile.handle}</CardDescription>\n\t\t\t\t</div>\n\t\t\t\t<p className=\"text-body-sm\">{profile.bio}</p>\n\t\t\t\t<div className=\"flex gap-4 text-body-sm\">\n\t\t\t\t\t{profile.stats.map((s) => (\n\t\t\t\t\t\t<span key={s.label}>\n\t\t\t\t\t\t\t<span className=\"text-label-lg\">{s.value}</span> <span className=\"text-muted-foreground\">{s.label}</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}"
|
|
19472
19709
|
},
|
|
19473
19710
|
{
|
|
19474
19711
|
"name": "ProfileFormBlock",
|
|
@@ -19491,7 +19728,7 @@
|
|
|
19491
19728
|
"The photo actions are type=\"button\", so neither one submits the form."
|
|
19492
19729
|
],
|
|
19493
19730
|
"imports": "import { Avatar, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, Field, Form, Grid, Input, Row, Select, SelectContent, SelectItem, SelectTrigger, Stack, Textarea } from \"@sableui/react\"",
|
|
19494
|
-
"code": "const profile = {\n\tname: \"Ada Lovelace\",\n\tinitials: \"AL\",\n\thandle: \"ada\",\n\tbio: \"Generative artist working in plotted ink and code. New editions of 25 to 100 every month.\",\n\ttimezone: \"europe-london\",\n}\n\nconst timezones = [\n\t{ value: \"europe-london\", label: \"London (GMT+1)\" },\n\t{ value: \"europe-paris\", label: \"Paris (GMT+2)\" },\n\t{ value: \"america-new-york\", label: \"New York (GMT−4)\" },\n\t{ value: \"america-los-angeles\", label: \"Los Angeles (GMT−7)\" },\n\t{ value: \"asia-tokyo\", label: \"Tokyo (GMT+9)\" },\n]\n\nexport interface ProfileFormBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the form's fields on Save profile. */\n\tonSave?: (fields: FormData) => void\n}\n\nexport function ProfileFormBlock({ className, framed = true, onSave }: ProfileFormBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle render={<h2 />}>Profile</CardTitle>\n\t\t\t\t<CardDescription>How collectors see you.</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t{/* The one explicit save: profile text is drafted, then committed. */}\n\t\t\t<Form\n\t\t\t\tclassName=\"gap-0\"\n\t\t\t\tonSubmit={(event) => {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\tonSave?.(new FormData(event.currentTarget))\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t<Avatar size=\"lg\" fallback={profile.initials} alt={profile.name} />\n\t\t\t\t\t\t\t<Stack gap={0.5} align=\"start\">\n\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\tUpload photo\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">Square PNG or JPG, at least 400 px.</p>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t<Field label=\"Display name\" name=\"displayName\" required>\n\t\t\t\t\t\t\t\t<Input defaultValue={profile.name} autoComplete=\"name\" />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t<Field label=\"Handle\" name=\"handle\" description={`atelier.com/@${profile.handle}`} required>\n\t\t\t\t\t\t\t\t<Input defaultValue={profile.handle} autoComplete=\"username\" spellCheck={false} />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t<Field label=\"Bio\" name=\"bio\" description=\"Shown on your profile and edition pages.\">\n\t\t\t\t\t\t\t<Textarea rows={3} defaultValue={profile.bio} />\n\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t<Field label=\"Timezone\" name=\"timezone\" description=\"Release times use this zone.\">\n\t\t\t\t\t\t\t\t<Select defaultValue={profile.timezone} items={timezones}>\n\t\t\t\t\t\t\t\t\t<SelectTrigger />\n\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t{timezones.map((zone) => (\n\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={zone.value} value={zone.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t{zone.label}\n\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</Grid>\n\t\t\t\t\t</Stack>\n\t\t\t\t</CardContent>\n\t\t\t\t<CardFooter className=\"justify-end\">\n\t\t\t\t\t<Button type=\"submit\">Save profile</Button>\n\t\t\t\t</CardFooter>\n\t\t\t</Form>\n\t\t</Card>\n\t)\n}"
|
|
19731
|
+
"code": "const profile = {\n\tname: \"Ada Lovelace\",\n\tinitials: \"AL\",\n\thandle: \"ada\",\n\tbio: \"Generative artist working in plotted ink and code. New editions of 25 to 100 every month.\",\n\ttimezone: \"europe-london\",\n}\n\nconst timezones = [\n\t{ value: \"europe-london\", label: \"London (GMT+1)\" },\n\t{ value: \"europe-paris\", label: \"Paris (GMT+2)\" },\n\t{ value: \"america-new-york\", label: \"New York (GMT−4)\" },\n\t{ value: \"america-los-angeles\", label: \"Los Angeles (GMT−7)\" },\n\t{ value: \"asia-tokyo\", label: \"Tokyo (GMT+9)\" },\n]\n\nexport interface ProfileFormBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\t/** Called with the form's fields on Save profile. */\n\tonSave?: (fields: FormData) => void\n}\n\nexport function ProfileFormBlock({ className, framed = true, onSave }: ProfileFormBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle render={<h2 />}>Profile</CardTitle>\n\t\t\t\t<CardDescription>How collectors see you.</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t{/* The one explicit save: profile text is drafted, then committed. */}\n\t\t\t<Form\n\t\t\t\tclassName=\"gap-0\"\n\t\t\t\tonSubmit={(event) => {\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\tonSave?.(new FormData(event.currentTarget))\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CardContent>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<Row gap={2}>\n\t\t\t\t\t\t\t<Avatar variant=\"art\" size=\"lg\" fallback={profile.initials} alt={profile.name} />\n\t\t\t\t\t\t\t<Stack gap={0.5} align=\"start\">\n\t\t\t\t\t\t\t\t<Row gap={1}>\n\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\tUpload photo\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t<Button type=\"button\" variant=\"ghost\" size=\"sm\">\n\t\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t\t\t<p className=\"text-caption text-muted-foreground\">Square PNG or JPG, at least 400 px.</p>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t<Field label=\"Display name\" name=\"displayName\" required>\n\t\t\t\t\t\t\t\t<Input defaultValue={profile.name} autoComplete=\"name\" />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t<Field label=\"Handle\" name=\"handle\" description={`atelier.com/@${profile.handle}`} required>\n\t\t\t\t\t\t\t\t<Input defaultValue={profile.handle} autoComplete=\"username\" spellCheck={false} />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</Grid>\n\t\t\t\t\t\t<Field label=\"Bio\" name=\"bio\" description=\"Shown on your profile and edition pages.\">\n\t\t\t\t\t\t\t<Textarea rows={3} defaultValue={profile.bio} />\n\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t<Grid cols={{ base: 1, sm: 2 }} gap={3}>\n\t\t\t\t\t\t\t<Field label=\"Timezone\" name=\"timezone\" description=\"Release times use this zone.\">\n\t\t\t\t\t\t\t\t<Select defaultValue={profile.timezone} items={timezones}>\n\t\t\t\t\t\t\t\t\t<SelectTrigger />\n\t\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t\t{timezones.map((zone) => (\n\t\t\t\t\t\t\t\t\t\t\t<SelectItem key={zone.value} value={zone.value}>\n\t\t\t\t\t\t\t\t\t\t\t\t{zone.label}\n\t\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</Grid>\n\t\t\t\t\t</Stack>\n\t\t\t\t</CardContent>\n\t\t\t\t<CardFooter actions className=\"justify-end\">\n\t\t\t\t\t<Button type=\"submit\">Save profile</Button>\n\t\t\t\t</CardFooter>\n\t\t\t</Form>\n\t\t</Card>\n\t)\n}"
|
|
19495
19732
|
},
|
|
19496
19733
|
{
|
|
19497
19734
|
"name": "RevenueBreakdownBlock",
|
|
@@ -19664,7 +19901,7 @@
|
|
|
19664
19901
|
"Roles are text in a Badge; tone only distinguishes the owner."
|
|
19665
19902
|
],
|
|
19666
19903
|
"imports": "import { Avatar, Badge, Button, Card, CardAction, CardDescription, CardHeader, CardList, CardTitle, cn, Entity, Icon, MenuItem, MenuSeparator, OverflowMenu } from \"@sableui/react\"",
|
|
19667
|
-
"code": "const members = [\n\t{ id: \"u1\", name: \"Ada Lovelace\", initials: \"AL\", email: \"ada@studio.com\", role: \"Owner\" },\n\t{ id: \"u2\", name: \"Mara Okafor\", initials: \"MO\", email: \"mara@studio.com\", role: \"Admin\" },\n\t{ id: \"u3\", name: \"Sam Rivera\", initials: \"SR\", email: \"sam@studio.com\", role: \"Member\" },\n\t{ id: \"u4\", name: \"jun@studio.com\", initials: \"J\", email: \"Invited 2 days ago\", role: \"Pending\" },\n]\n\nexport interface TeamMembersBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\tonInvite?: () => void\n\t/** Called with the member's id and the action chosen from their menu. */\n\tonMemberAction?: (id: string, action: \"make-admin\" | \"make-member\" | \"resend\" | \"remove\") => void\n}\n\nexport function TeamMembersBlock({ className, framed = true, onInvite, onMemberAction }: TeamMembersBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle render={<h2 />}>Members</CardTitle>\n\t\t\t\t<CardDescription>{members.length} people in this workspace.</CardDescription>\n\t\t\t\t<CardAction>\n\t\t\t\t\t<Button size=\"sm\" iconStart={<Icon name=\"plus\" />} onClick={onInvite}>\n\t\t\t\t\t\tInvite\n\t\t\t\t\t</Button>\n\t\t\t\t</CardAction>\n\t\t\t</CardHeader>\n\t\t\t<CardList>\n\t\t\t\t{members.map((m) => (\n\t\t\t\t\t<Entity\n\t\t\t\t\t\tkey={m.id}\n\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback={m.initials} alt={m.name} />}\n\t\t\t\t\t\ttitle={m.name}\n\t\t\t\t\t\tsubtitle={m.email}\n\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t<Badge appearance={m.role === \"Pending\" ? \"outline\" : \"plain\"} tone={m.role === \"Owner\" ? \"edition\" : \"neutral\"} size=\"sm\">\n\t\t\t\t\t\t\t\t\t{m.role}\n\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t{/* The owner is not managed from here; a placeholder keeps the column aligned. */}\n\t\t\t\t\t\t\t\t{m.role === \"Owner\" ? (\n\t\t\t\t\t\t\t\t\t<span aria-hidden className=\"size-(--control-sm)\" />\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<OverflowMenu label={`Manage ${m.name}`} size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t\t{m.role === \"Pending\" ? (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem onClick={() => onMemberAction?.(m.id, \"resend\")}>Resend invite</MenuItem>\n\t\t\t\t\t\t\t\t\t\t) : m.role === \"Admin\" ? (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem onClick={() => onMemberAction?.(m.id, \"make-member\")}>Make member</MenuItem>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem onClick={() => onMemberAction?.(m.id, \"make-admin\")}>Make admin</MenuItem>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t\t<MenuItem variant=\"destructive\" onClick={() => onMemberAction?.(m.id, \"remove\")}>\n\t\t\t\t\t\t\t\t\t\t\t{m.role === \"Pending\" ? \"Cancel invite\" : \"Remove from workspace\"}\n\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</CardList>\n\t\t</Card>\n\t)\n}"
|
|
19904
|
+
"code": "const members = [\n\t{ id: \"u1\", name: \"Ada Lovelace\", initials: \"AL\", email: \"ada@studio.com\", role: \"Owner\" },\n\t{ id: \"u2\", name: \"Mara Okafor\", initials: \"MO\", email: \"mara@studio.com\", role: \"Admin\" },\n\t{ id: \"u3\", name: \"Sam Rivera\", initials: \"SR\", email: \"sam@studio.com\", role: \"Member\" },\n\t{ id: \"u4\", name: \"jun@studio.com\", initials: \"J\", email: \"Invited 2 days ago\", role: \"Pending\" },\n]\n\nexport interface TeamMembersBlockProps {\n\tclassName?: string\n\t/** false drops the card's surface and border — for a muted section that already frames it. */\n\tframed?: boolean\n\tonInvite?: () => void\n\t/** Called with the member's id and the action chosen from their menu. */\n\tonMemberAction?: (id: string, action: \"make-admin\" | \"make-member\" | \"resend\" | \"remove\") => void\n}\n\nexport function TeamMembersBlock({ className, framed = true, onInvite, onMemberAction }: TeamMembersBlockProps) {\n\treturn (\n\t\t<Card variant={framed ? \"default\" : \"flat\"} className={cn(\"w-full max-w-2xl\", className)}>\n\t\t\t<CardHeader>\n\t\t\t\t<CardTitle render={<h2 />}>Members</CardTitle>\n\t\t\t\t<CardDescription>{members.length} people in this workspace.</CardDescription>\n\t\t\t\t<CardAction>\n\t\t\t\t\t<Button size=\"sm\" iconStart={<Icon name=\"plus\" />} onClick={onInvite}>\n\t\t\t\t\t\tInvite\n\t\t\t\t\t</Button>\n\t\t\t\t</CardAction>\n\t\t\t</CardHeader>\n\t\t\t<CardList>\n\t\t\t\t{members.map((m) => (\n\t\t\t\t\t<Entity\n\t\t\t\t\t\tkey={m.id}\n\t\t\t\t\t\tleading={<Avatar variant=\"art\" size=\"sm\" fallback={m.initials} alt={m.name} />}\n\t\t\t\t\t\ttitle={m.name}\n\t\t\t\t\t\tsubtitle={m.email}\n\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t<Badge appearance={m.role === \"Pending\" ? \"outline\" : \"plain\"} tone={m.role === \"Owner\" ? \"edition\" : \"neutral\"} size=\"sm\">\n\t\t\t\t\t\t\t\t\t{m.role}\n\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t{/* The owner is not managed from here; a placeholder keeps the column aligned. */}\n\t\t\t\t\t\t\t\t{m.role === \"Owner\" ? (\n\t\t\t\t\t\t\t\t\t<span aria-hidden className=\"size-(--control-sm)\" />\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<OverflowMenu label={`Manage ${m.name}`} size=\"icon-sm\">\n\t\t\t\t\t\t\t\t\t\t{m.role === \"Pending\" ? (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem onClick={() => onMemberAction?.(m.id, \"resend\")}>Resend invite</MenuItem>\n\t\t\t\t\t\t\t\t\t\t) : m.role === \"Admin\" ? (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem onClick={() => onMemberAction?.(m.id, \"make-member\")}>Make member</MenuItem>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem onClick={() => onMemberAction?.(m.id, \"make-admin\")}>Make admin</MenuItem>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t<MenuSeparator />\n\t\t\t\t\t\t\t\t\t\t<MenuItem variant=\"destructive\" onClick={() => onMemberAction?.(m.id, \"remove\")}>\n\t\t\t\t\t\t\t\t\t\t\t{m.role === \"Pending\" ? \"Cancel invite\" : \"Remove from workspace\"}\n\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t</OverflowMenu>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</CardList>\n\t\t</Card>\n\t)\n}"
|
|
19668
19905
|
},
|
|
19669
19906
|
{
|
|
19670
19907
|
"name": "TestimonialBlock",
|
|
@@ -19683,7 +19920,7 @@
|
|
|
19683
19920
|
"The section is labelled \"Customer story\" so it is reachable as a landmark."
|
|
19684
19921
|
],
|
|
19685
19922
|
"imports": "import { Avatar, Card, CardContent, cn, Region, Separator, Stack } from \"@sableui/react\"",
|
|
19686
|
-
"code": "const story = {\n\tquote: \"We used to chase approvals across four tools. Now every request has an owner, a deadline, and a record, and our auditors stopped asking for screenshots.\",\n\tname: \"Maya Okafor\",\n\tinitials: \"MO\",\n\trole: \"Head of operations, Fielddesk\",\n}\n\nconst outcomes = [\n\t{ value: \"3.2×\", label: \"faster approvals in the first quarter\" },\n\t{ value: \"68%\", label: \"fewer follow-ups on handoffs\" },\n\t{ value: \"1 day\", label: \"to prepare a quarterly audit\" },\n]\n\nexport interface TestimonialBlockProps {\n\tclassName?: string\n}\n\nexport function TestimonialBlock({ className }: TestimonialBlockProps) {\n\treturn (\n\t\t<Region as=\"section\" aria-label=\"Customer story\" max=\"wide\" className={cn(\"w-full py-16\", className)}>\n\t\t\t<Card render={<figure />}>\n\t\t\t\t<div className=\"grid lg:grid-cols-[1.5fr_1fr]\">\n\t\t\t\t\t<CardContent className=\"flex flex-col justify-between gap-10 sm:p-10\">\n\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Customer story</p>\n\t\t\t\t\t\t\t<blockquote className=\"text-balance text-heading-3 sm:text-heading-2\">“{story.quote}”</blockquote>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<figcaption className=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t<Avatar size=\"lg\" fallback={story.initials} alt={story.name} />\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<p className=\"text-title-sm\">{story.name}</p>\n\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{story.role}</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</figcaption>\n\t\t\t\t\t</CardContent>\n\t\t\t\t\t<div className=\"border-border max-lg:border-t lg:border-s\">\n\t\t\t\t\t\t<CardContent className=\"flex h-full flex-col justify-center gap-6 sm:p-10\">\n\t\t\t\t\t\t\t{outcomes.map((outcome, index) => (\n\t\t\t\t\t\t\t\t<Stack key={outcome.label} gap={3}>\n\t\t\t\t\t\t\t\t\t{index > 0 && <Separator />}\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-1 sm:text-display-lg\">{outcome.value}</p>\n\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-sm text-muted-foreground\">{outcome.label}</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</Card>\n\t\t</Region>\n\t)\n}"
|
|
19923
|
+
"code": "const story = {\n\tquote: \"We used to chase approvals across four tools. Now every request has an owner, a deadline, and a record, and our auditors stopped asking for screenshots.\",\n\tname: \"Maya Okafor\",\n\tinitials: \"MO\",\n\trole: \"Head of operations, Fielddesk\",\n}\n\nconst outcomes = [\n\t{ value: \"3.2×\", label: \"faster approvals in the first quarter\" },\n\t{ value: \"68%\", label: \"fewer follow-ups on handoffs\" },\n\t{ value: \"1 day\", label: \"to prepare a quarterly audit\" },\n]\n\nexport interface TestimonialBlockProps {\n\tclassName?: string\n}\n\nexport function TestimonialBlock({ className }: TestimonialBlockProps) {\n\treturn (\n\t\t<Region as=\"section\" aria-label=\"Customer story\" max=\"wide\" className={cn(\"w-full py-16\", className)}>\n\t\t\t<Card render={<figure />}>\n\t\t\t\t<div className=\"grid lg:grid-cols-[1.5fr_1fr]\">\n\t\t\t\t\t<CardContent className=\"flex flex-col justify-between gap-10 sm:p-10\">\n\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t<p className=\"text-overline text-muted-foreground\">Customer story</p>\n\t\t\t\t\t\t\t<blockquote className=\"text-balance text-heading-3 sm:text-heading-2\">“{story.quote}”</blockquote>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t<figcaption className=\"flex items-center gap-3\">\n\t\t\t\t\t\t\t<Avatar variant=\"art\" size=\"lg\" fallback={story.initials} alt={story.name} />\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<p className=\"text-title-sm\">{story.name}</p>\n\t\t\t\t\t\t\t\t<p className=\"text-body-sm text-muted-foreground\">{story.role}</p>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</figcaption>\n\t\t\t\t\t</CardContent>\n\t\t\t\t\t<div className=\"border-border max-lg:border-t lg:border-s\">\n\t\t\t\t\t\t<CardContent className=\"flex h-full flex-col justify-center gap-6 sm:p-10\">\n\t\t\t\t\t\t\t{outcomes.map((outcome, index) => (\n\t\t\t\t\t\t\t\t<Stack key={outcome.label} gap={3}>\n\t\t\t\t\t\t\t\t\t{index > 0 && <Separator />}\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-heading-1 sm:text-display-lg\">{outcome.value}</p>\n\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-sm text-muted-foreground\">{outcome.label}</p>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</Card>\n\t\t</Region>\n\t)\n}"
|
|
19687
19924
|
},
|
|
19688
19925
|
{
|
|
19689
19926
|
"name": "TrafficSourcesBlock",
|
|
@@ -20778,6 +21015,12 @@
|
|
|
20778
21015
|
"why": "A faded ring loses the contrast the theme tested.",
|
|
20779
21016
|
"enforcedBy": "sable-lint: faded-focus-ring"
|
|
20780
21017
|
},
|
|
21018
|
+
{
|
|
21019
|
+
"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.",
|
|
21020
|
+
"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.",
|
|
21021
|
+
"why": "The ratio alone passes labels that read muddy. A deliberate brand pair is allowed with a recorded reason (lint disable or preset exceptions).",
|
|
21022
|
+
"enforcedBy": "sable-lint: label-contrast"
|
|
21023
|
+
},
|
|
20781
21024
|
{
|
|
20782
21025
|
"never": "Blocking paste, or type=\"number\" for codes and card numbers.",
|
|
20783
21026
|
"instead": "type=\"text\" inputMode=\"numeric\" with the right autocomplete.",
|
|
@@ -21672,10 +21915,12 @@
|
|
|
21672
21915
|
"fallback",
|
|
21673
21916
|
"fallbackDelay",
|
|
21674
21917
|
"render",
|
|
21918
|
+
"seed",
|
|
21675
21919
|
"size",
|
|
21676
21920
|
"src",
|
|
21677
21921
|
"status",
|
|
21678
|
-
"style"
|
|
21922
|
+
"style",
|
|
21923
|
+
"variant"
|
|
21679
21924
|
],
|
|
21680
21925
|
"AvatarFallback": [
|
|
21681
21926
|
"className",
|
|
@@ -21888,6 +22133,9 @@
|
|
|
21888
22133
|
"CardContent": [
|
|
21889
22134
|
"flush"
|
|
21890
22135
|
],
|
|
22136
|
+
"CardFooter": [
|
|
22137
|
+
"actions"
|
|
22138
|
+
],
|
|
21891
22139
|
"CardTitle": [
|
|
21892
22140
|
"render"
|
|
21893
22141
|
],
|
|
@@ -22522,6 +22770,11 @@
|
|
|
22522
22770
|
"highlightClassName",
|
|
22523
22771
|
"orientation"
|
|
22524
22772
|
],
|
|
22773
|
+
"GradientArt": [
|
|
22774
|
+
"seed",
|
|
22775
|
+
"tones",
|
|
22776
|
+
"variant"
|
|
22777
|
+
],
|
|
22525
22778
|
"Grid": [
|
|
22526
22779
|
"cols",
|
|
22527
22780
|
"gap",
|