@sableui/react 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
- package/dist/{chunk-J23ELQQZ.js → chunk-AOWBYEQ6.js} +4177 -1917
- package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
- package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
- package/dist/foundations/index.js +1 -1
- package/dist/index.d.ts +155 -101
- package/dist/index.js +582 -2778
- package/dist/ios/index.d.ts +6 -1
- package/dist/ios/index.js +4 -4
- package/dist/ios/preview.js +1 -1
- package/dist/ios/styles.css +20 -6
- package/dist/patterns/index.js +1683 -469
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +1 -1
- package/dist/styles.css +238 -40
- package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
- package/dist/themes/atelier/atelier.css +3 -0
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/base/base.css +11 -4
- package/dist/themes/base/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +3 -0
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +10 -5
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +16 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +4 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +7 -2
- package/llms/components/bubble.md +5 -5
- package/llms/components/checkbox-card.md +1 -1
- package/llms/components/context-card.md +2 -2
- package/llms/components/dialog.md +1 -1
- package/llms/components/empty-state.md +4 -4
- package/llms/components/entity.md +1 -1
- package/llms/components/fieldset.md +1 -1
- package/llms/components/kbd.md +18 -6
- package/llms/components/pill.md +1 -1
- package/llms/components/side-nav.md +1 -1
- package/llms/components/skeleton.md +1 -1
- package/llms/components/theme-selector.md +1 -1
- package/llms/foundations/shape.md +1 -1
- package/llms/patterns/auth-page.md +132 -42
- package/llms/patterns/brand-landing-page.md +271 -175
- package/llms/patterns/chat-panel.md +120 -39
- package/llms/patterns/dashboard-page.md +277 -65
- package/llms/patterns/feed-page.md +245 -64
- package/llms/patterns/landing-page.md +473 -75
- package/llms/patterns/list-detail-page.md +385 -68
- package/llms/patterns/settings-page.md +279 -67
- package/llms/reference/token-intent.md +4 -2
- package/llms/reference/utilities.md +20 -5
- package/llms-full.txt +2244 -727
- package/llms.txt +14 -15
- package/package.json +1 -1
- package/registry.json +261 -238
- package/targets/ios.json +4 -15
- package/llms/components/description.md +0 -96
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.2.0",
|
|
5
5
|
"description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Base.",
|
|
6
6
|
"engine": {
|
|
7
7
|
"behavior": "@base-ui/react",
|
|
@@ -335,9 +335,15 @@
|
|
|
335
335
|
{
|
|
336
336
|
"name": "input",
|
|
337
337
|
"intent": "The outline of a form control specifically.",
|
|
338
|
-
"whenToUse": "Text inputs, textareas
|
|
338
|
+
"whenToUse": "Text inputs, textareas and selects. Not the edge of a checkbox, radio or switch — that is `control-border`.",
|
|
339
339
|
"insteadOf": "`border`. They are often the same value, but a theme is entitled to make form controls read differently from other chrome, and only this token gives it the hook."
|
|
340
340
|
},
|
|
341
|
+
{
|
|
342
|
+
"name": "control-border",
|
|
343
|
+
"intent": "The resting edge of a control whose outline is the whole control. Held to 3:1 (WCAG 1.4.11) on every surface.",
|
|
344
|
+
"whenToUse": "An unchecked checkbox, an unselected radio, an off switch track, a selection card's check or radio mark — anywhere removing the edge would make the control disappear.",
|
|
345
|
+
"insteadOf": "`input`. A text field is also found by its fill, caret and placeholder, so its outline may stay quiet; a checkbox has nothing else, and at `input`'s strength it measures about 1.4:1."
|
|
346
|
+
},
|
|
341
347
|
{
|
|
342
348
|
"name": "ring",
|
|
343
349
|
"intent": "The focus indicator colour.",
|
|
@@ -3893,7 +3899,7 @@
|
|
|
3893
3899
|
"group": "Chat & AI",
|
|
3894
3900
|
"register": "either",
|
|
3895
3901
|
"path": "components/bubble",
|
|
3896
|
-
"summary": "The surface a message's content sits on: four treatments, an align-aware tail
|
|
3902
|
+
"summary": "The surface a message's content sits on: four treatments, an align-aware tail, a readable measure cap, and an optional row of reaction pills.",
|
|
3897
3903
|
"keywords": [
|
|
3898
3904
|
"chat bubble",
|
|
3899
3905
|
"message bubble",
|
|
@@ -3916,7 +3922,7 @@
|
|
|
3916
3922
|
},
|
|
3917
3923
|
{
|
|
3918
3924
|
"guidance": "do",
|
|
3919
|
-
"description": "Pass the same `align` you gave the enclosing Message, so the
|
|
3925
|
+
"description": "Pass the same `align` you gave the enclosing Message, so the tail lands on the speaker's side."
|
|
3920
3926
|
},
|
|
3921
3927
|
{
|
|
3922
3928
|
"guidance": "do",
|
|
@@ -3947,7 +3953,7 @@
|
|
|
3947
3953
|
{
|
|
3948
3954
|
"name": "Bubble",
|
|
3949
3955
|
"required": true,
|
|
3950
|
-
"description": "The surface itself. Holds the variant, the side, and the
|
|
3956
|
+
"description": "The surface itself. Holds the variant, the side, and the tail."
|
|
3951
3957
|
},
|
|
3952
3958
|
{
|
|
3953
3959
|
"name": "BubbleReactions",
|
|
@@ -3980,13 +3986,13 @@
|
|
|
3980
3986
|
"name": "align",
|
|
3981
3987
|
"type": "\"start\" | \"end\"",
|
|
3982
3988
|
"default": "\"start\"",
|
|
3983
|
-
"description": "Which side of the conversation the bubble is on —
|
|
3989
|
+
"description": "Which side of the conversation the bubble is on — puts the tail on the speaker's side. Match it to the enclosing Message."
|
|
3984
3990
|
},
|
|
3985
3991
|
{
|
|
3986
3992
|
"name": "tail",
|
|
3987
3993
|
"type": "boolean",
|
|
3988
3994
|
"default": "true",
|
|
3989
|
-
"description": "
|
|
3995
|
+
"description": "Draw the tail: a small ear off the speaker-side top corner, which goes square to meet it. The same 5px in every theme's radius language, flat through fully pilled. The bubble keeps the ear's space either way, so turning it off for the continuation bubbles in a grouped run keeps the run aligned."
|
|
3990
3996
|
},
|
|
3991
3997
|
{
|
|
3992
3998
|
"name": "...div props",
|
|
@@ -5367,7 +5373,7 @@
|
|
|
5367
5373
|
{
|
|
5368
5374
|
"name": "icon",
|
|
5369
5375
|
"type": "ReactNode",
|
|
5370
|
-
"description": "Optional
|
|
5376
|
+
"description": "Optional glyph (a solid <Icon>) in the tone colour. Its presence switches to the two-dimensional card RadioCard uses — icon top-left, checkbox top-right, copy below — so the two cards match in one form."
|
|
5371
5377
|
},
|
|
5372
5378
|
{
|
|
5373
5379
|
"name": "tone",
|
|
@@ -6530,8 +6536,8 @@
|
|
|
6530
6536
|
},
|
|
6531
6537
|
{
|
|
6532
6538
|
"label": "With stat row",
|
|
6533
|
-
"code": "<ContextCard><ContextCardTrigger render={<a href='/u/nyx' className='text-title-sm underline-offset-2 hover:underline'>@nyx</a>} /><ContextCardContent><Entity className='[--entity-px:0] [--entity-py:0]' leading={<Avatar fallback='NY' size='lg' />} title='Nyx' subtitle='@nyx' /><
|
|
6534
|
-
"imports": "import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger,
|
|
6539
|
+
"code": "<ContextCard><ContextCardTrigger render={<a href='/u/nyx' className='text-title-sm underline-offset-2 hover:underline'>@nyx</a>} /><ContextCardContent><Entity className='[--entity-px:0] [--entity-py:0]' leading={<Avatar fallback='NY' size='lg' />} title='Nyx' subtitle='@nyx' /><div className='@container mt-4 border-t border-border pt-3'><dl className='sable-list-dl sable-list-dl-2'><dt>Posts</dt><dd>128</dd><dt>Followers</dt><dd>4.2k</dd></dl></div></ContextCardContent></ContextCard>",
|
|
6540
|
+
"imports": "import { Avatar, ContextCard, ContextCardContent, ContextCardTrigger, Entity } from \"@sableui/react\""
|
|
6535
6541
|
}
|
|
6536
6542
|
]
|
|
6537
6543
|
},
|
|
@@ -7004,130 +7010,6 @@
|
|
|
7004
7010
|
}
|
|
7005
7011
|
]
|
|
7006
7012
|
},
|
|
7007
|
-
{
|
|
7008
|
-
"name": "Description",
|
|
7009
|
-
"status": "stable",
|
|
7010
|
-
"group": "Data display",
|
|
7011
|
-
"register": "either",
|
|
7012
|
-
"path": "components/description",
|
|
7013
|
-
"summary": "A labeled key/value list (definition list). Compose with DescriptionTerm + DescriptionDetail, or the ergonomic DescriptionItem shorthand. Aligns terms and details into a grid; cols=\"2\" packs pairs two-wide.",
|
|
7014
|
-
"keywords": [
|
|
7015
|
-
"description list",
|
|
7016
|
-
"metadata",
|
|
7017
|
-
"details",
|
|
7018
|
-
"key value"
|
|
7019
|
-
],
|
|
7020
|
-
"usage": {
|
|
7021
|
-
"description": "Use Description to present label-value details where each term needs a clear corresponding value.",
|
|
7022
|
-
"bestPractices": [
|
|
7023
|
-
{
|
|
7024
|
-
"guidance": "do",
|
|
7025
|
-
"description": "Use it for object metadata, settings summaries, and read-only record details."
|
|
7026
|
-
},
|
|
7027
|
-
{
|
|
7028
|
-
"guidance": "do",
|
|
7029
|
-
"description": "Keep terms brief and values scannable so the list works as reference material."
|
|
7030
|
-
},
|
|
7031
|
-
{
|
|
7032
|
-
"guidance": "dont",
|
|
7033
|
-
"description": "Do not use Description for editable forms; pair Label and form controls instead."
|
|
7034
|
-
},
|
|
7035
|
-
{
|
|
7036
|
-
"guidance": "dont",
|
|
7037
|
-
"description": "Do not use it for a single key/value. A definition list of one is a labelled value — use a Label and the value, and keep the list semantics for actual lists."
|
|
7038
|
-
}
|
|
7039
|
-
],
|
|
7040
|
-
"anatomy": [
|
|
7041
|
-
{
|
|
7042
|
-
"name": "DescriptionItem",
|
|
7043
|
-
"required": true,
|
|
7044
|
-
"description": "One term and detail pair."
|
|
7045
|
-
},
|
|
7046
|
-
{
|
|
7047
|
-
"name": "DescriptionTerm",
|
|
7048
|
-
"required": true,
|
|
7049
|
-
"description": "The label for the value."
|
|
7050
|
-
},
|
|
7051
|
-
{
|
|
7052
|
-
"name": "DescriptionDetail",
|
|
7053
|
-
"required": true,
|
|
7054
|
-
"description": "The value or explanatory detail."
|
|
7055
|
-
}
|
|
7056
|
-
]
|
|
7057
|
-
},
|
|
7058
|
-
"related": {
|
|
7059
|
-
"components": [
|
|
7060
|
-
"Fieldset",
|
|
7061
|
-
"Entity"
|
|
7062
|
-
]
|
|
7063
|
-
},
|
|
7064
|
-
"props": [
|
|
7065
|
-
{
|
|
7066
|
-
"name": "cols",
|
|
7067
|
-
"type": "\"1\" | \"2\"",
|
|
7068
|
-
"default": "\"1\"",
|
|
7069
|
-
"description": "Column density. One term/detail column, or two pairs per row once the list's container is at least @md (28rem) wide; narrower, it stays one column. A container query, not a viewport breakpoint, so a narrow card on a wide screen stays one column. With cols=\"2\" the <dl> is wrapped in an @container div."
|
|
7070
|
-
}
|
|
7071
|
-
],
|
|
7072
|
-
"subcomponents": [
|
|
7073
|
-
{
|
|
7074
|
-
"name": "DescriptionTerm",
|
|
7075
|
-
"type": "<dt> props + className",
|
|
7076
|
-
"description": "The key. Muted label tier; sits in the first grid column."
|
|
7077
|
-
},
|
|
7078
|
-
{
|
|
7079
|
-
"name": "DescriptionDetail",
|
|
7080
|
-
"type": "<dd> props + tone?: \"default\" | \"muted\" | \"success\" | \"warning\" | \"destructive\" + className",
|
|
7081
|
-
"description": "The value. Body tier on the foreground; sits in the second grid column. tone colors the value to encode a status (e.g. Ended → muted, Active → success), replacing inline conditional color classes; defaults to \"default\"."
|
|
7082
|
-
},
|
|
7083
|
-
{
|
|
7084
|
-
"name": "DescriptionItem",
|
|
7085
|
-
"type": "{ term: ReactNode; detail?: ReactNode; tone?: \"default\" | \"muted\" | \"success\" | \"warning\" | \"destructive\"; children? }",
|
|
7086
|
-
"description": "Ergonomic shorthand for one term/detail pair — renders a DescriptionTerm followed by a DescriptionDetail. Pass the value via detail or children; tone (defaults to \"default\") colors the detail value the same way it does on DescriptionDetail."
|
|
7087
|
-
}
|
|
7088
|
-
],
|
|
7089
|
-
"variants": {
|
|
7090
|
-
"cols": [
|
|
7091
|
-
"1",
|
|
7092
|
-
"2"
|
|
7093
|
-
],
|
|
7094
|
-
"tone": [
|
|
7095
|
-
"default",
|
|
7096
|
-
"muted",
|
|
7097
|
-
"success",
|
|
7098
|
-
"warning",
|
|
7099
|
-
"destructive"
|
|
7100
|
-
]
|
|
7101
|
-
},
|
|
7102
|
-
"tokensUsed": [
|
|
7103
|
-
"muted-foreground",
|
|
7104
|
-
"foreground",
|
|
7105
|
-
"success",
|
|
7106
|
-
"tone-warning",
|
|
7107
|
-
"destructive"
|
|
7108
|
-
],
|
|
7109
|
-
"a11y": [
|
|
7110
|
-
"Renders semantic <dl>/<dt>/<dd> elements so assistive tech announces the term/detail relationship.",
|
|
7111
|
-
"tone is a visual emphasis only — keep the value text itself meaningful (e.g. \"Ended\"), never color alone."
|
|
7112
|
-
],
|
|
7113
|
-
"examples": [
|
|
7114
|
-
{
|
|
7115
|
-
"label": "Metadata list",
|
|
7116
|
-
"code": "<Description>\n\t<DescriptionItem term=\"Network\" detail=\"Solana\" />\n\t<DescriptionItem term=\"Supply\" detail=\"100\" />\n\t<DescriptionItem term=\"Royalty\" detail=\"5%\" />\n</Description>",
|
|
7117
|
-
"imports": "import { Description, DescriptionItem } from \"@sableui/react\""
|
|
7118
|
-
},
|
|
7119
|
-
{
|
|
7120
|
-
"label": "Composed term/detail",
|
|
7121
|
-
"code": "<Description>\n\t<DescriptionTerm>Network</DescriptionTerm>\n\t<DescriptionDetail>Solana</DescriptionDetail>\n\t<DescriptionTerm>Contract</DescriptionTerm>\n\t<DescriptionDetail className=\"text-mono-sm\">7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU</DescriptionDetail>\n</Description>",
|
|
7122
|
-
"imports": "import { Description, DescriptionDetail, DescriptionTerm } from \"@sableui/react\""
|
|
7123
|
-
},
|
|
7124
|
-
{
|
|
7125
|
-
"label": "Toned status value",
|
|
7126
|
-
"code": "<Description>\n\t<DescriptionItem term=\"Edition\" detail=\"Midnight Archive\" />\n\t<DescriptionItem term=\"Status\" detail={isEnded ? \"Ended\" : \"Active\"} tone={isEnded ? \"muted\" : \"success\"} />\n</Description>",
|
|
7127
|
-
"imports": "import { Description, DescriptionItem } from \"@sableui/react\""
|
|
7128
|
-
}
|
|
7129
|
-
]
|
|
7130
|
-
},
|
|
7131
7013
|
{
|
|
7132
7014
|
"name": "DestructiveActionModal",
|
|
7133
7015
|
"status": "stable",
|
|
@@ -7392,7 +7274,7 @@
|
|
|
7392
7274
|
{
|
|
7393
7275
|
"name": "DialogOverlay",
|
|
7394
7276
|
"type": "Base UI Backdrop props + size?: \"default\" | \"fullscreen\" + className",
|
|
7395
|
-
"description": "The styleable scrim/backdrop — bg-overlay/
|
|
7277
|
+
"description": "The styleable scrim/backdrop — bg-overlay/(--scrim-alpha) by default (50% light, 72% dark, so it dims a near-black page as visibly as a white one), near-opaque bg-overlay/95 at size=\"fullscreen\". Composed into DialogContent (at the matching size) by default; exported for manual styling."
|
|
7396
7278
|
},
|
|
7397
7279
|
{
|
|
7398
7280
|
"name": "DialogTrigger",
|
|
@@ -7635,7 +7517,7 @@
|
|
|
7635
7517
|
"group": "Status",
|
|
7636
7518
|
"register": "page",
|
|
7637
7519
|
"path": "components/empty-state",
|
|
7638
|
-
"summary": "The centered empty / error block: optional icon
|
|
7520
|
+
"summary": "The centered empty / error block: optional icon or error code, title, description and an action. tone=\"destructive\" makes it an announced error, size=\"lg\" a page-level state, surface=\"card\" a self-contained card.",
|
|
7639
7521
|
"keywords": [
|
|
7640
7522
|
"empty",
|
|
7641
7523
|
"zero state",
|
|
@@ -7718,7 +7600,7 @@
|
|
|
7718
7600
|
{
|
|
7719
7601
|
"name": "icon",
|
|
7720
7602
|
"type": "ReactNode",
|
|
7721
|
-
"description": "Optional
|
|
7603
|
+
"description": "Optional glyph above the title — an `<Icon />` takes the muted ink at 3xl; an image or illustration renders as given."
|
|
7722
7604
|
},
|
|
7723
7605
|
{
|
|
7724
7606
|
"name": "title",
|
|
@@ -7744,7 +7626,7 @@
|
|
|
7744
7626
|
"name": "tone",
|
|
7745
7627
|
"type": "\"neutral\" | \"destructive\"",
|
|
7746
7628
|
"default": "\"neutral\"",
|
|
7747
|
-
"description": "destructive is an error state: a destructive icon
|
|
7629
|
+
"description": "destructive is an error state: a destructive icon (a warning by default) and role=\"alert\" so it is announced."
|
|
7748
7630
|
},
|
|
7749
7631
|
{
|
|
7750
7632
|
"name": "size",
|
|
@@ -7780,7 +7662,7 @@
|
|
|
7780
7662
|
"destructive"
|
|
7781
7663
|
],
|
|
7782
7664
|
"a11y": [
|
|
7783
|
-
"Decorative
|
|
7665
|
+
"Decorative icons should not carry meaning the title doesn't already convey.",
|
|
7784
7666
|
"Pair with an actionable next step so the zero-state is a path forward, not a dead end."
|
|
7785
7667
|
],
|
|
7786
7668
|
"examples": [
|
|
@@ -7857,8 +7739,7 @@
|
|
|
7857
7739
|
"related": {
|
|
7858
7740
|
"components": [
|
|
7859
7741
|
"Avatar",
|
|
7860
|
-
"Badge"
|
|
7861
|
-
"Description"
|
|
7742
|
+
"Badge"
|
|
7862
7743
|
]
|
|
7863
7744
|
},
|
|
7864
7745
|
"props": [
|
|
@@ -8104,8 +7985,7 @@
|
|
|
8104
7985
|
"related": {
|
|
8105
7986
|
"components": [
|
|
8106
7987
|
"Form",
|
|
8107
|
-
"Label"
|
|
8108
|
-
"Description"
|
|
7988
|
+
"Label"
|
|
8109
7989
|
]
|
|
8110
7990
|
},
|
|
8111
7991
|
"props": [
|
|
@@ -8978,7 +8858,7 @@
|
|
|
8978
8858
|
"group": "Text & code",
|
|
8979
8859
|
"register": "either",
|
|
8980
8860
|
"path": "components/kbd",
|
|
8981
|
-
"summary": "A keyboard key indicator. Renders a
|
|
8861
|
+
"summary": "A keyboard key indicator. Renders a raised <kbd> key cap sized from the surrounding text, so one Kbd fits body copy, menus and tooltips; KbdGroup sets one cap per key for a combo. `mod` renders the platform modifier (⌘ or Ctrl); `variant=\"ghost\"` is the bare label for menu rows and tooltips.",
|
|
8982
8862
|
"keywords": [
|
|
8983
8863
|
"keyboard",
|
|
8984
8864
|
"shortcut",
|
|
@@ -8994,12 +8874,20 @@
|
|
|
8994
8874
|
},
|
|
8995
8875
|
{
|
|
8996
8876
|
"guidance": "do",
|
|
8997
|
-
"description": "
|
|
8877
|
+
"description": "Show a combo as one cap per key inside KbdGroup — `<KbdGroup><Kbd mod /><Kbd>K</Kbd></KbdGroup>` — not several keys typed into one cap."
|
|
8878
|
+
},
|
|
8879
|
+
{
|
|
8880
|
+
"guidance": "do",
|
|
8881
|
+
"description": "Use `variant=\"ghost\"` where the shortcut sits on a surface of its own — a menu row's trailing slot, a tooltip — so it reads as a hint, not a second control."
|
|
8998
8882
|
},
|
|
8999
8883
|
{
|
|
9000
8884
|
"guidance": "dont",
|
|
9001
8885
|
"description": "Do not use Kbd as a decorative badge or button replacement."
|
|
9002
8886
|
},
|
|
8887
|
+
{
|
|
8888
|
+
"guidance": "dont",
|
|
8889
|
+
"description": "Do not set a text size on a Kbd. It sizes itself from the text around it."
|
|
8890
|
+
},
|
|
9003
8891
|
{
|
|
9004
8892
|
"guidance": "dont",
|
|
9005
8893
|
"description": "Do not hardcode ⌘ or Ctrl. The modifier differs by platform; use `<Kbd mod />`, which renders ⌘ on macOS/iOS and Ctrl elsewhere, or Windows users read Mac keys."
|
|
@@ -9020,6 +8908,12 @@
|
|
|
9020
8908
|
"default": "false",
|
|
9021
8909
|
"description": "Render the platform's shortcut modifier in place of children: ⌘ on macOS and iOS, Ctrl everywhere else. Server-rendered as Ctrl and corrected on hydration."
|
|
9022
8910
|
},
|
|
8911
|
+
{
|
|
8912
|
+
"name": "variant",
|
|
8913
|
+
"type": "\"key\" | \"ghost\"",
|
|
8914
|
+
"default": "\"key\"",
|
|
8915
|
+
"description": "`key`: a raised cap on the surface it sits on, with a heavier bottom edge as the key's lip. `ghost`: the bare label, for menu rows, tooltips and other surfaces that are already a frame."
|
|
8916
|
+
},
|
|
9023
8917
|
{
|
|
9024
8918
|
"name": "className",
|
|
9025
8919
|
"type": "string",
|
|
@@ -9033,12 +8927,14 @@
|
|
|
9033
8927
|
],
|
|
9034
8928
|
"tokensUsed": [
|
|
9035
8929
|
"border",
|
|
9036
|
-
"
|
|
8930
|
+
"border-strong",
|
|
8931
|
+
"background",
|
|
9037
8932
|
"muted-foreground"
|
|
9038
8933
|
],
|
|
9039
8934
|
"a11y": [
|
|
9040
8935
|
"Uses the semantic <kbd> element so assistive tech announces keyboard input.",
|
|
9041
|
-
"
|
|
8936
|
+
"KbdGroup is itself a <kbd> around the per-key caps — the HTML spelling of one key combination — so the combo is announced as a single piece of input.",
|
|
8937
|
+
"Sized in em from the surrounding text, so it scales with the user's text size and never shrinks below 12px."
|
|
9042
8938
|
],
|
|
9043
8939
|
"examples": [
|
|
9044
8940
|
{
|
|
@@ -9048,8 +8944,13 @@
|
|
|
9048
8944
|
},
|
|
9049
8945
|
{
|
|
9050
8946
|
"label": "Combo",
|
|
9051
|
-
"code": "<
|
|
9052
|
-
"imports": "import { Kbd,
|
|
8947
|
+
"code": "<KbdGroup><Kbd mod /><Kbd>K</Kbd></KbdGroup>",
|
|
8948
|
+
"imports": "import { Kbd, KbdGroup } from \"@sableui/react\""
|
|
8949
|
+
},
|
|
8950
|
+
{
|
|
8951
|
+
"label": "In a menu row",
|
|
8952
|
+
"code": "<MenuItem>Search<KbdGroup className=\"ms-auto\"><Kbd variant=\"ghost\" mod /><Kbd variant=\"ghost\">K</Kbd></KbdGroup></MenuItem>",
|
|
8953
|
+
"imports": "import { Kbd, MenuItem } from \"@sableui/react\""
|
|
9053
8954
|
}
|
|
9054
8955
|
]
|
|
9055
8956
|
},
|
|
@@ -11577,7 +11478,7 @@
|
|
|
11577
11478
|
"name": "size",
|
|
11578
11479
|
"type": "\"md\" | \"sm\"",
|
|
11579
11480
|
"default": "\"md\"",
|
|
11580
|
-
"description": "
|
|
11481
|
+
"description": "md sits on the density ladder's xs control height (28px); sm is 24px, for chips under or inside other content."
|
|
11581
11482
|
},
|
|
11582
11483
|
{
|
|
11583
11484
|
"name": "icon",
|
|
@@ -13593,7 +13494,7 @@
|
|
|
13593
13494
|
},
|
|
13594
13495
|
{
|
|
13595
13496
|
"guidance": "do",
|
|
13596
|
-
"description": "
|
|
13497
|
+
"description": "Rows are rounded-sm, which nests in the usual rounded-lg p-2 frame. In any other rounded, padded frame give each item rounded-concentric-<outer>/<inset> — a same-radius row inset into a frame reads as a second system."
|
|
13597
13498
|
},
|
|
13598
13499
|
{
|
|
13599
13500
|
"guidance": "dont",
|
|
@@ -13784,7 +13685,7 @@
|
|
|
13784
13685
|
"slot": "activity",
|
|
13785
13686
|
"recipe": "motion-shimmer",
|
|
13786
13687
|
"part": "Root",
|
|
13787
|
-
"why": "The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page."
|
|
13688
|
+
"why": "The sweep is the entire point of the component: it is what separates 'still loading' from 'loaded, and empty'. A static grey block is indistinguishable from a broken layout, which is why users reload the page. It is one band of light against the viewport, so every skeleton in a loading layout lights in sequence as a single wave rather than each block flashing at its own speed; each pass eases in, glides and settles, then rests off-screen before the next."
|
|
13788
13689
|
}
|
|
13789
13690
|
],
|
|
13790
13691
|
"a11y": [
|
|
@@ -15298,7 +15199,7 @@
|
|
|
15298
15199
|
"bestPractices": [
|
|
15299
15200
|
{
|
|
15300
15201
|
"guidance": "do",
|
|
15301
|
-
"description": "Use layout=\"list\" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme's swatch strip, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (
|
|
15202
|
+
"description": "Use layout=\"list\" in a popover, a menu, or a docked panel. Rows sit on the --control-sm ladder and still show each theme's swatch strip, so the choice stays visible rather than described. Rows take `sable-frame-inner`, so host the list in a sable-frame (`sable-frame sable-surface sable-surface-popover sable-frame-popover` — PopoverContent is not one; give it that class list, or its p-4 leaves the rows unnested) and their corners nest concentrically."
|
|
15302
15203
|
},
|
|
15303
15204
|
{
|
|
15304
15205
|
"guidance": "do",
|
|
@@ -17001,45 +16902,55 @@
|
|
|
17001
16902
|
{
|
|
17002
16903
|
"name": "AuthPage",
|
|
17003
16904
|
"path": "patterns/auth-page.tsx",
|
|
17004
|
-
"summary": "
|
|
16905
|
+
"summary": "Split sign-in page: a form column (brand mark, heading, email + password, primary sign-in, passkey and wallet alternatives, sign-up switch, terms) beside a brand panel painted from the theme's tones with a featured release card. Below lg the panel drops away and the form is the whole screen.",
|
|
17005
16906
|
"intent": "Reach for this for sign-in, sign-up, forgot-password, and any single-purpose one-column form route (invite accept, unsubscribe). Not for multi-section forms (use SettingsPage).",
|
|
17006
16907
|
"notes": [
|
|
17007
|
-
"
|
|
17008
|
-
"
|
|
17009
|
-
"
|
|
17010
|
-
"
|
|
17011
|
-
"
|
|
16908
|
+
"The form column is a `form`-capped Region (480px), so the controls keep the same measure whether the panel is present or not.",
|
|
16909
|
+
"No card around the form: the page itself is the surface, and the brand panel carries the identity. A card on a blank page reads as a template.",
|
|
16910
|
+
"The heading is `text-heading-1` in an h1 — it is the page outline, so it takes a heading tier, not a card title.",
|
|
16911
|
+
"Brand mark, form and terms are a three-part column: mark pinned to the top, form vertically centred in the remaining space, terms pinned to the bottom.",
|
|
16912
|
+
"Forgot password sits under the password field, right-aligned, before the submit button — so it comes right after the field in tab order. Field has no label-row action slot yet.",
|
|
16913
|
+
"One primary action. Passkey and wallet are outline buttons under an `or` Separator; switching to sign-up is a quiet `link-underline` link.",
|
|
16914
|
+
"Validation uses Field `validate` for product copy instead of the browser's generic required message. A rejected sign-in comes back through Form `errors` on the password field; the submit Button shows `loading` while the request is in flight.",
|
|
16915
|
+
"The brand panel artwork is a mesh of the theme's decorative tones over `--primary`, so it re-paints for every theme and mode. The relative-colour `oklch(from var(--tone-*) …)` lift needs an `inline-color` disable with a reason.",
|
|
16916
|
+
"The panel is `hidden lg:flex`: on a phone the form is the whole screen, with no decorative scroll above it."
|
|
17012
16917
|
],
|
|
17013
16918
|
"a11y": [
|
|
17014
|
-
"The route renders inside <main> with
|
|
17015
|
-
"Field auto-wires labels, describedby, and invalid state onto the inputs.",
|
|
17016
|
-
"Autocomplete attributes (email
|
|
16919
|
+
"The route renders inside <main> with one h1; the brand panel is an <aside> labelled \"Featured release\".",
|
|
16920
|
+
"Field auto-wires labels, describedby, and invalid state onto the inputs; on submit, the first invalid field is focused.",
|
|
16921
|
+
"Autocomplete attributes (`email webauthn`, `current-password`) keep password managers and passkey autofill working.",
|
|
16922
|
+
"The Forgot password and Create one links use `hit-target` to meet the 44px touch floor without changing layout.",
|
|
16923
|
+
"The artwork swatches are aria-hidden decoration; the brand link's accessible name is the wordmark."
|
|
17017
16924
|
],
|
|
17018
|
-
"imports": "import {
|
|
17019
|
-
"code": "
|
|
16925
|
+
"imports": "import { useState } from \"react\"\nimport { Avatar, Badge, Button, Card, CardContent, Entity, Field, Form, Icon, Input, Region, Separator, Stack } from \"@sableui/react\"",
|
|
16926
|
+
"code": "// A soft mesh of the theme's decorative tones over its brand colour, so the\n// artwork follows every theme and mode. Each tone is lifted from its text\n// strength (dark, restrained) to an art strength.\nconst artwork = [\n\t// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look\n\t\"radial-gradient(in oklab at 18% 22%, oklch(from var(--tone-info) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 58%)\",\n\t// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look\n\t\"radial-gradient(in oklab at 82% 18%, oklch(from var(--tone-collectible) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)\",\n\t// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look\n\t\"radial-gradient(in oklab at 70% 88%, oklch(from var(--tone-edition) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 60%)\",\n\t// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look\n\t\"radial-gradient(in oklab at 12% 92%, oklch(from var(--tone-standard) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)\",\n\t\"var(--primary)\",\n].join(\", \")\n\nexport 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\">\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\">\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<div className=\"flex flex-1 flex-col justify-end rounded-xl p-8\" style={{ background: artwork }}>\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 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</div>\n\t\t\t</aside>\n\t\t</main>\n\t)\n}"
|
|
17020
16927
|
},
|
|
17021
16928
|
{
|
|
17022
16929
|
"name": "BrandLandingPage",
|
|
17023
16930
|
"path": "patterns/brand-landing-page.tsx",
|
|
17024
|
-
"summary": "High-fidelity brand/product landing page: pill navigation, editorial hero type, a dark product scene,
|
|
16931
|
+
"summary": "High-fidelity brand/product landing page: pill navigation, editorial hero type, a dark product scene, a value section, a how-it-works sequence, a customer story with outcomes, a closing CTA band, and a complete branded footer.",
|
|
17025
16932
|
"intent": "Use this for a polished product marketing route that still needs to show a credible app surface. It is warmer and more tactile than the canonical LandingPage, but avoids trend-driven neon and generic hype cues.",
|
|
17026
16933
|
"notes": [
|
|
17027
|
-
"
|
|
16934
|
+
"The story runs promise → product → value → process → proof → ask: hero, product scene, 'Why Cove' value cards, a three-step how-it-works list, a customer story with outcome figures, then one closing CTA. Each section adds new evidence; no number is repeated from the product scene.",
|
|
16935
|
+
"One display headline owns the first viewport; supporting copy stays in body-lg at prose measure. Section headings are heading-1 under a `text-overline` eyebrow.",
|
|
17028
16936
|
"The nav spends one compact pill surface, with a clear brand signal and two primary paths: sign in and request access.",
|
|
17029
|
-
"The product scene
|
|
17030
|
-
"The
|
|
17031
|
-
"
|
|
17032
|
-
"
|
|
17033
|
-
"
|
|
16937
|
+
"The product scene and the closing CTA band are dark islands: a `dark` class on the wrapper re-scopes every semantic token, so surfaces, muted ink, Badge tones and Buttons take the theme's own dark values in either page mode. Never fake a dark panel with overlay ink at reduced opacity — status tones tuned for a light page vanish on it.",
|
|
16938
|
+
"The scene is a picture of the product: `aria-hidden` + `inert` keep its sample button and figures out of the tab order and the outline. On narrow screens it drops the side column and keeps the signal map, so it scales instead of stacking into a second page.",
|
|
16939
|
+
"The chart keeps past weeks on the quiet `secondary` fill and gives the current week the one accent, so the signal reads at a glance.",
|
|
16940
|
+
"Value cards are real Cards with a neutral `sable-well` icon tile; the customer story is a Card rendered as a figure, with outcome figures split by Separator. The scene's nested panels take `rounded-concentric-xl/<inset>` from the shell's radius, so the corners stay parallel at any radius preset.",
|
|
16941
|
+
"The footer closes the page with brand reassurance, a plain status Badge, a three-column sitemap (three columns at every width), a digest signup, and legal links.",
|
|
16942
|
+
"All figures, names and the testimonial are sample content; replace them with real, verifiable claims."
|
|
17034
16943
|
],
|
|
17035
16944
|
"a11y": [
|
|
17036
16945
|
"The brand nav is a labeled nav landmark with real links and visible text.",
|
|
16946
|
+
"Each content section is a labelled region (by its h2, or aria-label for the customer story); the decorative product scene is hidden from assistive tech and inert.",
|
|
16947
|
+
"The how-it-works steps are an ordered list; the testimonial is a figure with a blockquote and a figcaption naming the speaker.",
|
|
17037
16948
|
"The footer is a real footer landmark with a labeled footer nav, visible link text, and a labeled email field.",
|
|
17038
|
-
"TextReveal preserves the full headline as one accessible label and honors reduced-motion policy.",
|
|
17039
|
-
"Status dots are paired with visible text labels so color is never the only signal."
|
|
16949
|
+
"TextReveal preserves the full headline as one accessible label and honors reduced-motion policy; Reveal shows the value cards immediately when motion is off.",
|
|
16950
|
+
"Status dots and the status Badge are paired with visible text labels so color is never the only signal."
|
|
17040
16951
|
],
|
|
17041
|
-
"imports": "import { Badge, Button, Card,
|
|
17042
|
-
"code": "const navItems = [\"Platform\", \"Signals\", \"Security\", \"Company\"]\n\nconst metrics = [\n\t{ label: \"Checks cleared\", value: \"1.8M\", detail: \"in the last 30 days\" },\n\t{ label: \"Review time\", value: \"42s\", detail: \"median approval path\" },\n\t{ label: \"Spaces online\", value: \"284\", detail: \"across active teams\" },\n]\n\nconst workflow = [\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\nconst bars = [\n\t{ height: \"38%\", tone: \"bg-tone-info\" },\n\t{ height: \"56%\", tone: \"bg-tone-edition\" },\n\t{ height: \"44%\", tone: \"bg-tone-standard\" },\n\t{ height: \"68%\", tone: \"bg-success\" },\n\t{ height: \"52%\", tone: \"bg-tone-info\" },\n\t{ height: \"78%\", tone: \"bg-highlight\" },\n\t{ height: \"60%\", tone: \"bg-tone-edition\" },\n\t{ height: \"86%\", tone: \"bg-success\" },\n\t{ height: \"64%\", tone: \"bg-tone-standard\" },\n]\n\nconst features = [\n\t{\n\t\ticon: \"lock\" as const,\n\t\ttitle: \"Private by default\",\n\t\tbody: \"Workspace roles, approval trails, and handoff history stay clear without turning the interface into compliance fog.\",\n\t},\n\t{\n\t\ticon: \"layout\" as const,\n\t\ttitle: \"Brand and product together\",\n\t\tbody: \"Editorial sections and dense console views share the same semantic tokens, radius, and motion contract.\",\n\t},\n\t{\n\t\ticon: \"circle-check\" as const,\n\t\ttitle: \"Calm confidence\",\n\t\tbody: \"Large type, tactile controls, and focused status tones make complex work feel understandable on first pass.\",\n\t},\n]\n\nconst reviews = [\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 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 footerSignals = [\n\t{ label: \"Uptime\", value: \"99.98%\" },\n\t{ label: \"Regions\", value: \"12\" },\n\t{ label: \"Response\", value: \"42s\" },\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} variant=\"ghost\" size=\"sm\" render={<a href={`#${item.toLowerCase()}`} />}>\n\t\t\t\t\t\t\t\t\t\t{item}\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-16 pt-14 sm:pt-20 lg:pb-24\">\n\t\t\t\t\t<div className=\"grid items-center gap-10 lg:grid-cols-[0.92fr_1.08fr]\">\n\t\t\t\t\t\t<Stack gap={4} className=\"max-w-3xl\">\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 digital work\"\n\t\t\t\t\t\t\t\tclassName=\"max-w-4xl text-balance text-display-xl sm:text-display-3xl\"\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>\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 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<div className=\"relative\">\n\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 the shell's xl minus every inset between it and the shell edge. */}\n\t\t\t\t\t\t\t<div className=\"overflow-hidden rounded-xl border border-border-floating bg-overlay p-3 text-overlay-foreground shadow-xl\">\n\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/3 border border-overlay-foreground/10 bg-overlay-foreground/8\">\n\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-between border-b border-overlay-foreground/10 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-overlay-foreground/70\">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\">\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"rounded-concentric-xl/6 border border-overlay-foreground/10 bg-overlay-foreground/8 p-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-label-sm text-overlay-foreground/60\">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{reviews.map((review) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={review.name} className=\"rounded-concentric-xl/10 bg-overlay 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\">{review.name}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-mono-sm text-overlay-foreground\">{review.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-overlay-foreground/60\">{review.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-overlay-foreground/10 bg-overlay-foreground/8 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\">Healthy</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{workflow.map((item) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Row key={item.label} justify=\"between\" className=\"rounded-full bg-overlay 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={item.tone} />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm\">{item.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-overlay-foreground/60\">{item.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-overlay-foreground/10 bg-overlay-foreground/8 p-4\">\n\t\t\t\t\t\t\t\t\t\t\t<Row justify=\"between\" className=\"border-b border-overlay-foreground/10 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-overlay-foreground/60\">Signal map</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<h2 className=\"mt-1 text-heading-3\">Every approval path in view</h2>\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-6 grid min-h-72 grid-rows-[1fr_auto] gap-5\">\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"grid grid-cols-9 items-end gap-2 rounded-concentric-xl/10 bg-overlay p-4\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{bars.map((bar, index) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div key={`${bar.tone}-${index}`} className=\"flex h-56 items-end\">\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.tone}`}\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 gap-2 sm:grid-cols-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{metrics.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-overlay-foreground/10 bg-overlay p-3\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-label-sm text-overlay-foreground/60\">{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-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-overlay-foreground/60\">{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\" className=\"border-y border-border bg-muted/40 py-14\">\n\t\t\t\t\t<Region max=\"wide\">\n\t\t\t\t\t\t<div className=\"grid gap-5 lg:grid-cols-[0.8fr_1.2fr]\">\n\t\t\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t\t\t<Badge>Design mood</Badge>\n\t\t\t\t\t\t\t\t<h2 className=\"text-heading-1\">Marketing warmth, product discipline.</h2>\n\t\t\t\t\t\t\t\t<p className=\"max-w-(--measure-prose) text-body-md text-muted-foreground\">\n\t\t\t\t\t\t\t\t\tThe page uses a pill nav, tactile surfaces, soft status tones, and\n\t\t\t\t\t\t\t\t\tslower ease-out movement. It stays broad in color so it does not\n\t\t\t\t\t\t\t\t\tcollapse into purple-brand sameness.\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<div className=\"grid gap-3 md:grid-cols-3\">\n\t\t\t\t\t\t\t\t{features.map((feature) => (\n\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<CardContent>\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"grid size-10 place-items-center rounded-md bg-highlight text-highlight-foreground\">\n\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</span>\n\t\t\t\t\t\t\t\t\t\t\t<h3 className=\"mt-5 text-title-lg\">{feature.title}</h3>\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-2 text-body-sm text-muted-foreground\">{feature.body}</p>\n\t\t\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t\t\t</Card>\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</div>\n\t\t\t\t\t</Region>\n\t\t\t\t</section>\n\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"py-14\">\n\t\t\t\t\t<div className=\"grid gap-3 md:grid-cols-3\">\n\t\t\t\t\t\t{metrics.map((metric) => (\n\t\t\t\t\t\t\t<Card key={metric.label}>\n\t\t\t\t\t\t\t\t<CardContent>\n\t\t\t\t\t\t\t\t\t<p className=\"text-label-md text-muted-foreground\">{metric.label}</p>\n\t\t\t\t\t\t\t\t\t<p className=\"mt-3 text-display-lg\">{metric.value}</p>\n\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-body-sm text-muted-foreground\">{metric.detail}</p>\n\t\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t))}\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-10 sm:py-12\">\n\t\t\t\t\t<div className=\"grid gap-8 lg:grid-cols-[1fr_1.35fr_1fr]\">\n\t\t\t\t\t\t<Stack gap={3} className=\"max-w-md\">\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<Row gap={1} wrap>\n\t\t\t\t\t\t\t\t<Badge size=\"sm\" icon={<Icon name=\"lock\" />}>\n\t\t\t\t\t\t\t\t\tPrivate\n\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t<Badge tone=\"success\" size=\"sm\" icon={<Icon name=\"circle-check\" />}>\n\t\t\t\t\t\t\t\t\tLive\n\t\t\t\t\t\t\t\t</Badge>\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<nav aria-label=\"Footer\" className=\"grid gap-6 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 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>\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>Signal 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 weekly note on trusted workflows and product releases.\n\t\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t\t\t<CardAction>\n\t\t\t\t\t\t\t\t\t<span className=\"grid size-9 place-items-center rounded-full bg-highlight text-highlight-foreground\">\n\t\t\t\t\t\t\t\t\t\t<Icon name=\"sparkles\" className=\"text-sm\" />\n\t\t\t\t\t\t\t\t\t</span>\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 className=\"flex flex-col gap-4\">\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\tJoin\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\t<div className=\"grid grid-cols-3 gap-2\">\n\t\t\t\t\t\t\t\t\t{footerSignals.map((signal) => (\n\t\t\t\t\t\t\t\t\t\t<div key={signal.label} className=\"rounded-md bg-muted p-3\">\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-mono-sm\">{signal.value}</p>\n\t\t\t\t\t\t\t\t\t\t\t<p className=\"mt-1 text-caption text-muted-foreground\">{signal.label}</p>\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</div>\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-10 flex flex-col gap-3 border-t border-border pt-5 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={3} wrap>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"/privacy\"\n\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>\n\t\t\t\t\t\t\t\tPrivacy\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"/terms\"\n\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>\n\t\t\t\t\t\t\t\tTerms\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\thref=\"/contact\"\n\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>\n\t\t\t\t\t\t\t\tContact\n\t\t\t\t\t\t\t</a>\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}"
|
|
16952
|
+
"imports": "import { Avatar, Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Grid, Icon, Input, Region, Reveal, Row, Separator, Stack, StatusDot, TextReveal } from \"@sableui/react\"",
|
|
16953
|
+
"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}"
|
|
17043
16954
|
},
|
|
17044
16955
|
{
|
|
17045
16956
|
"name": "ChatPanel",
|
|
@@ -17053,7 +16964,11 @@
|
|
|
17053
16964
|
"Region cap is `feed` (640px) — a conversation is a reading column, and the Bubble measure cap is tuned to the same assumption.",
|
|
17054
16965
|
"The composer sits outside the scroll region, in a bordered CardFooter, so it stays put while the transcript moves.",
|
|
17055
16966
|
"Grouping is derived from the data (`turns[i - 1].mine === turn.mine`) rather than hand-set, and the tail is dropped on continuations — that pairing is what makes a run read as one block.",
|
|
17056
|
-
"
|
|
16967
|
+
"What the assistant did before answering — a doc it read, a tool it ran — is a `Marker variant=\"note\"` in that message's `header`, so it reads as part of the turn rather than as an event between turns.",
|
|
16968
|
+
"The assistant's long-form answer uses `variant=\"plain\"` and continues the same grouped run. A tinted box around a code block reads as a quote rather than as a turn. A plain Bubble sizes to its content with no measure cap, so it takes `max-w-full` here: bounded by the column, a wide code line scrolls inside the CodeBlock instead of pushing the transcript sideways on a phone.",
|
|
16969
|
+
"A settled answer carries its actions (copy, regenerate) in the Message `footer` as a `Toolbar` of icon `ToolbarButton`s with tooltips — one tab stop, arrow keys between them. The answer still streaming has none: there is nothing to copy or regenerate until it settles, and Stop lives in the composer.",
|
|
16970
|
+
"One turn, one Message: the reader's question and the file it refers to stack inside a single end-aligned Message rather than being sent as two.",
|
|
16971
|
+
"Assistant turns share one Avatar with a glyph fallback and `alt=\"Assistant\"` — initials like \"AI\" read as a person, and a fallback-only avatar is otherwise unnamed.",
|
|
17057
16972
|
"The answer in flight is a `StreamingText` in a `plain` Bubble, and ONE `pending` flag drives both it and the composer's `busy`. They are the same wait reported at the two ends of the panel; letting them drift gives you a composer still saying 'sending' under an answer that already finished.",
|
|
17058
16973
|
"A spinner `Marker` (or `ThinkingOrb`) belongs BEFORE the first token and is replaced once tokens land, not kept alongside. Two indicators for one wait read as two things happening — which is also why `StreamingText` takes `caret={false}` when something adjacent already reports it.",
|
|
17059
16974
|
"MessageScroller needs no configuring for this: it re-follows on \"a message grew\" (ResizeObserver) as well as \"a message arrived\", so the newest token stays pinned while a reader who has scrolled up is left alone.",
|
|
@@ -17062,116 +16977,136 @@
|
|
|
17062
16977
|
"a11y": [
|
|
17063
16978
|
"The panel header's title is the page's <h1> (an h1 inside CardTitle, which carries the type); the transcript is a scroll region with native overflow, so keyboard scrolling works unchanged.",
|
|
17064
16979
|
"The answer in flight carries `aria-busy` rather than sitting in a live region. That is deliberate: a live region around streamed text announces every token, which is unusable. Announce once when the turn settles, from whatever owns the turn — the incomplete state is still exposed, just not narrated.",
|
|
17065
|
-
"The composer is a real <form> whose Enter handling skips IME composition, and every icon-only control in the panel carries an accessible label."
|
|
16980
|
+
"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\"`)."
|
|
17066
16981
|
],
|
|
17067
|
-
"imports": "import { Attachment, Avatar, Bubble,
|
|
17068
|
-
"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\"Anchor on the oldest rendered message and add back however far it moved. Appends\"\n\nconst turns = [\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\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\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<Button variant=\"outline\" size=\"icon-sm\" aria-label=\"Start a new chat\">\n\t\t\t\t\t\t\t<Icon name=\"plus\" />\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<Message\n\t\t\t\t\t\t\tkey={turn.id}\n\t\t\t\t\t\t\tmessageId={turn.id}\n\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\tgrouped={i > 0 && turns[i - 1].mine === turn.mine}\n\t\t\t\t\t\t\tavatar={turn.mine ? undefined : <Avatar size=\"sm\" fallback=\"AI\" />}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Bubble\n\t\t\t\t\t\t\t\tvariant={turn.mine ? \"primary\" : \"muted\"}\n\t\t\t\t\t\t\t\talign={turn.mine ? \"end\" : \"start\"}\n\t\t\t\t\t\t\t\ttail={i === 0 || turns[i - 1].mine !== turn.mine}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{turn.body}\n\t\t\t\t\t\t\t</Bubble>\n\t\t\t\t\t\t</Message>\n\t\t\t\t\t))}\n\n\t\t\t\t\t<Marker variant=\"note\" icon=\"sparkles\">\n\t\t\t\t\t\tRead message-scroller.md\n\t\t\t\t\t</Marker>\n\n\t\t\t\t\t{/* An assistant's long-form answer: no bubble, because a tinted box\n\t\t\t\t\t around a code block reads as a quote rather than as a turn. */}\n\t\t\t\t\t<Message messageId=\"t_04\" avatar={<Avatar size=\"sm\" fallback=\"AI\" />}>\n\t\t\t\t\t\t<Bubble variant=\"plain\">\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\t<BubbleReactions>\n\t\t\t\t\t\t\t<BubbleReaction emoji=\"🎯\" count={2} active />\n\t\t\t\t\t\t</BubbleReactions>\n\t\t\t\t\t</Message>\n\n\t\t\t\t\t<Message messageId=\"t_05\" align=\"end\">\n\t\t\t\t\t\t<Attachment name=\"scroller.tsx\" meta=\"4.2 KB · TSX\" />\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.\n\n\t\t\t\t\t No bubble, same as the long-form answer above: a tinted box around\n\t\t\t\t\t a growing paragraph makes the reflow look like the box breathing.\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 messageId=\"t_06\" avatar={<Avatar size=\"sm\" fallback=\"AI\" />}>\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\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}"
|
|
16982
|
+
"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\"",
|
|
16983
|
+
"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}"
|
|
17069
16984
|
},
|
|
17070
16985
|
{
|
|
17071
16986
|
"name": "DashboardPage",
|
|
17072
16987
|
"path": "patterns/dashboard-page.tsx",
|
|
17073
|
-
"summary": "App-shell dashboard:
|
|
17074
|
-
"intent": "Reach for this when a route's job is monitoring or managing many records (
|
|
16988
|
+
"summary": "App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader + BottomNav below lg), a KPI row, an edge-to-edge editions table with sell-through Meters, and supporting payout, collector, release and activity cards — the default shape for an operator or analytics home route.",
|
|
16989
|
+
"intent": "Reach for this when a route's job is monitoring or managing many records (sales, orders, editions, posts) with a few headline numbers. Not for reading-focused routes (use FeedPage) or single-record routes (use ListDetailPage's detail side).",
|
|
17075
16990
|
"notes": [
|
|
17076
16991
|
"Region cap is `wide` (1280px): dashboards hold tables and multi-column grids, never a reading column.",
|
|
17077
|
-
"
|
|
17078
|
-
"Stat cards
|
|
17079
|
-
"
|
|
17080
|
-
"The
|
|
17081
|
-
"
|
|
16992
|
+
"Hierarchy runs headline → records → context: KPI row, then the primary table beside its two closest companions (payout, top collectors), then the lower-priority lists (upcoming, activity). Every row of the grid is filled so the route ends on content, not empty canvas.",
|
|
16993
|
+
"Stat cards: CardHeader carries the label (CardDescription) and the value; KPI values are `text-mono-xl` — the metric mono tier, tabular figures — never a heading tier. The trend Badge sits with its comparison note in CardContent, so the value keeps the full card width when the row folds to 2×2 on a phone.",
|
|
16994
|
+
"Stat cards use an explicit cols map (2 → 4) — a 4-stat row must fold to 2×2, never strand an orphan the way auto-fit can.",
|
|
16995
|
+
"The table runs flush to the card edges with `<CardContent flush>`, which hands the card's padding to the table (`--table-edge-px`), so the first and last columns line up with the CardHeader at any card density — never hand-set pl-6/pr-6. Numeric columns align end in mono; Table scrolls horizontally inside the card on narrow screens.",
|
|
16996
|
+
"Sold / supply is a `Meter` (how full a known range is), not `Progress` (task progress). Status is `Badge appearance=\"plain\" dot`, with the app's own states mapped to tones once at the top of the file.",
|
|
16997
|
+
"Lists are Entity rows in CardList, inset to the card padding and divided by hairlines — never CardContent with hand-tuned px. A card that shares a grid row with a taller one becomes `flex flex-col` and pins its CardFooter with `mt-auto`.",
|
|
16998
|
+
"Brand and account live in the Sidebar: SidebarHeader holds the mark + wordmark, SidebarFooter holds the signed-in Entity with a DotsMenu. The footer rebinds `--foreground` to `--sidebar-foreground`, because Entity's title and the ghost trigger ink with page tokens and a theme may paint the rail dark on a light page; it keeps to a title line, since there is no sidebar-muted ink for a subtitle.",
|
|
17082
16999
|
"The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.",
|
|
17083
17000
|
"Chrome (Sidebar) is outside the content grid; the Page region caps only content.",
|
|
17084
|
-
"Sidebar hides below lg, so the shell's header carries an AppHeader and its footer a BottomNav with the same destinations — never strand mobile users without navigation."
|
|
17001
|
+
"Sidebar hides below lg, so the shell's header carries an AppHeader (brand + account) and its footer a BottomNav with the same destinations — never strand mobile users without navigation."
|
|
17085
17002
|
],
|
|
17086
17003
|
"a11y": [
|
|
17087
17004
|
"AppShell provides the skip link and <main id=\"main-content\"> landmark.",
|
|
17088
17005
|
"The active SidebarItem and BottomNavItem carry aria-current via `active`.",
|
|
17089
|
-
"DataTable headers are real <th>; sortable columns are announced."
|
|
17006
|
+
"DataTable headers are real <th>; sortable columns are announced.",
|
|
17007
|
+
"Each sell-through Meter has an aria-label naming the edition and aria-valuetext reading \"184 of 250 sold\".",
|
|
17008
|
+
"Avatars beside a visible name use an empty alt so the name is not read twice."
|
|
17090
17009
|
],
|
|
17091
|
-
"imports": "import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarHeader, SidebarItem, SidebarNav } from \"@sableui/react\"",
|
|
17092
|
-
"code": "const MIN = 60 * 1000\nconst HOUR = 60 * MIN\n\nconst stats = [\n\t{ label: \"Revenue\", value: \"$12,480\", delta: \"+8.2%\", up: true, note: \"vs. previous 30 days\" },\n\t{ label: \"Collects\", value: \"1,284\", delta: \"+3.1%\", up: true, note: \"182 from new collectors\" },\n\t{ label: \"New collectors\", value: \"312\", delta: \"+12.4%\", up: true, note: \"best month this year\" },\n\t{ label: \"Sell-through\", value: \"64%\", delta: \"−1.8%\", up: false, note: \"across 48 active editions\" },\n]\n\nconst deployments = [\n\t{ id: \"dp_01\", project: \"storefront\", commit: \"a41f2c9\", status: \"ready\" as const, duration: \"42s\", updated: 2 * MIN },\n\t{ id: \"dp_02\", project: \"api\", commit: \"7c03bd1\", status: \"building\" as const, duration: \"1m 08s\", updated: 9 * MIN },\n\t{ id: \"dp_03\", project: \"docs\", commit: \"e992a70\", status: \"queued\" as const, duration: \"—\", updated: 12 * MIN },\n\t{ id: \"dp_04\", project: \"worker\", commit: \"51b8e04\", status: \"failed\" as const, duration: \"18s\", updated: 1 * HOUR },\n\t{ id: \"dp_05\", project: \"storefront\", commit: \"90cc41a\", status: \"ready\" as const, duration: \"39s\", updated: 3 * HOUR },\n\t{ id: \"dp_06\", project: \"renderer\", commit: \"b7714dd\", status: \"ready\" as const, duration: \"2m 31s\", updated: 5 * HOUR },\n\t{ id: \"dp_07\", project: \"api\", commit: \"f30a9e2\", status: \"ready\" as const, duration: \"1m 02s\", updated: 8 * HOUR },\n]\n\nconst activity = [\n\t{ id: \"ac_01\", who: \"Nina Simone\", initials: \"NS\", what: \"collected Gold Rush #42\", when: 2 * MIN },\n\t{ id: \"ac_02\", who: \"Grace Hopper\", initials: \"GH\", what: \"minted a 100-run edition\", when: 26 * MIN },\n\t{ id: \"ac_03\", who: \"Ada Lovelace\", initials: \"AL\", what: \"followed you\", when: 1 * HOUR },\n\t{ id: \"ac_04\", who: \"Mary Shelley\", initials: \"MS\", what: \"collected Quiet Harbor\", when: 3 * HOUR },\n\t{ id: \"ac_05\", who: \"Frida Kahlo\", initials: \"FK\", what: \"tipped 0.2 SOL on Process\", 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={<AppHeader title={<span className=\"text-title-sm\">Atelier</span>} />}\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<span className=\"text-title-sm\">Atelier</span>\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 icon={<Icon name=\"bell\" />} label=\"Activity\" render={<a href=\"/activity\" />} />\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</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, collects, and edition 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\">Export</Button>\n\t\t\t\t\t\t\t<Button>New edition</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: 1, sm: 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\t<CardAction>\n\t\t\t\t\t\t\t\t\t<Badge tone={stat.up ? \"success\" : \"neutral\"} size=\"sm\">\n\t\t\t\t\t\t\t\t\t\t<Icon name={stat.up ? \"chevron-up\" : \"chevron-down\"} />\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</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<p className=\"text-caption text-muted-foreground\">{stat.note}</p>\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=\"xl:col-span-2\">\n\t\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t\t<CardTitle>Recent deployments</CardTitle>\n\t\t\t\t\t\t\t<CardDescription>Builds across every project, newest first.</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=\"/deployments\" />}>\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<CardContent flush>\n\t\t\t\t\t\t\t<DataTable\n\t\t\t\t\t\t\t\tdata={deployments}\n\t\t\t\t\t\t\t\trowKey={(row) => row.id}\n\t\t\t\t\t\t\t\tcolumns={[\n\t\t\t\t\t\t\t\t\t{ key: \"project\", header: \"Project\", sortable: true, className: \"text-label-md\" },\n\t\t\t\t\t\t\t\t\t{ key: \"commit\", header: \"Commit\", cell: (row) => <span className=\"text-mono-caption text-muted-foreground\">{row.commit}</span> },\n\t\t\t\t\t\t\t\t\t{ key: \"status\", header: \"Status\", cell: (row) => <Badge appearance=\"plain\" tone={statusTone(row.status)} dot>{row.status}</Badge> },\n\t\t\t\t\t\t\t\t\t{ key: \"duration\", header: \"Duration\", align: \"end\", cell: (row) => <span className=\"text-mono-caption\">{row.duration}</span> },\n\t\t\t\t\t\t\t\t\t{ key: \"updated\", header: \"Updated\", align: \"end\", cell: (row) => <RelativeTime date={Date.now() - row.updated} className=\"text-body-xs 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</CardContent>\n\t\t\t\t\t</Card>\n\t\t\t\t\t<Card>\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, mints, and follows.</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{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\tdensity=\"compact\"\n\t\t\t\t\t\t\t\t\tleading={<Avatar size=\"sm\" fallback={item.initials} alt={item.who} />}\n\t\t\t\t\t\t\t\t\ttitle={\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-body-sm\">\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-label-md\">{item.who}</span>{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground\">{item.what}</span>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t}\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}"
|
|
17010
|
+
"imports": "import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, DotsMenu, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from \"@sableui/react\"",
|
|
17011
|
+
"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{/* Entity and DotsMenu ink with page tokens; a theme can paint the rail\n\t\t\t\t\t dark on a light page, so this subtree reads the rail's ink instead. */}\n\t\t\t\t\t<SidebarFooter className=\"[--foreground:var(--sidebar-foreground)]\">\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\taction={\n\t\t\t\t\t\t\t\t<DotsMenu 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</DotsMenu>\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}"
|
|
17093
17012
|
},
|
|
17094
17013
|
{
|
|
17095
17014
|
"name": "FeedPage",
|
|
17096
17015
|
"path": "patterns/feed-page.tsx",
|
|
17097
|
-
"summary": "
|
|
17098
|
-
"intent": "Reach for this for timelines, activity streams, and any reverse-chronological reading surface. Not for record management (DashboardPage) or side-by-side browsing (ListDetailPage).",
|
|
17016
|
+
"summary": "Social reading feed with a side rail: a feed-width main column (composer, For you / Following Tabs, one Card per post, an edition drop with price, supply Meter and Collect, a Skeleton for the next page) beside a sticky rail of who-to-follow and upcoming drops that drops away below lg.",
|
|
17017
|
+
"intent": "Reach for this for timelines, activity streams, and any reverse-chronological reading surface where people also act on what they read. Not for record management (DashboardPage) or side-by-side browsing (ListDetailPage).",
|
|
17099
17018
|
"notes": [
|
|
17100
|
-
"
|
|
17101
|
-
"
|
|
17102
|
-
"The
|
|
17103
|
-
"
|
|
17104
|
-
"
|
|
17019
|
+
"The main column is capped at `--region-feed` (640px) inside a `wide` page, so posts keep a reading measure while the rail fills the desktop width. The rail is a fixed `w-80` aside, `sticky` at lg and `hidden` below it — its content is supplementary, so nothing is lost on mobile.",
|
|
17020
|
+
"Feed scope is `Tabs` (underline, stretched to the column with `flex`): peer views of the same feed. The composer sits above them because posting applies to both.",
|
|
17021
|
+
"The composer is a Card holding `Textarea autoResize` plus ghost icon Buttons and a secondary `Post` — not ChatComposer, which submits on Enter and is for conversations.",
|
|
17022
|
+
"Each post is a Card: an Entity byline in a CardList (with a DotsMenu for post actions), body copy, then media in an AspectBox at the asset's own ratio, inset with `rounded-concentric-lg/6` so its corners follow the card's in every theme.",
|
|
17023
|
+
"An edition drop adds a `Badge appearance=\"overlay\"` on the artwork, title and price, the one primary Button on the page (`Collect`), and a `Meter` for supply — collected against a known maximum is a level, not task progress.",
|
|
17024
|
+
"Actions are ghost sm Buttons (icon + count) in a Row pulled out by the button's own padding (`-ms-3`) so the first icon sits on the text edge.",
|
|
17025
|
+
"Loading the next page shows a Skeleton post in the shape the post will land in, above the explicit load-more Button in its `loading` state — predictable for keyboard and AT users, unlike infinite scroll."
|
|
17105
17026
|
],
|
|
17106
17027
|
"a11y": [
|
|
17107
|
-
"Each post is an <article>, so AT users can jump between posts.",
|
|
17108
|
-
"
|
|
17109
|
-
"
|
|
17028
|
+
"Each post is an <article>, so AT users can jump between posts; the rail is a labelled <aside> landmark.",
|
|
17029
|
+
"Rail card titles carry role=\"heading\" aria-level=2 under the page's one h1.",
|
|
17030
|
+
"Icon-only buttons and every Follow / Remind button carry aria-labels naming their target; counts stay in the accessible name.",
|
|
17031
|
+
"The supply Meter is role=meter with a getAriaValueText reading (\"62 of 100 collected\").",
|
|
17032
|
+
"The Skeleton card is aria-busy; the loading Button keeps its label (\"Loading posts\")."
|
|
17110
17033
|
],
|
|
17111
|
-
"imports": "import { AspectBox, Avatar, Button, Card, CardContent, CardList, Entity, Icon, Page, PageHeader, Row, Stack } from \"@sableui/react\"",
|
|
17112
|
-
"code": "
|
|
17034
|
+
"imports": "import { AspectBox, Avatar, Badge, Button, Card, CardContent, CardHeader, CardList, CardTitle, DotsMenu, Entity, Icon, MenuItem, MenuSeparator, Meter, MeterLabel, MeterTrack, MeterValue, Page, PageHeader, Row, Skeleton, Stack, Tabs, TabsList, TabsPanel, TabsTrigger, Textarea } from \"@sableui/react\"",
|
|
17035
|
+
"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<DotsMenu 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</DotsMenu>\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 role=\"heading\" aria-level={2}>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 role=\"heading\" aria-level={2}>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}"
|
|
17113
17036
|
},
|
|
17114
17037
|
{
|
|
17115
17038
|
"name": "LandingPage",
|
|
17116
17039
|
"path": "patterns/landing-page.tsx",
|
|
17117
|
-
"summary": "
|
|
17118
|
-
"intent": "Reach for this for the marketing
|
|
17040
|
+
"summary": "Product / open-source landing page: a centered hero with an eyebrow Badge, one display headline, lede, two CTAs and a copyable install command; a Browser-framed product preview composed from real components; a six-up feature grid; a setup section with tabbed code; an agent-workflow section with terminal output; a CTA card; and a link-column footer.",
|
|
17041
|
+
"intent": "Reach for this for the marketing route of a developer product, library, or tool: the page where the product shows itself working and the reader installs or signs up. For a branded, editorial marketing site use BrandLandingPage. Not for product UI (display type and one-per-view rules forbid it inside the app).",
|
|
17119
17042
|
"notes": [
|
|
17120
|
-
"Display type is the marketing voice: exactly one display headline per page
|
|
17121
|
-
"The eyebrow is a small outline Badge (`size=\"sm\"`)
|
|
17122
|
-
"The lede is body-lg capped at the prose measure
|
|
17123
|
-
"Two CTAs max: one primary, one quiet outline.
|
|
17124
|
-
"
|
|
17125
|
-
"
|
|
17043
|
+
"Display type is the marketing voice: exactly one display headline per page (`text-display-lg` on phones, `text-display-2xl` from `sm`). Every section below uses `text-heading-1` with a `text-overline` kicker.",
|
|
17044
|
+
"The eyebrow is a small outline Badge (`size=\"sm\"`): the badge's own size, never a type class forced onto its label. Hierarchy above the fold is eyebrow, display, lede, CTAs, install command.",
|
|
17045
|
+
"The lede is body-lg capped at the prose measure. The headline may be wide; reading text may not.",
|
|
17046
|
+
"Two CTAs max per row: one primary, one quiet outline. The install line is a `CodeBlock` with `prompt=\"$\"`, so it truncates on one line and copy takes only the command.",
|
|
17047
|
+
"Show the product instead of describing it. The preview is real components (PageHeader, metric Cards, a Meter, a CardList of Entity rows, Message and Bubble, ChatComposer) inside `Browser`, wrapped in an `inert` div so the picture's controls never take focus. Secondary columns hide on small screens rather than stacking into a long mock.",
|
|
17048
|
+
"Features are Cards in a fixed `Grid cols={{ base: 1, sm: 2, lg: 3 }}`, so six items always land as full rows (3 x 2, 2 x 3, 1 x 6) without an orphan.",
|
|
17049
|
+
"Multi-line code uses `CodeBlock` with a filename; switch related snippets with `Tabs`, defaulting to the most complete one. Terminal output passes `highlight={false}` so a code highlighter does not colour prose.",
|
|
17050
|
+
"Reveal wraps below-the-fold blocks only (features, code, CTA). It no-ops under reduced motion via the motion policy.",
|
|
17051
|
+
"Hide nav groups on narrow screens by wrapping them in a `max-md:hidden` div: a `Row` or `Button` sets its own display, which a `hidden` class on the same element does not reliably override."
|
|
17126
17052
|
],
|
|
17127
17053
|
"a11y": [
|
|
17128
|
-
"The hero headline is the page h1; feature titles are h2s
|
|
17054
|
+
"The hero headline is the page h1; section titles are h2s and feature and step titles are h3s. Footer column labels are h2s inside the footer nav.",
|
|
17055
|
+
"The product preview is a `figure` with a visible figcaption. Its content is `inert`, which removes it from the tab order and the accessibility tree, so its headings and buttons do not pollute the outline.",
|
|
17129
17056
|
"Reveal renders content immediately (no animation) when prefers-reduced-motion is set.",
|
|
17130
|
-
"CTA buttons are real links
|
|
17057
|
+
"CTA buttons are real links with visible text, with no icon-only CTAs. Footer links keep a 36px minimum row height for touch."
|
|
17131
17058
|
],
|
|
17132
|
-
"imports": "import { Badge, Button, Card, CardContent, Grid, Icon, Region, Reveal, Row, Stack } from \"@sableui/react\"",
|
|
17133
|
-
"code": "const features = [\n\t{\n\t\ticon: \"sparkles\" as const,\n\t\ttitle: \"Own the surface\",\n\t\tbody: \"Semantic tokens end-to-end — re-theme the whole product without touching a component.\",\n\t},\n\t{\n\t\ticon: \"component\" as const,\n\t\ttitle: \"Behavior included\",\n\t\tbody: \"Focus, keyboard nav, and ARIA ride on Base UI primitives under every surface.\",\n\t},\n\t{\n\t\ticon: \"layout\" as const,\n\t\ttitle: \"Pages, not just parts\",\n\t\tbody: \"Purpose-named regions, an 8px rhythm, and canonical page shells for the routes every product needs.\",\n\t},\n]\n\nexport function LandingPage() {\n\treturn (\n\t\t<div className=\"min-h-dvh bg-background\">\n\t\t\t<header>\n\t\t\t\t<Region as=\"nav\" aria-label=\"Main\" className=\"grid h-16 grid-cols-[1fr_auto_1fr] items-center\">\n\t\t\t\t\t<span className=\"justify-self-start text-title-sm\">Sable</span>\n\t\t\t\t\t<Row gap={1} className=\"justify-self-center max-sm:hidden\">\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/docs\" />}>\n\t\t\t\t\t\t\tDocs\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/patterns\" />}>\n\t\t\t\t\t\t\tPatterns\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" render={<a href=\"/themes\" />}>\n\t\t\t\t\t\t\tThemes\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Row>\n\t\t\t\t\t<Button size=\"sm\" className=\"justify-self-end\" render={<a href=\"/signup\" />}>\n\t\t\t\t\t\tGet started\n\t\t\t\t\t</Button>\n\t\t\t\t</Region>\n\t\t\t</header>\n\t\t\t<main>\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"py-24\">\n\t\t\t\t\t<Stack gap={4} align=\"center\" className=\"text-center\">\n\t\t\t\t\t\t<Badge appearance=\"outline\" size=\"sm\">\n\t\t\t\t\t\t\tNow in beta\n\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t<h1 className=\"max-w-4xl text-balance text-display-2xl\">\n\t\t\t\t\t\t\tShip product UI your agents can't get wrong\n\t\t\t\t\t\t</h1>\n\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\tAn owned, themeable component system with enforced tokens, typography,\n\t\t\t\t\t\t\tand motion — documented for machines as carefully as for people.\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<Row gap={2} justify=\"center\">\n\t\t\t\t\t\t\t<Button size=\"cta\">Get started</Button>\n\t\t\t\t\t\t\t<Button size=\"cta\" variant=\"outline\">\n\t\t\t\t\t\t\t\tRead the docs\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</Row>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Region>\n\t\t\t\t<Region as=\"section\" max=\"wide\" className=\"pb-24\">\n\t\t\t\t\t<Reveal>\n\t\t\t\t\t\t{/* Three-up that reflows on its own width (minItemWidth), one Card\n\t\t\t\t\t\t per feature — never a hand-drawn bordered grid. */}\n\t\t\t\t\t\t<Grid minItemWidth=\"calc(var(--spacing) * 64)\">\n\t\t\t\t\t\t\t{features.map((feature) => (\n\t\t\t\t\t\t\t\t<Card key={feature.title}>\n\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<Icon name={feature.icon} className=\"text-xl text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t\t<h2 className=\"text-title-lg\">{feature.title}</h2>\n\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</CardContent>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Grid>\n\t\t\t\t\t</Reveal>\n\t\t\t\t</Region>\n\t\t\t</main>\n\t\t\t<footer className=\"border-t border-border\">\n\t\t\t\t<Region className=\"flex h-16 items-center justify-between\">\n\t\t\t\t\t<span className=\"text-caption text-muted-foreground\">© 2026 Sable</span>\n\t\t\t\t\t<Row gap={3}>\n\t\t\t\t\t\t<a href=\"/github\" className=\"text-caption text-muted-foreground motion-interactive hover:text-foreground\">\n\t\t\t\t\t\t\tGitHub\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\t<a href=\"/changelog\" className=\"text-caption text-muted-foreground motion-interactive hover:text-foreground\">\n\t\t\t\t\t\t\tChangelog\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</Row>\n\t\t\t\t</Region>\n\t\t\t</footer>\n\t\t</div>\n\t)\n}"
|
|
17059
|
+
"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\"",
|
|
17060
|
+
"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}"
|
|
17134
17061
|
},
|
|
17135
17062
|
{
|
|
17136
17063
|
"name": "ListDetailPage",
|
|
17137
17064
|
"path": "patterns/list-detail-page.tsx",
|
|
17138
|
-
"summary": "
|
|
17139
|
-
"intent": "Reach for this when a route lists records AND
|
|
17065
|
+
"summary": "Searchable list beside the selected record's detail: interactive Entity rows in a CardList (5 of 12 columns) and a sticky detail pane (7) with an editable field, a label/value list, record actions with a destructive confirmation, and recent activity. Below lg it becomes a drill-in.",
|
|
17066
|
+
"intent": "Reach for this when a route lists records AND works on one at a time (members + member, inbox + message, orders + order). For pure browsing with no selection, a single-column FeedPage or a DashboardPage table is simpler.",
|
|
17140
17067
|
"notes": [
|
|
17141
|
-
"Region cap is `wide`;
|
|
17142
|
-
"
|
|
17143
|
-
"
|
|
17144
|
-
"
|
|
17145
|
-
"The
|
|
17068
|
+
"Region cap is `wide`; Columns 12 → Col 5 (list) + Col 7 (detail). Below lg the list and detail share one column as a drill-in: selecting a row shows the detail with an `All members` back button (lg:hidden), and the back button returns to the list. The panes swap with `max-lg:hidden`, so desktop always shows both.",
|
|
17069
|
+
"Rows are interactive Entity inside one Card + CardList. Selection is `data-selected` on the row painted by `sable-surface-interactive`; `sable-surface` on the list card makes that step 12% ink off the card, so a selected row stays distinct from its muted avatar.",
|
|
17070
|
+
"Search filters the list in place (name, email, role). No matches swap the CardList for an EmptyState with a Clear search action; the selected detail stays open.",
|
|
17071
|
+
"A pending invite lives in the same list: a mail glyph in the Avatar, an `Invited` Badge, and an invite-specific detail (role on joining, sent, expires, invited by; Resend and Revoke actions).",
|
|
17072
|
+
"The role Select edits the record in place; its Field description states what the role can do. The owner's role is read-only and the owner has no remove action.",
|
|
17073
|
+
"Read-only facts are a native `<dl className=\"sable-list-dl sable-list-dl-2\">` inside an `@container`, so pairs go two-up only when the pane is wide enough.",
|
|
17074
|
+
"Remove / revoke run through DestructiveActionModal; on confirm the next row is selected. Page-level counts (members, pending, seats) live in the PageHeader description and update with the list.",
|
|
17075
|
+
"The detail pane is sticky (lg:top-6) so it stays in view while a long list scrolls."
|
|
17146
17076
|
],
|
|
17147
17077
|
"a11y": [
|
|
17148
|
-
"
|
|
17149
|
-
"The
|
|
17078
|
+
"Each row is a real button (Entity `interactive` + `onSelect`): Tab moves through rows, Enter/Space selects. The selected row carries visually hidden `(selected)` text in its name; list avatars are aria-hidden so initials are not read before the name.",
|
|
17079
|
+
"The detail pane is a labelled region (`Member details`) whose title is a level-2 heading.",
|
|
17080
|
+
"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."
|
|
17150
17081
|
],
|
|
17151
|
-
"imports": "import { Avatar, Badge, Button, Card, CardContent, CardHeader, CardList, CardTitle, Col, Columns,
|
|
17152
|
-
"code": "const members = [\n\t{ id: \"u_01\", name: \"Ada Lovelace\", handle: \"@ada\", role: \"Owner\", initials: \"AL\", pending: false },\n\t{ id: \"u_02\", name: \"Grace Hopper\", handle: \"@grace\", role: \"Admin\", initials: \"GH\", pending: false },\n\t{ id: \"u_03\", name: \"Mary Shelley\", handle: \"@mary\", role: \"Member\", initials: \"MS\", pending: false },\n\t{ id: \"u_04\", name: \"Nina Simone\", handle: \"@nina\", role: \"Member\", initials: \"NS\", pending: false },\n\t{ id: \"u_05\", name: \"Frida Kahlo\", handle: \"@frida\", role: \"Member\", initials: \"FK\", pending: false },\n\t{ id: \"u_06\", name: \"hopper@navy.mil\", handle: \"invited 2 days ago\", role: \"Member\", initials: \"?\", pending: true },\n]\n\nexport function ListDetailPage() {\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=\"Everyone with access to this workspace.\"\n\t\t\t\tactions={<Button>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: 8 }}>\n\t\t\t\t\t<Stack gap={3}>\n\t\t\t\t\t\t<SearchInput placeholder=\"Search members\" aria-label=\"Search members\" />\n\t\t\t\t\t\t<Card className=\"overflow-hidden\">\n\t\t\t\t\t\t\t<CardList>\n\t\t\t\t\t\t\t\t\t{members.map((member) => (\n\t\t\t\t\t\t\t\t\t\t<Entity\n\t\t\t\t\t\t\t\t\t\t\tkey={member.id}\n\t\t\t\t\t\t\t\t\t\t\tleading={\n\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\tfallback={member.pending ? \"•\" : member.initials}\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={member.pending ? \"\" : member.name}\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\ttitle={\n\t\t\t\t\t\t\t\t\t\t\t\t<span className={member.pending ? \"text-muted-foreground\" : undefined}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{member.name}\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}\n\t\t\t\t\t\t\t\t\t\t\tsubtitle={member.handle}\n\t\t\t\t\t\t\t\t\t\t\taction={\n\t\t\t\t\t\t\t\t\t\t\t\tmember.pending ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge>Invited</Badge>\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<Badge appearance=\"outline\">{member.role}</Badge>\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</CardList>\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: 4 }}>\n\t\t\t\t\t<aside className=\"sticky top-6\">\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>Workspace</CardTitle>\n\t\t\t\t\t\t\t</CardHeader>\n\t\t\t\t\t\t\t<CardContent className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t\t\t{/* Label/value pairs are a Description list, not hand-built rows. */}\n\t\t\t\t\t\t\t\t<Description>\n\t\t\t\t\t\t\t\t\t<DescriptionItem term=\"Members\" detail={<span className=\"text-mono-md\">5</span>} />\n\t\t\t\t\t\t\t\t\t<DescriptionItem term=\"Pending invites\" detail={<span className=\"text-mono-md\">1</span>} />\n\t\t\t\t\t\t\t\t\t<DescriptionItem term=\"Seats used\" detail={<span className=\"text-mono-md\">6 / 10</span>} />\n\t\t\t\t\t\t\t\t</Description>\n\t\t\t\t\t\t\t\t<Button variant=\"outline\" size=\"sm\" className=\"mt-1 self-start\">\n\t\t\t\t\t\t\t\t\tManage plan\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</aside>\n\t\t\t\t</Col>\n\t\t\t</Columns>\n\t\t</Page>\n\t)\n}"
|
|
17082
|
+
"imports": "import { useState, useEffect } 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\"",
|
|
17083
|
+
"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\tdata-selected={isSelected ? \"\" : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t// No selected variant on Entity yet: the relative-surface recipe paints\n\t\t\t\t\t\t\t\t\t\t\t\t// data-selected one step off the card.\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"sable-surface-interactive\"\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\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={\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{m.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isSelected ? <span className=\"sr-only\"> (selected)</span> : null}\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\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 role=\"heading\" aria-level={2} 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 role=\"heading\" aria-level={3}>\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}"
|
|
17153
17084
|
},
|
|
17154
17085
|
{
|
|
17155
17086
|
"name": "SettingsPage",
|
|
17156
17087
|
"path": "patterns/settings-page.tsx",
|
|
17157
|
-
"summary": "
|
|
17088
|
+
"summary": "Account settings inside the app shell: a sticky SideNav section rail beside a column of section Cards — a Profile form with the page's one explicit save, Card + CardList + Entity rows with Switches that apply instantly, a read-only payouts summary, account rows, and a delete card confirmed through DestructiveActionModal.",
|
|
17158
17089
|
"intent": "Reach for this for account/profile/workspace settings and any 'edit a handful of grouped preferences' route. Not for a single short form (use AuthPage's centered form shape) or bulk record editing (use DashboardPage + DataTable).",
|
|
17159
17090
|
"notes": [
|
|
17160
|
-
"Region cap is `
|
|
17161
|
-
"
|
|
17162
|
-
"
|
|
17163
|
-
"
|
|
17164
|
-
"
|
|
17091
|
+
"Region cap is `wide`, split on a 12-column track: a 3-column SideNav rail and a 9-column section column. The rail fills the space a lone content-width column would leave empty and gives the page its table of contents.",
|
|
17092
|
+
"The rail is `SideNav` (the documented settings sub-nav), sticky, with in-page anchor items; it hides below lg, where the sections simply stack and the shell's BottomNav carries app navigation.",
|
|
17093
|
+
"One save model, stated in the copy: drafted text (the Profile form) commits through its single footer save; toggles apply the moment they change, and the Notifications description says so. Never pair a Switch with a Save button.",
|
|
17094
|
+
"Settings rows are Card + CardList + Entity with the Switch in `action` — the row owns its title and one-line description; keep descriptions short, because Entity truncates its subtitle.",
|
|
17095
|
+
"Each section is a Card whose CardTitle holds an h2, so the page has a real heading outline under PageHeader's h1; Cards carry the anchor id and `scroll-mt-*` so anchor jumps clear the top edge.",
|
|
17096
|
+
"Field wires label/description/error a11y automatically; never hand-roll <label> + <input> pairs. Pair fields side by side with Grid cols={{ base: 1, sm: 2 }}.",
|
|
17097
|
+
"The destructive action is last and isolated in its own Card, and it never acts directly: it opens DestructiveActionModal with type-to-confirm on the handle.",
|
|
17165
17098
|
"Lives in the same AppShell as DashboardPage: Sidebar from lg up, AppHeader + BottomNav below it, with Settings marked active in both."
|
|
17166
17099
|
],
|
|
17167
17100
|
"a11y": [
|
|
17168
17101
|
"AppShell provides the skip link and <main id=\"main-content\"> landmark; the active SidebarItem and BottomNavItem carry aria-current.",
|
|
17169
|
-
"
|
|
17102
|
+
"The section rail is a labelled <nav> (\"Settings sections\") distinct from the primary navigation; its active item carries aria-current.",
|
|
17103
|
+
"Every section has an h2, so screen-reader heading navigation mirrors the rail.",
|
|
17104
|
+
"Each Switch takes its row title as aria-label, since the Entity title is not a <label>.",
|
|
17170
17105
|
"Field auto-wires htmlFor/id, aria-describedby (description + error), aria-invalid, and required.",
|
|
17171
|
-
"
|
|
17106
|
+
"DestructiveActionModal is an alertdialog: focus opens on Cancel, the backdrop does not dismiss, and the confirm stays disabled until the handle is typed."
|
|
17172
17107
|
],
|
|
17173
|
-
"imports": "import { AppHeader, AppShell, BottomNav, BottomNavItem, Button,
|
|
17174
|
-
"code": "export function SettingsPage() {\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={<AppHeader title={<span className=\"text-title-sm\">Atelier</span>} />}\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<span className=\"text-title-sm\">Atelier</span>\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 icon={<Icon name=\"bell\" />} label=\"Activity\" render={<a href=\"/activity\" />} />\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</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=\"content\" gap={4} className=\"py-6\">\n\t\t\t\t<PageHeader\n\t\t\t\t\ttitle=\"Settings\"\n\t\t\t\t\tdescription=\"Your profile, notifications, and account controls.\"\n\t\t\t\t/>\n\t\t\t\t<Form onSubmit={(event) => event.preventDefault()}>\n\t\t\t\t\t<Fieldset>\n\t\t\t\t\t\t<FieldsetLegend>Profile</FieldsetLegend>\n\t\t\t\t\t\t<FieldsetDescription>How you appear across the app.</FieldsetDescription>\n\t\t\t\t\t\t<FieldsetContent>\n\t\t\t\t\t\t\t<Field label=\"Display name\" required>\n\t\t\t\t\t\t\t\t<Input name=\"displayName\" defaultValue=\"ada\" />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t<Field label=\"Bio\" description=\"Shown on your public profile.\">\n\t\t\t\t\t\t\t\t<Textarea name=\"bio\" rows={3} placeholder=\"Tell collectors who you are\" />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t<Field label=\"Timezone\">\n\t\t\t\t\t\t\t\t<Select defaultValue=\"utc\" name=\"timezone\">\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<SelectItem value=\"utc\">UTC</SelectItem>\n\t\t\t\t\t\t\t\t\t\t<SelectItem value=\"est\">Eastern (UTC−5)</SelectItem>\n\t\t\t\t\t\t\t\t\t\t<SelectItem value=\"pst\">Pacific (UTC−8)</SelectItem>\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</FieldsetContent>\n\t\t\t\t\t\t<FieldsetFooter>\n\t\t\t\t\t\t\t<Button type=\"submit\">Save profile</Button>\n\t\t\t\t\t\t</FieldsetFooter>\n\t\t\t\t\t</Fieldset>\n\t\t\t\t</Form>\n\t\t\t\t<Form onSubmit={(event) => event.preventDefault()}>\n\t\t\t\t\t<Fieldset>\n\t\t\t\t\t\t<FieldsetLegend>Notifications</FieldsetLegend>\n\t\t\t\t\t\t<FieldsetDescription>What we email you about.</FieldsetDescription>\n\t\t\t\t\t\t<FieldsetContent>\n\t\t\t\t\t\t\t<Field label=\"New collector alerts\" orientation=\"horizontal\">\n\t\t\t\t\t\t\t\t<Switch name=\"collectorAlerts\" defaultChecked />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t\t<Field label=\"Weekly digest\" orientation=\"horizontal\">\n\t\t\t\t\t\t\t\t<Switch name=\"weeklyDigest\" />\n\t\t\t\t\t\t\t</Field>\n\t\t\t\t\t\t</FieldsetContent>\n\t\t\t\t\t\t<FieldsetFooter>\n\t\t\t\t\t\t\t<Button type=\"submit\">Save notifications</Button>\n\t\t\t\t\t\t</FieldsetFooter>\n\t\t\t\t\t</Fieldset>\n\t\t\t\t</Form>\n\t\t\t\t<Fieldset>\n\t\t\t\t\t<FieldsetLegend>Danger zone</FieldsetLegend>\n\t\t\t\t\t<FieldsetDescription>\n\t\t\t\t\t\tDeleting your account removes your posts and collections permanently.\n\t\t\t\t\t</FieldsetDescription>\n\t\t\t\t\t<FieldsetFooter>\n\t\t\t\t\t\t<Button variant=\"destructive\">Delete account</Button>\n\t\t\t\t\t</FieldsetFooter>\n\t\t\t\t</Fieldset>\n\t\t\t</Page>\n\t\t</AppShell>\n\t)\n}"
|
|
17108
|
+
"imports": "import { useState } from \"react\"\nimport { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, DotsMenu, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from \"@sableui/react\"",
|
|
17109
|
+
"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{/* Entity and DotsMenu ink with page tokens; a theme can paint the rail\n\t\t\t\t\t dark on a light page, so this subtree reads the rail's ink instead. */}\n\t\t\t\t\t<SidebarFooter className=\"[--foreground:var(--sidebar-foreground)]\">\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\taction={\n\t\t\t\t\t\t\t\t<DotsMenu 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</DotsMenu>\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>\n\t\t\t\t\t\t\t\t\t\t<h2>Profile</h2>\n\t\t\t\t\t\t\t\t\t</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>\n\t\t\t\t\t\t\t\t\t\t<h2>Notifications</h2>\n\t\t\t\t\t\t\t\t\t</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>\n\t\t\t\t\t\t\t\t\t\t<h2>Payouts</h2>\n\t\t\t\t\t\t\t\t\t</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>\n\t\t\t\t\t\t\t\t\t\t<h2>Account</h2>\n\t\t\t\t\t\t\t\t\t</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>\n\t\t\t\t\t\t\t\t\t\t<h2>Delete account</h2>\n\t\t\t\t\t\t\t\t\t</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}"
|
|
17175
17110
|
}
|
|
17176
17111
|
]
|
|
17177
17112
|
},
|
|
@@ -17590,7 +17525,7 @@
|
|
|
17590
17525
|
},
|
|
17591
17526
|
{
|
|
17592
17527
|
"rule": "A list of rows inside a rounded popup uses popup-inset on the popup, rounded-popup-item on the rows and popup-bleed on a separator.",
|
|
17593
|
-
"why": "All three derive from the popup's radius: the inset grows to a third of it (never below --spacing(1), so density moves it too), the rows take what is left, and a separator bleeds by exactly the inset. A rounder look gets more room instead of rows that round into pills
|
|
17528
|
+
"why": "All three derive from the popup's radius: the inset grows to a third of it (never below --spacing(1), so density moves it too), the rows take what is left after the inset and the 1px border, and a separator bleeds by exactly the inset. A rounder look gets more room instead of rows that round into pills; the default ladder lands on a 4px inset and 7px rows.",
|
|
17594
17529
|
"enforcedBy": "sable-lint: popup-separator-bleed"
|
|
17595
17530
|
},
|
|
17596
17531
|
{
|
|
@@ -18357,6 +18292,25 @@
|
|
|
18357
18292
|
"sable-surface-popover"
|
|
18358
18293
|
]
|
|
18359
18294
|
},
|
|
18295
|
+
{
|
|
18296
|
+
"name": "sable-frame-tool",
|
|
18297
|
+
"kind": "utility",
|
|
18298
|
+
"family": "frame",
|
|
18299
|
+
"intent": "The frame for edge-to-edge tool chrome at the theme's radius, with no padding. It publishes concentric geometry for what's inside: `--panel-inset` (row side padding, grown so rows clear the corner), `--panel-field-radius` (compact fields curve parallel to the frame) and `--panel-edge` (the first and last section's extra top and bottom). PanelSection and the xs field shell read them. Applied over `sable-frame`.",
|
|
18300
|
+
"whenToUse": "Framing PanelSections, ScrubFields, a Tree, a StatusBar or a PanelGroup outside a Workspace (a docs demo, a floating inspector). Add `overflow-hidden` to clip to the corners.",
|
|
18301
|
+
"insteadOf": "`rounded-lg border overflow-hidden` by hand: the panel keeps its square-column 8px inset and small field radius, so a rounded preset crowds the first label and last field into the curve.",
|
|
18302
|
+
"example": "<div className=\"sable-frame sable-frame-tool overflow-hidden bg-panel\" data-density=\"compact\"><PanelSection title=\"Layer\">…</PanelSection></div>",
|
|
18303
|
+
"keywords": [
|
|
18304
|
+
"panel frame",
|
|
18305
|
+
"inspector frame",
|
|
18306
|
+
"tool panel",
|
|
18307
|
+
"workspace demo",
|
|
18308
|
+
"concentric"
|
|
18309
|
+
],
|
|
18310
|
+
"pairsWith": [
|
|
18311
|
+
"sable-frame"
|
|
18312
|
+
]
|
|
18313
|
+
},
|
|
18360
18314
|
{
|
|
18361
18315
|
"name": "sable-frame-borderless",
|
|
18362
18316
|
"kind": "utility",
|
|
@@ -18431,7 +18385,7 @@
|
|
|
18431
18385
|
"name": "rounded-concentric-*",
|
|
18432
18386
|
"kind": "utility",
|
|
18433
18387
|
"family": "geometry",
|
|
18434
|
-
"intent": "A border radius computed as an outer radius minus an inset: `rounded-concentric-<radius>/<spacing>`,
|
|
18388
|
+
"intent": "A border radius computed as an outer radius minus an inset: `rounded-concentric-<radius>/<spacing>`, floored at `radius-xs` so a deep inset never leaves a 1-2px sliver. `rounded-concentric-md/1` = `radius-md` − `spacing-1`.",
|
|
18435
18389
|
"whenToUse": "A one-off rounded element nested inside another — a tab trigger inside a tab list, a thumb inside a track, a row inside a well — where a `sable-frame` would be overkill. The outer radius and the inset are the parent's.",
|
|
18436
18390
|
"insteadOf": "Picking the next ladder step down by eye (`rounded-sm` inside `rounded-md p-1`). That is concentric only at the default ladder; the utility recomputes from live tokens. Registered in `cn()` as a radius, so it correctly overrides a component's own `rounded-*`.",
|
|
18437
18391
|
"example": "<div className=\"sable-well rounded-md p-1\"><button className=\"rounded-concentric-md/1 px-2 py-1\">…</button></div>",
|
|
@@ -18473,7 +18427,7 @@
|
|
|
18473
18427
|
"family": "geometry",
|
|
18474
18428
|
"intent": "The row radius that nests inside a `popup-inset` popup: `radius-md` minus the popup inset, clamped at zero.",
|
|
18475
18429
|
"whenToUse": "On every row inside a `popup-inset` container.",
|
|
18476
|
-
"insteadOf": "`rounded-sm` on menu rows, which matches
|
|
18430
|
+
"insteadOf": "`rounded-sm` on menu rows, which matches no ladder exactly (a 12px popup with a 4px inset and 1px border wants 7px rows).",
|
|
18477
18431
|
"example": "<div role=\"option\" className=\"rounded-popup-item px-2 py-1.5 sable-highlighted:bg-accent\">…</div>",
|
|
18478
18432
|
"keywords": [
|
|
18479
18433
|
"menu item radius",
|
|
@@ -18622,6 +18576,75 @@
|
|
|
18622
18576
|
"scrollbar-quiet"
|
|
18623
18577
|
]
|
|
18624
18578
|
},
|
|
18579
|
+
{
|
|
18580
|
+
"name": "sable-list",
|
|
18581
|
+
"kind": "utility",
|
|
18582
|
+
"family": "text",
|
|
18583
|
+
"intent": "A bulleted list as typography: body type, a hanging muted marker, and item rhythm. Put it on a native `<ul>`.",
|
|
18584
|
+
"whenToUse": "Running content with bullets: release notes, a feature list, steps that are not ordered.",
|
|
18585
|
+
"insteadOf": "A stack of `<div>`s with a hand-drawn dot, or an unstyled `<ul>` that inherits the reset (no markers, no indent).",
|
|
18586
|
+
"example": "<ul className=\"sable-list\"><li>Faster sync</li><li>Offline drafts</li></ul>",
|
|
18587
|
+
"keywords": [
|
|
18588
|
+
"bullet list",
|
|
18589
|
+
"unordered list",
|
|
18590
|
+
"ul",
|
|
18591
|
+
"bullets",
|
|
18592
|
+
"list"
|
|
18593
|
+
]
|
|
18594
|
+
},
|
|
18595
|
+
{
|
|
18596
|
+
"name": "sable-list-numbered",
|
|
18597
|
+
"kind": "utility",
|
|
18598
|
+
"family": "text",
|
|
18599
|
+
"intent": "A numbered list as typography: `sable-list` with decimal markers in tabular figures. Put it on a native `<ol>`.",
|
|
18600
|
+
"whenToUse": "Ordered content in running text: setup steps, a ranked list. For a stepped flow with state, use Stepper.",
|
|
18601
|
+
"insteadOf": "Numbers typed into the text, which neither renumber nor read as a list to assistive tech.",
|
|
18602
|
+
"example": "<ol className=\"sable-list-numbered\"><li>Install</li><li>Import the styles</li></ol>",
|
|
18603
|
+
"keywords": [
|
|
18604
|
+
"numbered list",
|
|
18605
|
+
"ordered list",
|
|
18606
|
+
"ol",
|
|
18607
|
+
"steps"
|
|
18608
|
+
]
|
|
18609
|
+
},
|
|
18610
|
+
{
|
|
18611
|
+
"name": "sable-list-dl",
|
|
18612
|
+
"kind": "utility",
|
|
18613
|
+
"family": "text",
|
|
18614
|
+
"intent": "Label/value pairs on a native `<dl>`: muted label-tier `<dt>`s and body-tier `<dd>`s on a baseline-aligned two-column grid; long values wrap inside their column. Colour a status value with a `text-*` utility on its `<dd>`.",
|
|
18615
|
+
"whenToUse": "Read-only details: an order summary, a record's metadata, a settings summary, profile stats.",
|
|
18616
|
+
"insteadOf": "A grid of `<div>`s (screen readers lose the term/value pairing), or Table for a single record. For editable values use Form and Field.",
|
|
18617
|
+
"example": "<dl className=\"sable-list-dl\"><dt>Network</dt><dd>Solana</dd><dt>Status</dt><dd className=\"text-success\">Active</dd></dl>",
|
|
18618
|
+
"keywords": [
|
|
18619
|
+
"description list",
|
|
18620
|
+
"definition list",
|
|
18621
|
+
"dl",
|
|
18622
|
+
"key value",
|
|
18623
|
+
"metadata",
|
|
18624
|
+
"details",
|
|
18625
|
+
"label value"
|
|
18626
|
+
],
|
|
18627
|
+
"pairsWith": [
|
|
18628
|
+
"sable-list-dl-2"
|
|
18629
|
+
]
|
|
18630
|
+
},
|
|
18631
|
+
{
|
|
18632
|
+
"name": "sable-list-dl-2",
|
|
18633
|
+
"kind": "utility",
|
|
18634
|
+
"family": "text",
|
|
18635
|
+
"intent": "Two label/value pairs a row once the nearest `@container` ancestor is at least 28rem wide. Add to `sable-list-dl`.",
|
|
18636
|
+
"whenToUse": "A compact metadata block in a wide card or panel. Put `@container` on a parent so it measures the room it actually has.",
|
|
18637
|
+
"insteadOf": "A viewport breakpoint, which gives a narrow card on a wide screen four columns it cannot fit.",
|
|
18638
|
+
"example": "<div className=\"@container\"><dl className=\"sable-list-dl sable-list-dl-2\">…</dl></div>",
|
|
18639
|
+
"keywords": [
|
|
18640
|
+
"two column details",
|
|
18641
|
+
"metadata grid",
|
|
18642
|
+
"dl columns"
|
|
18643
|
+
],
|
|
18644
|
+
"pairsWith": [
|
|
18645
|
+
"sable-list-dl"
|
|
18646
|
+
]
|
|
18647
|
+
},
|
|
18625
18648
|
{
|
|
18626
18649
|
"name": "text-overline",
|
|
18627
18650
|
"kind": "utility",
|