@usetheo/ui 0.8.0-next.0 → 0.9.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +18 -18
- package/dist/components.css +1 -1
- package/dist/index.d.ts +74 -1
- package/dist/index.js +277 -5
- package/dist/index.js.map +1 -1
- package/llms.txt +269 -0
- package/package.json +11 -2
- package/registry/index.json +12 -0
- package/registry/r/alert.json +22 -0
- package/registry/r/pagination.json +22 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# @usetheo/ui — Theo UI (Violet Forge)
|
|
2
|
+
|
|
3
|
+
> React component library for AI-agent surfaces and cloud (PaaS) dashboards. 114 components (87 primitives + 27 composites), framework-agnostic (React peer-dep only), ESM-only, Apache-2.0. Built on Radix UI + CVA + lucide-react + Tailwind v4. Same Radix foundation as shadcn — the wedge is the agent/PaaS-specific components built on top.
|
|
4
|
+
|
|
5
|
+
This file follows the [llms.txt convention](https://llmstxt.org/) and gives an LLM the **factual ground truth** of the project: package name, import paths, component inventory, locked conventions, anti-patterns, and links to the canonical documentation. Treat the bullets below as non-negotiable contracts unless the source files contradict them — in which case the source files win and this file is stale.
|
|
6
|
+
|
|
7
|
+
## Project metadata
|
|
8
|
+
|
|
9
|
+
- **npm package:** `@usetheo/ui`
|
|
10
|
+
- **Current version:** `0.8.0-next.0` (npm dist-tag `next`; latest release line)
|
|
11
|
+
- **License:** Apache-2.0
|
|
12
|
+
- **Repository:** `https://github.com/usetheodev/theo-ui`
|
|
13
|
+
- **Docs site:** `https://docs.usetheo.dev/theoui`
|
|
14
|
+
- **Registry (shadcn CLI):** `https://usetheodev.github.io/theo-ui/r/{slug}.json`
|
|
15
|
+
- **Design-system codename:** Violet Forge
|
|
16
|
+
- **Module format:** ESM only (no CJS). `"type": "module"`. Side-effects on `**/*.css`.
|
|
17
|
+
- **React peer range:** `>=18.2.0 <20` (works with 18.x and 19.x)
|
|
18
|
+
- **Distribution:** npm package install OR copy-paste source via shadcn CLI (registry). Both paths are first-class.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Full package (recommended for new projects)
|
|
24
|
+
pnpm add @usetheo/ui@next
|
|
25
|
+
|
|
26
|
+
# Per-component copy (shadcn-style)
|
|
27
|
+
npx shadcn@latest add https://usetheodev.github.io/theo-ui/r/copy-button.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Import path canonical form
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// All components ship via the single barrel.
|
|
34
|
+
import { Button, Table, ConfirmDialog, CopyButton } from "@usetheo/ui";
|
|
35
|
+
|
|
36
|
+
// Themes + provider:
|
|
37
|
+
import { ThemeProvider, violetForge, classicPaper, auroraTerminal } from "@usetheo/ui";
|
|
38
|
+
|
|
39
|
+
// Pure utility (no React):
|
|
40
|
+
import { cn } from "@usetheo/ui";
|
|
41
|
+
|
|
42
|
+
// Engines ship under subpaths (separate bundles, opt-in peer-deps):
|
|
43
|
+
import { Whiteboard } from "@usetheo/ui/whiteboard";
|
|
44
|
+
import { Slide } from "@usetheo/ui/slide";
|
|
45
|
+
import { SlideDeck } from "@usetheo/ui/slide-deck";
|
|
46
|
+
import { Mermaid } from "@usetheo/ui/slide/plugins/mermaid";
|
|
47
|
+
import { Math } from "@usetheo/ui/slide/plugins/math";
|
|
48
|
+
import { Shiki } from "@usetheo/ui/slide/plugins/shiki";
|
|
49
|
+
import { Emoji } from "@usetheo/ui/slide/plugins/emoji";
|
|
50
|
+
import { theoUIVitePlugin } from "@usetheo/ui/vite-plugin";
|
|
51
|
+
import "@usetheo/ui/preset"; // Tailwind v4 preset (zero-config)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
There are **131 subpath exports** in `package.json#exports` — one per component plus engine entry points. Per-component subpath imports (`@usetheo/ui/button`) exist but are **not the canonical form**; prefer the barrel for tree-shakeable production builds (`sideEffects: ["**/*.css"]` is honored).
|
|
55
|
+
|
|
56
|
+
## Peer dependencies (required vs optional)
|
|
57
|
+
|
|
58
|
+
**Required for the main barrel:**
|
|
59
|
+
- `react >=18.2.0 <20`
|
|
60
|
+
- `react-dom >=18.2.0 <20`
|
|
61
|
+
- `tailwindcss ^4.0.0`
|
|
62
|
+
|
|
63
|
+
**Optional (only required if you use the matching engine subpath):**
|
|
64
|
+
- `@tailwindcss/vite ^4.0.0`, `vite ^6 || ^7` — for `@usetheo/ui/vite-plugin`
|
|
65
|
+
- `roughjs ^4.6`, `perfect-freehand ^1.2` — for `@usetheo/ui/whiteboard`
|
|
66
|
+
- `mdast-util-from-markdown`, `mdast-util-gfm`, `mdast-util-to-hast`, `hast-util-sanitize`, `hast-util-to-jsx-runtime`, `micromark-extension-gfm`, `unist-util-visit`, `unist-util-visit-parents`, `yaml` — for `@usetheo/ui/slide`
|
|
67
|
+
- `shiki ^1.0` — for `@usetheo/ui/slide/plugins/shiki`
|
|
68
|
+
- `katex ^0.16`, `mdast-util-math`, `micromark-extension-math` — for `@usetheo/ui/slide/plugins/math`
|
|
69
|
+
- `mermaid ^11.0` — for `@usetheo/ui/slide/plugins/mermaid`
|
|
70
|
+
- `hast-util-from-html` — for `@usetheo/ui/slide/plugins/emoji`
|
|
71
|
+
|
|
72
|
+
**Already bundled (do NOT install separately):** `@radix-ui/*`, `class-variance-authority`, `clsx`, `cmdk`, `lucide-react`, `tailwind-merge`. These are `dependencies`, not peer.
|
|
73
|
+
|
|
74
|
+
## Taxonomy (mechanical rule, gate-enforced)
|
|
75
|
+
|
|
76
|
+
- **Primitive** = a component that imports **zero** other `@usetheo/ui` components. Lives in `src/components/primitives/`.
|
|
77
|
+
- **Composite** = a component that imports **one or more** primitives via their barrel `index.js`. Lives in `src/components/composites/`.
|
|
78
|
+
- The gate `scripts/validate-quality-gates.ts` is **hard-fail** if either rule is violated. A primitive that grows to need a sibling primitive must be **promoted to composite** by moving the folder + renaming via PR with rationale.
|
|
79
|
+
- Composites import siblings via barrel: `import { Button } from "../../primitives/button/index.js"` — NOT via the raw `.tsx` file (also gate-enforced).
|
|
80
|
+
- Sub-component pattern (e.g., `Table.Header`, `Sidebar.Item`, `DangerZone.Action`) is implemented via `Object.assign(Root, { SubA, SubB })` — Sidebar-style. Single import surface, attached as static properties on the root.
|
|
81
|
+
|
|
82
|
+
## Theme system
|
|
83
|
+
|
|
84
|
+
- **Provider:** `<ThemeProvider initial="violet-forge">{...}</ThemeProvider>`. Wrap once at the app root.
|
|
85
|
+
- **Built-in themes (10):** `violetForge` (default), `classicPaper`, `auroraTerminal`, `vercelMono`, `githubDark`, `dracula`, `oneDark`, `anthropicStyle`, `openaiStyle`, `linearGlass`.
|
|
86
|
+
- **Custom themes:** `defineTheme({ name, mode, fonts, palette })` — returns a `Theme` object you pass into `<ThemeProvider extra={[myTheme]} />`.
|
|
87
|
+
- **Color helpers:** `hex("#7c5cff")`, `rgb(124, 92, 255)` — both return CSS-ready strings.
|
|
88
|
+
- **Density:** `useDensity()` hook returns `"compact" | "comfortable" | "cozy"`. Cascading via `<DensityProvider>`.
|
|
89
|
+
- **Fonts (locked):** Geist Sans (display + body) + Geist Mono (mono). Loaded via CDN by default (`dist/fonts-cdn.css`) or locally (`dist/fonts.css`).
|
|
90
|
+
- **Tailwind v4:** CSS-first config via `@usetheo/ui/preset` (zero JS config). v3 legacy preset available at `@usetheo/ui/preset-v3-legacy`.
|
|
91
|
+
|
|
92
|
+
## Component catalog — primitives (87)
|
|
93
|
+
|
|
94
|
+
Located in `src/components/primitives/{slug}/`. Each ships `.tsx` + `.test.tsx` + `.stories.tsx` + `index.ts` + a `registry/{slug}.json` descriptor.
|
|
95
|
+
|
|
96
|
+
`agent-error-card`, `agent-event`, `agent-handoff`, `agent-profile`, `agent-starting-state`, `agent-streaming`, `artifact-preview`, `attachment-chip`, `audit-log-entry`, `auto-compact-notice`, `avatar`, `badge`, `browser-controls`, `build-log-stream`, `button`, `capability-indicator`, `card`, `chat-thread`, `checkbox`, `context-card`, `context-window-bar`, **`copy-button`** (NEW 0.8), `cost-meter`, `created-files-card`, `cron-job-card`, **`danger-zone`** (NEW 0.8), `dialog`, `diff-viewer`, `empty-state`, `folder-context-card`, `folder-selector`, `form-field`, `hook-config`, `hook-event-log`, `input`, `intent-selector`, `label`, `lane-board`, `login-split`, `mcp-server-card`, `memory-editor`, `mention-menu`, `metrics-panel`, `model-card`, `model-selector`, `permission-matrix`, **`plan-badge`** (0.7), **`progress`** (0.7), `progress-checklist`, `project-switcher`, `quick-action-chips`, `radio-group`, `recent-folders-list`, `rule-card`, `running-tasks-panel`, `run-stats`, `scroll-area`, `select`, `session-list-item`, `session-timeline`, `sheet`, `sidebar`, `skeleton`, `skill-card`, `slide`, `social-auth-row`, **`stat-tile`** (NEW 0.8), **`status-dot`** (NEW 0.8), `steps-rail`, `sub-agent-dispatch`, `switch`, `system-prompt-editor`, **`table`** (NEW 0.8), `tabs`, `task-plan`, `terminal-panel`, `textarea`, **`timestamp`** (NEW 0.8), `toast`, `token-usage-chart`, `tool-call`, `tool-call-card`, `tool-result`, `tools-list`, `tooltip`, `topnav`, `whiteboard`.
|
|
97
|
+
|
|
98
|
+
## Component catalog — composites (27)
|
|
99
|
+
|
|
100
|
+
Located in `src/components/composites/{slug}/`.
|
|
101
|
+
|
|
102
|
+
**`account-menu`** (0.7), `agent-composer`, `agent-editor`, `agent-stream`, `agent-timeline`, `approval-card`, `chat-composer`, `chat-message`, **`code-block`** (NEW 0.8), `command-palette`, **`confirm-dialog`** (NEW 0.8), `cron-jobs-list`, `deployment-row`, `domain-config`, `env-var-editor`, `mcp-server-list`, `permission-modal`, `preview-env-card`, `preview-panel`, `project-card`, `rollback-ui`, `rule-editor`, `skill-editor`, `skills-list`, `slide-deck`, `task-header`, **`usage-meter`** (0.7).
|
|
103
|
+
|
|
104
|
+
## Recent deliveries (PaaS shape)
|
|
105
|
+
|
|
106
|
+
- **0.8.0-next.0 (Brief #2 — cross-cutting, 8 components):** `Table` (sub-components + sortable headers), `StatusDot` (5 kinds + auto-pulse), `CopyButton` (clipboard + `aria-live` + SSR-safe), `Timestamp` (`Intl.RelativeTimeFormat`, native `title` tooltip, Unix ms only), `StatTile` (dual button/div mode), `DangerZone` (`.Action` sub-component), `ConfirmDialog` (typed-phrase guard + async loading + Enter-to-confirm), `CodeBlock` (terminal prefix + caption + raw-code copy). Consumer: TheoCloud dashboard. Zero new peer-deps; bundle +5.4% (rebaselined).
|
|
107
|
+
- **0.7.0-next.0 (Brief #1 — PaaS-shape, 4 components):** `UsageMeter` (multi-metric, over-quota warning + clamping), `Progress` (4 intents + indeterminate + motion-reduce aware), `PlanBadge` (5 canonical tiers), `AccountMenu` (avatar + name + plan + secondary, dual button/div).
|
|
108
|
+
|
|
109
|
+
## Component conventions (do this)
|
|
110
|
+
|
|
111
|
+
Every component file MUST:
|
|
112
|
+
|
|
113
|
+
1. Use `forwardRef<HTMLElementType, Props>` and set `displayName`. Example: `MyComponent.displayName = "MyComponent";`.
|
|
114
|
+
2. Compose classes via `cn(...)` from `lib/cn.js` (`tailwind-merge` + `clsx` underneath).
|
|
115
|
+
3. Use **design tokens** only — never raw hex/rgb. Approved tokens: `bg-card`, `bg-muted`, `bg-muted/40`, `bg-primary`, `bg-primary/10`, `bg-success`, `bg-warning`, `bg-destructive`, `bg-destructive/[0.02]`, `text-foreground`, `text-muted-foreground`, `text-success`, `text-warning`, `text-destructive`, `border-border/40`, `border-border/60`, `border-destructive/30`, `text-card-foreground`, `text-primary-foreground`, `ring-ring`, etc.
|
|
116
|
+
4. Use **typography scale**: `text-display-md`, `text-title-sm`, `text-body-sm`, `text-label`, `text-label-caps`, `font-display`, `font-sans`, `font-mono` — never raw `text-xl`/`text-base`/`text-sm`.
|
|
117
|
+
5. Use `lucide-react` for icons. Never inline SVG. Icons rendered as components (`<Copy aria-hidden="true" className="size-3.5" />`).
|
|
118
|
+
6. Use CVA (`class-variance-authority`) for variant APIs that have ≥3 options (Button, Badge, Progress intent, etc.).
|
|
119
|
+
7. Pass `aria-label` for icon-only triggers. Use `<span className="sr-only" aria-live="polite">` for state announcements (CopyButton pattern).
|
|
120
|
+
8. Tests use `vitest` + `@testing-library/react` + `vitest-axe`. Every interactive primitive ships `expect(await axe(container)).toHaveNoViolations()`. Async assertions use `findBy*` with `{ timeout: 5000 }` headroom for parallel vitest pool.
|
|
121
|
+
9. Stories ship in Ladle (`.stories.tsx`). Title pattern: `"Primitives / {Category} / {Name}"` or `"Composites / {Category} / {Name}"`.
|
|
122
|
+
10. Registry descriptor in `registry/{slug}.json` with shadcn schema. Each entry declares `dependencies` (npm) + `registryDependencies` (other registry items in this project).
|
|
123
|
+
|
|
124
|
+
## Anti-patterns (do NOT)
|
|
125
|
+
|
|
126
|
+
- **Do NOT** add raw hex/rgb. Always use tokens. The design-audit gate fails the build if it finds them.
|
|
127
|
+
- **Do NOT** install Radix / CVA / lucide-react separately — they're bundled `dependencies`.
|
|
128
|
+
- **Do NOT** import a sibling primitive into another primitive (taxonomy gate hard-fails). Either inline the dep or promote to composite.
|
|
129
|
+
- **Do NOT** import via `../../primitives/foo/foo.js` from a composite — use `../../primitives/foo/index.js` (gate hard-fails).
|
|
130
|
+
- **Do NOT** wrap dropdown/dialog/select in your own Portal. Radix manages portals correctly.
|
|
131
|
+
- **Do NOT** disable a quality gate or add `eslint-disable`/`biome-ignore` to silence one — fix the root cause. Acceptable biome-ignore comments must include a `WAI-ARIA …` or equivalent justification.
|
|
132
|
+
- **Do NOT** roll your own clipboard, focus-trap, accessible dialog, color parser, markdown renderer, syntax highlighter, or relative-time formatter — use `<CopyButton>`, Radix Dialog, `Intl.RelativeTimeFormat`, etc.
|
|
133
|
+
- **Do NOT** ship CJS (`require()`). ESM only. The TypeScript output is `.js` with `.d.ts` siblings.
|
|
134
|
+
- **Do NOT** rename the locked names (see below) without a strategic review.
|
|
135
|
+
- **Do NOT** use emojis in code, README, JSDoc, or CHANGELOG. (User-facing copy in docs is OK if explicitly requested.)
|
|
136
|
+
- **Do NOT** invent integration that doesn't exist. As of 2026-05-23, `@usetheo/ui` has **no** import from `@usetheo/sdk`, `theokit`, or `theo-code`. Verify with `grep` before claiming wiring.
|
|
137
|
+
|
|
138
|
+
## Locked names (immutable without strategic review)
|
|
139
|
+
|
|
140
|
+
| Item | Value |
|
|
141
|
+
|---|---|
|
|
142
|
+
| npm package | `@usetheo/ui` |
|
|
143
|
+
| Codename | Violet Forge |
|
|
144
|
+
| Default theme | `violet-forge` |
|
|
145
|
+
| Built-in themes (3 originals) | `violet-forge`, `classic-paper`, `aurora-terminal` |
|
|
146
|
+
| ThemeProvider component | `<ThemeProvider />` |
|
|
147
|
+
| Registry endpoint (planned canonical) | `ui.usetheo.dev/r/*.json` |
|
|
148
|
+
| Module format | ESM only |
|
|
149
|
+
| Quality-gate command | `pnpm quality:gates` |
|
|
150
|
+
| Test framework | Vitest |
|
|
151
|
+
| Stories framework | Ladle |
|
|
152
|
+
| Linter / formatter | Biome (`biome check`, `biome format`) |
|
|
153
|
+
| Component taxonomy | `primitive` (no internal deps) vs `composite` (≥1 internal dep) |
|
|
154
|
+
| Fonts | Geist Sans + Geist Mono (CDN by default) |
|
|
155
|
+
|
|
156
|
+
## Engines (subpath-isolated, opt-in)
|
|
157
|
+
|
|
158
|
+
These ship under their own subpath because they require heavy peer-deps. **Do NOT** include in the main barrel.
|
|
159
|
+
|
|
160
|
+
| Engine | Subpath | Required peer-deps |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| Whiteboard (Excalidraw-style view) | `@usetheo/ui/whiteboard` | `roughjs`, `perfect-freehand` |
|
|
163
|
+
| Slide (Marp-style view) | `@usetheo/ui/slide` | `mdast-util-*`, `hast-util-*`, `micromark-extension-gfm`, `unist-util-visit*`, `yaml` |
|
|
164
|
+
| SlideDeck | `@usetheo/ui/slide-deck` | inherits from Slide |
|
|
165
|
+
| Slide plugin — Shiki | `@usetheo/ui/slide/plugins/shiki` | `shiki` |
|
|
166
|
+
| Slide plugin — Math (KaTeX) | `@usetheo/ui/slide/plugins/math` | `katex`, `mdast-util-math`, `micromark-extension-math` |
|
|
167
|
+
| Slide plugin — Mermaid | `@usetheo/ui/slide/plugins/mermaid` | `mermaid` |
|
|
168
|
+
| Slide plugin — Emoji | `@usetheo/ui/slide/plugins/emoji` | `hast-util-from-html` |
|
|
169
|
+
| Vite plugin | `@usetheo/ui/vite-plugin` | `vite ^6 || ^7`, `@tailwindcss/vite` |
|
|
170
|
+
|
|
171
|
+
All engines documented under RFC 0001 / 0002 / 0003 / 0004 in `docs/rfcs/`. View-only (no editing) by design.
|
|
172
|
+
|
|
173
|
+
## Quality gates (run via `pnpm quality:gates`)
|
|
174
|
+
|
|
175
|
+
The full chain runs in CI and must be 100% green for any PR to merge:
|
|
176
|
+
|
|
177
|
+
1. `format:check` — Biome formatter
|
|
178
|
+
2. `lint:ci` — Biome linter (strict)
|
|
179
|
+
3. `typecheck` — `tsc --noEmit` (strict TS)
|
|
180
|
+
4. `test` — Vitest run (1532+ tests including `vitest-axe` a11y)
|
|
181
|
+
5. `build` — tsup (ESM + d.ts + CSS)
|
|
182
|
+
6. `registry:build` + `registry:validate` — shadcn descriptors
|
|
183
|
+
7. `quality:structure` — taxonomy + readme drift + design-audit + test/story coverage
|
|
184
|
+
8. `quality:bundle` — `dist/index.js` size delta within ±5% baseline (rebaseline explicit via `--update`)
|
|
185
|
+
9. `quality:a11y` — every interactive Ladle story axe-validated (~226 tests)
|
|
186
|
+
10. `ladle:build` — verify Ladle compiles
|
|
187
|
+
11. `dogfood:*` — end-to-end smoke against each engine
|
|
188
|
+
|
|
189
|
+
## Test conventions
|
|
190
|
+
|
|
191
|
+
- Framework: **Vitest** (NOT Jest). Use `vi.fn()`, `vi.spyOn()`, `vi.useFakeTimers()`.
|
|
192
|
+
- Renderer: `@testing-library/react`. Always `render(<C ... />)`, never manual `ReactDOM.render`.
|
|
193
|
+
- A11y: `import { axe } from "vitest-axe"; expect(await axe(container)).toHaveNoViolations();`
|
|
194
|
+
- Async assertions: `await screen.findByText(...)` (not `getByText` then `waitFor`). For tests under parallel vitest pool stress, add `{ timeout: 5000 }`.
|
|
195
|
+
- Radix dialog tests: use `baseElement` (not `container`) for portal-rendered content. Disable `aria-hidden-focus` rule on axe runs over Dialog content — it's a false positive on Radix's focus-guard spans.
|
|
196
|
+
- Clipboard tests: `vi.stubGlobal("navigator", { clipboard: { writeText: vi.fn() } });`.
|
|
197
|
+
- Time/Date tests: `vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-23T12:00:00Z"));`.
|
|
198
|
+
|
|
199
|
+
## Stories convention (Ladle)
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
import type { Story } from "@ladle/react";
|
|
203
|
+
import { MyComponent } from "./my-component.js";
|
|
204
|
+
|
|
205
|
+
export default { title: "Primitives / Category / MyComponent" };
|
|
206
|
+
|
|
207
|
+
export const Default: Story = () => <MyComponent prop="value" />;
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Run dev server: `pnpm dev` (Ladle on `http://localhost:61000`). Build static: `pnpm ladle:build`. Title pattern is enforced for sidebar grouping.
|
|
211
|
+
|
|
212
|
+
## Documentation URLs (canonical)
|
|
213
|
+
|
|
214
|
+
- **Site:** `https://docs.usetheo.dev/theoui`
|
|
215
|
+
- **Per-component MDX pages (curated, not auto-gen):**
|
|
216
|
+
- Primitives: `https://docs.usetheo.dev/theoui/primitives/{slug}`
|
|
217
|
+
- Composites: most live under their domain dir — `infra/`, `code/`, `agent/`, `auth/`, `tools/`, `models/`, `tasks/`. Examples:
|
|
218
|
+
- `https://docs.usetheo.dev/theoui/infra/account-menu` (PaaS surface)
|
|
219
|
+
- `https://docs.usetheo.dev/theoui/infra/usage-meter`
|
|
220
|
+
- `https://docs.usetheo.dev/theoui/code/code-block`
|
|
221
|
+
- `https://docs.usetheo.dev/theoui/primitives/table`
|
|
222
|
+
- `https://docs.usetheo.dev/theoui/primitives/confirm-dialog`
|
|
223
|
+
- **Theming guide:** `https://docs.usetheo.dev/theoui/theming`
|
|
224
|
+
- **Engines:** `https://docs.usetheo.dev/theoui/engines/{whiteboard,slide,slide-deck}`
|
|
225
|
+
- **CHANGELOG:** `https://github.com/usetheodev/theo-ui/blob/main/CHANGELOG.md`
|
|
226
|
+
|
|
227
|
+
## Source-of-truth files (read these before claiming anything)
|
|
228
|
+
|
|
229
|
+
1. `/CLAUDE.md` — project contract (locked names, taxonomy gate, voice/tone scope, inviolable rules)
|
|
230
|
+
2. `/README.md` — public catalog + quality gates + dev scripts (auto-synced from filesystem by `pnpm sync:readme`)
|
|
231
|
+
3. `/CHANGELOG.md` — every release entry, including [Unreleased] section
|
|
232
|
+
4. `/package.json#exports` — the authoritative list of subpath imports (auto-synced by `pnpm sync:exports`)
|
|
233
|
+
5. `/src/index.ts` — the barrel; every public component appears here
|
|
234
|
+
6. `/docs/architecture.md` — folder layout + module boundaries
|
|
235
|
+
7. `/docs/design-system.md` — tokens, fonts, type scale (Geist + Vercel scale, locked)
|
|
236
|
+
8. `/docs/rfcs/*.md` — engine RFCs (whiteboard 0001, slide 0002, slide-deck 0003, slide-rich-content 0004, themes 0007, integration 0008, chat-message 0009)
|
|
237
|
+
9. `/scripts/validate-quality-gates.ts` — the taxonomy + structure gate
|
|
238
|
+
10. `/.claude/knowledge-base/plans/*.md` — every change of ≥3 phases has a written plan; the most recent is `dashboard-paas-primitives-2-plan.md` (Brief #2)
|
|
239
|
+
|
|
240
|
+
## How to add a new component (the rote checklist)
|
|
241
|
+
|
|
242
|
+
1. Decide taxonomy: does it import another `@usetheo/ui` component? If yes → composite. If no → primitive.
|
|
243
|
+
2. Create folder: `src/components/{primitives,composites}/{kebab-name}/`
|
|
244
|
+
3. Create 4 files: `{slug}.tsx`, `{slug}.test.tsx`, `{slug}.stories.tsx`, `index.ts`
|
|
245
|
+
4. Implement with `forwardRef` + `displayName` + `cn()` + lucide icons + design tokens + typography scale
|
|
246
|
+
5. Tests: ≥6 unit tests including 1 `vitest-axe` assertion
|
|
247
|
+
6. Stories: ≥3 Ladle stories (variants/sizes/states)
|
|
248
|
+
7. Add registry descriptor: `registry/{slug}.json` with `$schema`, `name`, `type: "registry:ui"`, `title`, `description`, `dependencies`, `registryDependencies`, `files`
|
|
249
|
+
8. Add export to `src/index.ts` (the barrel)
|
|
250
|
+
9. Run `pnpm sync:exports && pnpm sync:readme` to regenerate `package.json#exports` + README + architecture catalog + Ladle stats
|
|
251
|
+
10. Add CHANGELOG entry under `## [Unreleased]` → `### Added` with one bullet per component
|
|
252
|
+
11. Run `pnpm quality:gates` — must be 100% green
|
|
253
|
+
12. If shipping to consumers, also write a curated MDX page in `theo-opendocs` (NEVER use auto-gen `/components/` dir — that's broken legacy)
|
|
254
|
+
|
|
255
|
+
## Cross-project positioning
|
|
256
|
+
|
|
257
|
+
`@usetheo/ui` is the **UI pillar** of the `usetheo` ecosystem. Other pillars:
|
|
258
|
+
|
|
259
|
+
- **Harness** — `@usetheo/sdk` (Claude Agent SDK consumer). No current import from `@usetheo/ui`.
|
|
260
|
+
- **Skills** — `theokit` (agent framework). No current import.
|
|
261
|
+
- **Runtime** — Theo PaaS (cloud platform, pre-release). No current import; `<DeploymentRow>`, `<BuildLogStream>`, `<RollbackUI>`, `<EnvVarEditor>`, `<DomainConfig>`, `<PreviewEnvCard>`, `<ProjectCard>`, `<MetricsPanel>`, and the new Brief #1 + #2 components are designed for it but currently unused.
|
|
262
|
+
|
|
263
|
+
`@usetheo/ui` is **community auxiliary**: Apache-2.0, usable standalone, no commitment to the rest of the stack. The integration story above is aspirational; verify with `grep` before claiming wiring exists in any consumer.
|
|
264
|
+
|
|
265
|
+
## When this file is stale
|
|
266
|
+
|
|
267
|
+
The code, the README, and the `package.json#exports` map are **authoritative**. If `llms.txt` disagrees with them, the code wins — update this file via PR with a one-line rationale. Locked names (Section "Locked names") and voice/narrative rules require a strategic review at the monorepo level before being weakened or repealed.
|
|
268
|
+
|
|
269
|
+
— Generated 2026-05-23 from filesystem inventory + `CLAUDE.md` + `README.md`. Regenerate by inspecting `src/components/{primitives,composites}/*/` directories and updating the catalog sections above.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usetheo/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-next.0",
|
|
4
4
|
"description": "Theo UI — framework-agnostic React component library with the Violet Forge design system. Focused on AI-agent interfaces, cloud dashboards, and developer-tooling surfaces.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
"types": "./dist/index.d.ts",
|
|
68
68
|
"import": "./dist/index.js"
|
|
69
69
|
},
|
|
70
|
+
"./alert": {
|
|
71
|
+
"types": "./dist/index.d.ts",
|
|
72
|
+
"import": "./dist/index.js"
|
|
73
|
+
},
|
|
70
74
|
"./approval-card": {
|
|
71
75
|
"types": "./dist/index.d.ts",
|
|
72
76
|
"import": "./dist/index.js"
|
|
@@ -267,6 +271,10 @@
|
|
|
267
271
|
"types": "./dist/index.d.ts",
|
|
268
272
|
"import": "./dist/index.js"
|
|
269
273
|
},
|
|
274
|
+
"./pagination": {
|
|
275
|
+
"types": "./dist/index.d.ts",
|
|
276
|
+
"import": "./dist/index.js"
|
|
277
|
+
},
|
|
270
278
|
"./permission-matrix": {
|
|
271
279
|
"types": "./dist/index.d.ts",
|
|
272
280
|
"import": "./dist/index.js"
|
|
@@ -510,7 +518,8 @@
|
|
|
510
518
|
"registry/index.json",
|
|
511
519
|
"LICENSE",
|
|
512
520
|
"NOTICE",
|
|
513
|
-
"CHANGELOG.md"
|
|
521
|
+
"CHANGELOG.md",
|
|
522
|
+
"llms.txt"
|
|
514
523
|
],
|
|
515
524
|
"peerDependencies": {
|
|
516
525
|
"@tailwindcss/vite": "^4.0.0",
|
package/registry/index.json
CHANGED
|
@@ -84,6 +84,12 @@
|
|
|
84
84
|
"title": "Theo UI agent types",
|
|
85
85
|
"description": "Shared TypeScript types for the agent timeline, events, and statuses."
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
"name": "alert",
|
|
89
|
+
"type": "registry:ui",
|
|
90
|
+
"title": "Alert",
|
|
91
|
+
"description": "Persistent inline notice primitive. Four intents (info / success / warning / destructive) with mapped lucide icons (Info / CheckCircle2 / TriangleAlert / AlertCircle) and design tokens. Optional title, description, action slot, and onDismiss handler. destructive intent renders role=alert (assertive); other intents render role=status (polite). Distinct from Toast (transient, auto-dismiss) and EmptyState (centered card)."
|
|
92
|
+
},
|
|
87
93
|
{
|
|
88
94
|
"name": "approval-card",
|
|
89
95
|
"type": "registry:block",
|
|
@@ -408,6 +414,12 @@
|
|
|
408
414
|
"title": "ModelSelector",
|
|
409
415
|
"description": "Chip dropdown for picking the active LLM."
|
|
410
416
|
},
|
|
417
|
+
{
|
|
418
|
+
"name": "pagination",
|
|
419
|
+
"type": "registry:ui",
|
|
420
|
+
"title": "Pagination",
|
|
421
|
+
"description": "Accessible page-number navigation primitive. Renders <nav aria-label=Pagination> with first/prev/numbers/next/last buttons and visual ellipses when totalPages exceeds the visible range. Active page carries aria-current=page. Keyboard nav: ArrowLeft / ArrowRight / Home / End. Configurable siblingCount + optional jump buttons. Returns null when totalPages <= 1. Exports a pure computePageRange helper for unit testing the range logic in isolation."
|
|
422
|
+
},
|
|
411
423
|
{
|
|
412
424
|
"name": "permission-matrix",
|
|
413
425
|
"type": "registry:ui",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "alert",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Alert",
|
|
6
|
+
"description": "Persistent inline notice primitive. Four intents (info / success / warning / destructive) with mapped lucide icons (Info / CheckCircle2 / TriangleAlert / AlertCircle) and design tokens. Optional title, description, action slot, and onDismiss handler. destructive intent renders role=alert (assertive); other intents render role=status (polite). Distinct from Toast (transient, auto-dismiss) and EmptyState (centered card).",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/alert/alert.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/alert.tsx",
|
|
19
|
+
"content": "import { AlertCircle, CheckCircle2, Info, TriangleAlert, X } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { ElementType, HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Alert — persistent inline notice. Distinct from `Toast` (transient,\n * corner-positioned, multiple stackable) and `EmptyState` (centered card\n * for no-data affordances). Full-width-of-section banner that stays\n * visible until the user acts.\n *\n * `intent` drives icon + color tokens. `destructive` intent renders\n * `role=\"alert\"` for assertive screen-reader announcement; the other\n * three intents render `role=\"status\"` (polite). `onDismiss`, when\n * provided, renders a trailing `X` button. `action` slot accepts any\n * ReactNode (typically a `<Button>` or anchor).\n *\n * @example\n * <Alert\n * intent=\"warning\"\n * title=\"Verify your email\"\n * description=\"We sent a link to your email.\"\n * action={<Button size=\"sm\" onClick={resend}>Resend</Button>}\n * />\n */\nexport type AlertIntent = \"info\" | \"success\" | \"warning\" | \"destructive\";\n\nexport interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\" | \"role\"> {\n intent?: AlertIntent;\n title?: ReactNode;\n description?: ReactNode;\n action?: ReactNode;\n onDismiss?: () => void;\n}\n\nconst INTENT: Record<\n AlertIntent,\n { icon: ElementType; border: string; bg: string; iconColor: string }\n> = {\n info: {\n icon: Info,\n border: \"border-primary/30\",\n bg: \"bg-primary/[0.04]\",\n iconColor: \"text-primary\",\n },\n success: {\n icon: CheckCircle2,\n border: \"border-success/30\",\n bg: \"bg-success/[0.04]\",\n iconColor: \"text-success\",\n },\n warning: {\n icon: TriangleAlert,\n border: \"border-warning/30\",\n bg: \"bg-warning/[0.04]\",\n iconColor: \"text-warning\",\n },\n destructive: {\n icon: AlertCircle,\n border: \"border-destructive/30\",\n bg: \"bg-destructive/[0.04]\",\n iconColor: \"text-destructive\",\n },\n};\n\nconst Alert = forwardRef<HTMLDivElement, AlertProps>(\n ({ className, intent = \"info\", title, description, action, onDismiss, ...props }, ref) => {\n const config = INTENT[intent];\n const Icon = config.icon;\n const hasTitle = title !== undefined && title !== null;\n const hasDescription = description !== undefined && description !== null;\n const role = intent === \"destructive\" ? \"alert\" : \"status\";\n\n return (\n <div\n ref={ref}\n role={role}\n className={cn(\n \"rounded-lg border\",\n config.border,\n config.bg,\n hasTitle && hasDescription ? \"p-4\" : \"p-3\",\n className,\n )}\n {...props}\n >\n <div className=\"flex items-start gap-3\">\n <Icon aria-hidden=\"true\" className={cn(\"mt-0.5 size-4 shrink-0\", config.iconColor)} />\n <div className=\"min-w-0 flex-1\">\n {hasTitle ? (\n <div className=\"font-medium font-sans text-body-sm text-foreground\">{title}</div>\n ) : null}\n {hasDescription ? (\n <div\n className={cn(\"font-sans text-body-sm text-muted-foreground\", hasTitle && \"mt-0.5\")}\n >\n {description}\n </div>\n ) : null}\n </div>\n {action !== undefined ? <div className=\"ml-auto shrink-0\">{action}</div> : null}\n {onDismiss !== undefined ? (\n <button\n type=\"button\"\n onClick={onDismiss}\n aria-label=\"Dismiss\"\n className={cn(\n \"shrink-0 rounded p-0.5 text-muted-foreground transition-colors\",\n \"hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <X aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : null}\n </div>\n </div>\n );\n },\n);\nAlert.displayName = \"Alert\";\n\nexport { Alert };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "pagination",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Pagination",
|
|
6
|
+
"description": "Accessible page-number navigation primitive. Renders <nav aria-label=Pagination> with first/prev/numbers/next/last buttons and visual ellipses when totalPages exceeds the visible range. Active page carries aria-current=page. Keyboard nav: ArrowLeft / ArrowRight / Home / End. Configurable siblingCount + optional jump buttons. Returns null when totalPages <= 1. Exports a pure computePageRange helper for unit testing the range logic in isolation.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/pagination/pagination.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/pagination.tsx",
|
|
19
|
+
"content": "import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { HTMLAttributes, KeyboardEvent } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Pagination — accessible page-number navigation primitive.\n *\n * Renders a `<nav aria-label=\"Pagination\">` containing a button group:\n * `[<<] [<] 1 ... 5 6 [7] 8 9 ... 42 [>] [>>]`. The active page carries\n * `aria-current=\"page\"`. Keyboard navigation (ArrowLeft / ArrowRight /\n * Home / End) is wired on the nav. Ellipses are rendered as\n * non-interactive `<span>` elements with `aria-hidden`.\n *\n * Renders nothing when `totalPages <= 1` (the page is the whole list).\n *\n * `siblingCount` controls how many neighbors of the current page are\n * always visible (default 1 → \"5 6 [7] 8 9\"). `showJumpButtons`\n * toggles the first/last `<<` / `>>` buttons.\n *\n * Consumers control state (`currentPage`) and are responsible for any\n * URL routing — the buttons are `<button>`, not `<a>`.\n *\n * @example\n * <Pagination\n * currentPage={page}\n * totalPages={42}\n * onPageChange={setPage}\n * />\n */\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n currentPage: number;\n totalPages: number;\n onPageChange: (page: number) => void;\n /** Neighbors of current page that stay visible. Default 1. */\n siblingCount?: number;\n /** Render `<<` / `>>` first/last buttons. Default true. */\n showJumpButtons?: boolean;\n /** Size variant. Default md. */\n size?: \"sm\" | \"md\";\n}\n\n/**\n * Pure helper: compute the visible page-range with ellipses.\n * Exported for unit testing — most pagination bugs live here.\n */\nexport function computePageRange(\n currentPage: number,\n totalPages: number,\n siblingCount = 1,\n): Array<number | \"ellipsis-start\" | \"ellipsis-end\"> {\n if (totalPages <= 1) return [];\n\n // Always keep first + last + siblings around current.\n // Total \"core\" buttons: 1 + (siblingCount * 2 + 1) + 1 = siblingCount * 2 + 3.\n // Plus possibly 2 ellipsis placeholders → max visible = siblingCount * 2 + 5.\n const totalNumbers = siblingCount * 2 + 3;\n const totalWithEdges = totalNumbers + 2;\n\n if (totalPages <= totalWithEdges) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const leftSibling = Math.max(currentPage - siblingCount, 1);\n const rightSibling = Math.min(currentPage + siblingCount, totalPages);\n\n const showLeftEllipsis = leftSibling > 2;\n const showRightEllipsis = rightSibling < totalPages - 1;\n\n if (!showLeftEllipsis && showRightEllipsis) {\n const leftRangeEnd = 1 + (siblingCount * 2 + 2);\n const leftRange = Array.from({ length: leftRangeEnd }, (_, i) => i + 1);\n return [...leftRange, \"ellipsis-end\", totalPages];\n }\n\n if (showLeftEllipsis && !showRightEllipsis) {\n const rightStart = totalPages - (siblingCount * 2 + 2);\n const rightRange = Array.from(\n { length: totalPages - rightStart + 1 },\n (_, i) => rightStart + i,\n );\n return [1, \"ellipsis-start\", ...rightRange];\n }\n\n // Both sides need ellipsis.\n const middleRange = Array.from(\n { length: rightSibling - leftSibling + 1 },\n (_, i) => leftSibling + i,\n );\n return [1, \"ellipsis-start\", ...middleRange, \"ellipsis-end\", totalPages];\n}\n\nconst SIZE: Record<NonNullable<PaginationProps[\"size\"]>, string> = {\n sm: \"size-7 text-label\",\n md: \"size-8 text-body-sm\",\n};\n\nconst ICON_SIZE: Record<NonNullable<PaginationProps[\"size\"]>, string> = {\n sm: \"size-3\",\n md: \"size-3.5\",\n};\n\nconst Pagination = forwardRef<HTMLElement, PaginationProps>(\n (\n {\n className,\n currentPage,\n totalPages,\n onPageChange,\n siblingCount = 1,\n showJumpButtons = true,\n size = \"md\",\n ...props\n },\n ref,\n ) => {\n if (totalPages <= 1) {\n return null;\n }\n\n const range = computePageRange(currentPage, totalPages, siblingCount);\n const prevDisabled = currentPage <= 1;\n const nextDisabled = currentPage >= totalPages;\n const sizeClass = SIZE[size];\n const iconClass = ICON_SIZE[size];\n\n const buttonBase = cn(\n \"inline-flex items-center justify-center rounded-md font-mono tabular-nums\",\n \"transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n sizeClass,\n );\n\n function go(page: number) {\n const clamped = Math.max(1, Math.min(totalPages, page));\n if (clamped !== currentPage) onPageChange(clamped);\n }\n\n function handleKeyDown(e: KeyboardEvent<HTMLElement>) {\n if (e.key === \"ArrowLeft\") {\n e.preventDefault();\n go(currentPage - 1);\n } else if (e.key === \"ArrowRight\") {\n e.preventDefault();\n go(currentPage + 1);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n go(1);\n } else if (e.key === \"End\") {\n e.preventDefault();\n go(totalPages);\n }\n }\n\n return (\n <nav\n ref={ref}\n aria-label=\"Pagination\"\n onKeyDown={handleKeyDown}\n className={cn(\"flex items-center gap-1\", className)}\n {...props}\n >\n {showJumpButtons ? (\n <button\n type=\"button\"\n onClick={() => go(1)}\n disabled={prevDisabled}\n aria-label=\"Go to first page\"\n aria-disabled={prevDisabled || undefined}\n className={cn(\n buttonBase,\n \"text-foreground hover:bg-muted\",\n prevDisabled && \"cursor-not-allowed opacity-40 hover:bg-transparent\",\n )}\n >\n <ChevronsLeft aria-hidden=\"true\" className={iconClass} />\n </button>\n ) : null}\n <button\n type=\"button\"\n onClick={() => go(currentPage - 1)}\n disabled={prevDisabled}\n aria-label=\"Go to previous page\"\n aria-disabled={prevDisabled || undefined}\n className={cn(\n buttonBase,\n \"text-foreground hover:bg-muted\",\n prevDisabled && \"cursor-not-allowed opacity-40 hover:bg-transparent\",\n )}\n >\n <ChevronLeft aria-hidden=\"true\" className={iconClass} />\n </button>\n {range.map((item) => {\n if (item === \"ellipsis-start\" || item === \"ellipsis-end\") {\n return (\n <span\n key={item}\n aria-hidden=\"true\"\n className={cn(\n \"inline-flex items-center justify-center text-muted-foreground\",\n sizeClass,\n )}\n >\n …\n </span>\n );\n }\n const isActive = item === currentPage;\n return (\n <button\n key={item}\n type=\"button\"\n onClick={() => go(item)}\n aria-label={`Go to page ${item}`}\n aria-current={isActive ? \"page\" : undefined}\n className={cn(\n buttonBase,\n isActive\n ? \"bg-primary text-primary-foreground hover:bg-primary\"\n : \"text-foreground hover:bg-muted\",\n )}\n >\n {item}\n </button>\n );\n })}\n <button\n type=\"button\"\n onClick={() => go(currentPage + 1)}\n disabled={nextDisabled}\n aria-label=\"Go to next page\"\n aria-disabled={nextDisabled || undefined}\n className={cn(\n buttonBase,\n \"text-foreground hover:bg-muted\",\n nextDisabled && \"cursor-not-allowed opacity-40 hover:bg-transparent\",\n )}\n >\n <ChevronRight aria-hidden=\"true\" className={iconClass} />\n </button>\n {showJumpButtons ? (\n <button\n type=\"button\"\n onClick={() => go(totalPages)}\n disabled={nextDisabled}\n aria-label=\"Go to last page\"\n aria-disabled={nextDisabled || undefined}\n className={cn(\n buttonBase,\n \"text-foreground hover:bg-muted\",\n nextDisabled && \"cursor-not-allowed opacity-40 hover:bg-transparent\",\n )}\n >\n <ChevronsRight aria-hidden=\"true\" className={iconClass} />\n </button>\n ) : null}\n </nav>\n );\n },\n);\nPagination.displayName = \"Pagination\";\n\nexport { Pagination };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|