chore: storybook (#15285)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
149
packages/ui/src/components/accordion.stories.tsx
Normal file
149
packages/ui/src/components/accordion.stories.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
// @ts-nocheck
|
||||
import { createEffect, createSignal } from "solid-js"
|
||||
import * as mod from "./accordion"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Accordion for collapsible content sections with optional multi-open behavior.
|
||||
|
||||
Use one trigger per item; keep content concise.
|
||||
|
||||
### API
|
||||
- Root supports Kobalte Accordion props: \`value\`, \`multiple\`, \`collapsible\`, \`onChange\`.
|
||||
- Compose with \`Accordion.Item\`, \`Header\`, \`Trigger\`, \`Content\`.
|
||||
|
||||
### Variants and states
|
||||
- Single or multiple open items.
|
||||
- Collapsible or fixed-open behavior.
|
||||
|
||||
### Behavior
|
||||
- Controlled via \`value\`/\`onChange\` when provided.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard navigation from Kobalte Accordion.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="accordion"\` and slot data attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Accordion", mod })
|
||||
export default {
|
||||
title: "UI/Accordion",
|
||||
id: "components-accordion",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
export const Basic = {
|
||||
args: {
|
||||
collapsible: true,
|
||||
multiple: false,
|
||||
value: "first",
|
||||
},
|
||||
argTypes: {
|
||||
collapsible: { control: "boolean" },
|
||||
multiple: { control: "boolean" },
|
||||
value: {
|
||||
control: "select",
|
||||
options: ["first", "second", "none"],
|
||||
mapping: {
|
||||
none: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (props) => {
|
||||
const [value, setValue] = createSignal(props.value)
|
||||
createEffect(() => {
|
||||
setValue(props.value)
|
||||
})
|
||||
|
||||
const current = () => {
|
||||
if (props.multiple) {
|
||||
if (Array.isArray(value())) return value()
|
||||
if (value()) return [value()]
|
||||
return []
|
||||
}
|
||||
|
||||
if (Array.isArray(value())) return value()[0]
|
||||
return value()
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px", width: "420px" }}>
|
||||
<mod.Accordion collapsible={props.collapsible} multiple={props.multiple} value={current()} onChange={setValue}>
|
||||
<mod.Accordion.Item value="first">
|
||||
<mod.Accordion.Header>
|
||||
<mod.Accordion.Trigger>First</mod.Accordion.Trigger>
|
||||
</mod.Accordion.Header>
|
||||
<mod.Accordion.Content>
|
||||
<div style={{ color: "var(--text-weak)", padding: "8px 0" }}>Accordion content.</div>
|
||||
</mod.Accordion.Content>
|
||||
</mod.Accordion.Item>
|
||||
<mod.Accordion.Item value="second">
|
||||
<mod.Accordion.Header>
|
||||
<mod.Accordion.Trigger>Second</mod.Accordion.Trigger>
|
||||
</mod.Accordion.Header>
|
||||
<mod.Accordion.Content>
|
||||
<div style={{ color: "var(--text-weak)", padding: "8px 0" }}>More content.</div>
|
||||
</mod.Accordion.Content>
|
||||
</mod.Accordion.Item>
|
||||
</mod.Accordion>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Multiple = {
|
||||
args: {
|
||||
collapsible: true,
|
||||
multiple: true,
|
||||
value: ["first", "second"],
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Accordion collapsible={props.collapsible} multiple={props.multiple} value={props.value}>
|
||||
<mod.Accordion.Item value="first">
|
||||
<mod.Accordion.Header>
|
||||
<mod.Accordion.Trigger>First</mod.Accordion.Trigger>
|
||||
</mod.Accordion.Header>
|
||||
<mod.Accordion.Content>
|
||||
<div style={{ color: "var(--text-weak)", padding: "8px 0" }}>Accordion content.</div>
|
||||
</mod.Accordion.Content>
|
||||
</mod.Accordion.Item>
|
||||
<mod.Accordion.Item value="second">
|
||||
<mod.Accordion.Header>
|
||||
<mod.Accordion.Trigger>Second</mod.Accordion.Trigger>
|
||||
</mod.Accordion.Header>
|
||||
<mod.Accordion.Content>
|
||||
<div style={{ color: "var(--text-weak)", padding: "8px 0" }}>More content.</div>
|
||||
</mod.Accordion.Content>
|
||||
</mod.Accordion.Item>
|
||||
</mod.Accordion>
|
||||
),
|
||||
}
|
||||
|
||||
export const NonCollapsible = {
|
||||
args: {
|
||||
collapsible: false,
|
||||
multiple: false,
|
||||
value: "first",
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Accordion collapsible={props.collapsible} multiple={props.multiple} value={props.value}>
|
||||
<mod.Accordion.Item value="first">
|
||||
<mod.Accordion.Header>
|
||||
<mod.Accordion.Trigger>First</mod.Accordion.Trigger>
|
||||
</mod.Accordion.Header>
|
||||
<mod.Accordion.Content>
|
||||
<div style={{ color: "var(--text-weak)", padding: "8px 0" }}>Accordion content.</div>
|
||||
</mod.Accordion.Content>
|
||||
</mod.Accordion.Item>
|
||||
</mod.Accordion>
|
||||
),
|
||||
}
|
||||
69
packages/ui/src/components/app-icon.stories.tsx
Normal file
69
packages/ui/src/components/app-icon.stories.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
// @ts-nocheck
|
||||
import { iconNames } from "./app-icons/types"
|
||||
import * as mod from "./app-icon"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Application icon renderer for known editor/terminal apps.
|
||||
|
||||
Use in provider or app selection lists.
|
||||
|
||||
### API
|
||||
- Required: \`id\` (app icon name).
|
||||
- Accepts standard img props except \`src\`.
|
||||
|
||||
### Variants and states
|
||||
- Auto-switches themed icons when available.
|
||||
|
||||
### Behavior
|
||||
- Watches color scheme changes to swap themed assets.
|
||||
|
||||
### Accessibility
|
||||
- Provide \`alt\` text when the icon conveys meaning.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="app-icon"\`.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/AppIcon", mod, args: { id: "vscode" } })
|
||||
export default {
|
||||
title: "UI/AppIcon",
|
||||
id: "components-app-icon",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
id: {
|
||||
control: "select",
|
||||
options: iconNames,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const AllIcons = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "12px",
|
||||
"grid-template-columns": "repeat(auto-fill, minmax(72px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{iconNames.map((id) => (
|
||||
<div style={{ display: "grid", gap: "6px", "justify-items": "center" }}>
|
||||
<mod.AppIcon id={id} alt={id} />
|
||||
<div style={{ "font-size": "10px", color: "var(--text-weak)", "text-align": "center" }}>{id}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
76
packages/ui/src/components/avatar.stories.tsx
Normal file
76
packages/ui/src/components/avatar.stories.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./avatar"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
User avatar with image fallback to initials.
|
||||
|
||||
Use in user lists and headers.
|
||||
|
||||
### API
|
||||
- Required: \`fallback\` string.
|
||||
- Optional: \`src\`, \`background\`, \`foreground\`, \`size\`.
|
||||
|
||||
### Variants and states
|
||||
- Sizes: small, normal, large.
|
||||
- Image vs fallback state.
|
||||
|
||||
### Behavior
|
||||
- Uses grapheme-aware fallback rendering.
|
||||
|
||||
### Accessibility
|
||||
- TODO: provide alt text when using images; currently image is decorative.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="avatar"\` with size and image state attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Avatar", mod, args: { fallback: "A" } })
|
||||
|
||||
export default {
|
||||
title: "UI/Avatar",
|
||||
id: "components-avatar",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["small", "normal", "large"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const WithImage = {
|
||||
args: {
|
||||
src: "https://placehold.co/80x80/png",
|
||||
fallback: "J",
|
||||
},
|
||||
}
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
|
||||
<mod.Avatar size="small" fallback="S" />
|
||||
<mod.Avatar size="normal" fallback="N" />
|
||||
<mod.Avatar size="large" fallback="L" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const CustomColors = {
|
||||
args: {
|
||||
fallback: "C",
|
||||
background: "#1f2a44",
|
||||
foreground: "#f2f5ff",
|
||||
},
|
||||
}
|
||||
133
packages/ui/src/components/basic-tool.stories.tsx
Normal file
133
packages/ui/src/components/basic-tool.stories.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
// @ts-nocheck
|
||||
import { createSignal } from "solid-js"
|
||||
import * as mod from "./basic-tool"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Expandable tool panel with a structured trigger and optional details.
|
||||
|
||||
Use structured triggers for consistent layout; custom triggers allowed.
|
||||
|
||||
### API
|
||||
- Required: \`icon\` and \`trigger\` (structured or custom JSX).
|
||||
- Optional: \`status\`, \`defaultOpen\`, \`forceOpen\`, \`defer\`, \`locked\`.
|
||||
|
||||
### Variants and states
|
||||
- Pending/running status animates the title via TextShimmer.
|
||||
|
||||
### Behavior
|
||||
- Uses Collapsible; can defer content rendering until open.
|
||||
- Locked state prevents closing.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm trigger semantics and aria labeling.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="tool-trigger"\` and related slots.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Basic Tool",
|
||||
mod,
|
||||
args: {
|
||||
icon: "mcp",
|
||||
defaultOpen: true,
|
||||
trigger: {
|
||||
title: "Basic Tool",
|
||||
subtitle: "Example subtitle",
|
||||
args: ["--flag", "value"],
|
||||
},
|
||||
children: "Details content",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Basic Tool",
|
||||
id: "components-basic-tool",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Pending = {
|
||||
args: {
|
||||
status: "pending",
|
||||
trigger: {
|
||||
title: "Running tool",
|
||||
subtitle: "Working...",
|
||||
},
|
||||
children: "Progress details",
|
||||
},
|
||||
}
|
||||
|
||||
export const Locked = {
|
||||
args: {
|
||||
locked: true,
|
||||
trigger: {
|
||||
title: "Locked tool",
|
||||
subtitle: "Cannot close",
|
||||
},
|
||||
children: "Locked details",
|
||||
},
|
||||
}
|
||||
|
||||
export const Deferred = {
|
||||
args: {
|
||||
defer: true,
|
||||
defaultOpen: false,
|
||||
trigger: {
|
||||
title: "Deferred tool",
|
||||
subtitle: "Content mounts on open",
|
||||
},
|
||||
children: "Deferred content",
|
||||
},
|
||||
}
|
||||
|
||||
export const ForceOpen = {
|
||||
args: {
|
||||
forceOpen: true,
|
||||
trigger: {
|
||||
title: "Forced open",
|
||||
subtitle: "Cannot close",
|
||||
},
|
||||
children: "Forced content",
|
||||
},
|
||||
}
|
||||
|
||||
export const HideDetails = {
|
||||
args: {
|
||||
hideDetails: true,
|
||||
trigger: {
|
||||
title: "Summary only",
|
||||
subtitle: "Details hidden",
|
||||
},
|
||||
children: "Hidden content",
|
||||
},
|
||||
}
|
||||
|
||||
export const SubtitleAction = {
|
||||
render: () => {
|
||||
const [message, setMessage] = createSignal("Subtitle not clicked")
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div style={{ "font-size": "12px", color: "var(--text-weak)" }}>{message()}</div>
|
||||
<mod.BasicTool
|
||||
icon="mcp"
|
||||
trigger={{ title: "Clickable subtitle", subtitle: "Click me" }}
|
||||
onSubtitleClick={() => setMessage("Subtitle clicked")}
|
||||
>
|
||||
Subtitle action details
|
||||
</mod.BasicTool>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
108
packages/ui/src/components/button.stories.tsx
Normal file
108
packages/ui/src/components/button.stories.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// @ts-nocheck
|
||||
import { Button } from "./button"
|
||||
|
||||
const docs = `### Overview
|
||||
Primary action button with size, variant, and optional icon support.
|
||||
|
||||
Use \`IconButton\` for icon-only actions.
|
||||
|
||||
### API
|
||||
- \`variant\`: "primary" | "secondary" | "ghost".
|
||||
- \`size\`: "small" | "normal" | "large".
|
||||
- \`icon\`: Icon name for a leading icon.
|
||||
- Inherits Kobalte Button props and native button attributes.
|
||||
|
||||
### Variants and states
|
||||
- Variants: primary, secondary, ghost.
|
||||
- States: disabled.
|
||||
|
||||
### Behavior
|
||||
- Renders an Icon when \`icon\` is set.
|
||||
|
||||
### Accessibility
|
||||
- Provide clear label text; use \`aria-label\` for icon-only buttons.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="button"\` with size/variant data attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Button",
|
||||
id: "components-button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
children: "Button",
|
||||
variant: "secondary",
|
||||
size: "normal",
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["primary", "secondary", "ghost"],
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["small", "normal", "large"],
|
||||
},
|
||||
icon: {
|
||||
control: "select",
|
||||
options: ["none", "check", "plus", "arrow-right"],
|
||||
mapping: {
|
||||
none: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Primary = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
},
|
||||
}
|
||||
|
||||
export const Secondary = {}
|
||||
|
||||
export const Ghost = {
|
||||
args: {
|
||||
variant: "ghost",
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIcon = {
|
||||
args: {
|
||||
children: "Continue",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
|
||||
<Button size="small" variant="secondary">
|
||||
Small
|
||||
</Button>
|
||||
<Button size="normal" variant="secondary">
|
||||
Normal
|
||||
</Button>
|
||||
<Button size="large" variant="secondary">
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
90
packages/ui/src/components/card.stories.tsx
Normal file
90
packages/ui/src/components/card.stories.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// @ts-nocheck
|
||||
import { Card } from "./card"
|
||||
import { Button } from "./button"
|
||||
|
||||
const docs = `### Overview
|
||||
Surface container for grouping related content and actions.
|
||||
|
||||
Pair with \`Button\` or \`Tag\` for quick actions.
|
||||
|
||||
### API
|
||||
- Optional: \`variant\` (normal, error, warning, success, info).
|
||||
- Accepts standard div props.
|
||||
|
||||
### Variants and states
|
||||
- Semantic variants for status-driven messaging.
|
||||
|
||||
### Behavior
|
||||
- Pure presentational container.
|
||||
|
||||
### Accessibility
|
||||
- Provide headings or aria labels when used in isolation.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="card"\` with variant data attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Card",
|
||||
id: "components-card",
|
||||
component: Card,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
variant: "normal",
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["normal", "error", "warning", "success", "info"],
|
||||
},
|
||||
},
|
||||
render: (props: { variant?: "normal" | "error" | "warning" | "success" | "info" }) => {
|
||||
return (
|
||||
<Card variant={props.variant}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>Card title</div>
|
||||
<div style={{ color: "var(--text-weak)", fontSize: "13px" }}>Small supporting text.</div>
|
||||
</div>
|
||||
<Button size="small" variant="ghost">
|
||||
Action
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Normal = {}
|
||||
|
||||
export const Error = {
|
||||
args: {
|
||||
variant: "error",
|
||||
},
|
||||
}
|
||||
|
||||
export const Warning = {
|
||||
args: {
|
||||
variant: "warning",
|
||||
},
|
||||
}
|
||||
|
||||
export const Success = {
|
||||
args: {
|
||||
variant: "success",
|
||||
},
|
||||
}
|
||||
|
||||
export const Info = {
|
||||
args: {
|
||||
variant: "info",
|
||||
},
|
||||
}
|
||||
71
packages/ui/src/components/checkbox.stories.tsx
Normal file
71
packages/ui/src/components/checkbox.stories.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
// @ts-nocheck
|
||||
import { Icon } from "./icon"
|
||||
import * as mod from "./checkbox"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Checkbox control for multi-select or agreement inputs.
|
||||
|
||||
Use in forms and multi-select lists.
|
||||
|
||||
### API
|
||||
- Uses Kobalte Checkbox props (\`checked\`, \`defaultChecked\`, \`onChange\`).
|
||||
- Optional: \`hideLabel\`, \`description\`, \`icon\`.
|
||||
- Children render as the label.
|
||||
|
||||
### Variants and states
|
||||
- Checked/unchecked, indeterminate, disabled (via Kobalte).
|
||||
|
||||
### Behavior
|
||||
- Controlled or uncontrolled usage.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm aria attributes from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="checkbox"\` and related slots.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Checkbox", mod, args: { children: "Checkbox", defaultChecked: true } })
|
||||
export default {
|
||||
title: "UI/Checkbox",
|
||||
id: "components-checkbox",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const States = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.Checkbox defaultChecked>Checked</mod.Checkbox>
|
||||
<mod.Checkbox>Unchecked</mod.Checkbox>
|
||||
<mod.Checkbox disabled>Disabled</mod.Checkbox>
|
||||
<mod.Checkbox description="Helper text">With description</mod.Checkbox>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const CustomIcon = {
|
||||
render: () => (
|
||||
<mod.Checkbox icon={<Icon name="check" size="small" />} defaultChecked>
|
||||
Custom icon
|
||||
</mod.Checkbox>
|
||||
),
|
||||
}
|
||||
|
||||
export const HiddenLabel = {
|
||||
args: {
|
||||
children: "Hidden label",
|
||||
hideLabel: true,
|
||||
},
|
||||
}
|
||||
70
packages/ui/src/components/code.stories.tsx
Normal file
70
packages/ui/src/components/code.stories.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./code"
|
||||
import { create } from "../storybook/scaffold"
|
||||
import { code } from "../storybook/fixtures"
|
||||
|
||||
const docs = `### Overview
|
||||
Syntax-highlighted code viewer with selection support and large-file virtualization.
|
||||
|
||||
Use alongside \`LineComment\` and \`Diff\` in review workflows.
|
||||
|
||||
### API
|
||||
- Required: \`file\` with file name + contents.
|
||||
- Optional: \`language\`, \`annotations\`, \`selectedLines\`, \`commentedLines\`.
|
||||
- Optional callbacks: \`onRendered\`, \`onLineSelectionEnd\`.
|
||||
|
||||
### Variants and states
|
||||
- Supports large-file virtualization automatically.
|
||||
|
||||
### Behavior
|
||||
- Re-renders when \`file\` or rendering options change.
|
||||
- Optional line selection integrates with selection callbacks.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard find and selection behavior.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="code"\` and Pierre CSS variables from \`styleVariables\`.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Code",
|
||||
mod,
|
||||
args: {
|
||||
file: code,
|
||||
language: "ts",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Code",
|
||||
id: "components-code",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const SelectedLines = {
|
||||
args: {
|
||||
enableLineSelection: true,
|
||||
selectedLines: { start: 2, end: 4 },
|
||||
},
|
||||
}
|
||||
|
||||
export const CommentedLines = {
|
||||
args: {
|
||||
commentedLines: [
|
||||
{ start: 1, end: 1 },
|
||||
{ start: 5, end: 6 },
|
||||
],
|
||||
},
|
||||
}
|
||||
86
packages/ui/src/components/collapsible.stories.tsx
Normal file
86
packages/ui/src/components/collapsible.stories.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./collapsible"
|
||||
|
||||
const docs = `### Overview
|
||||
Toggleable content region with optional arrow indicator.
|
||||
|
||||
Compose \`Collapsible.Trigger\`, \`Collapsible.Content\`, and \`Collapsible.Arrow\`.
|
||||
|
||||
### API
|
||||
- Root accepts Kobalte Collapsible props (\`open\`, \`defaultOpen\`, \`onOpenChange\`).
|
||||
- \`variant\` controls styling ("normal" | "ghost").
|
||||
|
||||
### Variants and states
|
||||
- Normal and ghost variants.
|
||||
- Open/closed states.
|
||||
|
||||
### Behavior
|
||||
- Trigger toggles the content visibility.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm ARIA attributes provided by Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="collapsible"\` and slots for trigger/content/arrow.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Collapsible",
|
||||
id: "components-collapsible",
|
||||
component: mod.Collapsible,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["normal", "ghost"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
variant: "normal",
|
||||
defaultOpen: true,
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Collapsible {...props}>
|
||||
<mod.Collapsible.Trigger data-slot="collapsible-trigger">
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "8px" }}>
|
||||
<span>Details</span>
|
||||
<mod.Collapsible.Arrow />
|
||||
</div>
|
||||
</mod.Collapsible.Trigger>
|
||||
<mod.Collapsible.Content data-slot="collapsible-content">
|
||||
<div style={{ color: "var(--text-weak)", "padding-top": "8px" }}>Optional details sit here.</div>
|
||||
</mod.Collapsible.Content>
|
||||
</mod.Collapsible>
|
||||
),
|
||||
}
|
||||
|
||||
export const Ghost = {
|
||||
args: {
|
||||
variant: "ghost",
|
||||
defaultOpen: false,
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Collapsible {...props}>
|
||||
<mod.Collapsible.Trigger data-slot="collapsible-trigger">
|
||||
<div style={{ display: "flex", "align-items": "center", gap: "8px" }}>
|
||||
<span>Ghost trigger</span>
|
||||
<mod.Collapsible.Arrow />
|
||||
</div>
|
||||
</mod.Collapsible.Trigger>
|
||||
<mod.Collapsible.Content data-slot="collapsible-content">
|
||||
<div style={{ color: "var(--text-weak)", "padding-top": "8px" }}>Ghost content.</div>
|
||||
</mod.Collapsible.Content>
|
||||
</mod.Collapsible>
|
||||
),
|
||||
}
|
||||
113
packages/ui/src/components/context-menu.stories.tsx
Normal file
113
packages/ui/src/components/context-menu.stories.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./context-menu"
|
||||
|
||||
const docs = `### Overview
|
||||
Context menu for right-click interactions with composable items and submenus.
|
||||
|
||||
Use \`ItemLabel\` and \`ItemDescription\` for rich items.
|
||||
|
||||
### API
|
||||
- Root accepts Kobalte ContextMenu props (\`open\`, \`defaultOpen\`, \`onOpenChange\`).
|
||||
- Compose \`Trigger\`, \`Content\`, \`Item\`, \`Separator\`, and optional \`Sub\` sections.
|
||||
|
||||
### Variants and states
|
||||
- Supports grouped sections and nested submenus.
|
||||
|
||||
### Behavior
|
||||
- Opens on context menu gesture over the trigger element.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard and focus behavior from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="context-menu"\` and slot attributes for styling.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/ContextMenu",
|
||||
id: "components-context-menu",
|
||||
component: mod.ContextMenu,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<mod.ContextMenu defaultOpen>
|
||||
<mod.ContextMenu.Trigger>
|
||||
<div
|
||||
style={{
|
||||
padding: "20px",
|
||||
border: "1px dashed var(--border-weak)",
|
||||
"border-radius": "8px",
|
||||
color: "var(--text-weak)",
|
||||
}}
|
||||
>
|
||||
Right click (or open) here
|
||||
</div>
|
||||
</mod.ContextMenu.Trigger>
|
||||
<mod.ContextMenu.Portal>
|
||||
<mod.ContextMenu.Content>
|
||||
<mod.ContextMenu.Group>
|
||||
<mod.ContextMenu.GroupLabel>Actions</mod.ContextMenu.GroupLabel>
|
||||
<mod.ContextMenu.Item>
|
||||
<mod.ContextMenu.ItemLabel>Copy</mod.ContextMenu.ItemLabel>
|
||||
</mod.ContextMenu.Item>
|
||||
<mod.ContextMenu.Item>
|
||||
<mod.ContextMenu.ItemLabel>Paste</mod.ContextMenu.ItemLabel>
|
||||
</mod.ContextMenu.Item>
|
||||
</mod.ContextMenu.Group>
|
||||
<mod.ContextMenu.Separator />
|
||||
<mod.ContextMenu.Sub>
|
||||
<mod.ContextMenu.SubTrigger>More</mod.ContextMenu.SubTrigger>
|
||||
<mod.ContextMenu.SubContent>
|
||||
<mod.ContextMenu.Item>
|
||||
<mod.ContextMenu.ItemLabel>Duplicate</mod.ContextMenu.ItemLabel>
|
||||
</mod.ContextMenu.Item>
|
||||
<mod.ContextMenu.Item>
|
||||
<mod.ContextMenu.ItemLabel>Move</mod.ContextMenu.ItemLabel>
|
||||
</mod.ContextMenu.Item>
|
||||
</mod.ContextMenu.SubContent>
|
||||
</mod.ContextMenu.Sub>
|
||||
</mod.ContextMenu.Content>
|
||||
</mod.ContextMenu.Portal>
|
||||
</mod.ContextMenu>
|
||||
),
|
||||
}
|
||||
|
||||
export const CheckboxRadio = {
|
||||
render: () => (
|
||||
<mod.ContextMenu defaultOpen>
|
||||
<mod.ContextMenu.Trigger>
|
||||
<div
|
||||
style={{
|
||||
padding: "20px",
|
||||
border: "1px dashed var(--border-weak)",
|
||||
"border-radius": "8px",
|
||||
color: "var(--text-weak)",
|
||||
}}
|
||||
>
|
||||
Right click (or open) here
|
||||
</div>
|
||||
</mod.ContextMenu.Trigger>
|
||||
<mod.ContextMenu.Portal>
|
||||
<mod.ContextMenu.Content>
|
||||
<mod.ContextMenu.CheckboxItem checked>Show line numbers</mod.ContextMenu.CheckboxItem>
|
||||
<mod.ContextMenu.CheckboxItem>Wrap lines</mod.ContextMenu.CheckboxItem>
|
||||
<mod.ContextMenu.Separator />
|
||||
<mod.ContextMenu.RadioGroup value="compact">
|
||||
<mod.ContextMenu.RadioItem value="compact">Compact</mod.ContextMenu.RadioItem>
|
||||
<mod.ContextMenu.RadioItem value="comfortable">Comfortable</mod.ContextMenu.RadioItem>
|
||||
</mod.ContextMenu.RadioGroup>
|
||||
</mod.ContextMenu.Content>
|
||||
</mod.ContextMenu.Portal>
|
||||
</mod.ContextMenu>
|
||||
),
|
||||
}
|
||||
173
packages/ui/src/components/dialog.stories.tsx
Normal file
173
packages/ui/src/components/dialog.stories.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
// @ts-nocheck
|
||||
import { onMount } from "solid-js"
|
||||
import * as mod from "./dialog"
|
||||
import { Button } from "./button"
|
||||
import { useDialog } from "../context/dialog"
|
||||
|
||||
const docs = `### Overview
|
||||
Dialog content wrapper used with the DialogProvider for modal flows.
|
||||
|
||||
Provide concise title/description and keep body focused.
|
||||
|
||||
### API
|
||||
- Optional: \`title\`, \`description\`, \`action\`.
|
||||
- \`size\`: normal | large | x-large.
|
||||
- \`fit\` and \`transition\` control layout and animation.
|
||||
|
||||
### Variants and states
|
||||
- Sizes and optional header/action controls.
|
||||
|
||||
### Behavior
|
||||
- Intended to be rendered via \`useDialog().show\`.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm focus trapping and aria attributes from Kobalte Dialog.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="dialog"\` and slot attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Dialog",
|
||||
id: "components-dialog",
|
||||
component: mod.Dialog,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => {
|
||||
const dialog = useDialog()
|
||||
const open = () =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog title="Dialog" description="Description">
|
||||
Dialog body content.
|
||||
</mod.Dialog>
|
||||
))
|
||||
|
||||
onMount(open)
|
||||
|
||||
return (
|
||||
<Button variant="secondary" onClick={open}>
|
||||
Open dialog
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Sizes = {
|
||||
render: () => {
|
||||
const dialog = useDialog()
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "12px" }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog title="Normal" description="Normal size">
|
||||
Normal dialog content.
|
||||
</mod.Dialog>
|
||||
))
|
||||
}
|
||||
>
|
||||
Normal
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog size="large" title="Large" description="Large size">
|
||||
Large dialog content.
|
||||
</mod.Dialog>
|
||||
))
|
||||
}
|
||||
>
|
||||
Large
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog size="x-large" title="Extra large" description="X-large size">
|
||||
X-large dialog content.
|
||||
</mod.Dialog>
|
||||
))
|
||||
}
|
||||
>
|
||||
X-Large
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Transition = {
|
||||
render: () => {
|
||||
const dialog = useDialog()
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog title="Transition" description="Animated" transition>
|
||||
Transition enabled.
|
||||
</mod.Dialog>
|
||||
))
|
||||
}
|
||||
>
|
||||
Open transition dialog
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomAction = {
|
||||
render: () => {
|
||||
const dialog = useDialog()
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog
|
||||
title="Custom action"
|
||||
description="Dialog with a custom header action"
|
||||
action={<Button variant="ghost">Help</Button>}
|
||||
>
|
||||
Dialog body content.
|
||||
</mod.Dialog>
|
||||
))
|
||||
}
|
||||
>
|
||||
Open action dialog
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Fit = {
|
||||
render: () => {
|
||||
const dialog = useDialog()
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
dialog.show(() => (
|
||||
<mod.Dialog title="Fit content" fit>
|
||||
Dialog fits its content.
|
||||
</mod.Dialog>
|
||||
))
|
||||
}
|
||||
>
|
||||
Open fit dialog
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
}
|
||||
81
packages/ui/src/components/diff-changes.stories.tsx
Normal file
81
packages/ui/src/components/diff-changes.stories.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./diff-changes"
|
||||
import { create } from "../storybook/scaffold"
|
||||
import { changes } from "../storybook/fixtures"
|
||||
|
||||
const docs = `### Overview
|
||||
Summarize additions/deletions as text or compact bars.
|
||||
|
||||
Pair with \`Diff\`/\`DiffSSR\` to contextualize a change set.
|
||||
|
||||
### API
|
||||
- Required: \`changes\` as { additions, deletions } or an array of those objects.
|
||||
- Optional: \`variant\` ("default" | "bars").
|
||||
|
||||
### Variants and states
|
||||
- Default text summary or bar visualization.
|
||||
- Handles zero-change state (renders nothing in default variant).
|
||||
|
||||
### Behavior
|
||||
- Aggregates arrays into total additions/deletions.
|
||||
|
||||
### Accessibility
|
||||
- Ensure surrounding context conveys meaning of the counts/bars.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="diff-changes"\` and diff color tokens.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/DiffChanges",
|
||||
mod,
|
||||
args: {
|
||||
changes,
|
||||
variant: "default",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/DiffChanges",
|
||||
id: "components-diff-changes",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["default", "bars"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Default = story.Basic
|
||||
|
||||
export const Bars = {
|
||||
args: {
|
||||
variant: "bars",
|
||||
},
|
||||
}
|
||||
|
||||
export const MultipleFiles = {
|
||||
args: {
|
||||
changes: [
|
||||
{ additions: 4, deletions: 1 },
|
||||
{ additions: 8, deletions: 3 },
|
||||
{ additions: 2, deletions: 0 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const Zero = {
|
||||
args: {
|
||||
changes: { additions: 0, deletions: 0 },
|
||||
},
|
||||
}
|
||||
97
packages/ui/src/components/diff-ssr.stories.tsx
Normal file
97
packages/ui/src/components/diff-ssr.stories.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
// @ts-nocheck
|
||||
import { preloadMultiFileDiff } from "@pierre/diffs/ssr"
|
||||
import { createResource, Show } from "solid-js"
|
||||
import * as mod from "./diff-ssr"
|
||||
import { createDefaultOptions } from "../pierre"
|
||||
import { WorkerPoolProvider } from "../context/worker-pool"
|
||||
import { getWorkerPools } from "../pierre/worker"
|
||||
import { diff } from "../storybook/fixtures"
|
||||
|
||||
const docs = `### Overview
|
||||
Server-rendered diff hydration component for preloaded Pierre diff output.
|
||||
|
||||
Use alongside server routes that preload diffs.
|
||||
Pair with \`DiffChanges\` for summaries.
|
||||
|
||||
### API
|
||||
- Required: \`before\`, \`after\`, and \`preloadedDiff\` from \`preloadMultiFileDiff\`.
|
||||
- Optional: \`diffStyle\`, \`annotations\`, \`selectedLines\`, \`commentedLines\`.
|
||||
|
||||
### Variants and states
|
||||
- Unified/split styles (preloaded must match the style used during preload).
|
||||
|
||||
### Behavior
|
||||
- Hydrates pre-rendered diff HTML into a live diff instance.
|
||||
- Requires a worker pool provider for syntax highlighting.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard behavior from the Pierre diff engine.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="diff"\` with Pierre CSS variables and theme tokens.
|
||||
|
||||
`
|
||||
|
||||
const load = async () => {
|
||||
return preloadMultiFileDiff({
|
||||
oldFile: diff.before,
|
||||
newFile: diff.after,
|
||||
options: createDefaultOptions("unified"),
|
||||
})
|
||||
}
|
||||
|
||||
const loadSplit = async () => {
|
||||
return preloadMultiFileDiff({
|
||||
oldFile: diff.before,
|
||||
newFile: diff.after,
|
||||
options: createDefaultOptions("split"),
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "UI/DiffSSR",
|
||||
id: "components-diff-ssr",
|
||||
component: mod.Diff,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => {
|
||||
const [data] = createResource(load)
|
||||
return (
|
||||
<WorkerPoolProvider pools={getWorkerPools()}>
|
||||
<Show when={data()} fallback={<div>Loading pre-rendered diff...</div>}>
|
||||
{(preloaded) => (
|
||||
<div style={{ "max-width": "960px" }}>
|
||||
<mod.Diff before={diff.before} after={diff.after} diffStyle="unified" preloadedDiff={preloaded()} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</WorkerPoolProvider>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Split = {
|
||||
render: () => {
|
||||
const [data] = createResource(loadSplit)
|
||||
return (
|
||||
<WorkerPoolProvider pools={getWorkerPools()}>
|
||||
<Show when={data()} fallback={<div>Loading pre-rendered diff...</div>}>
|
||||
{(preloaded) => (
|
||||
<div style={{ "max-width": "960px" }}>
|
||||
<mod.Diff before={diff.before} after={diff.after} diffStyle="split" preloadedDiff={preloaded()} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</WorkerPoolProvider>
|
||||
)
|
||||
},
|
||||
}
|
||||
96
packages/ui/src/components/diff.stories.tsx
Normal file
96
packages/ui/src/components/diff.stories.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./diff"
|
||||
import { create } from "../storybook/scaffold"
|
||||
import { diff } from "../storybook/fixtures"
|
||||
|
||||
const docs = `### Overview
|
||||
Render a code diff with OpenCode styling using the Pierre diff engine.
|
||||
|
||||
Pair with \`DiffChanges\` for summary counts.
|
||||
Use \`LineComment\` or external UI for annotation workflows.
|
||||
|
||||
### API
|
||||
- Required: \`before\` and \`after\` file contents (name + contents).
|
||||
- Optional: \`diffStyle\` ("unified" | "split"), \`annotations\`, \`selectedLines\`, \`commentedLines\`.
|
||||
- Optional interaction: \`enableLineSelection\`, \`onLineSelectionEnd\`.
|
||||
- Passes through Pierre FileDiff options (see component source).
|
||||
|
||||
### Variants and states
|
||||
- Unified and split diff styles.
|
||||
- Optional line selection + commented line highlighting.
|
||||
|
||||
### Behavior
|
||||
- Re-renders when \`before\`/\`after\` or diff options change.
|
||||
- Line selection uses mouse drag/selection when enabled.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard behavior from the Pierre diff engine.
|
||||
- Provide surrounding labels or headings when used as a standalone view.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="diff"\` and Pierre CSS variables from \`styleVariables\`.
|
||||
- Colors derive from theme tokens (diff add/delete, background, text).
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Diff",
|
||||
mod,
|
||||
args: {
|
||||
before: diff.before,
|
||||
after: diff.after,
|
||||
diffStyle: "unified",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Diff",
|
||||
id: "components-diff",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
diffStyle: {
|
||||
control: "select",
|
||||
options: ["unified", "split"],
|
||||
},
|
||||
enableLineSelection: {
|
||||
control: "boolean",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Unified = story.Basic
|
||||
|
||||
export const Split = {
|
||||
args: {
|
||||
diffStyle: "split",
|
||||
},
|
||||
}
|
||||
|
||||
export const Selectable = {
|
||||
args: {
|
||||
enableLineSelection: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const SelectedLines = {
|
||||
args: {
|
||||
selectedLines: { start: 2, end: 4 },
|
||||
},
|
||||
}
|
||||
|
||||
export const CommentedLines = {
|
||||
args: {
|
||||
commentedLines: [
|
||||
{ start: 1, end: 1 },
|
||||
{ start: 4, end: 4 },
|
||||
],
|
||||
},
|
||||
}
|
||||
62
packages/ui/src/components/dock-prompt.stories.tsx
Normal file
62
packages/ui/src/components/dock-prompt.stories.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./dock-prompt"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Docked prompt layout for questions and permission requests.
|
||||
|
||||
Use with form controls or confirmation buttons in the footer.
|
||||
|
||||
### API
|
||||
- Required: \`kind\` (question | permission), \`header\`, \`children\`, \`footer\`.
|
||||
- Optional: \`ref\` for measuring or focus management.
|
||||
|
||||
### Variants and states
|
||||
- Question and permission layouts (data attributes).
|
||||
|
||||
### Behavior
|
||||
- Pure layout component; behavior handled by parent.
|
||||
|
||||
### Accessibility
|
||||
- Ensure header and footer content provide clear context and actions.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="dock-prompt"\` with kind data attribute.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/DockPrompt",
|
||||
mod,
|
||||
args: {
|
||||
kind: "question",
|
||||
header: "Header",
|
||||
children: "Prompt content",
|
||||
footer: "Footer",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/DockPrompt",
|
||||
id: "components-dock-prompt",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Permission = {
|
||||
args: {
|
||||
kind: "permission",
|
||||
header: "Allow access?",
|
||||
children: "This action needs permission to proceed.",
|
||||
footer: "Approve or deny",
|
||||
},
|
||||
}
|
||||
97
packages/ui/src/components/dropdown-menu.stories.tsx
Normal file
97
packages/ui/src/components/dropdown-menu.stories.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./dropdown-menu"
|
||||
import { Button } from "./button"
|
||||
|
||||
const docs = `### Overview
|
||||
Dropdown menu built on Kobalte with composable items, groups, and submenus.
|
||||
|
||||
Use \`DropdownMenu.ItemLabel\`/\`ItemDescription\` for richer rows.
|
||||
|
||||
### API
|
||||
- Root accepts Kobalte DropdownMenu props (\`open\`, \`defaultOpen\`, \`onOpenChange\`).
|
||||
- Compose with \`Trigger\`, \`Content\`, \`Item\`, \`Separator\`, and optional \`Sub\` sections.
|
||||
|
||||
### Variants and states
|
||||
- Supports item groups, separators, and nested submenus.
|
||||
|
||||
### Behavior
|
||||
- Menu opens from trigger and renders in a portal by default.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard navigation from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="dropdown-menu"\` and slot attributes for styling.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/DropdownMenu",
|
||||
id: "components-dropdown-menu",
|
||||
component: mod.DropdownMenu,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<mod.DropdownMenu defaultOpen>
|
||||
<mod.DropdownMenu.Trigger as={Button} variant="secondary" size="small">
|
||||
Open menu
|
||||
</mod.DropdownMenu.Trigger>
|
||||
<mod.DropdownMenu.Portal>
|
||||
<mod.DropdownMenu.Content>
|
||||
<mod.DropdownMenu.Group>
|
||||
<mod.DropdownMenu.GroupLabel>Actions</mod.DropdownMenu.GroupLabel>
|
||||
<mod.DropdownMenu.Item>
|
||||
<mod.DropdownMenu.ItemLabel>New file</mod.DropdownMenu.ItemLabel>
|
||||
</mod.DropdownMenu.Item>
|
||||
<mod.DropdownMenu.Item>
|
||||
<mod.DropdownMenu.ItemLabel>Rename</mod.DropdownMenu.ItemLabel>
|
||||
<mod.DropdownMenu.ItemDescription>Shift+R</mod.DropdownMenu.ItemDescription>
|
||||
</mod.DropdownMenu.Item>
|
||||
</mod.DropdownMenu.Group>
|
||||
<mod.DropdownMenu.Separator />
|
||||
<mod.DropdownMenu.Sub>
|
||||
<mod.DropdownMenu.SubTrigger>More options</mod.DropdownMenu.SubTrigger>
|
||||
<mod.DropdownMenu.SubContent>
|
||||
<mod.DropdownMenu.Item>
|
||||
<mod.DropdownMenu.ItemLabel>Duplicate</mod.DropdownMenu.ItemLabel>
|
||||
</mod.DropdownMenu.Item>
|
||||
<mod.DropdownMenu.Item>
|
||||
<mod.DropdownMenu.ItemLabel>Move</mod.DropdownMenu.ItemLabel>
|
||||
</mod.DropdownMenu.Item>
|
||||
</mod.DropdownMenu.SubContent>
|
||||
</mod.DropdownMenu.Sub>
|
||||
</mod.DropdownMenu.Content>
|
||||
</mod.DropdownMenu.Portal>
|
||||
</mod.DropdownMenu>
|
||||
),
|
||||
}
|
||||
|
||||
export const CheckboxRadio = {
|
||||
render: () => (
|
||||
<mod.DropdownMenu defaultOpen>
|
||||
<mod.DropdownMenu.Trigger as={Button} variant="secondary" size="small">
|
||||
Open menu
|
||||
</mod.DropdownMenu.Trigger>
|
||||
<mod.DropdownMenu.Portal>
|
||||
<mod.DropdownMenu.Content>
|
||||
<mod.DropdownMenu.CheckboxItem checked>Show line numbers</mod.DropdownMenu.CheckboxItem>
|
||||
<mod.DropdownMenu.CheckboxItem>Wrap lines</mod.DropdownMenu.CheckboxItem>
|
||||
<mod.DropdownMenu.Separator />
|
||||
<mod.DropdownMenu.RadioGroup value="compact">
|
||||
<mod.DropdownMenu.RadioItem value="compact">Compact</mod.DropdownMenu.RadioItem>
|
||||
<mod.DropdownMenu.RadioItem value="comfortable">Comfortable</mod.DropdownMenu.RadioItem>
|
||||
</mod.DropdownMenu.RadioGroup>
|
||||
</mod.DropdownMenu.Content>
|
||||
</mod.DropdownMenu.Portal>
|
||||
</mod.DropdownMenu>
|
||||
),
|
||||
}
|
||||
49
packages/ui/src/components/favicon.stories.tsx
Normal file
49
packages/ui/src/components/favicon.stories.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./favicon"
|
||||
|
||||
const docs = `### Overview
|
||||
Injects favicon and app icon meta tags for the document head.
|
||||
|
||||
Render once near the app root (head management).
|
||||
|
||||
### API
|
||||
- No props.
|
||||
|
||||
### Variants and states
|
||||
- Single configuration.
|
||||
|
||||
### Behavior
|
||||
- Registers link and meta tags via Solid Meta components.
|
||||
|
||||
### Accessibility
|
||||
- Not applicable.
|
||||
|
||||
### Theming/tokens
|
||||
- Not applicable.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Favicon",
|
||||
id: "components-favicon",
|
||||
component: mod.Favicon,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<mod.Favicon />
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>
|
||||
Head tags are injected for favicon and app icons.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
94
packages/ui/src/components/file-icon.stories.tsx
Normal file
94
packages/ui/src/components/file-icon.stories.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./file-icon"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
File and folder icon renderer based on file name and extension.
|
||||
|
||||
Use in file trees and lists.
|
||||
|
||||
### API
|
||||
- Required: \`node\` with \`path\` and \`type\`.
|
||||
- Optional: \`expanded\` (for folders), \`mono\` for monochrome rendering.
|
||||
|
||||
### Variants and states
|
||||
- Folder vs file icons; expanded folder variant.
|
||||
|
||||
### Behavior
|
||||
- Maps file names and extensions to sprite icons.
|
||||
|
||||
### Accessibility
|
||||
- Provide adjacent text labels for filenames; icons are decorative.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="file-icon"\` and sprite-based styling.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/FileIcon",
|
||||
mod,
|
||||
args: {
|
||||
node: { path: "package.json", type: "file" },
|
||||
mono: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/FileIcon",
|
||||
id: "components-file-icon",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Folder = {
|
||||
args: {
|
||||
node: { path: "src", type: "directory" },
|
||||
expanded: true,
|
||||
mono: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Samples = {
|
||||
render: () => {
|
||||
const items = [
|
||||
{ path: "README.md", type: "file" },
|
||||
{ path: "package.json", type: "file" },
|
||||
{ path: "tsconfig.json", type: "file" },
|
||||
{ path: "index.ts", type: "file" },
|
||||
{ path: "styles.css", type: "file" },
|
||||
{ path: "logo.svg", type: "file" },
|
||||
{ path: "photo.png", type: "file" },
|
||||
{ path: "Dockerfile", type: "file" },
|
||||
{ path: ".env", type: "file" },
|
||||
{ path: "src", type: "directory" },
|
||||
{ path: "public", type: "directory" },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "12px",
|
||||
"grid-template-columns": "repeat(auto-fill, minmax(120px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{items.map((node) => (
|
||||
<div style={{ display: "flex", gap: "8px", "align-items": "center" }}>
|
||||
<mod.FileIcon node={{ path: node.path, type: node.type }} mono={false} />
|
||||
<div style={{ "font-size": "12px", color: "var(--text-weak)" }}>{node.path}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
48
packages/ui/src/components/font.stories.tsx
Normal file
48
packages/ui/src/components/font.stories.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./font"
|
||||
|
||||
const docs = `### Overview
|
||||
Loads OpenCode typography assets and mono nerd fonts.
|
||||
|
||||
Render once at the app root or Storybook preview.
|
||||
|
||||
### API
|
||||
- No props.
|
||||
|
||||
### Variants and states
|
||||
- Fonts include sans and multiple mono families.
|
||||
|
||||
### Behavior
|
||||
- Injects @font-face rules and preload links into the document head.
|
||||
|
||||
### Accessibility
|
||||
- Not applicable.
|
||||
|
||||
### Theming/tokens
|
||||
- Provides font families used by theme tokens.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Font",
|
||||
id: "components-font",
|
||||
component: mod.Font,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<mod.Font />
|
||||
<div style={{ "font-family": "var(--font-family-sans)" }}>OpenCode Sans Sample</div>
|
||||
<div style={{ "font-family": "var(--font-family-mono)" }}>OpenCode Mono Sample</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
70
packages/ui/src/components/hover-card.stories.tsx
Normal file
70
packages/ui/src/components/hover-card.stories.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// @ts-nocheck
|
||||
import { createSignal } from "solid-js"
|
||||
import * as mod from "./hover-card"
|
||||
|
||||
const docs = `### Overview
|
||||
Hover-triggered card for lightweight previews and metadata.
|
||||
|
||||
Use for short summaries; avoid dense interactive controls.
|
||||
|
||||
### API
|
||||
- Required: \`trigger\` element.
|
||||
- Children render inside the hover card body.
|
||||
|
||||
### Variants and states
|
||||
- None; content and trigger are fully composable.
|
||||
|
||||
### Behavior
|
||||
- Opens on hover/focus over the trigger.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm focus and hover intent behavior from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="hover-card-content"\` and slots for styling.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/HoverCard",
|
||||
id: "components-hover-card",
|
||||
component: mod.HoverCard,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<mod.HoverCard trigger={<span style={{ "text-decoration": "underline", cursor: "default" }}>Hover me</span>}>
|
||||
<div style={{ display: "grid", gap: "6px" }}>
|
||||
<div style={{ "font-weight": 600 }}>Preview</div>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Short supporting text.</div>
|
||||
</div>
|
||||
</mod.HoverCard>
|
||||
),
|
||||
}
|
||||
|
||||
export const InlineMount = {
|
||||
render: () => {
|
||||
const [mount, setMount] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||
return (
|
||||
<div ref={setMount} style={{ padding: "16px", border: "1px dashed var(--border-weak)" }}>
|
||||
<mod.HoverCard
|
||||
mount={mount()}
|
||||
trigger={<span style={{ "text-decoration": "underline", cursor: "default" }}>Hover me</span>}
|
||||
>
|
||||
<div style={{ display: "grid", gap: "6px" }}>
|
||||
<div style={{ "font-weight": 600 }}>Mounted inside</div>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Uses custom mount node.</div>
|
||||
</div>
|
||||
</mod.HoverCard>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
74
packages/ui/src/components/icon-button.stories.tsx
Normal file
74
packages/ui/src/components/icon-button.stories.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./icon-button"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Compact icon-only button with size and variant control.
|
||||
|
||||
Use \`Button\` for text labels and primary actions.
|
||||
|
||||
### API
|
||||
- Required: \`icon\` icon name.
|
||||
- Optional: \`size\`, \`iconSize\`, \`variant\`.
|
||||
- Inherits Kobalte Button props and native button attributes.
|
||||
|
||||
### Variants and states
|
||||
- Variants: primary, secondary, ghost.
|
||||
- Sizes: small, normal, large.
|
||||
|
||||
### Behavior
|
||||
- Icon size adapts to button size unless overridden.
|
||||
|
||||
### Accessibility
|
||||
- Provide \`aria-label\` when there is no visible text.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="icon-button"\` and size/variant data attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/IconButton", mod, args: { icon: "check", "aria-label": "Icon" } })
|
||||
export default {
|
||||
title: "UI/IconButton",
|
||||
id: "components-icon-button",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
|
||||
<mod.IconButton icon="check" size="small" aria-label="Small" />
|
||||
<mod.IconButton icon="check" size="normal" aria-label="Normal" />
|
||||
<mod.IconButton icon="check" size="large" aria-label="Large" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Variants = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
|
||||
<mod.IconButton icon="check" variant="primary" aria-label="Primary" />
|
||||
<mod.IconButton icon="check" variant="secondary" aria-label="Secondary" />
|
||||
<mod.IconButton icon="check" variant="ghost" aria-label="Ghost" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const IconSizeOverride = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
|
||||
<mod.IconButton icon="check" size="small" iconSize="large" aria-label="Small with large icon" />
|
||||
<mod.IconButton icon="check" size="large" iconSize="small" aria-label="Large with small icon" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
170
packages/ui/src/components/icon.stories.tsx
Normal file
170
packages/ui/src/components/icon.stories.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./icon"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Inline icon renderer using the built-in OpenCode icon set.
|
||||
|
||||
Use with \`Button\`, \`IconButton\`, and menu items.
|
||||
|
||||
### API
|
||||
- Required: \`name\` (icon key).
|
||||
- Optional: \`size\` (small | normal | medium | large).
|
||||
- Accepts standard SVG props.
|
||||
|
||||
### Variants and states
|
||||
- Size variants only.
|
||||
|
||||
### Behavior
|
||||
- Uses an internal SVG path map.
|
||||
|
||||
### Accessibility
|
||||
- Icons are aria-hidden by default; wrap with accessible text when needed.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="icon"\` with size data attributes.
|
||||
|
||||
`
|
||||
|
||||
const names = [
|
||||
"align-right",
|
||||
"arrow-up",
|
||||
"arrow-left",
|
||||
"arrow-right",
|
||||
"archive",
|
||||
"bubble-5",
|
||||
"prompt",
|
||||
"brain",
|
||||
"bullet-list",
|
||||
"check-small",
|
||||
"chevron-down",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
"chevron-grabber-vertical",
|
||||
"chevron-double-right",
|
||||
"circle-x",
|
||||
"close",
|
||||
"close-small",
|
||||
"checklist",
|
||||
"console",
|
||||
"expand",
|
||||
"collapse",
|
||||
"code",
|
||||
"code-lines",
|
||||
"circle-ban-sign",
|
||||
"edit-small-2",
|
||||
"eye",
|
||||
"enter",
|
||||
"folder",
|
||||
"file-tree",
|
||||
"file-tree-active",
|
||||
"magnifying-glass",
|
||||
"plus-small",
|
||||
"plus",
|
||||
"new-session",
|
||||
"pencil-line",
|
||||
"mcp",
|
||||
"glasses",
|
||||
"magnifying-glass-menu",
|
||||
"window-cursor",
|
||||
"task",
|
||||
"stop",
|
||||
"layout-left",
|
||||
"layout-left-partial",
|
||||
"layout-left-full",
|
||||
"layout-right",
|
||||
"layout-right-partial",
|
||||
"layout-right-full",
|
||||
"square-arrow-top-right",
|
||||
"open-file",
|
||||
"speech-bubble",
|
||||
"comment",
|
||||
"folder-add-left",
|
||||
"github",
|
||||
"discord",
|
||||
"layout-bottom",
|
||||
"layout-bottom-partial",
|
||||
"layout-bottom-full",
|
||||
"dot-grid",
|
||||
"circle-check",
|
||||
"copy",
|
||||
"check",
|
||||
"photo",
|
||||
"share",
|
||||
"download",
|
||||
"menu",
|
||||
"server",
|
||||
"branch",
|
||||
"edit",
|
||||
"help",
|
||||
"settings-gear",
|
||||
"dash",
|
||||
"cloud-upload",
|
||||
"trash",
|
||||
"sliders",
|
||||
"keyboard",
|
||||
"selector",
|
||||
"arrow-down-to-line",
|
||||
"warning",
|
||||
"link",
|
||||
"providers",
|
||||
"models",
|
||||
]
|
||||
|
||||
const story = create({ title: "UI/Icon", mod, args: { name: "check" } })
|
||||
|
||||
export default {
|
||||
title: "UI/Icon",
|
||||
id: "components-icon",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
name: {
|
||||
control: "select",
|
||||
options: names,
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["small", "normal", "medium", "large"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "12px", "align-items": "center" }}>
|
||||
<mod.Icon name="check" size="small" />
|
||||
<mod.Icon name="check" size="normal" />
|
||||
<mod.Icon name="check" size="medium" />
|
||||
<mod.Icon name="check" size="large" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Gallery = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "12px",
|
||||
"grid-template-columns": "repeat(auto-fill, minmax(88px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{names.map((name) => (
|
||||
<div style={{ display: "grid", gap: "6px", "justify-items": "center" }}>
|
||||
<mod.Icon name={name} />
|
||||
<div style={{ "font-size": "10px", color: "var(--text-weak)", "text-align": "center" }}>{name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
59
packages/ui/src/components/image-preview.stories.tsx
Normal file
59
packages/ui/src/components/image-preview.stories.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// @ts-nocheck
|
||||
import { onMount } from "solid-js"
|
||||
import * as mod from "./image-preview"
|
||||
import { Button } from "./button"
|
||||
import { useDialog } from "../context/dialog"
|
||||
|
||||
const docs = `### Overview
|
||||
Image preview content intended to render inside the dialog stack.
|
||||
|
||||
Use for full-size image inspection; keep images optimized.
|
||||
|
||||
### API
|
||||
- Required: \`src\`.
|
||||
- Optional: \`alt\` text.
|
||||
|
||||
### Variants and states
|
||||
- Single layout with close action.
|
||||
|
||||
### Behavior
|
||||
- Intended to be used via \`useDialog().show\`.
|
||||
|
||||
### Accessibility
|
||||
- Uses localized aria-label for close button.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="image-preview"\` and slot attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/ImagePreview",
|
||||
id: "components-image-preview",
|
||||
component: mod.ImagePreview,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => {
|
||||
const dialog = useDialog()
|
||||
const src = "https://placehold.co/640x360/png"
|
||||
|
||||
const open = () => dialog.show(() => <mod.ImagePreview src={src} alt="Preview" />)
|
||||
|
||||
onMount(open)
|
||||
|
||||
return (
|
||||
<Button variant="secondary" onClick={open}>
|
||||
Open image preview
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
}
|
||||
50
packages/ui/src/components/inline-input.stories.tsx
Normal file
50
packages/ui/src/components/inline-input.stories.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./inline-input"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Compact inline input for short values.
|
||||
|
||||
Use inside text or table rows for quick edits.
|
||||
|
||||
### API
|
||||
- Optional: \`width\` to set a fixed width.
|
||||
- Accepts standard input props.
|
||||
|
||||
### Variants and states
|
||||
- No built-in variants; style via class or width.
|
||||
|
||||
### Behavior
|
||||
- Uses inline width when provided.
|
||||
|
||||
### Accessibility
|
||||
- Provide a label or aria-label when used standalone.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="inline-input"\`.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/InlineInput", mod, args: { placeholder: "Type...", value: "Inline" } })
|
||||
export default {
|
||||
title: "UI/InlineInput",
|
||||
id: "components-inline-input",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const FixedWidth = {
|
||||
args: {
|
||||
value: "80px",
|
||||
width: "80px",
|
||||
},
|
||||
}
|
||||
43
packages/ui/src/components/keybind.stories.tsx
Normal file
43
packages/ui/src/components/keybind.stories.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./keybind"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Keyboard shortcut pill for displaying keybindings.
|
||||
|
||||
Pair with menu items or command palettes.
|
||||
|
||||
### API
|
||||
- Children render the key sequence text.
|
||||
- Accepts standard span props.
|
||||
|
||||
### Variants and states
|
||||
- Single visual style.
|
||||
|
||||
### Behavior
|
||||
- Presentational only.
|
||||
|
||||
### Accessibility
|
||||
- Ensure text conveys the shortcut (e.g., "Cmd+K").
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="keybind"\`.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Keybind", mod, args: { children: "Cmd+K" } })
|
||||
export default {
|
||||
title: "UI/Keybind",
|
||||
id: "components-keybind",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
115
packages/ui/src/components/line-comment.stories.tsx
Normal file
115
packages/ui/src/components/line-comment.stories.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
// @ts-nocheck
|
||||
import { createSignal } from "solid-js"
|
||||
import * as mod from "./line-comment"
|
||||
|
||||
const docs = `### Overview
|
||||
Inline comment anchor and editor for code review or annotation flows.
|
||||
|
||||
Pair with \`Diff\` or \`Code\` to align comments to lines.
|
||||
|
||||
### API
|
||||
- \`LineCommentAnchor\`: position with \`top\`, control \`open\`, render custom children.
|
||||
- \`LineComment\`: convenience wrapper for displaying comment + selection label.
|
||||
- \`LineCommentEditor\`: controlled textarea with submit/cancel handlers.
|
||||
|
||||
### Variants and states
|
||||
- Default display and editor display variants.
|
||||
|
||||
### Behavior
|
||||
- Anchor positions relative to a containing element.
|
||||
- Editor submits on Enter (Shift+Enter for newline).
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm ARIA labeling for comment button and editor textarea.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="line-comment"\` and related slots.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/LineComment",
|
||||
id: "components-line-comment",
|
||||
component: mod.LineComment,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Default = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "160px",
|
||||
padding: "16px 16px 16px 40px",
|
||||
border: "1px solid var(--border-weak)",
|
||||
"border-radius": "8px",
|
||||
"font-family": "var(--font-family-mono)",
|
||||
"font-size": "12px",
|
||||
color: "var(--text-weak)",
|
||||
}}
|
||||
>
|
||||
<div>12 | const total = sum(values)</div>
|
||||
<div>13 | return total / values.length</div>
|
||||
<mod.LineComment open top={18} comment="Consider guarding against empty arrays." selection="L12-L13" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Editor = {
|
||||
render: () => {
|
||||
const [value, setValue] = createSignal("Add context for this change.")
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "220px",
|
||||
padding: "16px 16px 16px 40px",
|
||||
border: "1px solid var(--border-weak)",
|
||||
"border-radius": "8px",
|
||||
"font-family": "var(--font-family-mono)",
|
||||
"font-size": "12px",
|
||||
color: "var(--text-weak)",
|
||||
}}
|
||||
>
|
||||
<div>40 | if (values.length === 0) return 0</div>
|
||||
<mod.LineCommentEditor
|
||||
top={24}
|
||||
value={value()}
|
||||
selection="L40"
|
||||
onInput={setValue}
|
||||
onCancel={() => setValue("")}
|
||||
onSubmit={(next) => setValue(next)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const AnchorOnly = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "120px",
|
||||
padding: "16px 16px 16px 40px",
|
||||
border: "1px solid var(--border-weak)",
|
||||
"border-radius": "8px",
|
||||
"font-family": "var(--font-family-mono)",
|
||||
"font-size": "12px",
|
||||
color: "var(--text-weak)",
|
||||
}}
|
||||
>
|
||||
<div>20 | const ready = true</div>
|
||||
<mod.LineCommentAnchor top={18} open={false}>
|
||||
<div data-slot="line-comment-content">Anchor content</div>
|
||||
</mod.LineCommentAnchor>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
170
packages/ui/src/components/list.stories.tsx
Normal file
170
packages/ui/src/components/list.stories.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./list"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Filterable list with keyboard navigation and optional search input.
|
||||
|
||||
Use within panels or popovers where keyboard navigation is expected.
|
||||
|
||||
### API
|
||||
- Required: \`items\` and \`key\`.
|
||||
- Required: \`children\` render function for items.
|
||||
- Optional: \`search\`, \`filterKeys\`, \`groupBy\`, \`onSelect\`, \`onKeyEvent\`.
|
||||
|
||||
### Variants and states
|
||||
- Optional search bar and group headers.
|
||||
|
||||
### Behavior
|
||||
- Uses fuzzy search when \`search\` is enabled.
|
||||
- Keyboard navigation via arrow keys; Enter selects.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm ARIA roles for list items and search input.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="list"\` and data slots for structure.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/List",
|
||||
mod,
|
||||
args: {
|
||||
items: ["One", "Two", "Three", "Four"],
|
||||
key: (x: string) => x,
|
||||
children: (x: string) => x,
|
||||
search: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/List",
|
||||
id: "components-list",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Grouped = {
|
||||
render: () => {
|
||||
const items = [
|
||||
{ id: "a1", title: "Alpha", group: "Group A" },
|
||||
{ id: "a2", title: "Bravo", group: "Group A" },
|
||||
{ id: "b1", title: "Delta", group: "Group B" },
|
||||
]
|
||||
return (
|
||||
<mod.List items={items} key={(item) => item.id} groupBy={(item) => item.group} search={true}>
|
||||
{(item) => item.title}
|
||||
</mod.List>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty = {
|
||||
render: () => (
|
||||
<mod.List items={[]} key={(item) => item} search={true}>
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithAdd = {
|
||||
render: () => (
|
||||
<mod.List
|
||||
items={["One", "Two"]}
|
||||
key={(item) => item}
|
||||
search={true}
|
||||
add={{
|
||||
render: () => (
|
||||
<button type="button" data-slot="list-item">
|
||||
Add item
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const Divider = {
|
||||
render: () => (
|
||||
<mod.List items={["One", "Two", "Three"]} key={(item) => item} divider={true}>
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const ActiveIcon = {
|
||||
render: () => (
|
||||
<mod.List items={["Alpha", "Beta", "Gamma"]} key={(item) => item} activeIcon="chevron-right">
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const NoSearch = {
|
||||
render: () => (
|
||||
<mod.List items={["One", "Two", "Three"]} key={(item) => item} search={false}>
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const SearchOptions = {
|
||||
render: () => (
|
||||
<mod.List
|
||||
items={["Apple", "Banana", "Cherry"]}
|
||||
key={(item) => item}
|
||||
search={{
|
||||
placeholder: "Filter...",
|
||||
hideIcon: true,
|
||||
action: <button type="button">Action</button>,
|
||||
}}
|
||||
>
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const ItemWrapper = {
|
||||
render: () => (
|
||||
<mod.List
|
||||
items={["One", "Two", "Three"]}
|
||||
key={(item) => item}
|
||||
itemWrapper={(item, node) => (
|
||||
<div style={{ border: "1px solid var(--border-weak)", "border-radius": "6px", margin: "4px 0" }}>{node}</div>
|
||||
)}
|
||||
>
|
||||
{(item) => item}
|
||||
</mod.List>
|
||||
),
|
||||
}
|
||||
|
||||
export const GroupHeader = {
|
||||
render: () => {
|
||||
const items = [
|
||||
{ id: "a1", title: "Alpha", group: "Group A" },
|
||||
{ id: "b1", title: "Beta", group: "Group B" },
|
||||
]
|
||||
return (
|
||||
<mod.List
|
||||
items={items}
|
||||
key={(item) => item.id}
|
||||
groupBy={(item) => item.group}
|
||||
groupHeader={(group) => <strong>{group.category}</strong>}
|
||||
>
|
||||
{(item) => item.title}
|
||||
</mod.List>
|
||||
)
|
||||
},
|
||||
}
|
||||
57
packages/ui/src/components/logo.stories.tsx
Normal file
57
packages/ui/src/components/logo.stories.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./logo"
|
||||
|
||||
const docs = `### Overview
|
||||
OpenCode logo assets: mark, splash, and wordmark.
|
||||
|
||||
Use Mark for compact spaces, Logo for headers, Splash for hero sections.
|
||||
|
||||
### API
|
||||
- \`Mark\`, \`Splash\`, and \`Logo\` components accept standard SVG props.
|
||||
|
||||
### Variants and states
|
||||
- Multiple logo variants for different contexts.
|
||||
|
||||
### Behavior
|
||||
- Pure SVG rendering.
|
||||
|
||||
### Accessibility
|
||||
- Provide title/aria-label when logos convey meaning.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses theme color tokens via CSS variables.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Logo",
|
||||
id: "components-logo",
|
||||
component: mod.Logo,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "16px", "align-items": "start" }}>
|
||||
<div>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Mark</div>
|
||||
<mod.Mark />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Splash</div>
|
||||
<mod.Splash style={{ width: "80px", height: "100px" }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Logo</div>
|
||||
<mod.Logo style={{ width: "200px" }} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
53
packages/ui/src/components/markdown.stories.tsx
Normal file
53
packages/ui/src/components/markdown.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./markdown"
|
||||
import { create } from "../storybook/scaffold"
|
||||
import { markdown } from "../storybook/fixtures"
|
||||
|
||||
const docs = `### Overview
|
||||
Render sanitized Markdown with code blocks, inline code, and safe links.
|
||||
|
||||
Pair with \`Code\` for standalone code views.
|
||||
|
||||
### API
|
||||
- Required: \`text\` Markdown string.
|
||||
- Uses the Marked context provider for parsing and sanitization.
|
||||
|
||||
### Variants and states
|
||||
- Code blocks include copy buttons when rendered.
|
||||
|
||||
### Behavior
|
||||
- Sanitizes HTML and auto-converts inline URL code to links.
|
||||
- Adds copy buttons to code blocks.
|
||||
|
||||
### Accessibility
|
||||
- Copy buttons include aria-labels from i18n.
|
||||
- TODO: confirm link target behavior in sanitized output.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="markdown"\` and related slots for styling.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Markdown",
|
||||
mod,
|
||||
args: {
|
||||
text: markdown,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Markdown",
|
||||
id: "components-markdown",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
7
packages/ui/src/components/message-nav.stories.tsx
Normal file
7
packages/ui/src/components/message-nav.stories.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./message-nav"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const story = create({ title: "UI/MessageNav", mod })
|
||||
export default { title: "UI/MessageNav", id: "components-message-nav", component: story.meta.component }
|
||||
export const Basic = story.Basic
|
||||
7
packages/ui/src/components/message-part.stories.tsx
Normal file
7
packages/ui/src/components/message-part.stories.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./message-part"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const story = create({ title: "UI/MessagePart", mod })
|
||||
export default { title: "UI/MessagePart", id: "components-message-part", component: story.meta.component }
|
||||
export const Basic = story.Basic
|
||||
87
packages/ui/src/components/popover.stories.tsx
Normal file
87
packages/ui/src/components/popover.stories.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
// @ts-nocheck
|
||||
import { createSignal } from "solid-js"
|
||||
import * as mod from "./popover"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Composable popover with optional title, description, and close button.
|
||||
|
||||
Use for small contextual details; avoid long forms.
|
||||
|
||||
### API
|
||||
- \`trigger\` and \`children\` define the anchor and content.
|
||||
- Optional: \`title\`, \`description\`, \`portal\`, \`open\`, \`defaultOpen\`.
|
||||
|
||||
### Variants and states
|
||||
- Supports controlled and uncontrolled open state.
|
||||
|
||||
### Behavior
|
||||
- Closes on outside click or Escape by default.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm focus management from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="popover-content"\` and related slots.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Popover",
|
||||
mod,
|
||||
args: {
|
||||
trigger: "Open popover",
|
||||
title: "Popover",
|
||||
description: "Optional description",
|
||||
defaultOpen: true,
|
||||
children: "Popover content",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Popover",
|
||||
id: "components-popover",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const NoHeader = {
|
||||
args: {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
children: "Popover body only",
|
||||
},
|
||||
}
|
||||
|
||||
export const Inline = {
|
||||
args: {
|
||||
portal: false,
|
||||
defaultOpen: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Controlled = {
|
||||
render: () => {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
return (
|
||||
<mod.Popover
|
||||
open={open()}
|
||||
onOpenChange={setOpen}
|
||||
trigger="Toggle popover"
|
||||
title="Controlled"
|
||||
description="Open state is controlled"
|
||||
>
|
||||
Controlled content
|
||||
</mod.Popover>
|
||||
)
|
||||
},
|
||||
}
|
||||
59
packages/ui/src/components/progress-circle.stories.tsx
Normal file
59
packages/ui/src/components/progress-circle.stories.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./progress-circle"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Circular progress indicator for compact loading states.
|
||||
|
||||
Pair with labels for clarity in dashboards.
|
||||
|
||||
### API
|
||||
- Required: \`percentage\` (0-100).
|
||||
- Optional: \`size\`, \`strokeWidth\`.
|
||||
|
||||
### Variants and states
|
||||
- Single visual style; size and stroke width adjust appearance.
|
||||
|
||||
### Behavior
|
||||
- Percentage is clamped between 0 and 100.
|
||||
|
||||
### Accessibility
|
||||
- Use alongside text or aria-live messaging for progress context.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="progress-circle"\` with background/progress slots.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/ProgressCircle", mod, args: { percentage: 65, size: 48 } })
|
||||
|
||||
export default {
|
||||
title: "UI/ProgressCircle",
|
||||
id: "components-progress-circle",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
percentage: {
|
||||
control: { type: "range", min: 0, max: 100, step: 1 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const States = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
|
||||
<mod.ProgressCircle percentage={0} size={32} />
|
||||
<mod.ProgressCircle percentage={50} size={32} />
|
||||
<mod.ProgressCircle percentage={100} size={32} />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
67
packages/ui/src/components/progress.stories.tsx
Normal file
67
packages/ui/src/components/progress.stories.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./progress"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Linear progress indicator with optional label and value display.
|
||||
|
||||
Use in forms, uploads, or background tasks.
|
||||
|
||||
### API
|
||||
- \`value\` and \`maxValue\` control progress.
|
||||
- Optional: \`showValueLabel\`, \`hideLabel\`.
|
||||
- Children provide the label text.
|
||||
|
||||
### Variants and states
|
||||
- Supports indeterminate state via Kobalte props (if provided).
|
||||
|
||||
### Behavior
|
||||
- Uses Kobalte Progress for value calculation.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm ARIA attributes from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="progress"\` with track/fill slots.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Progress",
|
||||
mod,
|
||||
args: {
|
||||
value: 60,
|
||||
maxValue: 100,
|
||||
children: "Progress",
|
||||
showValueLabel: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Progress",
|
||||
id: "components-progress",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const NoLabel = {
|
||||
args: {
|
||||
children: "",
|
||||
hideLabel: true,
|
||||
showValueLabel: false,
|
||||
value: 30,
|
||||
},
|
||||
}
|
||||
|
||||
export const Indeterminate = {
|
||||
render: () => <mod.Progress>Loading</mod.Progress>,
|
||||
}
|
||||
69
packages/ui/src/components/provider-icon.stories.tsx
Normal file
69
packages/ui/src/components/provider-icon.stories.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
// @ts-nocheck
|
||||
import { iconNames } from "./provider-icons/types"
|
||||
import * as mod from "./provider-icon"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Provider icon sprite renderer for model/provider badges.
|
||||
|
||||
Use in model pickers or provider lists.
|
||||
|
||||
### API
|
||||
- Required: \`id\` (provider icon name).
|
||||
- Accepts standard SVG props.
|
||||
|
||||
### Variants and states
|
||||
- Single visual style; size via CSS.
|
||||
|
||||
### Behavior
|
||||
- Renders from the provider SVG sprite sheet.
|
||||
|
||||
### Accessibility
|
||||
- Provide accessible text nearby when the icon conveys meaning.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="provider-icon"\`.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/ProviderIcon", mod, args: { id: "openai" } })
|
||||
export default {
|
||||
title: "UI/ProviderIcon",
|
||||
id: "components-provider-icon",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
id: {
|
||||
control: "select",
|
||||
options: iconNames,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const AllIcons = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "12px",
|
||||
"grid-template-columns": "repeat(auto-fill, minmax(80px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{iconNames.map((id) => (
|
||||
<div style={{ display: "grid", gap: "6px", "justify-items": "center" }}>
|
||||
<mod.ProviderIcon id={id} width="28" height="28" aria-label={id} />
|
||||
<div style={{ "font-size": "10px", color: "var(--text-weak)", "text-align": "center" }}>{id}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 262 KiB |
@@ -10,6 +10,7 @@ export const iconNames = [
|
||||
"xai",
|
||||
"wandb",
|
||||
"vultr",
|
||||
"vivgrid",
|
||||
"vercel",
|
||||
"venice",
|
||||
"v0",
|
||||
@@ -17,11 +18,16 @@ export const iconNames = [
|
||||
"togetherai",
|
||||
"synthetic",
|
||||
"submodel",
|
||||
"stepfun",
|
||||
"stackit",
|
||||
"siliconflow",
|
||||
"siliconflow-cn",
|
||||
"scaleway",
|
||||
"sap-ai-core",
|
||||
"requesty",
|
||||
"qiniu-ai",
|
||||
"qihang-ai",
|
||||
"privatemode-ai",
|
||||
"poe",
|
||||
"perplexity",
|
||||
"ovhcloud",
|
||||
@@ -30,19 +36,28 @@ export const iconNames = [
|
||||
"openai",
|
||||
"ollama-cloud",
|
||||
"nvidia",
|
||||
"novita-ai",
|
||||
"nova",
|
||||
"nebius",
|
||||
"nano-gpt",
|
||||
"morph",
|
||||
"moonshotai",
|
||||
"moonshotai-cn",
|
||||
"modelscope",
|
||||
"moark",
|
||||
"mistral",
|
||||
"minimax",
|
||||
"minimax-coding-plan",
|
||||
"minimax-cn",
|
||||
"minimax-cn-coding-plan",
|
||||
"meganova",
|
||||
"lucidquery",
|
||||
"lmstudio",
|
||||
"llama",
|
||||
"kuae-cloud-coding-plan",
|
||||
"kimi-for-coding",
|
||||
"kilo",
|
||||
"jiekou",
|
||||
"io-net",
|
||||
"inference",
|
||||
"inception",
|
||||
@@ -53,9 +68,11 @@ export const iconNames = [
|
||||
"google",
|
||||
"google-vertex",
|
||||
"google-vertex-anthropic",
|
||||
"gitlab",
|
||||
"github-models",
|
||||
"github-copilot",
|
||||
"friendli",
|
||||
"firmware",
|
||||
"fireworks-ai",
|
||||
"fastrouter",
|
||||
"deepseek",
|
||||
@@ -64,8 +81,10 @@ export const iconNames = [
|
||||
"cohere",
|
||||
"cloudflare-workers-ai",
|
||||
"cloudflare-ai-gateway",
|
||||
"cloudferro-sherlock",
|
||||
"chutes",
|
||||
"cerebras",
|
||||
"berget",
|
||||
"baseten",
|
||||
"bailing",
|
||||
"azure",
|
||||
@@ -76,6 +95,7 @@ export const iconNames = [
|
||||
"alibaba-cn",
|
||||
"aihubmix",
|
||||
"abacus",
|
||||
"302ai",
|
||||
] as const
|
||||
|
||||
export type IconName = (typeof iconNames)[number]
|
||||
|
||||
92
packages/ui/src/components/radio-group.stories.tsx
Normal file
92
packages/ui/src/components/radio-group.stories.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./radio-group"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Segmented radio group for choosing a single option.
|
||||
|
||||
Use for view toggles or mode selection.
|
||||
|
||||
### API
|
||||
- Required: \`options\`.
|
||||
- Optional: \`current\`, \`defaultValue\`, \`value\`, \`label\`, \`onSelect\`.
|
||||
- Optional layout: \`size\`, \`fill\`, \`pad\`.
|
||||
|
||||
### Variants and states
|
||||
- Size variants: small, medium.
|
||||
- Optional fill and padding behavior.
|
||||
|
||||
### Behavior
|
||||
- Maps options to segmented items and manages selection.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm role/aria attributes from Kobalte SegmentedControl.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="radio-group"\` with size/pad data attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/RadioGroup",
|
||||
mod,
|
||||
args: {
|
||||
options: ["One", "Two", "Three"],
|
||||
defaultValue: "One",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/RadioGroup",
|
||||
id: "components-radio-group",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["small", "medium"],
|
||||
},
|
||||
pad: {
|
||||
control: "select",
|
||||
options: ["none", "normal"],
|
||||
},
|
||||
fill: {
|
||||
control: "boolean",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.RadioGroup options={["One", "Two"]} defaultValue="One" size="small" />
|
||||
<mod.RadioGroup options={["One", "Two"]} defaultValue="One" size="medium" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Filled = {
|
||||
args: {
|
||||
fill: true,
|
||||
pad: "none",
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomLabels = {
|
||||
render: () => (
|
||||
<mod.RadioGroup
|
||||
options={["list", "grid"]}
|
||||
defaultValue="list"
|
||||
label={(value) => (value === "list" ? "List view" : "Grid view")}
|
||||
/>
|
||||
),
|
||||
}
|
||||
156
packages/ui/src/components/resize-handle.stories.tsx
Normal file
156
packages/ui/src/components/resize-handle.stories.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
// @ts-nocheck
|
||||
import { createSignal } from "solid-js"
|
||||
import * as mod from "./resize-handle"
|
||||
|
||||
const docs = `### Overview
|
||||
Drag handle for resizing panels or split views.
|
||||
|
||||
Use alongside resizable panels and split layouts.
|
||||
|
||||
### API
|
||||
- Required: \`direction\`, \`size\`, \`min\`, \`max\`, \`onResize\`.
|
||||
- Optional: \`edge\`, \`onCollapse\`, \`collapseThreshold\`.
|
||||
|
||||
### Variants and states
|
||||
- Horizontal and vertical directions.
|
||||
|
||||
### Behavior
|
||||
- Drag updates size and calls \`onResize\` with clamped values.
|
||||
|
||||
### Accessibility
|
||||
- TODO: provide keyboard resizing guidance if needed.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="resize-handle"\` with direction/edge data attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/ResizeHandle",
|
||||
id: "components-resize-handle",
|
||||
component: mod.ResizeHandle,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => {
|
||||
const [size, setSize] = createSignal(240)
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Size: {size()}px</div>
|
||||
<div
|
||||
style={{
|
||||
width: `${size()}px`,
|
||||
height: "48px",
|
||||
"background-color": "var(--background-stronger)",
|
||||
"border-radius": "6px",
|
||||
}}
|
||||
/>
|
||||
<mod.ResizeHandle
|
||||
direction="horizontal"
|
||||
size={size()}
|
||||
min={120}
|
||||
max={480}
|
||||
onResize={setSize}
|
||||
style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Vertical = {
|
||||
render: () => {
|
||||
const [size, setSize] = createSignal(180)
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px", width: "220px" }}>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Size: {size()}px</div>
|
||||
<div
|
||||
style={{
|
||||
height: `${size()}px`,
|
||||
"background-color": "var(--background-stronger)",
|
||||
"border-radius": "6px",
|
||||
}}
|
||||
/>
|
||||
<mod.ResizeHandle
|
||||
direction="vertical"
|
||||
size={size()}
|
||||
min={120}
|
||||
max={320}
|
||||
onResize={setSize}
|
||||
style="width:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Collapse = {
|
||||
render: () => {
|
||||
const [size, setSize] = createSignal(200)
|
||||
const [collapsed, setCollapsed] = createSignal(false)
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>
|
||||
{collapsed() ? "Collapsed" : `Size: ${size()}px`}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: `${collapsed() ? 0 : size()}px`,
|
||||
height: "48px",
|
||||
"background-color": "var(--background-stronger)",
|
||||
"border-radius": "6px",
|
||||
}}
|
||||
/>
|
||||
<mod.ResizeHandle
|
||||
direction="horizontal"
|
||||
size={size()}
|
||||
min={80}
|
||||
max={360}
|
||||
collapseThreshold={100}
|
||||
onResize={(next) => {
|
||||
setCollapsed(false)
|
||||
setSize(next)
|
||||
}}
|
||||
onCollapse={() => setCollapsed(true)}
|
||||
style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const EdgeStart = {
|
||||
render: () => {
|
||||
const [size, setSize] = createSignal(240)
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div style={{ color: "var(--text-weak)", "font-size": "12px" }}>Size: {size()}px</div>
|
||||
<div
|
||||
style={{
|
||||
width: `${size()}px`,
|
||||
height: "48px",
|
||||
"background-color": "var(--background-stronger)",
|
||||
"border-radius": "6px",
|
||||
}}
|
||||
/>
|
||||
<mod.ResizeHandle
|
||||
direction="horizontal"
|
||||
edge="start"
|
||||
size={size()}
|
||||
min={120}
|
||||
max={480}
|
||||
onResize={setSize}
|
||||
style="height:24px;border:1px dashed color-mix(in oklab, var(--text-base) 20%, transparent)"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
113
packages/ui/src/components/select.stories.tsx
Normal file
113
packages/ui/src/components/select.stories.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./select"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Select menu for choosing a single option with optional grouping.
|
||||
|
||||
Use \`children\` to customize option rendering.
|
||||
|
||||
### API
|
||||
- Required: \`options\`.
|
||||
- Optional: \`current\`, \`placeholder\`, \`value\`, \`label\`, \`groupBy\`.
|
||||
- Accepts Button props for the trigger (\`variant\`, \`size\`).
|
||||
|
||||
### Variants and states
|
||||
- Trigger supports "settings" style via \`triggerVariant\`.
|
||||
|
||||
### Behavior
|
||||
- Uses Kobalte Select with optional item highlight callbacks.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard navigation and aria attributes from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="select"\` with slot attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Select",
|
||||
mod,
|
||||
args: {
|
||||
options: ["One", "Two", "Three"],
|
||||
current: "One",
|
||||
placeholder: "Choose...",
|
||||
variant: "secondary",
|
||||
size: "normal",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Select",
|
||||
id: "components-select",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
triggerVariant: {
|
||||
control: "select",
|
||||
options: ["settings", undefined],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Grouped = {
|
||||
render: () => {
|
||||
const options = [
|
||||
{ id: "alpha", label: "Alpha", group: "Group A" },
|
||||
{ id: "bravo", label: "Bravo", group: "Group A" },
|
||||
{ id: "delta", label: "Delta", group: "Group B" },
|
||||
]
|
||||
return (
|
||||
<mod.Select
|
||||
options={options}
|
||||
current={options[0]}
|
||||
value={(item) => item.id}
|
||||
label={(item) => item.label}
|
||||
groupBy={(item) => item.group}
|
||||
placeholder="Choose..."
|
||||
variant="secondary"
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const SettingsTrigger = {
|
||||
args: {
|
||||
triggerVariant: "settings",
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomRender = {
|
||||
render: () => (
|
||||
<mod.Select
|
||||
options={["Primary", "Secondary", "Ghost"]}
|
||||
current="Primary"
|
||||
placeholder="Choose..."
|
||||
variant="secondary"
|
||||
>
|
||||
{(item) => <span style={{ "text-transform": "uppercase" }}>{item}</span>}
|
||||
</mod.Select>
|
||||
),
|
||||
}
|
||||
|
||||
export const CustomTriggerStyle = {
|
||||
args: {
|
||||
triggerStyle: { "min-width": "180px", "justify-content": "space-between" },
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
7
packages/ui/src/components/session-review.stories.tsx
Normal file
7
packages/ui/src/components/session-review.stories.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./session-review"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const story = create({ title: "UI/SessionReview", mod })
|
||||
export default { title: "UI/SessionReview", id: "components-session-review", component: story.meta.component }
|
||||
export const Basic = story.Basic
|
||||
7
packages/ui/src/components/session-turn.stories.tsx
Normal file
7
packages/ui/src/components/session-turn.stories.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./session-turn"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const story = create({ title: "UI/SessionTurn", mod })
|
||||
export default { title: "UI/SessionTurn", id: "components-session-turn", component: story.meta.component }
|
||||
export const Basic = story.Basic
|
||||
53
packages/ui/src/components/spinner.stories.tsx
Normal file
53
packages/ui/src/components/spinner.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./spinner"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Animated loading indicator for inline or page-level loading states.
|
||||
|
||||
Use with \`Button\` or in empty states.
|
||||
|
||||
### API
|
||||
- Accepts standard SVG props (class, style).
|
||||
|
||||
### Variants and states
|
||||
- Single default animation style.
|
||||
|
||||
### Behavior
|
||||
- Animation is CSS-driven via data attributes.
|
||||
|
||||
### Accessibility
|
||||
- Use alongside text or aria-live regions to convey loading state.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="spinner"\` for styling hooks.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Spinner", mod })
|
||||
|
||||
export default {
|
||||
title: "UI/Spinner",
|
||||
id: "components-spinner",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "16px", "align-items": "center" }}>
|
||||
<mod.Spinner style={{ width: "12px", height: "12px" }} />
|
||||
<mod.Spinner style={{ width: "20px", height: "20px" }} />
|
||||
<mod.Spinner style={{ width: "28px", height: "28px" }} />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// @ts-nocheck
|
||||
import { Accordion } from "./accordion"
|
||||
import * as mod from "./sticky-accordion-header"
|
||||
|
||||
const docs = `### Overview
|
||||
Sticky accordion header wrapper for persistent section labels.
|
||||
|
||||
Use only inside \`Accordion.Item\` with \`Accordion.Trigger\`.
|
||||
|
||||
### API
|
||||
- Accepts standard header props and children.
|
||||
|
||||
### Variants and states
|
||||
- Inherits accordion states.
|
||||
|
||||
### Behavior
|
||||
- Renders inside an Accordion item header.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm semantics from Accordion.Header usage.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="sticky-accordion-header"\`.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/StickyAccordionHeader",
|
||||
id: "components-sticky-accordion-header",
|
||||
component: mod.StickyAccordionHeader,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<Accordion value="first">
|
||||
<Accordion.Item value="first">
|
||||
<mod.StickyAccordionHeader>
|
||||
<Accordion.Trigger>Sticky header</Accordion.Trigger>
|
||||
</mod.StickyAccordionHeader>
|
||||
<Accordion.Content>
|
||||
<div style={{ color: "var(--text-weak)", padding: "8px 0" }}>Accordion content.</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
),
|
||||
}
|
||||
68
packages/ui/src/components/switch.stories.tsx
Normal file
68
packages/ui/src/components/switch.stories.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./switch"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Toggle control for binary settings.
|
||||
|
||||
Use in settings panels or forms.
|
||||
|
||||
### API
|
||||
- Uses Kobalte Switch props (\`checked\`, \`defaultChecked\`, \`onChange\`).
|
||||
- Optional: \`hideLabel\`, \`description\`.
|
||||
- Children render as the label.
|
||||
|
||||
### Variants and states
|
||||
- Checked/unchecked, disabled states.
|
||||
|
||||
### Behavior
|
||||
- Controlled or uncontrolled usage via Kobalte props.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm aria attributes from Kobalte.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="switch"\` and slot attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/Switch",
|
||||
mod,
|
||||
args: { defaultChecked: true, children: "Enable notifications" },
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/Switch",
|
||||
id: "components-switch",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const States = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.Switch defaultChecked>Enabled</mod.Switch>
|
||||
<mod.Switch>Disabled</mod.Switch>
|
||||
<mod.Switch disabled>Disabled switch</mod.Switch>
|
||||
<mod.Switch description="Optional description">With description</mod.Switch>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const HiddenLabel = {
|
||||
args: {
|
||||
children: "Hidden label",
|
||||
hideLabel: true,
|
||||
defaultChecked: true,
|
||||
},
|
||||
}
|
||||
179
packages/ui/src/components/tabs.stories.tsx
Normal file
179
packages/ui/src/components/tabs.stories.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
// @ts-nocheck
|
||||
import { IconButton } from "./icon-button"
|
||||
import { createSignal } from "solid-js"
|
||||
import * as mod from "./tabs"
|
||||
|
||||
const docs = `### Overview
|
||||
Tabbed navigation for switching between related panels.
|
||||
|
||||
Compose \`Tabs.List\` + \`Tabs.Trigger\` + \`Tabs.Content\`.
|
||||
|
||||
### API
|
||||
- Root accepts Kobalte Tabs props (\`value\`, \`defaultValue\`, \`onChange\`).
|
||||
- \`variant\` sets visual style: normal, alt, pill, settings.
|
||||
- \`orientation\` supports horizontal or vertical layouts.
|
||||
- Trigger supports \`closeButton\`, \`hideCloseButton\`, and \`onMiddleClick\`.
|
||||
|
||||
### Variants and states
|
||||
- Normal, alt, pill, settings variants.
|
||||
- Horizontal and vertical orientations.
|
||||
|
||||
### Behavior
|
||||
- Uses Kobalte Tabs for roving focus and selection management.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm keyboard interactions from Kobalte Tabs.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="tabs"\` with variant/orientation data attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Tabs",
|
||||
id: "components-tabs",
|
||||
component: mod.Tabs,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["normal", "alt", "pill", "settings"],
|
||||
},
|
||||
orientation: {
|
||||
control: "select",
|
||||
options: ["horizontal", "vertical"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
variant: "normal",
|
||||
orientation: "horizontal",
|
||||
defaultValue: "overview",
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Tabs {...props}>
|
||||
<mod.Tabs.List>
|
||||
<mod.Tabs.Trigger value="overview">Overview</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="details">Details</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="activity">Activity</mod.Tabs.Trigger>
|
||||
</mod.Tabs.List>
|
||||
<mod.Tabs.Content value="overview">Overview content</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="details">Details content</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="activity">Activity content</mod.Tabs.Content>
|
||||
</mod.Tabs>
|
||||
),
|
||||
}
|
||||
|
||||
export const Settings = {
|
||||
args: {
|
||||
variant: "settings",
|
||||
orientation: "horizontal",
|
||||
defaultValue: "general",
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Tabs {...props}>
|
||||
<mod.Tabs.List>
|
||||
<mod.Tabs.Trigger value="general">General</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="appearance">Appearance</mod.Tabs.Trigger>
|
||||
</mod.Tabs.List>
|
||||
<mod.Tabs.Content value="general">General settings</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="appearance">Appearance settings</mod.Tabs.Content>
|
||||
</mod.Tabs>
|
||||
),
|
||||
}
|
||||
|
||||
export const Alt = {
|
||||
args: {
|
||||
variant: "alt",
|
||||
orientation: "horizontal",
|
||||
defaultValue: "first",
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Tabs {...props}>
|
||||
<mod.Tabs.List>
|
||||
<mod.Tabs.Trigger value="first">First</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="second">Second</mod.Tabs.Trigger>
|
||||
</mod.Tabs.List>
|
||||
<mod.Tabs.Content value="first">Alt content</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="second">Alt content 2</mod.Tabs.Content>
|
||||
</mod.Tabs>
|
||||
),
|
||||
}
|
||||
|
||||
export const Vertical = {
|
||||
args: {
|
||||
variant: "pill",
|
||||
orientation: "vertical",
|
||||
defaultValue: "alpha",
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Tabs {...props}>
|
||||
<mod.Tabs.List>
|
||||
<mod.Tabs.Trigger value="alpha">Alpha</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="beta">Beta</mod.Tabs.Trigger>
|
||||
</mod.Tabs.List>
|
||||
<mod.Tabs.Content value="alpha">Alpha content</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="beta">Beta content</mod.Tabs.Content>
|
||||
</mod.Tabs>
|
||||
),
|
||||
}
|
||||
|
||||
export const Closable = {
|
||||
args: {
|
||||
variant: "normal",
|
||||
orientation: "horizontal",
|
||||
defaultValue: "tab-1",
|
||||
},
|
||||
render: (props) => (
|
||||
<mod.Tabs {...props}>
|
||||
<mod.Tabs.List>
|
||||
<mod.Tabs.Trigger
|
||||
value="tab-1"
|
||||
closeButton={<IconButton icon="close" size="small" variant="ghost" aria-label="Close tab" />}
|
||||
>
|
||||
Tab 1
|
||||
</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="tab-2">Tab 2</mod.Tabs.Trigger>
|
||||
</mod.Tabs.List>
|
||||
<mod.Tabs.Content value="tab-1">Closable content</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="tab-2">Standard content</mod.Tabs.Content>
|
||||
</mod.Tabs>
|
||||
),
|
||||
}
|
||||
|
||||
export const MiddleClick = {
|
||||
args: {
|
||||
variant: "normal",
|
||||
orientation: "horizontal",
|
||||
defaultValue: "tab-1",
|
||||
},
|
||||
render: (props) => {
|
||||
const [message, setMessage] = createSignal("Middle click a tab")
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
<div style={{ "font-size": "12px", color: "var(--text-weak)" }}>{message()}</div>
|
||||
<mod.Tabs {...props}>
|
||||
<mod.Tabs.List>
|
||||
<mod.Tabs.Trigger value="tab-1" onMiddleClick={() => setMessage("Middle clicked tab-1")}>
|
||||
Tab 1
|
||||
</mod.Tabs.Trigger>
|
||||
<mod.Tabs.Trigger value="tab-2" onMiddleClick={() => setMessage("Middle clicked tab-2")}>
|
||||
Tab 2
|
||||
</mod.Tabs.Trigger>
|
||||
</mod.Tabs.List>
|
||||
<mod.Tabs.Content value="tab-1">Tab 1 content</mod.Tabs.Content>
|
||||
<mod.Tabs.Content value="tab-2">Tab 2 content</mod.Tabs.Content>
|
||||
</mod.Tabs>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
58
packages/ui/src/components/tag.stories.tsx
Normal file
58
packages/ui/src/components/tag.stories.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./tag"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Small label tag for metadata and status chips.
|
||||
|
||||
Use alongside headings or lists for quick metadata.
|
||||
|
||||
### API
|
||||
- Optional: \`size\` (normal | large).
|
||||
- Accepts standard span props.
|
||||
|
||||
### Variants and states
|
||||
- Size variants only.
|
||||
|
||||
### Behavior
|
||||
- Inline element; size controls padding and font size via CSS.
|
||||
|
||||
### Accessibility
|
||||
- Ensure text conveys meaning; avoid color-only distinction.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="tag"\` with size data attributes.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Tag", mod, args: { children: "Tag" } })
|
||||
export default {
|
||||
title: "UI/Tag",
|
||||
id: "components-tag",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["normal", "large"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Sizes = {
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "8px", "align-items": "center" }}>
|
||||
<mod.Tag size="normal">Normal</mod.Tag>
|
||||
<mod.Tag size="large">Large</mod.Tag>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
111
packages/ui/src/components/text-field.stories.tsx
Normal file
111
packages/ui/src/components/text-field.stories.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./text-field"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Text input with label, description, and optional copy-to-clipboard action.
|
||||
|
||||
Pair with \`Tooltip\` and \`IconButton\` for copy affordance (built in).
|
||||
|
||||
### API
|
||||
- Supports Kobalte TextField props: \`value\`, \`defaultValue\`, \`onChange\`, \`disabled\`, \`readOnly\`.
|
||||
- Optional: \`label\`, \`description\`, \`error\`, \`variant\`, \`copyable\`, \`multiline\`.
|
||||
|
||||
### Variants and states
|
||||
- Normal and ghost variants.
|
||||
- Supports multiline textarea.
|
||||
|
||||
### Behavior
|
||||
- When \`copyable\` is true, clicking copies the current value.
|
||||
|
||||
### Accessibility
|
||||
- Label is hidden when \`hideLabel\` is true (sr-only).
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="input"\` with slot attributes for styling.
|
||||
|
||||
`
|
||||
|
||||
const story = create({
|
||||
title: "UI/TextField",
|
||||
mod,
|
||||
args: {
|
||||
label: "Label",
|
||||
placeholder: "Type here...",
|
||||
defaultValue: "Hello",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/TextField",
|
||||
id: "components-text-field",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Variants = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px", width: "320px" }}>
|
||||
<mod.TextField label="Normal" placeholder="Type here..." defaultValue="Value" />
|
||||
<mod.TextField label="Ghost" variant="ghost" placeholder="Type here..." defaultValue="Value" />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Multiline = {
|
||||
args: {
|
||||
label: "Description",
|
||||
multiline: true,
|
||||
defaultValue: "Line one\nLine two",
|
||||
},
|
||||
}
|
||||
|
||||
export const Copyable = {
|
||||
args: {
|
||||
label: "Invite link",
|
||||
defaultValue: "https://example.com/invite/abc",
|
||||
copyable: true,
|
||||
copyKind: "link",
|
||||
},
|
||||
}
|
||||
|
||||
export const Error = {
|
||||
args: {
|
||||
label: "Email",
|
||||
defaultValue: "invalid@",
|
||||
error: "Enter a valid email address",
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
label: "Disabled",
|
||||
defaultValue: "Readonly",
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const ReadOnly = {
|
||||
args: {
|
||||
label: "Read only",
|
||||
defaultValue: "Read only value",
|
||||
readOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const HiddenLabel = {
|
||||
args: {
|
||||
label: "Hidden label",
|
||||
hideLabel: true,
|
||||
placeholder: "Hidden label",
|
||||
},
|
||||
}
|
||||
59
packages/ui/src/components/text-shimmer.stories.tsx
Normal file
59
packages/ui/src/components/text-shimmer.stories.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./text-shimmer"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Animated shimmer effect for loading text placeholders.
|
||||
|
||||
Use for pending states inside buttons or list rows.
|
||||
|
||||
### API
|
||||
- Required: \`text\` string.
|
||||
- Optional: \`as\`, \`active\`, \`stepMs\`, \`durationMs\`.
|
||||
|
||||
### Variants and states
|
||||
- Active/inactive state via \`active\`.
|
||||
|
||||
### Behavior
|
||||
- Characters animate with staggered delays.
|
||||
|
||||
### Accessibility
|
||||
- Uses \`aria-label\` with the full text.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="text-shimmer"\` and CSS custom properties for timing.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/TextShimmer", mod, args: { text: "Loading..." } })
|
||||
|
||||
export default {
|
||||
title: "UI/TextShimmer",
|
||||
id: "components-text-shimmer",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Inactive = {
|
||||
args: {
|
||||
text: "Static text",
|
||||
active: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomTiming = {
|
||||
args: {
|
||||
text: "Custom timing",
|
||||
stepMs: 80,
|
||||
durationMs: 1800,
|
||||
},
|
||||
}
|
||||
138
packages/ui/src/components/toast.stories.tsx
Normal file
138
packages/ui/src/components/toast.stories.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./toast"
|
||||
import { Button } from "./button"
|
||||
|
||||
const docs = `### Overview
|
||||
Toast notifications with optional icons, actions, and progress.
|
||||
|
||||
Use brief titles/descriptions; limit actions to 1-2.
|
||||
|
||||
### API
|
||||
- Use \`showToast\` or \`showPromiseToast\` to trigger toasts.
|
||||
- Render \`Toast.Region\` once per page.
|
||||
- \`Toast\` subcomponents compose the structure.
|
||||
|
||||
### Variants and states
|
||||
- Variants: default, success, error, loading.
|
||||
- Optional actions and persistent toasts.
|
||||
|
||||
### Behavior
|
||||
- Toasts render in a portal and auto-dismiss unless persistent.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm aria-live behavior from Kobalte Toast.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="toast"\` and slot data attributes.
|
||||
|
||||
`
|
||||
|
||||
export default {
|
||||
title: "UI/Toast",
|
||||
id: "components-toast",
|
||||
component: mod.Toast,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.Toast.Region />
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
mod.showToast({
|
||||
title: "Saved",
|
||||
description: "Your changes are stored.",
|
||||
variant: "success",
|
||||
icon: "check",
|
||||
})
|
||||
}
|
||||
>
|
||||
Show success toast
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
mod.showToast({
|
||||
description: "This action needs attention.",
|
||||
variant: "error",
|
||||
icon: "warning",
|
||||
})
|
||||
}
|
||||
>
|
||||
Show error toast
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Actions = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.Toast.Region />
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
mod.showToast({
|
||||
title: "Update available",
|
||||
description: "Restart to apply the update.",
|
||||
actions: [
|
||||
{ label: "Restart", onClick: "dismiss" },
|
||||
{ label: "Later", onClick: "dismiss" },
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Show action toast
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Promise = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.Toast.Region />
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
mod.showPromiseToast(() => new Promise((resolve) => setTimeout(() => resolve(true), 800)), {
|
||||
loading: "Saving...",
|
||||
success: () => "Saved",
|
||||
error: () => "Failed",
|
||||
})
|
||||
}
|
||||
>
|
||||
Show promise toast
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Loading = {
|
||||
render: () => (
|
||||
<div style={{ display: "grid", gap: "12px" }}>
|
||||
<mod.Toast.Region />
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
mod.showToast({
|
||||
description: "Syncing...",
|
||||
variant: "loading",
|
||||
persistent: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show loading toast
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
64
packages/ui/src/components/tooltip.stories.tsx
Normal file
64
packages/ui/src/components/tooltip.stories.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./tooltip"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Tooltip for contextual hints and keybind callouts.
|
||||
|
||||
Use for short hints; avoid long descriptions.
|
||||
|
||||
### API
|
||||
- Required: \`value\` (tooltip content).
|
||||
- Optional: \`inactive\`, \`forceOpen\`, placement props from Kobalte.
|
||||
|
||||
### Variants and states
|
||||
- Supports keybind-style tooltip via \`TooltipKeybind\`.
|
||||
|
||||
### Behavior
|
||||
- Opens on hover/focus; can be forced open.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm trigger semantics and focus behavior.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`data-component="tooltip"\` and related slots.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Tooltip", mod, args: { value: "Tooltip", children: "Hover me" } })
|
||||
|
||||
export default {
|
||||
title: "UI/Tooltip",
|
||||
id: "components-tooltip",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Keybind = {
|
||||
render: () => (
|
||||
<mod.TooltipKeybind title="Search" keybind="Cmd+K">
|
||||
<span style={{ "text-decoration": "underline" }}>Hover for keybind</span>
|
||||
</mod.TooltipKeybind>
|
||||
),
|
||||
}
|
||||
|
||||
export const ForcedOpen = {
|
||||
args: {
|
||||
forceOpen: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Inactive = {
|
||||
args: {
|
||||
inactive: true,
|
||||
},
|
||||
}
|
||||
51
packages/ui/src/components/typewriter.stories.tsx
Normal file
51
packages/ui/src/components/typewriter.stories.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./typewriter"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const docs = `### Overview
|
||||
Animated typewriter text effect for short inline messages.
|
||||
|
||||
Use for short status lines; avoid long paragraphs.
|
||||
|
||||
### API
|
||||
- Optional: \`text\` string; if absent, nothing is rendered.
|
||||
- Optional: \`as\` to change the rendered element.
|
||||
|
||||
### Variants and states
|
||||
- Single animation style with cursor blink.
|
||||
|
||||
### Behavior
|
||||
- Types one character at a time with randomized delays.
|
||||
|
||||
### Accessibility
|
||||
- TODO: confirm if cursor should be aria-hidden in all contexts.
|
||||
|
||||
### Theming/tokens
|
||||
- Uses \`blinking-cursor\` class for cursor styling.
|
||||
|
||||
`
|
||||
|
||||
const story = create({ title: "UI/Typewriter", mod, args: { text: "Typewriter text" } })
|
||||
|
||||
export default {
|
||||
title: "UI/Typewriter",
|
||||
id: "components-typewriter",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: docs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
|
||||
export const Inline = {
|
||||
args: {
|
||||
text: "Inline typewriter",
|
||||
as: "span",
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user