@sableui/react 0.0.0-stage → 0.1.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.
Files changed (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +50 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. package/third-party/reicon-LICENSE.txt +20 -0
@@ -0,0 +1,423 @@
1
+ # BrandLandingPage
2
+
3
+ High-fidelity brand/product landing page: pill navigation, editorial hero type, a dark product scene, calm status tones, CTA pair, feature cards, metrics, and a complete branded footer.
4
+
5
+ **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.
6
+
7
+ ## Notes
8
+
9
+ - One display headline owns the first viewport; supporting copy stays in body-lg at prose measure.
10
+ - The nav spends one compact pill surface, with a clear brand signal and two primary paths: sign in and request access.
11
+ - The product scene uses overlay tokens for a stable dark media surface, then semantic tones for status and chart signals.
12
+ - The footer closes the page with brand reassurance, a three-column sitemap, a digest signup, operational mini-metrics, and legal links.
13
+ - Every surface outside the product scene is a real Card (feature, metric and digest cards); status marks are StatusDot. The scene's nested panels take `rounded-concentric-xl/<inset>` from the shell's radius, so the corners stay parallel at any radius preset.
14
+ - Radius and shadow are theme-controlled. In Cove the same rounded-lg/xl classes read larger and softer; in other themes the structure still holds.
15
+ - Color is intentionally broad: porcelain/plum/lavender/blue/mint/saffron/coral through semantic roles, not a one-note purple gradient.
16
+
17
+ ## Accessibility
18
+
19
+ - The brand nav is a labeled nav landmark with real links and visible text.
20
+ - The footer is a real footer landmark with a labeled footer nav, visible link text, and a labeled email field.
21
+ - TextReveal preserves the full headline as one accessible label and honors reduced-motion policy.
22
+ - Status dots are paired with visible text labels so color is never the only signal.
23
+
24
+ ## Source
25
+
26
+ ```tsx
27
+ import { Badge, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, Icon, Input, Region, Row, Stack, StatusDot, TextReveal } from "@sableui/react"
28
+
29
+ const navItems = ["Platform", "Signals", "Security", "Company"]
30
+
31
+ const metrics = [
32
+ { label: "Checks cleared", value: "1.8M", detail: "in the last 30 days" },
33
+ { label: "Review time", value: "42s", detail: "median approval path" },
34
+ { label: "Spaces online", value: "284", detail: "across active teams" },
35
+ ]
36
+
37
+ const workflow = [
38
+ { label: "Intake", state: "Open", tone: "info" as const },
39
+ { label: "Review", state: "Live", tone: "edition" as const },
40
+ { label: "Archive", state: "Safe", tone: "success" as const },
41
+ ]
42
+
43
+ const bars = [
44
+ { height: "38%", tone: "bg-tone-info" },
45
+ { height: "56%", tone: "bg-tone-edition" },
46
+ { height: "44%", tone: "bg-tone-standard" },
47
+ { height: "68%", tone: "bg-success" },
48
+ { height: "52%", tone: "bg-tone-info" },
49
+ { height: "78%", tone: "bg-highlight" },
50
+ { height: "60%", tone: "bg-tone-edition" },
51
+ { height: "86%", tone: "bg-success" },
52
+ { height: "64%", tone: "bg-tone-standard" },
53
+ ]
54
+
55
+ const features = [
56
+ {
57
+ icon: "lock" as const,
58
+ title: "Private by default",
59
+ body: "Workspace roles, approval trails, and handoff history stay clear without turning the interface into compliance fog.",
60
+ },
61
+ {
62
+ icon: "layout" as const,
63
+ title: "Brand and product together",
64
+ body: "Editorial sections and dense console views share the same semantic tokens, radius, and motion contract.",
65
+ },
66
+ {
67
+ icon: "circle-check" as const,
68
+ title: "Calm confidence",
69
+ body: "Large type, tactile controls, and focused status tones make complex work feel understandable on first pass.",
70
+ },
71
+ ]
72
+
73
+ const reviews = [
74
+ { name: "Waypoint", value: "98.4%", label: "clear routes" },
75
+ { name: "Fielddesk", value: "12", label: "open holds" },
76
+ { name: "Archive", value: "4.8k", label: "sealed records" },
77
+ ]
78
+
79
+ const footerGroups = [
80
+ {
81
+ title: "Platform",
82
+ links: [
83
+ { label: "Overview", href: "#platform" },
84
+ { label: "Signals", href: "#signals" },
85
+ { label: "Approvals", href: "#approvals" },
86
+ { label: "Records", href: "#records" },
87
+ ],
88
+ },
89
+ {
90
+ title: "Company",
91
+ links: [
92
+ { label: "About", href: "/about" },
93
+ { label: "Security", href: "/security" },
94
+ { label: "Customers", href: "/customers" },
95
+ { label: "Careers", href: "/careers" },
96
+ ],
97
+ },
98
+ {
99
+ title: "Resources",
100
+ links: [
101
+ { label: "Guides", href: "/guides" },
102
+ { label: "Status", href: "/status" },
103
+ { label: "Changelog", href: "/changelog" },
104
+ { label: "Docs", href: "/docs" },
105
+ ],
106
+ },
107
+ ]
108
+
109
+ const footerSignals = [
110
+ { label: "Uptime", value: "99.98%" },
111
+ { label: "Regions", value: "12" },
112
+ { label: "Response", value: "42s" },
113
+ ]
114
+
115
+ export function BrandLandingPage() {
116
+ return (
117
+ <div className="min-h-dvh overflow-hidden bg-background text-foreground">
118
+ <header className="relative z-10 pt-4">
119
+ <Region max="wide">
120
+ <nav
121
+ aria-label="Main"
122
+ className="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"
123
+ >
124
+ <a href="/" className="flex items-center gap-2 rounded-full px-2 motion-interactive motion-press">
125
+ <span className="grid size-8 place-items-center rounded-full bg-primary text-primary-foreground">
126
+ <Icon name="sparkles" className="text-sm" />
127
+ </span>
128
+ <span className="text-title-lg">Cove</span>
129
+ </a>
130
+ <div className="hidden lg:block">
131
+ <Row gap={1}>
132
+ {navItems.map((item) => (
133
+ <Button key={item} variant="ghost" size="sm" render={<a href={`#${item.toLowerCase()}`} />}>
134
+ {item}
135
+ </Button>
136
+ ))}
137
+ </Row>
138
+ </div>
139
+ <Row gap={1}>
140
+ <Button variant="ghost" size="sm" className="max-sm:hidden" render={<a href="/login" />}>
141
+ Sign in
142
+ </Button>
143
+ <Button size="sm" render={<a href="/demo" />}>
144
+ Request access
145
+ </Button>
146
+ </Row>
147
+ </nav>
148
+ </Region>
149
+ </header>
150
+
151
+ <main>
152
+ <Region as="section" max="wide" className="pb-16 pt-14 sm:pt-20 lg:pb-24">
153
+ <div className="grid items-center gap-10 lg:grid-cols-[0.92fr_1.08fr]">
154
+ <Stack gap={4} className="max-w-3xl">
155
+ <Badge appearance="outline" icon={<Icon name="lock" className="text-sm" />}>
156
+ Private operations, made legible
157
+ </Badge>
158
+ <TextReveal
159
+ as="h1"
160
+ text="A calmer command center for high-trust digital work"
161
+ className="max-w-4xl text-balance text-display-xl sm:text-display-3xl"
162
+ once
163
+ />
164
+ <p className="max-w-(--measure-prose) text-body-lg text-muted-foreground">
165
+ Cove gives teams a polished front door and a serious operating
166
+ surface: approvals, signals, records, and handoffs in one
167
+ understandable flow.
168
+ </p>
169
+ <Row gap={2} wrap>
170
+ <Button size="cta" render={<a href="/demo" />}>
171
+ Book a walkthrough
172
+ </Button>
173
+ <Button size="cta" variant="outline" render={<a href="#platform" />}>
174
+ Explore platform
175
+ </Button>
176
+ </Row>
177
+ </Stack>
178
+
179
+ <div className="relative">
180
+ {/* Nested surfaces take concentric corners: each level's radius is
181
+ the shell's xl minus every inset between it and the shell edge. */}
182
+ <div className="overflow-hidden rounded-xl border border-border-floating bg-overlay p-3 text-overlay-foreground shadow-xl">
183
+ <div className="rounded-concentric-xl/3 border border-overlay-foreground/10 bg-overlay-foreground/8">
184
+ <div className="flex items-center justify-between border-b border-overlay-foreground/10 px-4 py-3">
185
+ <Row gap={1}>
186
+ <StatusDot tone="warning" />
187
+ <StatusDot tone="edition" />
188
+ <StatusDot tone="success" />
189
+ </Row>
190
+ <span className="text-label-sm text-overlay-foreground/70">Live workspace</span>
191
+ </div>
192
+ <div className="grid gap-3 p-3 md:grid-cols-[0.7fr_1.3fr]">
193
+ <div className="space-y-3">
194
+ <div className="rounded-concentric-xl/6 border border-overlay-foreground/10 bg-overlay-foreground/8 p-4">
195
+ <p className="text-label-sm text-overlay-foreground/60">Spaces</p>
196
+ <div className="mt-4 space-y-2">
197
+ {reviews.map((review) => (
198
+ <div key={review.name} className="rounded-concentric-xl/10 bg-overlay p-3">
199
+ <Row justify="between">
200
+ <span className="text-title-sm">{review.name}</span>
201
+ <span className="text-mono-sm text-overlay-foreground">{review.value}</span>
202
+ </Row>
203
+ <p className="mt-1 text-body-xs text-overlay-foreground/60">{review.label}</p>
204
+ </div>
205
+ ))}
206
+ </div>
207
+ </div>
208
+ <div className="rounded-concentric-xl/6 border border-overlay-foreground/10 bg-overlay-foreground/8 p-4">
209
+ <Row justify="between">
210
+ <span className="text-title-sm">Route status</span>
211
+ <Badge tone="success" size="sm">Healthy</Badge>
212
+ </Row>
213
+ <div className="mt-4 space-y-2">
214
+ {workflow.map((item) => (
215
+ <Row key={item.label} justify="between" className="rounded-full bg-overlay px-3 py-2">
216
+ <Row gap={1}>
217
+ <StatusDot tone={item.tone} />
218
+ <span className="text-body-sm">{item.label}</span>
219
+ </Row>
220
+ <span className="text-label-sm text-overlay-foreground/60">{item.state}</span>
221
+ </Row>
222
+ ))}
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <div className="rounded-concentric-xl/6 border border-overlay-foreground/10 bg-overlay-foreground/8 p-4">
228
+ <Row justify="between" className="border-b border-overlay-foreground/10 pb-4">
229
+ <div>
230
+ <p className="text-label-sm text-overlay-foreground/60">Signal map</p>
231
+ <h2 className="mt-1 text-heading-3">Every approval path in view</h2>
232
+ </div>
233
+ <Button variant="secondary" size="sm">Review</Button>
234
+ </Row>
235
+ <div className="mt-6 grid min-h-72 grid-rows-[1fr_auto] gap-5">
236
+ <div className="grid grid-cols-9 items-end gap-2 rounded-concentric-xl/10 bg-overlay p-4">
237
+ {bars.map((bar, index) => (
238
+ <div key={`${bar.tone}-${index}`} className="flex h-56 items-end">
239
+ <div
240
+ className={`w-full rounded-full ${bar.tone}`}
241
+ style={{ height: bar.height }}
242
+ />
243
+ </div>
244
+ ))}
245
+ </div>
246
+ <div className="grid gap-2 sm:grid-cols-3">
247
+ {metrics.map((metric) => (
248
+ <div key={metric.label} className="rounded-concentric-xl/10 border border-overlay-foreground/10 bg-overlay p-3">
249
+ <p className="text-label-sm text-overlay-foreground/60">{metric.label}</p>
250
+ <p className="mt-2 text-heading-3">{metric.value}</p>
251
+ <p className="mt-1 text-body-xs text-overlay-foreground/60">{metric.detail}</p>
252
+ </div>
253
+ ))}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </Region>
263
+
264
+ <section id="platform" className="border-y border-border bg-muted/40 py-14">
265
+ <Region max="wide">
266
+ <div className="grid gap-5 lg:grid-cols-[0.8fr_1.2fr]">
267
+ <Stack gap={3}>
268
+ <Badge>Design mood</Badge>
269
+ <h2 className="text-heading-1">Marketing warmth, product discipline.</h2>
270
+ <p className="max-w-(--measure-prose) text-body-md text-muted-foreground">
271
+ The page uses a pill nav, tactile surfaces, soft status tones, and
272
+ slower ease-out movement. It stays broad in color so it does not
273
+ collapse into purple-brand sameness.
274
+ </p>
275
+ </Stack>
276
+ <div className="grid gap-3 md:grid-cols-3">
277
+ {features.map((feature) => (
278
+ <Card key={feature.title} render={<article />} className="h-full">
279
+ <CardContent>
280
+ <span className="grid size-10 place-items-center rounded-md bg-highlight text-highlight-foreground">
281
+ <Icon name={feature.icon} className="text-xl" />
282
+ </span>
283
+ <h3 className="mt-5 text-title-lg">{feature.title}</h3>
284
+ <p className="mt-2 text-body-sm text-muted-foreground">{feature.body}</p>
285
+ </CardContent>
286
+ </Card>
287
+ ))}
288
+ </div>
289
+ </div>
290
+ </Region>
291
+ </section>
292
+
293
+ <Region as="section" max="wide" className="py-14">
294
+ <div className="grid gap-3 md:grid-cols-3">
295
+ {metrics.map((metric) => (
296
+ <Card key={metric.label}>
297
+ <CardContent>
298
+ <p className="text-label-md text-muted-foreground">{metric.label}</p>
299
+ <p className="mt-3 text-display-lg">{metric.value}</p>
300
+ <p className="mt-1 text-body-sm text-muted-foreground">{metric.detail}</p>
301
+ </CardContent>
302
+ </Card>
303
+ ))}
304
+ </div>
305
+ </Region>
306
+ </main>
307
+
308
+ <footer className="border-t border-border bg-muted/35">
309
+ <Region max="wide" className="py-10 sm:py-12">
310
+ <div className="grid gap-8 lg:grid-cols-[1fr_1.35fr_1fr]">
311
+ <Stack gap={3} className="max-w-md">
312
+ <a href="/" className="flex w-fit items-center gap-2 rounded-full motion-interactive motion-press">
313
+ <span className="grid size-9 place-items-center rounded-full bg-primary text-primary-foreground">
314
+ <Icon name="sparkles" className="text-sm" />
315
+ </span>
316
+ <span className="text-title-lg">Cove</span>
317
+ </a>
318
+ <p className="text-body-sm text-muted-foreground">
319
+ A calm operating surface for teams that need trust, speed, and
320
+ context without the noise.
321
+ </p>
322
+ <Row gap={1} wrap>
323
+ <Badge size="sm" icon={<Icon name="lock" />}>
324
+ Private
325
+ </Badge>
326
+ <Badge tone="success" size="sm" icon={<Icon name="circle-check" />}>
327
+ Live
328
+ </Badge>
329
+ </Row>
330
+ </Stack>
331
+
332
+ <nav aria-label="Footer" className="grid gap-6 sm:grid-cols-3">
333
+ {footerGroups.map((group) => (
334
+ <div key={group.title}>
335
+ <h2 className="text-label-md text-muted-foreground">{group.title}</h2>
336
+ <ul className="mt-3 space-y-1">
337
+ {group.links.map((link) => (
338
+ <li key={link.label}>
339
+ <a
340
+ href={link.href}
341
+ className="flex min-h-9 items-center text-body-sm text-muted-foreground motion-interactive hover:text-foreground"
342
+ >
343
+ {link.label}
344
+ </a>
345
+ </li>
346
+ ))}
347
+ </ul>
348
+ </div>
349
+ ))}
350
+ </nav>
351
+
352
+ <Card>
353
+ <CardHeader>
354
+ {/* CardTitle carries the type; the h2 inside it keeps the outline. */}
355
+ <CardTitle>
356
+ <h2>Signal digest</h2>
357
+ </CardTitle>
358
+ <CardDescription>
359
+ One weekly note on trusted workflows and product releases.
360
+ </CardDescription>
361
+ <CardAction>
362
+ <span className="grid size-9 place-items-center rounded-full bg-highlight text-highlight-foreground">
363
+ <Icon name="sparkles" className="text-sm" />
364
+ </span>
365
+ </CardAction>
366
+ </CardHeader>
367
+ <CardContent className="flex flex-col gap-4">
368
+ <form action="/newsletter" className="grid gap-2 sm:grid-cols-[1fr_auto]">
369
+ <label htmlFor="brand-footer-email" className="sr-only">
370
+ Work email
371
+ </label>
372
+ <Input
373
+ id="brand-footer-email"
374
+ name="email"
375
+ type="email"
376
+ autoComplete="email"
377
+ placeholder="you@company.com"
378
+ />
379
+ <Button type="submit" className="w-full sm:w-auto">
380
+ Join
381
+ </Button>
382
+ </form>
383
+ <div className="grid grid-cols-3 gap-2">
384
+ {footerSignals.map((signal) => (
385
+ <div key={signal.label} className="rounded-md bg-muted p-3">
386
+ <p className="text-mono-sm">{signal.value}</p>
387
+ <p className="mt-1 text-caption text-muted-foreground">{signal.label}</p>
388
+ </div>
389
+ ))}
390
+ </div>
391
+ </CardContent>
392
+ </Card>
393
+ </div>
394
+
395
+ <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">
396
+ <p>© 2026 Cove. All rights reserved.</p>
397
+ <Row gap={3} wrap>
398
+ <a
399
+ href="/privacy"
400
+ className="flex min-h-9 min-w-9 items-center motion-interactive hover:text-foreground"
401
+ >
402
+ Privacy
403
+ </a>
404
+ <a
405
+ href="/terms"
406
+ className="flex min-h-9 min-w-9 items-center motion-interactive hover:text-foreground"
407
+ >
408
+ Terms
409
+ </a>
410
+ <a
411
+ href="/contact"
412
+ className="flex min-h-9 min-w-9 items-center motion-interactive hover:text-foreground"
413
+ >
414
+ Contact
415
+ </a>
416
+ </Row>
417
+ </div>
418
+ </Region>
419
+ </footer>
420
+ </div>
421
+ )
422
+ }
423
+ ```
@@ -0,0 +1,210 @@
1
+ # ChatPanel
2
+
3
+ A full-height conversation surface: titled header, scrolling transcript, and a composer pinned to the bottom — the assembled shape of an AI assistant, a support thread, or a DM.
4
+
5
+ **Intent:** Reach for this for any conversation that owns its screen — an assistant, a support inbox thread, a DM. Not for a chat docked beside other work (compose the same three parts inside a Sheet or a PanelGroup instead), and not for a comment thread under an article, which is a FeedPage concern.
6
+
7
+ ## Notes
8
+
9
+ - The panel is a fixed-height column, not a growing page. `h-[calc(100dvh_-_--spacing(12))]` on the Card (the viewport minus the Page's py-6, in spacing units so density moves both together — a fixed `3rem` drifts the moment density moves the padding) and `flex-1 min-h-0` on the MessageScroller are what make the transcript scroll instead of the document — the single most common thing to get wrong here, and it fails by pushing the composer off the bottom of the screen.
10
+ - The panel is a Card, not a hand-built bordered section: CardHeader (title, description, CardAction), the MessageScroller and a CardFooter all inset by the card's own `--card-p`, so the three regions share one edge at any card density.
11
+ - `dvh` rather than `vh`: on mobile the URL bar collapses, and a `vh`-sized panel puts the composer under it exactly when someone is trying to type.
12
+ - Region cap is `feed` (640px) — a conversation is a reading column, and the Bubble measure cap is tuned to the same assumption.
13
+ - The composer sits outside the scroll region, in a bordered CardFooter, so it stays put while the transcript moves.
14
+ - 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.
15
+ - The assistant's long-form answer uses `variant="plain"`. A tinted box around a code block reads as a quote rather than as a turn.
16
+ - 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.
17
+ - 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.
18
+ - 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.
19
+ - For a new thread, swap the transcript's children for an `<EmptyState>` inside the same MessageScroller — the panel, header and composer are unchanged, which is the point of keeping them separate.
20
+
21
+ ## Accessibility
22
+
23
+ - 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.
24
+ - 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.
25
+ - The composer is a real <form> whose Enter handling skips IME composition, and every icon-only control in the panel carries an accessible label.
26
+
27
+ ## Source
28
+
29
+ ```tsx
30
+ import { Attachment, Avatar, Bubble, BubbleReaction, BubbleReactions, Button, Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle, ChatComposer, CodeBlock, Icon, Marker, MenuRadioGroup, MenuRadioItem, Message, MessageScroller, Page, SettingsMenu, SettingsRow, StreamingText } from "@sableui/react"
31
+
32
+ /**
33
+ * The request is in flight. In an app this is your send state — from `useChat`,
34
+ * a fetch, whatever owns the turn — and it is deliberately ONE flag driving two
35
+ * places: the composer's `busy` and the answer's `streaming`. They are the same
36
+ * wait reported at both ends of the panel, and letting them drift is how you get
37
+ * a composer that still says "sending" under an answer that already finished.
38
+ */
39
+ const pending = true
40
+
41
+ /** The answer so far. Append tokens to this; `StreamingText` assumes it only grows. */
42
+ const streamed =
43
+ "Anchor on the oldest rendered message and add back however far it moved. Appends"
44
+
45
+ const turns = [
46
+ {
47
+ id: "t_01",
48
+ mine: true,
49
+ body: "Our chat scroll jumps every time older messages load. What are we doing wrong?",
50
+ },
51
+ {
52
+ id: "t_02",
53
+ mine: false,
54
+ body: "Almost always the anchor. If you preserve the scroll position by measuring the container, prepending history moves everything under the reader.",
55
+ },
56
+ {
57
+ id: "t_03",
58
+ mine: false,
59
+ body: "Anchor on the oldest rendered message instead and add back however far it moved. Appends leave it alone, so the same rule covers both.",
60
+ },
61
+ ]
62
+
63
+ export function ChatPanel() {
64
+ return (
65
+ <Page as="main" max="feed" gap={0} className="py-6">
66
+ {/* h-dvh minus the page's own padding (py-6 = 12 spacing units, in
67
+ spacing units so density moves both together): the panel owns the
68
+ viewport, and the transcript — not the page — is the thing that
69
+ scrolls. The panel is a Card; its parts inset by the card's own
70
+ --card-p, so header, transcript and composer share one edge. */}
71
+ <Card
72
+ render={<section aria-labelledby="chat-title" />}
73
+ className="flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden"
74
+ >
75
+ <CardHeader className="border-b border-border pb-(--card-p)">
76
+ {/* CardTitle carries the type; the h1 inside it is the page heading. */}
77
+ <CardTitle>
78
+ <h1 id="chat-title">Scroll behaviour</h1>
79
+ </CardTitle>
80
+ <CardDescription>Assistant · answers from your team's docs</CardDescription>
81
+ <CardAction>
82
+ <Button variant="outline" size="icon-sm" aria-label="Start a new chat">
83
+ <Icon name="plus" />
84
+ </Button>
85
+ </CardAction>
86
+ </CardHeader>
87
+
88
+ {/* flex-1 min-h-0 is the whole layout: it bounds the scroll region
89
+ against the panel instead of letting the transcript grow the page. */}
90
+ <MessageScroller className="min-h-0 flex-1 p-(--card-p)">
91
+ <Marker variant="separator">Today</Marker>
92
+
93
+ {turns.map((turn, i) => (
94
+ <Message
95
+ key={turn.id}
96
+ messageId={turn.id}
97
+ align={turn.mine ? "end" : "start"}
98
+ grouped={i > 0 && turns[i - 1].mine === turn.mine}
99
+ avatar={turn.mine ? undefined : <Avatar size="sm" fallback="AI" />}
100
+ >
101
+ <Bubble
102
+ variant={turn.mine ? "primary" : "muted"}
103
+ align={turn.mine ? "end" : "start"}
104
+ tail={i === 0 || turns[i - 1].mine !== turn.mine}
105
+ >
106
+ {turn.body}
107
+ </Bubble>
108
+ </Message>
109
+ ))}
110
+
111
+ <Marker variant="note" icon="sparkles">
112
+ Read message-scroller.md
113
+ </Marker>
114
+
115
+ {/* An assistant's long-form answer: no bubble, because a tinted box
116
+ around a code block reads as a quote rather than as a turn. */}
117
+ <Message messageId="t_04" avatar={<Avatar size="sm" fallback="AI" />}>
118
+ <Bubble variant="plain">
119
+ <p className="text-body-md text-foreground">
120
+ Here is the shape of it:
121
+ </p>
122
+ <CodeBlock
123
+ code={
124
+ "const shift = anchor.offsetTop - lastTop\nif (shift !== 0) viewport.scrollTop += shift"
125
+ }
126
+ filename="preserve.ts"
127
+ />
128
+ </Bubble>
129
+ <BubbleReactions>
130
+ <BubbleReaction emoji="🎯" count={2} active />
131
+ </BubbleReactions>
132
+ </Message>
133
+
134
+ <Message messageId="t_05" align="end">
135
+ <Attachment name="scroller.tsx" meta="4.2 KB · TSX" />
136
+ </Message>
137
+
138
+ {/* The answer arriving. A `Marker` with a spinner — or ThinkingOrb —
139
+ is what sits here BEFORE the first token; once tokens land it is
140
+ replaced rather than kept, because two indicators for one wait
141
+ read as two things happening.
142
+
143
+ No bubble, same as the long-form answer above: a tinted box around
144
+ a growing paragraph makes the reflow look like the box breathing.
145
+
146
+ MessageScroller needs no help with this — it already re-follows on
147
+ "a message grew" (ResizeObserver), not just on "a message arrived",
148
+ so the newest token stays pinned while the reader can still scroll
149
+ up without being yanked back. */}
150
+ <Message messageId="t_06" avatar={<Avatar size="sm" fallback="AI" />}>
151
+ <Bubble variant="plain">
152
+ <StreamingText text={streamed} streaming={pending} />
153
+ </Bubble>
154
+ </Message>
155
+ </MessageScroller>
156
+
157
+ <CardFooter className="border-t border-border pt-(--card-p)">
158
+ <ChatComposer
159
+ className="flex-1"
160
+ placeholder="Ask a follow-up…"
161
+ // Same flag as the answer's `streaming`, which is what turns Send
162
+ // into Stop. A composer that stays sendable mid-answer invites a
163
+ // second request on top of the one still running.
164
+ busy={pending}
165
+ onStop={() => undefined}
166
+ onAttach={() => undefined}
167
+ footer="Enter to send · Shift+Enter for a new line"
168
+ // Model and effort sit in `actions`, at the send end of the row:
169
+ // they qualify the message about to be sent, so they belong beside
170
+ // the button that sends it rather than next to the attach control.
171
+ actions={
172
+ <SettingsMenu
173
+ variant="subtle"
174
+ size="xs"
175
+ label="Change model and effort"
176
+ summary={
177
+ <>
178
+ <span className="text-foreground">5.6 Sol</span>{" "}
179
+ <span className="text-muted-foreground">High</span>
180
+ </>
181
+ }
182
+ >
183
+ <SettingsRow label="Model" value="5.6 Sol">
184
+ <MenuRadioGroup defaultValue="sol">
185
+ <MenuRadioItem value="sol">5.6 Sol</MenuRadioItem>
186
+ <MenuRadioItem value="lite">5.6 Lite</MenuRadioItem>
187
+ </MenuRadioGroup>
188
+ </SettingsRow>
189
+ <SettingsRow label="Effort" value="High">
190
+ <MenuRadioGroup defaultValue="high">
191
+ <MenuRadioItem value="low">Low</MenuRadioItem>
192
+ <MenuRadioItem value="standard">Standard</MenuRadioItem>
193
+ <MenuRadioItem value="high">High</MenuRadioItem>
194
+ </MenuRadioGroup>
195
+ </SettingsRow>
196
+ <SettingsRow label="Speed" value="Standard">
197
+ <MenuRadioGroup defaultValue="standard">
198
+ <MenuRadioItem value="standard">Standard</MenuRadioItem>
199
+ <MenuRadioItem value="priority">Priority</MenuRadioItem>
200
+ </MenuRadioGroup>
201
+ </SettingsRow>
202
+ </SettingsMenu>
203
+ }
204
+ />
205
+ </CardFooter>
206
+ </Card>
207
+ </Page>
208
+ )
209
+ }
210
+ ```