@sableui/react 0.2.0 → 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.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +59 -0
- package/CONTRIBUTING.md +8 -0
- package/README.md +1 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +68 -26
- package/dist/blocks/index.d.ts +396 -0
- package/dist/blocks/index.js +1997 -0
- package/dist/chunk-6LCIBMMG.js +4908 -0
- package/dist/{chunk-AOWBYEQ6.js → chunk-CKNM7KSB.js} +1598 -6464
- package/dist/chunk-K6LII3OE.js +5093 -0
- package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
- package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
- package/dist/icon/index.js +2 -2
- package/dist/icon-sets/reicon.js +1 -1
- package/dist/index.d.ts +471 -34
- package/dist/index.js +588 -2589
- package/dist/ios/index.d.ts +8 -1
- package/dist/ios/index.js +6 -5
- package/dist/ios/preview.d.ts +11 -1
- package/dist/ios/preview.js +13 -8
- package/dist/ios/styles.css +6 -0
- package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
- package/dist/patterns/index.d.ts +1 -1
- package/dist/patterns/index.js +29 -30
- package/dist/themes/base/base.css +23 -0
- package/dist/themes/index.js +2 -2
- package/llms/blocks/api-keys-block.md +79 -0
- package/llms/blocks/chat-thread-block.md +51 -0
- package/llms/blocks/command-palette-block.md +48 -0
- package/llms/blocks/cta-band-block.md +62 -0
- package/llms/blocks/danger-zone-block.md +69 -0
- package/llms/blocks/device-comparison-block.md +91 -0
- package/llms/blocks/event-card-block.md +84 -0
- package/llms/blocks/faq-block.md +72 -0
- package/llms/blocks/feature-grid-block.md +75 -0
- package/llms/blocks/feedback-form-block.md +66 -0
- package/llms/blocks/goal-progress-block.md +54 -0
- package/llms/blocks/hero-block.md +65 -0
- package/llms/blocks/invite-team-block.md +61 -0
- package/llms/blocks/invoice-history-block.md +84 -0
- package/llms/blocks/kpi-sparkline-block.md +57 -0
- package/llms/blocks/media-player-block.md +100 -0
- package/llms/blocks/newsletter-block.md +64 -0
- package/llms/blocks/not-found-block.md +56 -0
- package/llms/blocks/notification-list-block.md +63 -0
- package/llms/blocks/notification-settings-block.md +54 -0
- package/llms/blocks/order-tracking-block.md +62 -0
- package/llms/blocks/pricing-card-block.md +82 -0
- package/llms/blocks/pricing-table-block.md +129 -0
- package/llms/blocks/product-card-block.md +69 -0
- package/llms/blocks/profile-card-block.md +103 -0
- package/llms/blocks/profile-form-block.md +114 -0
- package/llms/blocks/revenue-breakdown-block.md +70 -0
- package/llms/blocks/sign-in-block.md +64 -0
- package/llms/blocks/sign-up-block.md +78 -0
- package/llms/blocks/site-footer-block.md +96 -0
- package/llms/blocks/stat-row-block.md +59 -0
- package/llms/blocks/steps-block.md +73 -0
- package/llms/blocks/task-list-block.md +81 -0
- package/llms/blocks/team-members-block.md +91 -0
- package/llms/blocks/testimonial-block.md +78 -0
- package/llms/blocks/traffic-sources-block.md +63 -0
- package/llms/blocks/traffic-trend-block.md +95 -0
- package/llms/blocks/upload-list-block.md +66 -0
- package/llms/blocks/verify-code-block.md +59 -0
- package/llms/blocks/wallet-block.md +84 -0
- package/llms/components/area-chart.md +112 -0
- package/llms/components/bar-chart.md +131 -0
- package/llms/components/button.md +1 -1
- package/llms/components/card.md +2 -2
- package/llms/components/context-menu.md +1 -1
- package/llms/components/date-tile.md +79 -0
- package/llms/components/entity.md +4 -2
- package/llms/components/line-chart.md +101 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
- package/llms/components/pie-chart.md +103 -0
- package/llms/components/radar-chart.md +106 -0
- package/llms/components/radial-chart.md +102 -0
- package/llms/components/segmented-bar.md +102 -0
- package/llms/components/settings-menu.md +2 -2
- package/llms/components/sparkline.md +80 -0
- package/llms/components/table.md +1 -1
- package/llms/patterns/auth-page.md +2 -2
- package/llms/patterns/dashboard-page.md +6 -7
- package/llms/patterns/feed-page.md +7 -7
- package/llms/patterns/list-detail-page.md +7 -15
- package/llms/patterns/settings-page.md +10 -21
- package/llms/tasks/building-an-ios-prototype.md +4 -1
- package/llms/tasks/choosing-a-component.md +2 -2
- package/llms-full.txt +4296 -277
- package/llms.txt +55 -3
- package/package.json +5 -1
- package/registry.json +6383 -3741
- package/targets/ios.json +90 -9
- package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
package/dist/ios/index.d.ts
CHANGED
|
@@ -21,8 +21,13 @@ declare function iosThemeStyle(tokens: IosBrandTokens): CSSProperties;
|
|
|
21
21
|
* the chrome and any Sable content then follow the preset together. `tokens`
|
|
22
22
|
* overrides individual semantic values. `mode` also sets `dark` on <html>, so
|
|
23
23
|
* Sable popups portaled to <body> follow the prototype's appearance.
|
|
24
|
+
*
|
|
25
|
+
* `systemChrome={false}` is for a prototype shown on a real phone, without
|
|
26
|
+
* PhoneFrame: the device already draws its own status bar and home indicator,
|
|
27
|
+
* so the fake ones go, and the safe areas come from the device
|
|
28
|
+
* (`env(safe-area-inset-*)`) instead of the standard phone's fixed geometry.
|
|
24
29
|
*/
|
|
25
|
-
declare function NativeApp({ children, mode, glassTint, reducedMotion, className, tokens, style, }: {
|
|
30
|
+
declare function NativeApp({ children, mode, glassTint, reducedMotion, className, tokens, style, systemChrome, }: {
|
|
26
31
|
children: ReactNode;
|
|
27
32
|
mode?: "light" | "dark";
|
|
28
33
|
glassTint?: number;
|
|
@@ -30,6 +35,8 @@ declare function NativeApp({ children, mode, glassTint, reducedMotion, className
|
|
|
30
35
|
className?: string;
|
|
31
36
|
tokens?: IosBrandTokens;
|
|
32
37
|
style?: CSSProperties;
|
|
38
|
+
/** Draw the status bar, island and home indicator. Turn off on a real phone. @default true */
|
|
39
|
+
systemChrome?: boolean;
|
|
33
40
|
}): react.JSX.Element;
|
|
34
41
|
|
|
35
42
|
type Navigation = {
|
package/dist/ios/index.js
CHANGED
|
@@ -124,7 +124,8 @@ function NativeApp({
|
|
|
124
124
|
reducedMotion = false,
|
|
125
125
|
className = "",
|
|
126
126
|
tokens = {},
|
|
127
|
-
style
|
|
127
|
+
style,
|
|
128
|
+
systemChrome = true
|
|
128
129
|
}) {
|
|
129
130
|
const dark = mode === "dark";
|
|
130
131
|
useEffect(() => {
|
|
@@ -136,18 +137,18 @@ function NativeApp({
|
|
|
136
137
|
{
|
|
137
138
|
theme: "ios",
|
|
138
139
|
dark,
|
|
139
|
-
className: `ios-prototype ${dark ? "dark" : ""} ${className}`,
|
|
140
|
+
className: `ios-prototype ${dark ? "dark" : ""} ${systemChrome ? "" : "ios-bare"} ${className}`,
|
|
140
141
|
"data-reduced-motion": reducedMotion || void 0,
|
|
141
142
|
style: { ...iosThemeStyle(tokens), "--ios-glass-tint": tint, ...style },
|
|
142
143
|
children: [
|
|
143
144
|
/* @__PURE__ */ jsx(LiquidGlassMaterial, {}),
|
|
144
|
-
/* @__PURE__ */ jsxs("div", { className: "ios-status-bar", "aria-hidden": "true", children: [
|
|
145
|
+
systemChrome ? /* @__PURE__ */ jsxs("div", { className: "ios-status-bar", "aria-hidden": "true", children: [
|
|
145
146
|
/* @__PURE__ */ jsx("span", { children: "9:41" }),
|
|
146
147
|
/* @__PURE__ */ jsx("span", { className: "ios-island" }),
|
|
147
148
|
/* @__PURE__ */ jsx(StatusIcons, { className: "ios-status-icons" })
|
|
148
|
-
] }),
|
|
149
|
+
] }) : null,
|
|
149
150
|
children,
|
|
150
|
-
/* @__PURE__ */ jsx("div", { className: "ios-home-indicator", "aria-hidden": "true" })
|
|
151
|
+
systemChrome ? /* @__PURE__ */ jsx("div", { className: "ios-home-indicator", "aria-hidden": "true" }) : null
|
|
151
152
|
]
|
|
152
153
|
}
|
|
153
154
|
);
|
package/dist/ios/preview.d.ts
CHANGED
|
@@ -26,11 +26,21 @@ declare const phoneSizes: {
|
|
|
26
26
|
* scaled, never the viewport, so the prototype lays out as it would on a phone.
|
|
27
27
|
* The document draws its own status bar, island and home indicator, so the
|
|
28
28
|
* frame draws neither.
|
|
29
|
+
*
|
|
30
|
+
* `fit="width"` (the default) scales the device to its host's width and sizes
|
|
31
|
+
* the host to match. `fit="contain"` scales it to fit BOTH of the host's
|
|
32
|
+
* dimensions and centres it, so the whole device stays in view: give the host
|
|
33
|
+
* a height through `className` (a viewport-relative one keeps the phone on
|
|
34
|
+
* screen beside other controls).
|
|
29
35
|
*/
|
|
30
|
-
declare function PhoneFrame({ src, title, size, frameRef, onLoad, }: {
|
|
36
|
+
declare function PhoneFrame({ src, title, size, fit, className, frameRef, onLoad, }: {
|
|
31
37
|
src: string;
|
|
32
38
|
title?: string;
|
|
33
39
|
size?: keyof typeof phoneSizes;
|
|
40
|
+
/** "width" fills the host's width; "contain" fits the host's width and height. @default "width" */
|
|
41
|
+
fit?: "width" | "contain";
|
|
42
|
+
/** Classes for the host box. With fit="contain", this is where its height comes from. */
|
|
43
|
+
className?: string;
|
|
34
44
|
frameRef?: Ref<HTMLIFrameElement>;
|
|
35
45
|
onLoad?: ReactEventHandler<HTMLIFrameElement>;
|
|
36
46
|
}): react.JSX.Element;
|
package/dist/ios/preview.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Phone } from '../chunk-KFZ43RKU.js';
|
|
2
2
|
import '../chunk-NEQNXI2G.js';
|
|
3
|
-
import '../chunk-SRQWQUTW.js';
|
|
3
|
+
import { cn } from '../chunk-SRQWQUTW.js';
|
|
4
4
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
5
5
|
import { jsx } from 'react/jsx-runtime';
|
|
6
6
|
|
|
@@ -15,12 +15,14 @@ function PhoneFrame({
|
|
|
15
15
|
src,
|
|
16
16
|
title = "iPhone prototype",
|
|
17
17
|
size = "standard",
|
|
18
|
+
fit = "width",
|
|
19
|
+
className,
|
|
18
20
|
frameRef,
|
|
19
21
|
onLoad
|
|
20
22
|
}) {
|
|
21
23
|
const host = useRef(null);
|
|
22
24
|
const device = useRef(null);
|
|
23
|
-
const [
|
|
25
|
+
const [box, setBox] = useState({ scale: 1, width: 0, height: 0, offset: 0 });
|
|
24
26
|
const { width, height } = phoneSizes[size];
|
|
25
27
|
useEffect(() => {
|
|
26
28
|
const hostEl = host.current;
|
|
@@ -28,26 +30,29 @@ function PhoneFrame({
|
|
|
28
30
|
if (!hostEl || !deviceEl) return;
|
|
29
31
|
const measure = () => {
|
|
30
32
|
const natural = { width: deviceEl.offsetWidth, height: deviceEl.offsetHeight };
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
if (!natural.width) return setBox({ scale: 1, ...natural, offset: 0 });
|
|
34
|
+
const byHeight = fit === "contain" && hostEl.clientHeight ? hostEl.clientHeight / natural.height : 1;
|
|
35
|
+
const scale = Math.min(1, hostEl.clientWidth / natural.width, byHeight);
|
|
36
|
+
const offset = fit === "contain" ? Math.max(0, (hostEl.clientWidth - natural.width * scale) / 2) : 0;
|
|
37
|
+
setBox({ scale, ...natural, offset });
|
|
33
38
|
};
|
|
34
39
|
const observer = new ResizeObserver(measure);
|
|
35
40
|
observer.observe(hostEl);
|
|
36
41
|
observer.observe(deviceEl);
|
|
37
42
|
return () => observer.disconnect();
|
|
38
|
-
}, []);
|
|
43
|
+
}, [fit]);
|
|
39
44
|
return /* @__PURE__ */ jsx(
|
|
40
45
|
"div",
|
|
41
46
|
{
|
|
42
47
|
ref: host,
|
|
43
|
-
className: "ios-preview-host",
|
|
44
|
-
style: { maxWidth:
|
|
48
|
+
className: cn("ios-preview-host", className),
|
|
49
|
+
style: fit === "contain" ? void 0 : { maxWidth: box.width || void 0, height: box.height ? box.height * box.scale : height },
|
|
45
50
|
children: /* @__PURE__ */ jsx(
|
|
46
51
|
"div",
|
|
47
52
|
{
|
|
48
53
|
ref: device,
|
|
49
54
|
className: "ios-phone-frame",
|
|
50
|
-
style: { width: width / 0.926, transform: `scale(${
|
|
55
|
+
style: { width: width / 0.926, insetInlineStart: box.offset, transform: `scale(${box.scale})` },
|
|
51
56
|
children: /* @__PURE__ */ jsx(Phone, { statusBar: false, systemChrome: false, className: "w-full", children: /* @__PURE__ */ jsx("iframe", { ref: frameRef, onLoad, src, title, className: "ios-phone-screen" }) })
|
|
52
57
|
}
|
|
53
58
|
)
|
package/dist/ios/styles.css
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
--ios-motion: 300ms; --ios-safe-top: 62px; --ios-header-height: 154px; --ios-tabbar-bottom: 28px; --ios-content-bottom: 110px;
|
|
25
25
|
font-family: var(--font-sans, system-ui, sans-serif); color: var(--ios-ink); background: var(--ios-canvas); height: 100dvh; overflow: hidden;
|
|
26
26
|
}
|
|
27
|
+
/* On a real phone (NativeApp systemChrome={false}): the device's own insets, the
|
|
28
|
+
same chrome heights above and below them. */
|
|
29
|
+
.ios-prototype.ios-bare {
|
|
30
|
+
--ios-safe-top: env(safe-area-inset-top, 0px); --ios-header-height: calc(92px + var(--ios-safe-top));
|
|
31
|
+
--ios-tabbar-bottom: max(12px, env(safe-area-inset-bottom, 0px)); --ios-content-bottom: calc(82px + var(--ios-tabbar-bottom));
|
|
32
|
+
}
|
|
27
33
|
.ios-prototype.dark {
|
|
28
34
|
--ios-canvas: var(--background, #000); --ios-ink: var(--foreground, #f2f2f7); --ios-secondary: var(--muted-foreground, #aeaeb2);
|
|
29
35
|
--ios-panel: var(--card, #1c1c1e); --ios-primary-ink: var(--primary-foreground, #fff);
|
|
@@ -140,6 +140,42 @@ interface PatternMeta {
|
|
|
140
140
|
/** Accessibility notes. */
|
|
141
141
|
a11y?: string[];
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* A block: a section-sized composition of components with sample content —
|
|
145
|
+
* smaller than a page shell, with no behaviour of its own to upgrade. Ships
|
|
146
|
+
* the way patterns do (copy-in source + importable preview). Its props are
|
|
147
|
+
* the block contract only — `className`, `framed` on Card-built blocks, and
|
|
148
|
+
* the primary action's callback — never content; content is the constant at
|
|
149
|
+
* the top of the source, edited after copying.
|
|
150
|
+
*/
|
|
151
|
+
interface BlockMeta {
|
|
152
|
+
/** Block name, e.g. "SignInBlock". Always ends in "Block". */
|
|
153
|
+
name: string;
|
|
154
|
+
/** One-line summary. */
|
|
155
|
+
summary: string;
|
|
156
|
+
/** When to reach for this block — and when a component or page shell fits better. */
|
|
157
|
+
intent: string;
|
|
158
|
+
/**
|
|
159
|
+
* "card" — one self-contained object that fills its container up to a
|
|
160
|
+
* designed width. "section" — a full-width band of a page.
|
|
161
|
+
*/
|
|
162
|
+
kind: "card" | "section";
|
|
163
|
+
/**
|
|
164
|
+
* The width range it is designed for, as CSS lengths. The block renders
|
|
165
|
+
* `w-full` capped at `max`; `min` is the narrowest it still holds together
|
|
166
|
+
* (a grid's `minItemWidth`). Section blocks omit it.
|
|
167
|
+
*/
|
|
168
|
+
width?: {
|
|
169
|
+
min: string;
|
|
170
|
+
max: string;
|
|
171
|
+
};
|
|
172
|
+
/** Groups related blocks in the catalog ("Auth", "Commerce", …). */
|
|
173
|
+
family: string;
|
|
174
|
+
/** Canonical decisions the block encodes. */
|
|
175
|
+
notes?: string[];
|
|
176
|
+
/** Accessibility notes. */
|
|
177
|
+
a11y?: string[];
|
|
178
|
+
}
|
|
143
179
|
interface ComponentMeta {
|
|
144
180
|
/** Public component name, e.g. "Button". Must be an actual export. */
|
|
145
181
|
name: string;
|
|
@@ -202,4 +238,4 @@ interface ComponentMeta {
|
|
|
202
238
|
examples: ExampleMeta[];
|
|
203
239
|
}
|
|
204
240
|
|
|
205
|
-
export type { AnatomyMeta as A,
|
|
241
|
+
export type { AnatomyMeta as A, BlockMeta as B, ComponentMeta as C, ExampleMeta as E, MotionMeta as M, PatternMeta as P, RelatedMeta as R, SubcomponentMeta as S, UsageMeta as U, BestPracticeMeta as a, MotionSlot as b, PropMeta as c };
|
package/dist/patterns/index.d.ts
CHANGED
package/dist/patterns/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TextReveal, Reveal, MessageScroller, Marker, Toolbar, ToolbarButton, StreamingText, ChatComposer, SettingsMenu, SettingsRow, AppShell, PageHeader, DataTable, Meter, RelativeTime, Sidebar, SidebarHeader, SidebarNav, SidebarItem, NotificationBadge, SidebarFooter, BottomNav, BottomNavItem, AppHeader, Tabs, TabsList, TabsTrigger, TabsPanel, Skeleton, Browser, MeterLabel, MeterValue, MeterTrack, SearchInput, EmptyState, SideNav, SideNavItem } from '../chunk-6LCIBMMG.js';
|
|
2
|
+
import { Form, Field, Input, Button, Separator, Card, CardContent, Badge, Entity, Avatar, StatusDot, CardHeader, CardTitle, CardDescription, CardAction, Message, Bubble, CodeBlock, SimpleTooltip, Attachment, CardFooter, MenuRadioGroup, MenuRadioItem, CardList, OverflowMenu, MenuItem, MenuSeparator, Textarea, Code, Select, SelectTrigger, SelectContent, SelectItem, DestructiveActionModal, Switch } from '../chunk-CKNM7KSB.js';
|
|
2
3
|
import { Region, Stack, Row, Grid, Page, Columns, Col, AspectBox } from '../chunk-3IWZYBGH.js';
|
|
3
4
|
import '../chunk-SRQWQUTW.js';
|
|
4
|
-
import { Icon } from '../chunk-
|
|
5
|
-
import '../chunk-
|
|
5
|
+
import { Icon } from '../chunk-NGBDP3BM.js';
|
|
6
|
+
import '../chunk-TH563WLW.js';
|
|
6
7
|
import { useState, useRef, useEffect } from 'react';
|
|
7
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
9
|
|
|
@@ -55,8 +56,8 @@ function AuthPage() {
|
|
|
55
56
|
),
|
|
56
57
|
/* @__PURE__ */ jsx(Separator, { label: "or" }),
|
|
57
58
|
/* @__PURE__ */ jsxs(Stack, { gap: 1.5, children: [
|
|
58
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "cta", className: "w-full", children: "Sign in with a passkey" }),
|
|
59
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "cta", className: "w-full", children: "Connect a wallet" })
|
|
59
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "cta", className: "w-full", iconStart: /* @__PURE__ */ jsx(Icon, { name: "key" }), children: "Sign in with a passkey" }),
|
|
60
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "cta", className: "w-full", iconStart: /* @__PURE__ */ jsx(Icon, { name: "wallet" }), children: "Connect a wallet" })
|
|
60
61
|
] }),
|
|
61
62
|
/* @__PURE__ */ jsxs("p", { className: "text-body-md text-muted-foreground", children: [
|
|
62
63
|
"No account?",
|
|
@@ -740,13 +741,14 @@ function DashboardPage() {
|
|
|
740
741
|
),
|
|
741
742
|
/* @__PURE__ */ jsx(SidebarItem, { icon: /* @__PURE__ */ jsx(Icon, { name: "gear" }), label: "Settings", render: /* @__PURE__ */ jsx("a", { href: "/settings" }) })
|
|
742
743
|
] }),
|
|
743
|
-
/* @__PURE__ */ jsx(SidebarFooter, {
|
|
744
|
+
/* @__PURE__ */ jsx(SidebarFooter, { children: /* @__PURE__ */ jsx(
|
|
744
745
|
Entity,
|
|
745
746
|
{
|
|
746
747
|
className: "[--entity-py:0]",
|
|
747
748
|
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: "AL", alt: "" }),
|
|
748
749
|
title: "Ada Lovelace",
|
|
749
|
-
|
|
750
|
+
subtitle: "@ada",
|
|
751
|
+
action: /* @__PURE__ */ jsxs(OverflowMenu, { label: "Account", size: "icon-sm", children: [
|
|
750
752
|
/* @__PURE__ */ jsx(MenuItem, { render: /* @__PURE__ */ jsx("a", { href: "/@ada" }), children: "View profile" }),
|
|
751
753
|
/* @__PURE__ */ jsx(MenuItem, { render: /* @__PURE__ */ jsx("a", { href: "/settings" }), children: "Settings" }),
|
|
752
754
|
/* @__PURE__ */ jsx(MenuSeparator, {}),
|
|
@@ -954,7 +956,7 @@ var dashboardPageMeta = {
|
|
|
954
956
|
"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 \u2014 never hand-set pl-6/pr-6. Numeric columns align end in mono; Table scrolls horizontally inside the card on narrow screens.",
|
|
955
957
|
'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.',
|
|
956
958
|
"Lists are Entity rows in CardList, inset to the card padding and divided by hairlines \u2014 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`.",
|
|
957
|
-
"Brand and account live in the Sidebar: SidebarHeader holds the mark + wordmark, SidebarFooter holds the signed-in Entity with
|
|
959
|
+
"Brand and account live in the Sidebar: SidebarHeader holds the mark + wordmark, SidebarFooter holds the signed-in Entity with an OverflowMenu. Sidebar rebinds the page ink tokens to its own, so the Entity and OverflowMenu read correctly even when a theme paints the rail dark on a light page.",
|
|
958
960
|
"The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.",
|
|
959
961
|
"Chrome (Sidebar) is outside the content grid; the Page region caps only content.",
|
|
960
962
|
"Sidebar hides below lg, so the shell's header carries an AppHeader (brand + account) and its footer a BottomNav with the same destinations \u2014 never strand mobile users without navigation."
|
|
@@ -1020,7 +1022,7 @@ var renderPost = (post) => /* @__PURE__ */ jsx("article", { children: /* @__PURE
|
|
|
1020
1022
|
leading: /* @__PURE__ */ jsx(Avatar, { fallback: post.initials, alt: post.author }),
|
|
1021
1023
|
title: post.author,
|
|
1022
1024
|
subtitle: `${post.handle} \xB7 ${post.time}`,
|
|
1023
|
-
action: /* @__PURE__ */ jsxs(
|
|
1025
|
+
action: /* @__PURE__ */ jsxs(OverflowMenu, { label: `Post options for ${post.author}`, size: "icon-sm", children: [
|
|
1024
1026
|
/* @__PURE__ */ jsx(MenuItem, { children: "Copy link" }),
|
|
1025
1027
|
/* @__PURE__ */ jsxs(MenuItem, { children: [
|
|
1026
1028
|
"Mute ",
|
|
@@ -1125,7 +1127,7 @@ function FeedPage() {
|
|
|
1125
1127
|
] }),
|
|
1126
1128
|
/* @__PURE__ */ jsx("aside", { "aria-label": "Suggestions", className: "sticky top-6 hidden w-80 shrink-0 lg:block", children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
1127
1129
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
1128
|
-
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, {
|
|
1130
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Who to follow" }) }),
|
|
1129
1131
|
/* @__PURE__ */ jsx(CardList, { children: suggestions.map((person) => /* @__PURE__ */ jsx(
|
|
1130
1132
|
Entity,
|
|
1131
1133
|
{
|
|
@@ -1138,7 +1140,7 @@ function FeedPage() {
|
|
|
1138
1140
|
)) })
|
|
1139
1141
|
] }),
|
|
1140
1142
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
1141
|
-
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, {
|
|
1143
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Upcoming drops" }) }),
|
|
1142
1144
|
/* @__PURE__ */ jsx(CardList, { children: upcoming2.map((drop) => /* @__PURE__ */ jsx(
|
|
1143
1145
|
Entity,
|
|
1144
1146
|
{
|
|
@@ -1163,14 +1165,14 @@ var feedPageMeta = {
|
|
|
1163
1165
|
"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 \u2014 its content is supplementary, so nothing is lost on mobile.",
|
|
1164
1166
|
"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.",
|
|
1165
1167
|
"The composer is a Card holding `Textarea autoResize` plus ghost icon Buttons and a secondary `Post` \u2014 not ChatComposer, which submits on Enter and is for conversations.",
|
|
1166
|
-
"Each post is a Card: an Entity byline in a CardList (with
|
|
1168
|
+
"Each post is a Card: an Entity byline in a CardList (with an OverflowMenu 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.",
|
|
1167
1169
|
'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 \u2014 collected against a known maximum is a level, not task progress.',
|
|
1168
1170
|
"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.",
|
|
1169
1171
|
"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 \u2014 predictable for keyboard and AT users, unlike infinite scroll."
|
|
1170
1172
|
],
|
|
1171
1173
|
a11y: [
|
|
1172
1174
|
"Each post is an <article>, so AT users can jump between posts; the rail is a labelled <aside> landmark.",
|
|
1173
|
-
|
|
1175
|
+
"Rail card titles render as h2 (CardTitle `render={<h2 />}`) under the page's one h1.",
|
|
1174
1176
|
"Icon-only buttons and every Follow / Remind button carry aria-labels naming their target; counts stay in the accessible name.",
|
|
1175
1177
|
'The supply Meter is role=meter with a getAriaValueText reading ("62 of 100 collected").',
|
|
1176
1178
|
'The Skeleton card is aria-busy; the loading Button keeps its label ("Loading posts").'
|
|
@@ -1713,9 +1715,8 @@ function ListDetailPage() {
|
|
|
1713
1715
|
Entity,
|
|
1714
1716
|
{
|
|
1715
1717
|
"data-member-id": m.id,
|
|
1716
|
-
"data-selected": isSelected ? "" : void 0,
|
|
1717
|
-
className: "sable-surface-interactive",
|
|
1718
1718
|
interactive: true,
|
|
1719
|
+
selected: isSelected,
|
|
1719
1720
|
onSelect: () => select(m.id),
|
|
1720
1721
|
leading: /* @__PURE__ */ jsx(
|
|
1721
1722
|
Avatar,
|
|
@@ -1725,10 +1726,7 @@ function ListDetailPage() {
|
|
|
1725
1726
|
"aria-hidden": true
|
|
1726
1727
|
}
|
|
1727
1728
|
),
|
|
1728
|
-
title:
|
|
1729
|
-
m.name,
|
|
1730
|
-
isSelected ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (selected)" }) : null
|
|
1731
|
-
] }),
|
|
1729
|
+
title: m.name,
|
|
1732
1730
|
subtitle: m.pending ? `${roleLabel[m.role]} \xB7 invite sent ${m.sent?.toLowerCase()}` : `${roleLabel[m.role]} \xB7 ${m.handle}`,
|
|
1733
1731
|
action: m.pending ? /* @__PURE__ */ jsx(Badge, { tone: "info", children: "Invited" }) : void 0
|
|
1734
1732
|
},
|
|
@@ -1782,7 +1780,7 @@ function ListDetailPage() {
|
|
|
1782
1780
|
}
|
|
1783
1781
|
),
|
|
1784
1782
|
/* @__PURE__ */ jsxs(Stack, { gap: 0, className: "min-w-0", children: [
|
|
1785
|
-
/* @__PURE__ */ jsx(CardTitle, {
|
|
1783
|
+
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), className: "truncate", children: selected.name }),
|
|
1786
1784
|
/* @__PURE__ */ jsx(CardDescription, { className: "truncate", children: selected.pending ? "Hasn't joined yet" : `${selected.handle} \xB7 ${selected.email}` })
|
|
1787
1785
|
] })
|
|
1788
1786
|
] }),
|
|
@@ -1843,7 +1841,7 @@ function ListDetailPage() {
|
|
|
1843
1841
|
] }) }) : null
|
|
1844
1842
|
] }),
|
|
1845
1843
|
selected.activity.length > 0 ? /* @__PURE__ */ jsxs(Card, { children: [
|
|
1846
|
-
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, {
|
|
1844
|
+
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h3", {}), children: "Recent activity" }) }),
|
|
1847
1845
|
/* @__PURE__ */ jsx(CardList, { children: selected.activity.map((a) => /* @__PURE__ */ jsx(
|
|
1848
1846
|
Entity,
|
|
1849
1847
|
{
|
|
@@ -1878,7 +1876,7 @@ var listDetailPageMeta = {
|
|
|
1878
1876
|
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.",
|
|
1879
1877
|
notes: [
|
|
1880
1878
|
"Region cap is `wide`; Columns 12 \u2192 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.",
|
|
1881
|
-
"Rows are interactive Entity inside one Card + CardList. Selection is `
|
|
1879
|
+
"Rows are interactive Entity inside one Card + CardList. Selection is Entity `selected`; `sable-surface` on the list card makes its selected step 12% ink off the card, so a selected row stays distinct from its muted avatar.",
|
|
1882
1880
|
"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.",
|
|
1883
1881
|
"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).",
|
|
1884
1882
|
"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.",
|
|
@@ -1887,7 +1885,7 @@ var listDetailPageMeta = {
|
|
|
1887
1885
|
"The detail pane is sticky (lg:top-6) so it stays in view while a long list scrolls."
|
|
1888
1886
|
],
|
|
1889
1887
|
a11y: [
|
|
1890
|
-
"Each row is a real button (Entity `interactive` + `onSelect`): Tab moves through rows, Enter/Space selects. The selected row
|
|
1888
|
+
"Each row is a real button (Entity `interactive` + `onSelect`): Tab moves through rows, Enter/Space selects. The selected row is Entity `selected`, which paints the selected fill and sets aria-current; list avatars are aria-hidden so initials are not read before the name.",
|
|
1891
1889
|
"The detail pane is a labelled region (`Member details`) whose title is a level-2 heading.",
|
|
1892
1890
|
"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."
|
|
1893
1891
|
]
|
|
@@ -1960,13 +1958,14 @@ function SettingsPage() {
|
|
|
1960
1958
|
),
|
|
1961
1959
|
/* @__PURE__ */ jsx(SidebarItem, { icon: /* @__PURE__ */ jsx(Icon, { name: "gear" }), label: "Settings", active: true, render: /* @__PURE__ */ jsx("a", { href: "/settings" }) })
|
|
1962
1960
|
] }),
|
|
1963
|
-
/* @__PURE__ */ jsx(SidebarFooter, {
|
|
1961
|
+
/* @__PURE__ */ jsx(SidebarFooter, { children: /* @__PURE__ */ jsx(
|
|
1964
1962
|
Entity,
|
|
1965
1963
|
{
|
|
1966
1964
|
className: "[--entity-py:0]",
|
|
1967
1965
|
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: "AL", alt: "" }),
|
|
1968
1966
|
title: "Ada Lovelace",
|
|
1969
|
-
|
|
1967
|
+
subtitle: "@ada",
|
|
1968
|
+
action: /* @__PURE__ */ jsxs(OverflowMenu, { label: "Account", size: "icon-sm", children: [
|
|
1970
1969
|
/* @__PURE__ */ jsx(MenuItem, { render: /* @__PURE__ */ jsx("a", { href: "/@ada" }), children: "View profile" }),
|
|
1971
1970
|
/* @__PURE__ */ jsx(MenuItem, { render: /* @__PURE__ */ jsx("a", { href: "/settings" }), children: "Settings" }),
|
|
1972
1971
|
/* @__PURE__ */ jsx(MenuSeparator, {}),
|
|
@@ -1998,7 +1997,7 @@ function SettingsPage() {
|
|
|
1998
1997
|
/* @__PURE__ */ jsx(Col, { span: { base: 12, lg: 9 }, children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
1999
1998
|
/* @__PURE__ */ jsxs(Card, { id: "profile", className: "scroll-mt-6", children: [
|
|
2000
1999
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
2001
|
-
/* @__PURE__ */ jsx(CardTitle, {
|
|
2000
|
+
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Profile" }),
|
|
2002
2001
|
/* @__PURE__ */ jsx(CardDescription, { children: "How collectors see you on Atelier." })
|
|
2003
2002
|
] }),
|
|
2004
2003
|
/* @__PURE__ */ jsxs(Form, { onSubmit: (event) => event.preventDefault(), className: "gap-0", children: [
|
|
@@ -2034,7 +2033,7 @@ function SettingsPage() {
|
|
|
2034
2033
|
] }),
|
|
2035
2034
|
/* @__PURE__ */ jsxs(Card, { id: "notifications", className: "scroll-mt-6", children: [
|
|
2036
2035
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
2037
|
-
/* @__PURE__ */ jsx(CardTitle, {
|
|
2036
|
+
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Notifications" }),
|
|
2038
2037
|
/* @__PURE__ */ jsx(CardDescription, { children: "Email updates about your editions. Changes save automatically." })
|
|
2039
2038
|
] }),
|
|
2040
2039
|
/* @__PURE__ */ jsx(CardList, { children: notifications.map((item) => /* @__PURE__ */ jsx(
|
|
@@ -2056,7 +2055,7 @@ function SettingsPage() {
|
|
|
2056
2055
|
] }),
|
|
2057
2056
|
/* @__PURE__ */ jsxs(Card, { id: "payouts", className: "scroll-mt-6", children: [
|
|
2058
2057
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
2059
|
-
/* @__PURE__ */ jsx(CardTitle, {
|
|
2058
|
+
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Payouts" }),
|
|
2060
2059
|
/* @__PURE__ */ jsx(CardDescription, { children: "Paid in USD every Friday to Chase checking \u2022\u20224821." }),
|
|
2061
2060
|
/* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", render: /* @__PURE__ */ jsx("a", { href: "/settings/payouts" }), children: "Manage payouts" }) })
|
|
2062
2061
|
] }),
|
|
@@ -2068,7 +2067,7 @@ function SettingsPage() {
|
|
|
2068
2067
|
] }),
|
|
2069
2068
|
/* @__PURE__ */ jsxs(Card, { id: "account", className: "scroll-mt-6", children: [
|
|
2070
2069
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
2071
|
-
/* @__PURE__ */ jsx(CardTitle, {
|
|
2070
|
+
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Account" }),
|
|
2072
2071
|
/* @__PURE__ */ jsx(CardDescription, { children: "Sign-in details and your data." })
|
|
2073
2072
|
] }),
|
|
2074
2073
|
/* @__PURE__ */ jsxs(CardList, { children: [
|
|
@@ -2094,7 +2093,7 @@ function SettingsPage() {
|
|
|
2094
2093
|
] }),
|
|
2095
2094
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
2096
2095
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
2097
|
-
/* @__PURE__ */ jsx(CardTitle, {
|
|
2096
|
+
/* @__PURE__ */ jsx(CardTitle, { render: /* @__PURE__ */ jsx("h2", {}), children: "Delete account" }),
|
|
2098
2097
|
/* @__PURE__ */ jsx(CardDescription, { children: "Permanently remove your profile, editions, and sales history. Collectors keep the editions they own." })
|
|
2099
2098
|
] }),
|
|
2100
2099
|
/* @__PURE__ */ jsx(CardFooter, { className: "justify-end", children: /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => setDeleteOpen(true), children: "Delete account" }) })
|
|
@@ -15,6 +15,19 @@
|
|
|
15
15
|
|
|
16
16
|
:root,
|
|
17
17
|
[data-sable-theme="base"] {
|
|
18
|
+
/* Chart series colours — a categorical palette validated for colour-blind
|
|
19
|
+
separation (adjacent pairs), a normal-vision floor, a lightness band and a
|
|
20
|
+
chroma floor against this theme's surfaces. Charts take them in order
|
|
21
|
+
(series 1 → --chart-1). Global defaults: every theme inherits them, and a
|
|
22
|
+
theme or preset may override the set — re-validate it if it does. */
|
|
23
|
+
--chart-1: oklch(57.53% 0.1626 255.53); /* #2a78d6 */
|
|
24
|
+
--chart-2: oklch(67.08% 0.1750 40.64); /* #eb6834 */
|
|
25
|
+
--chart-3: oklch(66.90% 0.1408 162.11); /* #1baf7a */
|
|
26
|
+
--chart-4: oklch(76.44% 0.1612 75.12); /* #eda100 */
|
|
27
|
+
--chart-5: oklch(71.63% 0.1412 357.39); /* #e87ba4 */
|
|
28
|
+
--chart-6: oklch(52.85% 0.1798 142.50); /* #008300 */
|
|
29
|
+
--chart-7: oklch(52.71% 0.1968 283.44); /* #6250d6 */
|
|
30
|
+
--chart-8: oklch(62.26% 0.1909 24.91); /* #e34948 */
|
|
18
31
|
/* Font stacks (theme-internal; host app provides the actual font files) */
|
|
19
32
|
--font-sans: "Figtree", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
20
33
|
--font-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
@@ -374,6 +387,16 @@
|
|
|
374
387
|
/* A preset-scoped subtree inside a dark page follows the page into dark mode;
|
|
375
388
|
add `.light` to that subtree to pin it light. */
|
|
376
389
|
.dark [data-sable-theme="base"]:not(.light) {
|
|
390
|
+
/* The same eight hues, stepped for the dark band (L 0.48–0.67, >= 3:1 on
|
|
391
|
+
the dark surfaces) and validated as a set — not a lightness flip. */
|
|
392
|
+
--chart-1: oklch(62.21% 0.1612 255.05); /* #3987e5 */
|
|
393
|
+
--chart-2: oklch(62.21% 0.1726 40.11); /* #d95926 */
|
|
394
|
+
--chart-3: oklch(62.12% 0.1283 163.11); /* #199e70 */
|
|
395
|
+
--chart-4: oklch(66.99% 0.1425 73.23); /* #c98500 */
|
|
396
|
+
--chart-5: oklch(62.24% 0.1712 0.84); /* #d55181 */
|
|
397
|
+
--chart-6: oklch(52.85% 0.1798 142.50); /* #008300 */
|
|
398
|
+
--chart-7: oklch(66.96% 0.1452 286.83); /* #9085e9 */
|
|
399
|
+
--chart-8: oklch(66.93% 0.1586 22.31); /* #e66767 */
|
|
377
400
|
/* Elevation, dark. A drop barely registers on near-black, so depth is the
|
|
378
401
|
surface step (popover above card) plus a faint top-edge highlight that
|
|
379
402
|
catches "light" on anything floating. Deeper drops than light mode, since
|
package/dist/themes/index.js
CHANGED
|
@@ -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-
|
|
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';
|
|
@@ -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
|
+
```
|