@sableui/react 0.1.1 → 0.3.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 (137) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +120 -0
  3. package/CONTRIBUTING.md +8 -0
  4. package/README.md +1 -0
  5. package/bin/sable-lint.mjs +0 -0
  6. package/bin/sable.mjs +68 -26
  7. package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
  8. package/dist/blocks/index.d.ts +396 -0
  9. package/dist/blocks/index.js +1997 -0
  10. package/dist/chunk-6LCIBMMG.js +4908 -0
  11. package/dist/{chunk-J23ELQQZ.js → chunk-CKNM7KSB.js} +1664 -4270
  12. package/dist/chunk-K6LII3OE.js +5093 -0
  13. package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
  14. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  15. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  16. package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
  17. package/dist/foundations/index.js +1 -1
  18. package/dist/icon/index.js +2 -2
  19. package/dist/icon-sets/reicon.js +1 -1
  20. package/dist/index.d.ts +624 -133
  21. package/dist/index.js +767 -4964
  22. package/dist/ios/index.d.ts +14 -2
  23. package/dist/ios/index.js +10 -9
  24. package/dist/ios/preview.d.ts +11 -1
  25. package/dist/ios/preview.js +14 -9
  26. package/dist/ios/styles.css +26 -6
  27. package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
  28. package/dist/patterns/index.d.ts +1 -1
  29. package/dist/patterns/index.js +1684 -471
  30. package/dist/presets/index.d.ts +1 -1
  31. package/dist/presets/index.js +1 -1
  32. package/dist/styles.css +238 -40
  33. package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
  34. package/dist/themes/atelier/atelier.css +3 -0
  35. package/dist/themes/atelier/index.d.ts +1 -1
  36. package/dist/themes/base/base.css +34 -4
  37. package/dist/themes/base/index.d.ts +1 -1
  38. package/dist/themes/ember/ember.css +3 -0
  39. package/dist/themes/ember/index.d.ts +1 -1
  40. package/dist/themes/index.d.ts +2 -2
  41. package/dist/themes/index.js +2 -2
  42. package/dist/themes/meridian/index.d.ts +1 -1
  43. package/dist/themes/meridian/meridian.css +10 -5
  44. package/dist/themes/poppy/index.d.ts +1 -1
  45. package/dist/themes/poppy/poppy.css +16 -0
  46. package/dist/themes/prism/index.d.ts +1 -1
  47. package/dist/themes/prism/prism.css +4 -1
  48. package/dist/themes/verdant/index.d.ts +1 -1
  49. package/dist/themes/verdant/verdant.css +7 -2
  50. package/llms/blocks/api-keys-block.md +79 -0
  51. package/llms/blocks/chat-thread-block.md +51 -0
  52. package/llms/blocks/command-palette-block.md +48 -0
  53. package/llms/blocks/cta-band-block.md +62 -0
  54. package/llms/blocks/danger-zone-block.md +69 -0
  55. package/llms/blocks/device-comparison-block.md +91 -0
  56. package/llms/blocks/event-card-block.md +84 -0
  57. package/llms/blocks/faq-block.md +72 -0
  58. package/llms/blocks/feature-grid-block.md +75 -0
  59. package/llms/blocks/feedback-form-block.md +66 -0
  60. package/llms/blocks/goal-progress-block.md +54 -0
  61. package/llms/blocks/hero-block.md +65 -0
  62. package/llms/blocks/invite-team-block.md +61 -0
  63. package/llms/blocks/invoice-history-block.md +84 -0
  64. package/llms/blocks/kpi-sparkline-block.md +57 -0
  65. package/llms/blocks/media-player-block.md +100 -0
  66. package/llms/blocks/newsletter-block.md +64 -0
  67. package/llms/blocks/not-found-block.md +56 -0
  68. package/llms/blocks/notification-list-block.md +63 -0
  69. package/llms/blocks/notification-settings-block.md +54 -0
  70. package/llms/blocks/order-tracking-block.md +62 -0
  71. package/llms/blocks/pricing-card-block.md +82 -0
  72. package/llms/blocks/pricing-table-block.md +129 -0
  73. package/llms/blocks/product-card-block.md +69 -0
  74. package/llms/blocks/profile-card-block.md +103 -0
  75. package/llms/blocks/profile-form-block.md +114 -0
  76. package/llms/blocks/revenue-breakdown-block.md +70 -0
  77. package/llms/blocks/sign-in-block.md +64 -0
  78. package/llms/blocks/sign-up-block.md +78 -0
  79. package/llms/blocks/site-footer-block.md +96 -0
  80. package/llms/blocks/stat-row-block.md +59 -0
  81. package/llms/blocks/steps-block.md +73 -0
  82. package/llms/blocks/task-list-block.md +81 -0
  83. package/llms/blocks/team-members-block.md +91 -0
  84. package/llms/blocks/testimonial-block.md +78 -0
  85. package/llms/blocks/traffic-sources-block.md +63 -0
  86. package/llms/blocks/traffic-trend-block.md +95 -0
  87. package/llms/blocks/upload-list-block.md +66 -0
  88. package/llms/blocks/verify-code-block.md +59 -0
  89. package/llms/blocks/wallet-block.md +84 -0
  90. package/llms/components/area-chart.md +112 -0
  91. package/llms/components/bar-chart.md +131 -0
  92. package/llms/components/bubble.md +5 -5
  93. package/llms/components/button.md +1 -1
  94. package/llms/components/card.md +2 -2
  95. package/llms/components/checkbox-card.md +1 -1
  96. package/llms/components/context-card.md +2 -2
  97. package/llms/components/context-menu.md +1 -1
  98. package/llms/components/date-tile.md +79 -0
  99. package/llms/components/dialog.md +1 -1
  100. package/llms/components/empty-state.md +4 -4
  101. package/llms/components/entity.md +5 -3
  102. package/llms/components/fieldset.md +1 -1
  103. package/llms/components/kbd.md +18 -6
  104. package/llms/components/line-chart.md +101 -0
  105. package/llms/components/menu.md +1 -1
  106. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  107. package/llms/components/pie-chart.md +103 -0
  108. package/llms/components/pill.md +1 -1
  109. package/llms/components/radar-chart.md +106 -0
  110. package/llms/components/radial-chart.md +102 -0
  111. package/llms/components/segmented-bar.md +102 -0
  112. package/llms/components/settings-menu.md +2 -2
  113. package/llms/components/side-nav.md +1 -1
  114. package/llms/components/skeleton.md +1 -1
  115. package/llms/components/sparkline.md +80 -0
  116. package/llms/components/table.md +1 -1
  117. package/llms/components/theme-selector.md +1 -1
  118. package/llms/foundations/shape.md +1 -1
  119. package/llms/patterns/auth-page.md +132 -42
  120. package/llms/patterns/brand-landing-page.md +271 -175
  121. package/llms/patterns/chat-panel.md +120 -39
  122. package/llms/patterns/dashboard-page.md +276 -65
  123. package/llms/patterns/feed-page.md +245 -64
  124. package/llms/patterns/landing-page.md +473 -75
  125. package/llms/patterns/list-detail-page.md +377 -68
  126. package/llms/patterns/settings-page.md +268 -67
  127. package/llms/reference/token-intent.md +4 -2
  128. package/llms/reference/utilities.md +20 -5
  129. package/llms/tasks/building-an-ios-prototype.md +4 -1
  130. package/llms/tasks/choosing-a-component.md +2 -2
  131. package/llms-full.txt +6428 -892
  132. package/llms.txt +67 -16
  133. package/package.json +5 -1
  134. package/registry.json +3217 -552
  135. package/targets/ios.json +88 -18
  136. package/llms/components/description.md +0 -96
  137. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
@@ -156,6 +156,8 @@
156
156
  --border: oklch(88% 0.008 74);
157
157
  --border-strong: oklch(82% 0.010 73);
158
158
  --input: oklch(85% 0.009 74);
159
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
160
+ --control-border: oklch(60.2% 0.0095 70);
159
161
  --ring: oklch(58% 0.245 32.9);
160
162
  --primary: oklch(58% 0.245 32.9);
161
163
  --primary-foreground: oklch(100% 0 0);
@@ -234,6 +236,7 @@
234
236
  --border: oklch(32% 0.012 67.2); /* lifted with the fills: a hairline must still show on muted */
235
237
  --border-strong: oklch(37% 0.010 61.1);
236
238
  --input: oklch(33% 0.012 65);
239
+ --control-border: oklch(55.6% 0.0084 67.7);
237
240
  --ring: oklch(65.2% 0.235 32.9);
238
241
  --primary: oklch(65.2% 0.235 32.9);
239
242
  --primary-foreground: oklch(14.1% 0.018 70.5);
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /**
4
4
  * Ember — a Sable theme. Concrete token values live in
@@ -1,4 +1,4 @@
1
- export { A as AnyThemeId, D as DEFAULT_THEME_ID, S as SABLE_COLOR_STORAGE_KEY, a as SABLE_THEME_ATTRIBUTE, b as SABLE_THEME_STORAGE_KEY, c as SableColorMode, d as SableThemeContextValue, e as SableThemePreference, f as SableThemeProvider, g as SableThemeProviderProps, T as ThemeId, h as applySableColorMode, i as applySableTheme, j as buildThemeManifestById, k as defaultThemeId, l as featuredThemeManifests, m as getThemeManifest, n as isKnownThemeId, o as isThemeId, t as themeIds, p as themeManifestById, q as themeManifests, u as useSableTheme } from '../theme-provider-BWcizhAt.js';
1
+ export { A as AnyThemeId, D as DEFAULT_THEME_ID, S as SABLE_COLOR_STORAGE_KEY, a as SABLE_THEME_ATTRIBUTE, b as SABLE_THEME_STORAGE_KEY, c as SableColorMode, d as SableThemeContextValue, e as SableThemePreference, f as SableThemeProvider, g as SableThemeProviderProps, T as ThemeId, h as applySableColorMode, i as applySableTheme, j as buildThemeManifestById, k as defaultThemeId, l as featuredThemeManifests, m as getThemeManifest, n as isKnownThemeId, o as isThemeId, t as themeIds, p as themeManifestById, q as themeManifests, u as useSableTheme } from '../theme-provider-BE-KqUG-.js';
2
2
  export { atelierTheme } from './atelier/index.js';
3
3
  export { baseTheme } from './base/index.js';
4
4
  export { default as emberTheme } from './ember/index.js';
@@ -7,4 +7,4 @@ export { default as poppyTheme } from './poppy/index.js';
7
7
  export { default as prismTheme } from './prism/index.js';
8
8
  export { default as verdantTheme } from './verdant/index.js';
9
9
  import 'react';
10
- import '../_contract-CYScOdf5.js';
10
+ import '../_contract-o3tUc8Ig.js';
@@ -1,8 +1,8 @@
1
- export { DEFAULT_THEME_ID, SABLE_COLOR_STORAGE_KEY, SABLE_THEME_ATTRIBUTE, SABLE_THEME_STORAGE_KEY, SableThemeProvider, applySableColorMode, applySableTheme, buildThemeManifestById, defaultThemeId, featuredThemeManifests, getThemeManifest, isKnownThemeId, isThemeId, themeIds, themeManifestById, themeManifests, useSableTheme } from '../chunk-L6Y46C62.js';
1
+ export { DEFAULT_THEME_ID, SABLE_COLOR_STORAGE_KEY, SABLE_THEME_ATTRIBUTE, SABLE_THEME_STORAGE_KEY, SableThemeProvider, applySableColorMode, applySableTheme, buildThemeManifestById, defaultThemeId, featuredThemeManifests, getThemeManifest, isKnownThemeId, isThemeId, themeIds, themeManifestById, themeManifests, useSableTheme } from '../chunk-MXJX3NZI.js';
2
+ export { prismTheme } from '../chunk-7MG4SHYX.js';
2
3
  export { verdantTheme } from '../chunk-KO3NAEBF.js';
3
4
  export { emberTheme } from '../chunk-L4IEIQQA.js';
4
5
  export { baseTheme } from '../chunk-EP55KSYA.js';
5
6
  export { poppyTheme } from '../chunk-ADLLDWLW.js';
6
7
  export { meridianTheme } from '../chunk-K7RUHUGC.js';
7
8
  export { atelierTheme } from '../chunk-YXBOYWBN.js';
8
- export { prismTheme } from '../chunk-7MG4SHYX.js';
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /**
4
4
  * Meridian — a Sable theme. Concrete token values live in
@@ -117,15 +117,19 @@
117
117
  ---------------------------------------------------------------------- */
118
118
  --background: oklch(94.6% 0.0083 91.48);
119
119
  --foreground: oklch(26.25% 0.0162 102.18);
120
- --card: oklch(94.6% 0.0083 91.48);
120
+ /* A step above the paper page, as in every preset with a tinted page: a card
121
+ level with it left fields reading as part of the page, not a box to type in. */
122
+ --card: oklch(97.6% 0.006 91.48);
121
123
  --card-foreground: oklch(26.25% 0.0162 102.18);
122
- --popover: oklch(94.6% 0.0083 91.48);
124
+ --popover: oklch(97.6% 0.006 91.48);
123
125
  --popover-foreground: oklch(26.25% 0.0162 102.18);
124
126
  --muted: oklch(91.24% 0.0138 92.99);
125
127
  --muted-foreground: oklch(49.93% 0.0412 93.54);
126
128
  --border: oklch(87.54% 0.0194 93.69);
127
129
  --border-strong: oklch(81.94% 0.0252 94.12);
128
130
  --input: oklch(87.54% 0.0194 93.69);
131
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
132
+ --control-border: oklch(58.9% 0.0346 93.54);
129
133
  --ring: oklch(62% 0.2063 32.56);
130
134
  --primary: oklch(26.25% 0.0162 102.18);
131
135
  --primary-foreground: oklch(94.6% 0.0083 91.48);
@@ -167,7 +171,7 @@
167
171
  --sidebar-primary-foreground: oklch(22.46% 0.0094 107.13);
168
172
  --sidebar-accent: oklch(27.1% 0.0143 101.41);
169
173
  --sidebar-accent-foreground: oklch(94.3% 0.0042 91.45);
170
- --sidebar-border: oklch(27.1% 0.0143 101.41);
174
+ --sidebar-border: oklch(34.69% 0.0186 98.75);
171
175
  --sidebar-ring: oklch(65.11% 0.2063 32.56);
172
176
 
173
177
  /* ----------------------------------------------------------------------
@@ -216,8 +220,9 @@
216
220
  * only the edge separates. Caught by the non-text contrast gate.
217
221
  */
218
222
  --border: oklch(34.69% 0.0186 98.75);
219
- --border-strong: oklch(34.69% 0.0186 98.75);
223
+ --border-strong: oklch(44% 0.0225 96); /* one visible step above --border, which was lifted to the old strong value */
220
224
  --input: oklch(34.69% 0.0186 98.75);
225
+ --control-border: oklch(60% 0.002 0);
221
226
  --ring: oklch(65.11% 0.2063 32.56);
222
227
  --primary: oklch(94.3% 0.0042 91.45);
223
228
  --primary-foreground: oklch(22.46% 0.0094 107.13);
@@ -250,6 +255,6 @@
250
255
  --sidebar-primary-foreground: oklch(22.46% 0.0094 107.13);
251
256
  --sidebar-accent: oklch(27.1% 0.0143 101.41);
252
257
  --sidebar-accent-foreground: oklch(94.3% 0.0042 91.45);
253
- --sidebar-border: oklch(27.1% 0.0143 101.41);
258
+ --sidebar-border: oklch(34.69% 0.0186 98.75);
254
259
  --sidebar-ring: oklch(65.11% 0.2063 32.56);
255
260
  }
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /** Original Sable preset; no third-party artwork or reference-site code. */
4
4
  declare const poppyTheme: ThemeManifest;
@@ -69,6 +69,8 @@
69
69
  --border: oklch(69.96908% 0.055090 333.3586);
70
70
  --border-strong: oklch(54.87925% 0.056525 333.9456);
71
71
  --input: oklch(69.96908% 0.055090 333.3586);
72
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
73
+ --control-border: oklch(60% 0.0598 321.39);
72
74
  --ring: oklch(40.99289% 0.130336 325.3125);
73
75
  --primary: oklch(38.38082% 0.116390 323.6155);
74
76
  --primary-foreground: oklch(100.00000% 0.000000 89.8756);
@@ -119,14 +121,27 @@
119
121
  --spacing: 0.28rem;
120
122
  --theme-button-radius: 12px;
121
123
  --button-radius: var(--theme-button-radius);
124
+ /* Hard offsets the whole way up. Only sm and md were declared, so the
125
+ default, lg and xl steps fell through to Base's soft blurs and one ladder
126
+ spoke two shadow languages. Offset doubles roughly per step. */
122
127
  --theme-shadow-sm: 3px 3px 0 oklch(32% 0.05 320 / 0.12);
128
+ --theme-shadow: 4px 4px 0 oklch(32% 0.05 320 / 0.13);
123
129
  --theme-shadow-md: 5px 5px 0 oklch(32% 0.05 320 / 0.14);
130
+ --theme-shadow-lg: 8px 8px 0 oklch(32% 0.05 320 / 0.16);
131
+ --theme-shadow-xl: 12px 12px 0 oklch(32% 0.05 320 / 0.18);
124
132
  }
125
133
 
126
134
  .dark[data-sable-theme="poppy"],
127
135
  [data-sable-theme="poppy"].dark,
128
136
  [data-sable-theme="poppy"] .dark,
129
137
  .dark [data-sable-theme="poppy"]:not(.light) {
138
+ /* The light ladder's ink is 32% lightness — LIGHTER than this 20% page, so
139
+ in dark mode it drew a faint glow, not a shadow. Same offsets, near-black. */
140
+ --theme-shadow-sm: 3px 3px 0 oklch(8% 0.03 320 / 0.5);
141
+ --theme-shadow: 4px 4px 0 oklch(8% 0.03 320 / 0.52);
142
+ --theme-shadow-md: 5px 5px 0 oklch(8% 0.03 320 / 0.55);
143
+ --theme-shadow-lg: 8px 8px 0 oklch(8% 0.03 320 / 0.6);
144
+ --theme-shadow-xl: 12px 12px 0 oklch(8% 0.03 320 / 0.65);
130
145
  --background: oklch(20.33265% 0.047438 308.8247);
131
146
  --foreground: oklch(96.99788% 0.031195 84.5922);
132
147
  --card: oklch(28.82474% 0.059607 315.2029);
@@ -138,6 +153,7 @@
138
153
  --border: oklch(62.48980% 0.070560 325.5308);
139
154
  --border-strong: oklch(72.17143% 0.060424 328.9840);
140
155
  --input: oklch(62.48980% 0.070560 325.5308);
156
+ --control-border: oklch(67.3% 0.0412 332.63);
141
157
  --ring: oklch(86.64637% 0.118528 86.6791);
142
158
  --primary: oklch(86.64637% 0.118528 86.6791);
143
159
  --primary-foreground: oklch(27.91553% 0.070219 323.8141);
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /** Original Sable preset; no third-party artwork or reference-site code. */
4
4
  declare const prismTheme: ThemeManifest;
@@ -12,7 +12,7 @@
12
12
  --region-ultrawide: 1800px;
13
13
  --theme-radius-none: 0px;
14
14
  --theme-radius-xs: 2px;
15
- --theme-radius-sm: 4px;
15
+ --theme-radius-sm: 3px; /* a step of its own — sm and md were both 4px, which erased a rung of the ladder */
16
16
  --theme-radius-md: 4px;
17
17
  --theme-radius-lg: 6px;
18
18
  --theme-radius-xl: 8px;
@@ -69,6 +69,8 @@
69
69
  --border: oklch(79.84882% 0.029246 251.5080);
70
70
  --border-strong: oklch(64.27577% 0.042824 253.1390);
71
71
  --input: oklch(79.84882% 0.029246 251.5080);
72
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
73
+ --control-border: oklch(60% 0.0298 252.77);
72
74
  --ring: oklch(50.45463% 0.203215 263.5596);
73
75
  --primary: oklch(50.45463% 0.203215 263.5596);
74
76
  --primary-foreground: oklch(100.00000% 0.000000 89.8756);
@@ -147,6 +149,7 @@
147
149
  --border: oklch(58.06940% 0.052170 251.5856);
148
150
  --border-strong: oklch(69.19219% 0.042050 253.1082);
149
151
  --input: oklch(58.06940% 0.052170 251.5856);
152
+ --control-border: oklch(67.7% 0.0275 255.1);
150
153
  --ring: oklch(82.28684% 0.088371 261.1846);
151
154
  --primary: oklch(82.28684% 0.088371 261.1846);
152
155
  --primary-foreground: oklch(25.20350% 0.047984 259.9119);
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../../_contract-CYScOdf5.js';
1
+ import { T as ThemeManifest } from '../../_contract-o3tUc8Ig.js';
2
2
 
3
3
  /**
4
4
  * Verdant — a Sable theme. Concrete token values live in
@@ -90,15 +90,19 @@
90
90
  ---------------------------------------------------------------------- */
91
91
  --background: oklch(98.6% 0.004 106);
92
92
  --foreground: oklch(24% 0.035 175);
93
- --card: oklch(96.1% 0.005 106);
93
+ /* Cards, fields and popovers sit at or above the page, never recessed into it:
94
+ a card darker than the page made every field read as a filled grey box. */
95
+ --card: oklch(99.7% 0.002 106);
94
96
  --card-foreground: oklch(24% 0.035 175);
95
- --popover: oklch(98.6% 0.004 106);
97
+ --popover: oklch(99.7% 0.002 106);
96
98
  --popover-foreground: oklch(24% 0.035 175);
97
99
  --muted: oklch(93% 0.007 106);
98
100
  --muted-foreground: oklch(49% 0.018 175);
99
101
  --border: oklch(88.5% 0.005 106);
100
102
  --border-strong: oklch(81% 0.008 130);
101
103
  --input: oklch(88.5% 0.005 106);
104
+ /* The resting edge of a control whose outline IS the control (checkbox, radio, an off switch track). Held to WCAG 1.4.11 3:1 on every surface, unlike the quieter --input field outline. */
105
+ --control-border: oklch(59.9% 0.0149 175);
102
106
  --ring: oklch(24% 0.035 175);
103
107
  --primary: oklch(24% 0.035 175);
104
108
  --primary-foreground: oklch(98.6% 0.004 106);
@@ -174,6 +178,7 @@
174
178
  --border: oklch(36% 0.03 175);
175
179
  --border-strong: oklch(40% 0.028 175);
176
180
  --input: oklch(36% 0.03 175);
181
+ --control-border: oklch(58.7% 0.0158 172);
177
182
  --ring: oklch(85% 0.015 170);
178
183
  --primary: oklch(98.6% 0.004 106);
179
184
  --primary-foreground: oklch(24% 0.035 175);
@@ -0,0 +1,79 @@
1
+ # ApiKeysBlock
2
+
3
+ An API keys card: each key's name with its masked hint in Code, when it was created and last used, and an OverflowMenu to rename, roll or revoke.
4
+
5
+ **Intent:** Reach for this for developer settings — API keys, access tokens, webhook secrets. Show a newly created key once, in a Dialog with a copy button; this list only ever shows the masked hint.
6
+
7
+ **Block:** Settings & team · card · designed for 20rem–42rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Only the last four characters are shown. A full key never appears in a list, so there is no copy action here.
12
+ - "Last used" sits in the detail line: it is what tells someone a key is safe to revoke.
13
+ - Revoke is the destructive item, last and after a separator; confirm it in your handler with DestructiveActionModal.
14
+
15
+ ## Accessibility
16
+
17
+ - Each OverflowMenu is labelled with the key's name, so the triggers are distinguishable.
18
+ - The masked hint is a Code element inside the title, read after the key's name.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Button, Card, CardAction, CardDescription, CardHeader, CardList, CardTitle, cn, Code, Entity, Icon, MenuItem, MenuSeparator, OverflowMenu } from "@sableui/react"
24
+
25
+ const keys = [
26
+ { id: "k1", name: "Production", hint: "sk_live_••••4f2a", detail: "Created Mar 2 · used 4 minutes ago" },
27
+ { id: "k2", name: "Staging", hint: "sk_test_••••91cd", detail: "Created Feb 11 · used yesterday" },
28
+ { id: "k3", name: "CI deploys", hint: "sk_live_••••0b7e", detail: "Created Jan 5 · never used" },
29
+ ]
30
+
31
+ export interface ApiKeysBlockProps {
32
+ className?: string
33
+ /** false drops the card's surface and border — for a muted section that already frames it. */
34
+ framed?: boolean
35
+ onCreate?: () => void
36
+ /** Called with the key's id and the action chosen from its menu. */
37
+ onKeyAction?: (id: string, action: "rename" | "roll" | "revoke") => void
38
+ }
39
+
40
+ export function ApiKeysBlock({ className, framed = true, onCreate, onKeyAction }: ApiKeysBlockProps) {
41
+ return (
42
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-2xl", className)}>
43
+ <CardHeader>
44
+ <CardTitle render={<h2 />}>API keys</CardTitle>
45
+ <CardDescription>A key is shown once, when you create it. Store it somewhere safe.</CardDescription>
46
+ <CardAction>
47
+ <Button size="sm" variant="outline" iconStart={<Icon name="key" />} onClick={onCreate}>
48
+ Create key
49
+ </Button>
50
+ </CardAction>
51
+ </CardHeader>
52
+ <CardList>
53
+ {keys.map((k) => (
54
+ <Entity
55
+ key={k.id}
56
+ title={
57
+ <span className="flex flex-wrap items-center gap-x-2">
58
+ {k.name}
59
+ <Code>{k.hint}</Code>
60
+ </span>
61
+ }
62
+ subtitle={k.detail}
63
+ action={
64
+ <OverflowMenu label={`Manage the ${k.name} key`} size="icon-sm">
65
+ <MenuItem onClick={() => onKeyAction?.(k.id, "rename")}>Rename</MenuItem>
66
+ <MenuItem onClick={() => onKeyAction?.(k.id, "roll")}>Roll key</MenuItem>
67
+ <MenuSeparator />
68
+ <MenuItem variant="destructive" onClick={() => onKeyAction?.(k.id, "revoke")}>
69
+ Revoke
70
+ </MenuItem>
71
+ </OverflowMenu>
72
+ }
73
+ />
74
+ ))}
75
+ </CardList>
76
+ </Card>
77
+ )
78
+ }
79
+ ```
@@ -0,0 +1,51 @@
1
+ # ChatThreadBlock
2
+
3
+ A short message thread on a card: Message rows with incoming muted Bubbles on the start side and your own primary Bubbles on the end.
4
+
5
+ **Intent:** Reach for this to show a conversation excerpt — a support preview, a comment thread on a dashboard, a recent-DM card. A working chat with a composer, streaming and history is the ChatPanel page shell.
6
+
7
+ **Block:** Social & messaging · card · designed for 16rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Sit it on a card, not straight on a muted page: incoming bubbles are the muted surface, and would vanish into it.
12
+ - Your own messages align to the end with the primary Bubble; everyone else's align to the start.
13
+
14
+ ## Accessibility
15
+
16
+ - Each Message carries a messageId, so a thread can be addressed and announced item by item.
17
+ - Direction is shown by alignment and colour; when senders matter, add the sender's name to each Message.
18
+
19
+ ## Source
20
+
21
+ ```tsx
22
+ import { Bubble, Card, CardContent, cn, Message } from "@sableui/react"
23
+
24
+ const messages = [
25
+ { id: "m1", text: "Is the new cover ready?", mine: false },
26
+ { id: "m2", text: "Uploading it now 🎉", mine: true },
27
+ { id: "m3", text: "Perfect, thank you!", mine: false },
28
+ ]
29
+
30
+ export interface ChatThreadBlockProps {
31
+ className?: string
32
+ /** false drops the card's surface and border. Keep it framed on a muted page: incoming bubbles are the muted surface too. */
33
+ framed?: boolean
34
+ }
35
+
36
+ export function ChatThreadBlock({ className, framed = true }: ChatThreadBlockProps) {
37
+ return (
38
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
39
+ <CardContent className="flex flex-col gap-2">
40
+ {messages.map((m) => (
41
+ <Message key={m.id} messageId={m.id} align={m.mine ? "end" : undefined}>
42
+ <Bubble variant={m.mine ? "primary" : undefined} align={m.mine ? "end" : undefined}>
43
+ {m.text}
44
+ </Bubble>
45
+ </Message>
46
+ ))}
47
+ </CardContent>
48
+ </Card>
49
+ )
50
+ }
51
+ ```
@@ -0,0 +1,48 @@
1
+ # CommandPaletteBlock
2
+
3
+ An inline command palette: CommandMenuPanel with a filter field and grouped actions, each with an icon and its keyboard shortcut.
4
+
5
+ **Intent:** Reach for this to show a command list in place — an empty state that teaches shortcuts, an onboarding step, a quick-actions card. The ⌘K palette that opens over the app is CommandMenu; this is its panel without the dialog.
6
+
7
+ **Block:** Navigation · card · designed for 18rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Each command shows its shortcut as a Kbd hint, so the palette also teaches the keys.
12
+ - `onCommand` receives the command's id; ids also key the rows, so two commands may share a label.
13
+ - No card around it: the panel is its own surface, lifted with `shadow-sm` when it sits on a muted section.
14
+
15
+ ## Accessibility
16
+
17
+ - CommandMenuPanel wires the filter input to the list as a combobox, with arrow-key navigation and the active row announced.
18
+ - Shortcut hints are Kbd elements, read as keys.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { cn, CommandMenuPanel, Kbd, type CommandItem } from "@sableui/react"
24
+
25
+ const commands = [
26
+ { id: "new-project", icon: "plus", label: "New project", key: "N" },
27
+ { id: "invite", icon: "user", label: "Invite teammate", key: "I" },
28
+ { id: "settings", icon: "gear", label: "Open settings", key: "," },
29
+ ]
30
+
31
+ export interface CommandPaletteBlockProps {
32
+ className?: string
33
+ /** Called with the chosen command's id. */
34
+ onCommand?: (id: string) => void
35
+ }
36
+
37
+ export function CommandPaletteBlock({ className, onCommand }: CommandPaletteBlockProps) {
38
+ const items: CommandItem[] = commands.map((c) => ({
39
+ id: c.id,
40
+ group: "Actions",
41
+ icon: c.icon,
42
+ label: c.label,
43
+ hint: <Kbd>{c.key}</Kbd>,
44
+ onSelect: () => onCommand?.(c.id),
45
+ }))
46
+ return <CommandMenuPanel className={cn("w-full max-w-xs shadow-sm", className)} placeholder="Type a command…" items={items} />
47
+ }
48
+ ```
@@ -0,0 +1,62 @@
1
+ # CtaBandBlock
2
+
3
+ A closing call to action: a centred h2 and one-line lede on a card, with a primary and an outline action.
4
+
5
+ **Intent:** Reach for this as the last section before the footer, repeating the hero's primary action once the reader has seen the case. Mid-page prompts to sign up belong inline, not in a band.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the LandingPage page shell's closing call to action.
12
+ - The primary action matches the hero's, so the page asks for one thing twice rather than two things once.
13
+ - Sitting on a Card lifts it from the page without a coloured band, so it follows any preset.
14
+
15
+ ## Accessibility
16
+
17
+ - The section is labelled by its h2.
18
+ - Both actions are links styled as Buttons, because they navigate.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useId } from "react"
24
+ import { Button, Card, CardContent, cn, Region, Row, Stack } from "@sableui/react"
25
+
26
+ const cta = {
27
+ title: "Build the product, not another component library",
28
+ lede: "Install Sable, pick a preset, and ship the first screen today.",
29
+ primary: { label: "Get started", href: "/docs/getting-started" },
30
+ secondary: { label: "Star on GitHub", href: "/github" },
31
+ }
32
+
33
+ export interface CtaBandBlockProps {
34
+ className?: string
35
+ }
36
+
37
+ export function CtaBandBlock({ className }: CtaBandBlockProps) {
38
+ const titleId = useId()
39
+ return (
40
+ <Region as="section" aria-labelledby={titleId} max="wide" className={cn("w-full py-16", className)}>
41
+ <Card>
42
+ <CardContent>
43
+ <Stack gap={4} align="center" className="py-8 text-center sm:py-12">
44
+ <h2 id={titleId} className="max-w-2xl text-balance text-heading-1">
45
+ {cta.title}
46
+ </h2>
47
+ <p className="max-w-(--measure-prose) text-body-lg text-muted-foreground">{cta.lede}</p>
48
+ <Row gap={2} justify="center" wrap className="pt-2">
49
+ <Button size="cta" render={<a href={cta.primary.href} />}>
50
+ {cta.primary.label}
51
+ </Button>
52
+ <Button size="cta" variant="outline" render={<a href={cta.secondary.href} />}>
53
+ {cta.secondary.label}
54
+ </Button>
55
+ </Row>
56
+ </Stack>
57
+ </CardContent>
58
+ </Card>
59
+ </Region>
60
+ )
61
+ }
62
+ ```
@@ -0,0 +1,69 @@
1
+ # DangerZoneBlock
2
+
3
+ A delete-account card: what deleting removes and what survives, one destructive Button, and a DestructiveActionModal that asks for the handle to be typed before it acts.
4
+
5
+ **Intent:** Reach for this as the last section of account, workspace or project settings. Deleting a single record from a list is an OverflowMenu item that opens DestructiveActionModal directly, not a card.
6
+
7
+ **Block:** Settings & team · card · designed for 20rem–42rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Copied from the SettingsPage page shell's Delete account section.
12
+ - The button never acts directly: it opens the modal, whose confirm stays disabled until the handle is typed.
13
+ - The description says what is lost AND what is kept ("Collectors keep the editions they own"), which is what people want to know before they decide.
14
+
15
+ ## Accessibility
16
+
17
+ - DestructiveActionModal is an alertdialog: focus opens on Cancel and the backdrop does not dismiss it.
18
+ - The CardTitle is an h2, so the section is reachable by heading.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useState } from "react"
24
+ import { Button, Card, CardDescription, CardFooter, CardHeader, CardTitle, cn, DestructiveActionModal } from "@sableui/react"
25
+
26
+ const account = { handle: "ada", editions: 12 }
27
+
28
+ export interface DangerZoneBlockProps {
29
+ className?: string
30
+ /** false drops the card's surface and border — for a muted section that already frames it. */
31
+ framed?: boolean
32
+ /** Called once the person has typed the handle and confirmed. */
33
+ onDelete?: () => void
34
+ }
35
+
36
+ export function DangerZoneBlock({ className, framed = true, onDelete }: DangerZoneBlockProps) {
37
+ const [open, setOpen] = useState(false)
38
+ return (
39
+ <>
40
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-2xl", className)}>
41
+ <CardHeader>
42
+ <CardTitle render={<h2 />}>Delete account</CardTitle>
43
+ <CardDescription>
44
+ Permanently remove your profile, editions, and sales history. Collectors keep the editions they own.
45
+ </CardDescription>
46
+ </CardHeader>
47
+ <CardFooter className="justify-end">
48
+ <Button variant="destructive" onClick={() => setOpen(true)}>
49
+ Delete account
50
+ </Button>
51
+ </CardFooter>
52
+ </Card>
53
+ <DestructiveActionModal
54
+ open={open}
55
+ onOpenChange={setOpen}
56
+ title="Delete your account?"
57
+ description={`This permanently removes your profile, ${account.editions} editions, and sales history. Pending payouts are still sent. This can't be undone.`}
58
+ confirmLabel="Delete account"
59
+ confirmingLabel="Deleting…"
60
+ confirmText={account.handle}
61
+ onConfirm={() => {
62
+ setOpen(false)
63
+ onDelete?.()
64
+ }}
65
+ />
66
+ </>
67
+ )
68
+ }
69
+ ```
@@ -0,0 +1,91 @@
1
+ # DeviceComparisonBlock
2
+
3
+ A comparison card: each series' total sits in the header as a pressable tile, and the one pressed is plotted below as a daily BarChart.
4
+
5
+ **Intent:** Reach for this when two or three measures share a page and the reader looks at one at a time — devices, regions, plans. To see them together, plot both in one BarChart or LineChart instead.
6
+
7
+ **Block:** Dashboard · card · designed for 20rem–48rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The totals are the switch: Toggles in a labelled ToggleGroup, so the number and the control are one thing, with the group's own pressed and hover states.
12
+ - Switching eases every bar to its new height rather than redrawing.
13
+ - Below sm the tiles stack under the title and divide with a top rule instead of a side rule.
14
+
15
+ ## Accessibility
16
+
17
+ - Each total is a Toggle (aria-pressed) named by its label and value, in a ToggleGroup labelled "Device" with arrow-key movement between tiles.
18
+ - The chart's name says which series is plotted, so the data table is unambiguous.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useState } from "react"
24
+ import { BarChart, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Toggle, ToggleGroup } from "@sableui/react"
25
+
26
+ /** 30 days of visitors — replace with your own rows ({ date, desktop, mobile }). */
27
+ const days = Array.from({ length: 30 }, (_, i) => ({
28
+ date: new Date(2026, 8, 1 + i).toISOString().slice(0, 10),
29
+ desktop: Math.round(320 + Math.sin(i / 3) * 90 + (i % 5) * 14),
30
+ mobile: Math.round(260 + Math.cos(i / 4) * 80 + (i % 4) * 18),
31
+ }))
32
+
33
+ const series = [
34
+ { key: "desktop", label: "Desktop" },
35
+ { key: "mobile", label: "Mobile" },
36
+ ] as const
37
+
38
+ const shortDate = (iso: string) => new Date(`${iso}T00:00:00`).toLocaleDateString("en-US", { month: "short", day: "numeric" })
39
+
40
+ export interface DeviceComparisonBlockProps {
41
+ className?: string
42
+ /** false drops the card's surface and border — for a muted section that already frames it. */
43
+ framed?: boolean
44
+ }
45
+
46
+ export function DeviceComparisonBlock({ className, framed = true }: DeviceComparisonBlockProps) {
47
+ const [active, setActive] = useState<string>("desktop")
48
+ const totals = Object.fromEntries(series.map((s) => [s.key, days.reduce((sum, d) => sum + d[s.key], 0)]))
49
+ const current = series.find((s) => s.key === active) ?? series[0]
50
+ return (
51
+ // overflow-hidden: the square tiles meet the card's rounded corner.
52
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-3xl overflow-hidden", className)}>
53
+ <div className="flex flex-col border-b border-border sm:flex-row">
54
+ <CardHeader className="flex-1 pb-[var(--card-p,--spacing(6))]">
55
+ <CardTitle render={<h2 />}>Visitors by device</CardTitle>
56
+ <CardDescription>The last 30 days. Pick a device to plot it.</CardDescription>
57
+ </CardHeader>
58
+ {/* The totals ARE the switch: one pressed tile per series. */}
59
+ <ToggleGroup
60
+ value={[active]}
61
+ onValueChange={(v: unknown[]) => v[0] && setActive(String(v[0]))}
62
+ aria-label="Device"
63
+ className="items-stretch gap-0"
64
+ >
65
+ {series.map((s) => (
66
+ <Toggle
67
+ key={s.key}
68
+ value={s.key}
69
+ className="h-auto flex-1 flex-col items-start justify-center gap-1 rounded-none border-border px-6 py-4 text-start text-foreground max-sm:border-t sm:border-s"
70
+ >
71
+ <span className="text-caption text-muted-foreground">{s.label}</span>
72
+ <span className="text-mono-lg">{totals[s.key].toLocaleString("en-US")}</span>
73
+ </Toggle>
74
+ ))}
75
+ </ToggleGroup>
76
+ </div>
77
+ <CardContent>
78
+ <BarChart
79
+ label={`${current.label} visitors per day, last 30 days`}
80
+ data={days}
81
+ index="date"
82
+ series={[current]}
83
+ indexFormatter={shortDate}
84
+ radius={3}
85
+ className="h-64"
86
+ />
87
+ </CardContent>
88
+ </Card>
89
+ )
90
+ }
91
+ ```