@geomak/ui 7.19.0 → 7.20.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/.claude/commands/ui-auth-shell.md +48 -0
- package/.claude/commands/ui-bootstrap.md +36 -0
- package/.claude/commands/ui-find.md +12 -0
- package/.claude/commands/ui-form.md +38 -0
- package/.claude/commands/ui-lookup.md +13 -0
- package/.claude/commands/ui-modal.md +50 -0
- package/.claude/commands/ui-page.md +36 -0
- package/.claude/commands/ui-scaffold.md +73 -0
- package/.claude/commands/ui-story.md +30 -0
- package/.claude/commands/ui-table.md +38 -0
- package/.claude/skills/oxygen-ui/SKILL.md +402 -0
- package/README.md +67 -27
- package/package.json +9 -2
- package/scripts/setup-claude.mjs +136 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate a complete authenticated app shell with SecureLayout, sidebar, routing, and optional Wizard tour
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Generate an authenticated app shell for: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as `[AppName] [route1,route2,...]`. Examples:
|
|
8
|
+
```
|
|
9
|
+
VesOPS dashboard,vessels,settings,reports
|
|
10
|
+
MyApp dashboard,users
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
AppName defaults to `App`. Routes default to `dashboard,settings`.
|
|
14
|
+
|
|
15
|
+
## What this generates
|
|
16
|
+
|
|
17
|
+
A single `AppRoutes.tsx` file (or updates an existing one) with:
|
|
18
|
+
|
|
19
|
+
- `AppShell` with `TopBar` (brand + `ThemeSwitch`) and `sidebarSections`
|
|
20
|
+
- `SecureLayout` parent route guarding all provided routes
|
|
21
|
+
- Lazy-loaded placeholder pages for each route
|
|
22
|
+
- `useRef` arrays + icon wrapper spans for a `Wizard` onboarding tour
|
|
23
|
+
- A `Wizard` wrapping `AppShell` with `storageKey` based on the app name
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. **Fetch the bootstrap code**: Call `get_app_bootstrap` with `router: true`
|
|
28
|
+
and the app name to get the reference implementation.
|
|
29
|
+
|
|
30
|
+
2. **Call `get_component` for `secure-layout`** to confirm current prop names
|
|
31
|
+
(isAuthenticated, fallback, loadingFallback).
|
|
32
|
+
|
|
33
|
+
3. **Generate `AppRoutes.tsx`** with:
|
|
34
|
+
- One `useRef<HTMLSpanElement>` per route for the Wizard tour
|
|
35
|
+
- `sidebarSections` array with one item per route; icon is a `<span ref={...}>` wrapping
|
|
36
|
+
an appropriate `<Icon.*>` (pick the most relevant icon per route name)
|
|
37
|
+
- `wizardSteps` array with title + description for each route
|
|
38
|
+
- `SecureLayout` wrapping all provided routes as child routes
|
|
39
|
+
- Each route uses `lazy(() => import('../pages/RouteNamePage'))` with a `Suspense` fallback
|
|
40
|
+
- `Wizard` wrapping `AppShell` with `steps={wizardSteps}` and `storageKey="{appName}-tour"`
|
|
41
|
+
|
|
42
|
+
4. **Generate stub page files** for each route under `src/pages/` (e.g. `DashboardPage.tsx`)
|
|
43
|
+
if they do not already exist. Each stub is a minimal component with a heading.
|
|
44
|
+
|
|
45
|
+
5. **Report** what was created and list:
|
|
46
|
+
- Which icons were guessed (developer may want to swap them)
|
|
47
|
+
- How to wire real authentication (what to replace the placeholder check with)
|
|
48
|
+
- How to add a route: duplicate an entry in `sidebarSections`, add a `useRef`, add a `wizardStep`, add a `<Route>`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate the full app bootstrap for a new project using OxygenUI
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Bootstrap a new app using OxygenUI: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as an optional app name (e.g. `MyApp`, `VesOPS`). Defaults to `App`.
|
|
8
|
+
|
|
9
|
+
## What this generates
|
|
10
|
+
|
|
11
|
+
Four files that give a new project a working, production-ready shell:
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `src/main.tsx` | Entry point, mounts Bootstrap into the DOM |
|
|
16
|
+
| `src/Bootstrap.tsx` | Full provider stack in the correct nesting order |
|
|
17
|
+
| `src/approutes/AppRoutes.tsx` | AppShell + SecureLayout + lazy routes skeleton |
|
|
18
|
+
| `src/utils/brandTokens.ts` | Token override object for custom branding |
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
1. **Fetch the bootstrap code**: Call the `oxygen-ui` MCP tool `get_app_bootstrap`
|
|
23
|
+
with `router: true`, `reactQuery: true`, `customTheme: true`, and the app name.
|
|
24
|
+
|
|
25
|
+
2. **Check what exists**: Before writing, check if any of the four target files
|
|
26
|
+
already exist. If they do, report the conflict and ask the user how to proceed
|
|
27
|
+
rather than overwriting.
|
|
28
|
+
|
|
29
|
+
3. **Write the four files** with the content from the tool, substituting the app name
|
|
30
|
+
throughout (component names, display strings, localStorage keys).
|
|
31
|
+
|
|
32
|
+
4. **Report** what was created and list the next steps:
|
|
33
|
+
- Add real routes to `AppRoutes.tsx`
|
|
34
|
+
- Wire real authentication (replace the `localStorage.getItem('token')` check)
|
|
35
|
+
- Populate `BRAND_TOKENS` if custom branding is needed
|
|
36
|
+
- Run `yarn add @tanstack/react-query react-router-dom` if not already installed
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Search OxygenUI for components matching a description or use-case
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Search OxygenUI for components that match: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
1. Call the `oxygen-ui` MCP tool `find_component` with the argument as the query.
|
|
8
|
+
2. Present the results as a concise table: **Name** · slug · category · description.
|
|
9
|
+
3. For the top 1-2 results, briefly describe why they match and what the user
|
|
10
|
+
should look for in the docs (e.g. key props, integration patterns).
|
|
11
|
+
4. If the search finds nothing, suggest alternative queries and offer to call
|
|
12
|
+
`list_components` to browse all available entries.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate a fully-wired OxygenUI Form from a field specification
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Generate a wired OxygenUI form for: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as a space-separated list of `field-name:input-type` pairs.
|
|
8
|
+
Append `?` to mark a field as optional (no required rule). Examples:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
email:text name:text role:dropdown notify:switch terms:checkbox
|
|
12
|
+
start:date end:date priority:slider tags:tags
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Supported input types: `text`, `textarea`, `password`, `number`, `email`,
|
|
16
|
+
`dropdown`, `switch`, `checkbox`, `radio`, `slider`, `date`, `daterange`,
|
|
17
|
+
`time`, `tags`, `color`, `rating`, `autocomplete`, `treeselect`
|
|
18
|
+
|
|
19
|
+
## Steps
|
|
20
|
+
|
|
21
|
+
1. **Resolve binders**: For each field, call the `oxygen-ui` MCP tool
|
|
22
|
+
`get_form_binding` with the input type. This returns the correct binder
|
|
23
|
+
method, value type, and a paste-ready snippet.
|
|
24
|
+
|
|
25
|
+
2. **Generate the form component**: Produce a self-contained React component:
|
|
26
|
+
- `useForm` with `defaultValues` typed to match each field
|
|
27
|
+
- One `FormField` per field with the correct binder
|
|
28
|
+
- Required rules on all non-optional (`?`) fields
|
|
29
|
+
- Email pattern rule if the type is `email`
|
|
30
|
+
- A submit `Button`
|
|
31
|
+
- `onFinish` stub with a `TODO` comment
|
|
32
|
+
|
|
33
|
+
3. **Output inline**: Deliver the component as a code block. Do not write to a
|
|
34
|
+
file unless the user explicitly asks to save it.
|
|
35
|
+
|
|
36
|
+
4. **Offer next steps**: After the form, suggest:
|
|
37
|
+
- Adding the form inside a Modal (`/ui-modal form-in-modal`)
|
|
38
|
+
- Making it part of a full page (`/ui-page crud EntityName`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Look up a specific OxygenUI component, full props API and usage examples
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Look up the OxygenUI component or hook: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
1. Call the `oxygen-ui` MCP tool `get_component` with the appropriate slug.
|
|
8
|
+
If the slug is not obvious, call `find_component` first with the argument as
|
|
9
|
+
the query, then call `get_component` with the best-matching slug.
|
|
10
|
+
2. Return the full documentation clearly formatted, props table, usage examples,
|
|
11
|
+
and any caveats or recipes that are relevant.
|
|
12
|
+
3. If the component accepts children or sub-components (e.g. `Modal.Body`),
|
|
13
|
+
highlight the composition pattern.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate the complete state + Modal setup for a named OxygenUI modal pattern
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Generate a Modal pattern for: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as one of: `confirm-delete`, `form-in-modal`, `detail-view`, `alert`
|
|
8
|
+
|
|
9
|
+
## Patterns
|
|
10
|
+
|
|
11
|
+
### confirm-delete
|
|
12
|
+
State + PopConfirm-style Modal for deleting a named item. Includes:
|
|
13
|
+
- `open` + `target` state
|
|
14
|
+
- `onDelete(item)` setter
|
|
15
|
+
- `onConfirm` async handler with `useNotification` feedback
|
|
16
|
+
- Full Modal JSX with cancel/confirm buttons
|
|
17
|
+
|
|
18
|
+
### form-in-modal
|
|
19
|
+
State + Modal containing a Form. Includes:
|
|
20
|
+
- `open` + `selected` state (null = create, item = edit)
|
|
21
|
+
- `openCreate` / `openEdit(item)` / `closeModal` helpers
|
|
22
|
+
- `useEffect` to reset the form when `open` changes
|
|
23
|
+
- Form with two example fields (developer fills in actual fields)
|
|
24
|
+
- Modal with `onOk={form.submit}` wiring
|
|
25
|
+
|
|
26
|
+
### detail-view
|
|
27
|
+
Read-only Modal that displays a selected item's details. Includes:
|
|
28
|
+
- `open` + `item` state
|
|
29
|
+
- Trigger pattern
|
|
30
|
+
- Modal with a descriptive layout (label/value pairs using Typography)
|
|
31
|
+
|
|
32
|
+
### alert
|
|
33
|
+
Simple informational Modal with a single confirm button and an optional icon.
|
|
34
|
+
|
|
35
|
+
## Steps
|
|
36
|
+
|
|
37
|
+
1. **Fetch component API**: Call `get_component` with slug `modal` to confirm
|
|
38
|
+
the current prop names (onClose vs onOpenChange may differ across versions).
|
|
39
|
+
|
|
40
|
+
2. **Generate the pattern**: Produce the complete code block:
|
|
41
|
+
- State declarations (paste above the return statement)
|
|
42
|
+
- Helper functions
|
|
43
|
+
- The Modal JSX (paste into the render tree)
|
|
44
|
+
- Any supporting imports
|
|
45
|
+
|
|
46
|
+
3. **Output inline** as a clearly labelled code block with sections:
|
|
47
|
+
`// --- STATE ---`, `// --- HANDLERS ---`, `// --- JSX ---`
|
|
48
|
+
|
|
49
|
+
4. **Offer next steps**: Suggest wrapping the form-in-modal inside a full
|
|
50
|
+
CRUD page with `/ui-page crud EntityName`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffold a complete page component using OxygenUI patterns
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scaffold a complete page for: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as `<type> [EntityName]` where type is one of:
|
|
8
|
+
`crud`, `dashboard`, `settings`, `detail`, `multi-step-form`
|
|
9
|
+
|
|
10
|
+
EntityName is optional (e.g. `crud Vessel`, `detail Order`, `dashboard`).
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **Look up the pattern**: Call the `oxygen-ui` MCP tool `get_pattern` with the type
|
|
15
|
+
and entity name. This returns the full recipe: components needed, state to declare,
|
|
16
|
+
wiring steps, and a complete code example.
|
|
17
|
+
|
|
18
|
+
2. **Resolve any unfamiliar props**: If the pattern uses a component whose API you are
|
|
19
|
+
not certain about, call `get_component` with the slug before writing code.
|
|
20
|
+
|
|
21
|
+
3. **Determine the output path**
|
|
22
|
+
- Check if `src/pages/` exists; if not, check `src/views/` or `src/screens/`
|
|
23
|
+
- File name: `{EntityName}Page.tsx` (e.g. `VesselsPage.tsx`, `DashboardPage.tsx`)
|
|
24
|
+
|
|
25
|
+
4. **Write the file**: Generate the complete, self-contained page component:
|
|
26
|
+
- TypeScript interface for the entity (if applicable)
|
|
27
|
+
- All imports from `@geomak/ui`
|
|
28
|
+
- All state declarations
|
|
29
|
+
- All event handlers wired up (create, edit, delete, save, etc.)
|
|
30
|
+
- Full JSX tree matching the pattern
|
|
31
|
+
- Replace all `api.*` calls with `TODO: replace with your API call`
|
|
32
|
+
|
|
33
|
+
5. **Report** what was created and list:
|
|
34
|
+
- Which `api.*` calls need to be wired to real endpoints
|
|
35
|
+
- Which types/interfaces need to be expanded
|
|
36
|
+
- Any optional enhancements (e.g. "add Drawer instead of Modal for more context")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffold a new OxygenUI component, implementation + story + MDX guide
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scaffold a new OxygenUI component named: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as `ComponentName [category]` where category is optional
|
|
8
|
+
(defaults to the most appropriate one based on the component name).
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. **Plan**: Determine:
|
|
13
|
+
- Target directory: `src/components/{core|inputs|forms|layout|marketing}/`
|
|
14
|
+
- Story title: `"Category/ComponentName"`
|
|
15
|
+
- Whether the component wraps a Radix primitive or is fully custom
|
|
16
|
+
|
|
17
|
+
2. **Check for overlap**, Call `find_component` to confirm no existing component
|
|
18
|
+
already covers this use case before creating anything.
|
|
19
|
+
|
|
20
|
+
3. **Scaffold files**: Create the following three files:
|
|
21
|
+
|
|
22
|
+
### `ComponentName.tsx`
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import React from 'react'
|
|
26
|
+
|
|
27
|
+
export interface ComponentNameProps {
|
|
28
|
+
// props here
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ComponentName({ className, ...props }: ComponentNameProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div className={className}>
|
|
35
|
+
{/* implementation */}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default ComponentName
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `ComponentName.stories.tsx`
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import React from 'react'
|
|
47
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
48
|
+
import ComponentName from './ComponentName'
|
|
49
|
+
|
|
50
|
+
const meta: Meta<typeof ComponentName> = {
|
|
51
|
+
title: 'Category/ComponentName',
|
|
52
|
+
component: ComponentName,
|
|
53
|
+
parameters: { layout: 'centered' },
|
|
54
|
+
tags: ['autodocs'],
|
|
55
|
+
}
|
|
56
|
+
export default meta
|
|
57
|
+
type Story = StoryObj<typeof ComponentName>
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
args: {},
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `src/docs/ComponentName.mdx`
|
|
65
|
+
|
|
66
|
+
Use the MDX guide structure with HTML tables (not GFM markdown tables).
|
|
67
|
+
Use the exported `th / td / td0 / tbl` style constants from Accordion.mdx as a template.
|
|
68
|
+
|
|
69
|
+
4. **Export**, Add the new component to `src/index.ts` in the correct section
|
|
70
|
+
(Core components, Input components, etc.).
|
|
71
|
+
|
|
72
|
+
5. **Report** what was created and what the developer should fill in next
|
|
73
|
+
(implementation logic, additional stories, tests).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Add or improve Storybook stories for an existing OxygenUI component
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Add or improve Storybook stories for the OxygenUI component: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. **Read the component**: Find and read:
|
|
10
|
+
- The component source file (`src/components/**/$ARGUMENTS.tsx` or similar)
|
|
11
|
+
- The existing story file if one exists
|
|
12
|
+
- The MDX guide if one exists in `src/docs/`
|
|
13
|
+
|
|
14
|
+
2. **Identify gaps**: Look for:
|
|
15
|
+
- Props that have no story coverage
|
|
16
|
+
- State combinations (loading, error, empty, disabled)
|
|
17
|
+
- Controlled vs uncontrolled usage
|
|
18
|
+
- Composition patterns (compound components with sub-components)
|
|
19
|
+
- Responsive or theme-sensitive behaviour
|
|
20
|
+
|
|
21
|
+
3. **Write stories** following the CSF3 format used throughout the codebase:
|
|
22
|
+
- `Default`: the simplest working example with sensible args
|
|
23
|
+
- One story per major variant or visual state
|
|
24
|
+
- Use `render` functions for complex compositions
|
|
25
|
+
- Use `play` functions only when testing interactions (focus, type, click)
|
|
26
|
+
- Prefer realistic content that reflects actual usage in the VesOPS portal
|
|
27
|
+
|
|
28
|
+
4. **Verify** that story `title` matches the existing Storybook sidebar path.
|
|
29
|
+
|
|
30
|
+
5. **Report** which stories were added and what each one demonstrates.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate a typed OxygenUI Table component with columns, actions, and optional CRUD wiring
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Generate a typed Table for: **$ARGUMENTS**
|
|
6
|
+
|
|
7
|
+
Parse the argument as `EntityName [col:type ...]`. Column type hints are optional.
|
|
8
|
+
Examples:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Vessel name:string flag:string dwt:number status:badge actions:menu
|
|
12
|
+
Order id:number customer:string total:number status:badge
|
|
13
|
+
User
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Column types: `string`, `number`, `boolean`, `badge`, `date`, `link`, `menu`
|
|
17
|
+
- `badge` renders a `<Badge>` with tone based on the value
|
|
18
|
+
- `menu` adds an actions column with Edit and Delete buttons + PopConfirm on delete
|
|
19
|
+
- Any unlisted type defaults to a plain text renderer
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. **Fetch Table API**: Call `get_component` with slug `table` to confirm the
|
|
24
|
+
exact `TableColumn<T>` shape and any props that have changed.
|
|
25
|
+
|
|
26
|
+
2. **Generate the component**: Produce:
|
|
27
|
+
- TypeScript interface for the entity with all specified columns
|
|
28
|
+
- `TableColumn<Entity>[]` array with correct renderers for typed columns
|
|
29
|
+
- For `badge` columns: a `<Badge>` renderer with a sensible tone map
|
|
30
|
+
- For `menu` columns: Edit button + PopConfirm-wrapped Delete button, wired to
|
|
31
|
+
`onEdit` and `onDelete` callback props
|
|
32
|
+
- Table component with `hasSearch`, `pagination`, and `getRowKey={r => r.id}`
|
|
33
|
+
|
|
34
|
+
3. **Output inline** as a complete, importable component.
|
|
35
|
+
|
|
36
|
+
4. **Offer extensions**: After the output, offer to:
|
|
37
|
+
- Add a create/edit Modal + Form (`/ui-modal form-in-modal` or `/ui-page crud EntityName`)
|
|
38
|
+
- Wrap it in a full CRUD page (`/ui-page crud EntityName`)
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oxygen-ui
|
|
3
|
+
description: >
|
|
4
|
+
Context skill for working with the OxygenUI (@geomak/ui) design system.
|
|
5
|
+
Equips Claude Code with knowledge about available components, tokens, import
|
|
6
|
+
patterns, and how to use the MCP server for real-time docs lookup.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# OxygenUI Design System
|
|
10
|
+
|
|
11
|
+
You are working inside the `@geomak/ui` repository, a React component library
|
|
12
|
+
built with Radix UI primitives, Tailwind CSS utilities, and a three-layer token
|
|
13
|
+
system. Everything ships as tree-shakeable ESM.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## MCP server
|
|
18
|
+
|
|
19
|
+
An `oxygen-ui` MCP server is configured in `.mcp.json`. Use its tools whenever
|
|
20
|
+
you need to look up component APIs, prop tables, or design tokens.
|
|
21
|
+
|
|
22
|
+
| Tool | When to call |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `find_component` | Before writing any UI, check what the design system already has |
|
|
25
|
+
| `get_component` | Get full props + examples for a specific component (use the slug) |
|
|
26
|
+
| `list_components` | Browse all 100+ entries when unsure what exists |
|
|
27
|
+
| `get_token` | Look up CSS custom properties by name or category prefix |
|
|
28
|
+
| `get_pattern` | Get a full wired recipe for a page-level pattern (crud-list, dashboard, etc.) |
|
|
29
|
+
| `get_form_binding` | Look up the correct `useForm` binder for any input component |
|
|
30
|
+
| `compare_components` | Get a decision guide when choosing between similar components |
|
|
31
|
+
| `get_app_bootstrap` | Get the full provider stack + app shell bootstrap code |
|
|
32
|
+
|
|
33
|
+
**Always call `find_component` first.** OxygenUI ships 100+ components. Writing
|
|
34
|
+
from scratch without checking is almost always redundant.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Import pattern
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// Named imports from the main entry (tree-shakeable)
|
|
42
|
+
import { Button, Modal, DataGrid, useExcel } from '@geomak/ui'
|
|
43
|
+
|
|
44
|
+
// Styles: include once at app root
|
|
45
|
+
import '@geomak/ui/styles'
|
|
46
|
+
|
|
47
|
+
// Token values for JS/TS code
|
|
48
|
+
import { vars, semanticTokens, palette } from '@geomak/ui/tokens'
|
|
49
|
+
|
|
50
|
+
// Icons
|
|
51
|
+
import { Icon } from '@geomak/ui'
|
|
52
|
+
<Icon.Search size={20} />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Token system
|
|
58
|
+
|
|
59
|
+
Three layers:
|
|
60
|
+
|
|
61
|
+
| Layer | Use case |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `palette` | Raw brand hex values |
|
|
64
|
+
| `semanticTokens` | Resolved hex/px values in `{ light, dark, shared }` objects |
|
|
65
|
+
| `vars` | CSS custom-property strings: `vars.color.accent`, `vars.radius.md` |
|
|
66
|
+
|
|
67
|
+
### CSS custom properties (use in inline styles or SCSS)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<div style={{
|
|
71
|
+
background: 'var(--color-surface)',
|
|
72
|
+
border: '1px solid var(--color-border)',
|
|
73
|
+
borderRadius: 'var(--radius-md)',
|
|
74
|
+
color: 'var(--color-foreground)',
|
|
75
|
+
}}>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Tailwind utilities
|
|
79
|
+
|
|
80
|
+
| Utility | Token |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `bg-surface` | `--color-surface` |
|
|
83
|
+
| `bg-surface-raised` | `--color-surface-raised` |
|
|
84
|
+
| `border-border` | `--color-border` |
|
|
85
|
+
| `text-foreground` | `--color-foreground` |
|
|
86
|
+
| `text-foreground-muted` | `--color-foreground-muted` |
|
|
87
|
+
| `bg-accent` | `--color-accent` |
|
|
88
|
+
| `text-accent-fg` | `--color-accent-foreground` |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Form binder cheat sheet
|
|
93
|
+
|
|
94
|
+
**This is the most common source of silent bugs.** Every `useForm` field must use
|
|
95
|
+
the correct binder for its input type or values will not update.
|
|
96
|
+
|
|
97
|
+
| Input component | Binder method | Value type stored |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `TextInput`, `TextArea`, `Password`, `SearchInput` | `fieldNative` | `string` |
|
|
100
|
+
| `NumberInput` | `fieldNative` | `number` |
|
|
101
|
+
| `Switch`, `Checkbox` | `fieldChecked` | `boolean` |
|
|
102
|
+
| `Dropdown`, `TreeSelect`, `AutoComplete` | `fieldTarget` | `key` or `key[]` |
|
|
103
|
+
| `RadioGroup` | `field` | option value |
|
|
104
|
+
| `Slider` | `field` | `number` or `[number, number]` |
|
|
105
|
+
| `DatePicker`, `TimePicker` | `field` | `Date \| null` |
|
|
106
|
+
| `DateRangePicker` | `field` | `{ from: Date; to: Date } \| null` |
|
|
107
|
+
| `TagsInput` | `field` | `string[]` |
|
|
108
|
+
| `ColorPicker` | `field` | `string` (hex) |
|
|
109
|
+
| `Rating` | `field` | `number` |
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// Example: mixed form with correct binders
|
|
113
|
+
const form = useForm({ defaultValues: { name: '', role: '', active: false, joined: null } })
|
|
114
|
+
|
|
115
|
+
<Form form={form} onFinish={handleSubmit}>
|
|
116
|
+
<FormField name="name"><TextInput label="Name" {...form.fieldNative('name')} /></FormField>
|
|
117
|
+
<FormField name="role"><Dropdown label="Role" items={roles} {...form.fieldTarget('role')} /></FormField>
|
|
118
|
+
<FormField name="active"><Switch label="Active" {...form.fieldChecked('active')} /></FormField>
|
|
119
|
+
<FormField name="joined"><DatePicker label="Joined" {...form.field('joined')} /></FormField>
|
|
120
|
+
</Form>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Call `get_form_binding` MCP tool when unsure which binder to use for a specific input.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Component decision guide
|
|
128
|
+
|
|
129
|
+
### Modal vs Drawer vs PopConfirm
|
|
130
|
+
|
|
131
|
+
| Component | Use when |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `Modal` | The action needs full attention; no background context helps (confirm, form submit) |
|
|
134
|
+
| `Drawer` | User benefits from seeing the page behind (filter panel, row inspector, quick edit) |
|
|
135
|
+
| `PopConfirm` | Inline destructive confirmation on a specific row/button; no overlay needed |
|
|
136
|
+
|
|
137
|
+
Both Modal and Drawer share the same controlled API: `open` + `onClose` + `hasFooter` + `onOk` + `onCancel`.
|
|
138
|
+
|
|
139
|
+
### Table vs DataGrid vs VirtualList vs List
|
|
140
|
+
|
|
141
|
+
| Component | Use when |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `Table` | Standard paginated, sortable, searchable data; editable cells optional |
|
|
144
|
+
| `DataGrid` | Excel-like: column reorder/resize, bulk edits, copy-paste cell behaviour |
|
|
145
|
+
| `VirtualList` | 10k+ rows where DOM count is the bottleneck; no built-in sort/filter |
|
|
146
|
+
| List (custom) | Single-column items, icon + label, no comparison between columns |
|
|
147
|
+
|
|
148
|
+
### Tabs vs SegmentedControl
|
|
149
|
+
|
|
150
|
+
| Component | Use when |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `Tabs` | Three or more sibling content panes; content is substantial |
|
|
153
|
+
| `SegmentedControl` | Two to four compact mutually exclusive filter-style options |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## App bootstrap (provider nesting order)
|
|
158
|
+
|
|
159
|
+
This order is required. Getting it wrong causes token overrides, notification
|
|
160
|
+
portals, or dark mode to break silently.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
// main.tsx or Bootstrap.tsx
|
|
164
|
+
import { ThemeProvider, NotificationProvider } from '@geomak/ui'
|
|
165
|
+
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
|
|
166
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
167
|
+
|
|
168
|
+
const queryClient = new QueryClient()
|
|
169
|
+
|
|
170
|
+
export function Bootstrap() {
|
|
171
|
+
return (
|
|
172
|
+
<ThemeProvider colorScheme={colorScheme}> {/* always outermost */}
|
|
173
|
+
<BrowserRouter>
|
|
174
|
+
<QueryClientProvider client={queryClient}>
|
|
175
|
+
<NotificationProvider> {/* must be inside ThemeProvider */}
|
|
176
|
+
<AppRoutes />
|
|
177
|
+
</NotificationProvider>
|
|
178
|
+
</QueryClientProvider>
|
|
179
|
+
</BrowserRouter>
|
|
180
|
+
</ThemeProvider>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Dark mode toggle: maintain `colorScheme` state in Bootstrap, pass it to `ThemeProvider`,
|
|
186
|
+
and also toggle `document.documentElement.classList.toggle('dark', isDark)` so that
|
|
187
|
+
Tailwind dark utilities work inside portals that render outside the ThemeProvider div.
|
|
188
|
+
|
|
189
|
+
Custom brand tokens (optional):
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
const BRAND_TOKENS = {
|
|
193
|
+
'--color-accent': '#1A56DB',
|
|
194
|
+
'--color-accent-hover': '#1e429f',
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
<ThemeProvider colorScheme={colorScheme} theme={BRAND_TOKENS} darkTheme={BRAND_TOKENS}>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## useNotification
|
|
203
|
+
|
|
204
|
+
Must be used inside a component that is a descendant of `NotificationProvider`.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { useNotification } from '@geomak/ui'
|
|
208
|
+
|
|
209
|
+
function MyPage() {
|
|
210
|
+
const notification = useNotification()
|
|
211
|
+
|
|
212
|
+
const onSave = async () => {
|
|
213
|
+
try {
|
|
214
|
+
await api.save(data)
|
|
215
|
+
notification.success({ title: 'Saved', description: 'Changes applied.', duration: 3000 })
|
|
216
|
+
} catch (err) {
|
|
217
|
+
notification.error({ title: 'Save failed', description: err.message, duration: 5000 })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Methods: `notification.success`, `notification.info`, `notification.warning`, `notification.error`.
|
|
224
|
+
All accept `{ title, description?, duration? }`.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## React Query integration
|
|
229
|
+
|
|
230
|
+
Map React Query state to OxygenUI component props:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
const { data, isPending, isFetching, isError, refetch } = useQuery({ queryKey, queryFn })
|
|
234
|
+
|
|
235
|
+
// First load: full skeleton
|
|
236
|
+
if (isPending) return <SkeletonBox className="h-96 w-full" />
|
|
237
|
+
|
|
238
|
+
// Error state
|
|
239
|
+
if (isError) return <ErrorState onRetry={refetch} />
|
|
240
|
+
|
|
241
|
+
// Background refetch: show spinner overlay on existing content (not a full skeleton)
|
|
242
|
+
// isPending=false, isFetching=true means data is already on screen
|
|
243
|
+
return (
|
|
244
|
+
<Table
|
|
245
|
+
data={data}
|
|
246
|
+
loading={isFetching && !isPending} // subtle refetch indicator
|
|
247
|
+
columns={columns}
|
|
248
|
+
/>
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
For mutations:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
const mutation = useMutation({
|
|
256
|
+
mutationFn: api.create,
|
|
257
|
+
onSuccess: () => {
|
|
258
|
+
notification.success({ title: 'Created' })
|
|
259
|
+
queryClient.invalidateQueries({ queryKey })
|
|
260
|
+
},
|
|
261
|
+
onError: (err) => notification.error({ title: 'Failed', description: err.message }),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
<Button loading={mutation.isPending} onClick={() => mutation.mutate(formData)}>
|
|
265
|
+
Save
|
|
266
|
+
</Button>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## SecureLayout + route guard pattern
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
// AppRoutes.tsx
|
|
275
|
+
import { AppShell, SecureLayout } from '@geomak/ui'
|
|
276
|
+
import { Routes, Route, Navigate } from 'react-router-dom'
|
|
277
|
+
|
|
278
|
+
function AppRoutes() {
|
|
279
|
+
return (
|
|
280
|
+
<AppShell topBar={<TopBar />} sidebarSections={sidebarSections}>
|
|
281
|
+
<Routes>
|
|
282
|
+
{/* Auth-gated routes: wrap parent Route with SecureLayout */}
|
|
283
|
+
<Route
|
|
284
|
+
element={
|
|
285
|
+
<SecureLayout
|
|
286
|
+
isAuthenticated={isAuthenticated}
|
|
287
|
+
fallback={<Navigate to="/login" replace />}
|
|
288
|
+
loadingFallback={null}
|
|
289
|
+
/>
|
|
290
|
+
}
|
|
291
|
+
>
|
|
292
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
293
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
294
|
+
</Route>
|
|
295
|
+
|
|
296
|
+
<Route path="/login" element={<LoginPage />} />
|
|
297
|
+
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
298
|
+
</Routes>
|
|
299
|
+
</AppShell>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Wizard + AppShell onboarding tour
|
|
307
|
+
|
|
308
|
+
Wizard must WRAP AppShell (not be inside it) so it can overlay the full viewport.
|
|
309
|
+
Refs for Wizard steps go on wrapper `<span>` elements inside the `icon` prop of each
|
|
310
|
+
`SidebarItem`, because `SidebarItem` has no `ref` prop.
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
const dashboardRef = useRef<HTMLSpanElement>(null)
|
|
314
|
+
const settingsRef = useRef<HTMLSpanElement>(null)
|
|
315
|
+
|
|
316
|
+
const sidebarSections = [{
|
|
317
|
+
items: [
|
|
318
|
+
{
|
|
319
|
+
label: 'Dashboard',
|
|
320
|
+
href: '/dashboard',
|
|
321
|
+
icon: <span ref={dashboardRef}><Icon.Home size={18} /></span>,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
label: 'Settings',
|
|
325
|
+
href: '/settings',
|
|
326
|
+
icon: <span ref={settingsRef}><Icon.Settings size={18} /></span>,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
}]
|
|
330
|
+
|
|
331
|
+
const wizardSteps = [
|
|
332
|
+
{ target: dashboardRef, title: 'Dashboard', description: 'Your overview at a glance.' },
|
|
333
|
+
{ target: settingsRef, title: 'Settings', description: 'Configure your preferences.' },
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
<Wizard steps={wizardSteps} storageKey="my-app-tour">
|
|
337
|
+
<AppShell topBar={...} sidebarSections={sidebarSections}>
|
|
338
|
+
<Routes>...</Routes>
|
|
339
|
+
</AppShell>
|
|
340
|
+
</Wizard>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Page patterns reference
|
|
346
|
+
|
|
347
|
+
| Pattern | Key components |
|
|
348
|
+
|---|---|
|
|
349
|
+
| **CRUD list** | Table + Modal (create/edit) + Form + useForm + PopConfirm + useNotification |
|
|
350
|
+
| **Dashboard overview** | Statistic cards + Skeleton + Dropdown (filter) + optional chart cards |
|
|
351
|
+
| **Settings / admin tabs** | Tabs + Form per panel + Switch/Dropdown + useNotification on save |
|
|
352
|
+
| **Multi-step wizard form** | Stepper + Form (per step or sectioned) + validation before advancing |
|
|
353
|
+
| **Detail / inspection page** | Tabs + Statistic + Timeline + Drawer (row inspector) + Badge |
|
|
354
|
+
| **Authenticated app shell** | ThemeProvider + NotificationProvider + AppShell + SecureLayout + Wizard tour |
|
|
355
|
+
|
|
356
|
+
Use `get_pattern` MCP tool for the full wired recipe (components + state + wiring steps + code example) for any of these.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Key component patterns
|
|
361
|
+
|
|
362
|
+
### ThemeProvider (required at app root)
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { ThemeProvider } from '@geomak/ui'
|
|
366
|
+
|
|
367
|
+
<ThemeProvider defaultTheme="system">
|
|
368
|
+
<App />
|
|
369
|
+
</ThemeProvider>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Modal
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
import { Modal } from '@geomak/ui'
|
|
376
|
+
|
|
377
|
+
const [open, setOpen] = useState(false)
|
|
378
|
+
|
|
379
|
+
<Modal open={open} onClose={() => setOpen(false)} title="Confirm" onOk={handleOk}>
|
|
380
|
+
<p>Are you sure?</p>
|
|
381
|
+
</Modal>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Storybook
|
|
387
|
+
|
|
388
|
+
Stories live in `src/**/*.stories.tsx` (CSF3 format). The Storybook dev server
|
|
389
|
+
is in `.claude/launch.json` as `"storybook"` (port 6006).
|
|
390
|
+
|
|
391
|
+
Guide MDX files live in `src/docs/` (one per component).
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Repository conventions
|
|
396
|
+
|
|
397
|
+
- Component source: `src/components/{core,inputs,forms,layout,marketing}/`
|
|
398
|
+
- Hooks: `src/hooks/`
|
|
399
|
+
- Story title format: `"Category/ComponentName"` or `"Category/ComponentName/Guide"`
|
|
400
|
+
- All MDX guides use HTML tables (not GFM) with exported `th/td/td0/tbl` style constants
|
|
401
|
+
- Commit convention: `fix:` (patch), `feat:` (minor), `docs:` (no bump); semantic-release runs in CI
|
|
402
|
+
- Never bump `version` in `package.json` manually
|
package/README.md
CHANGED
|
@@ -137,53 +137,93 @@ The standard **gray / slate / zinc** ramps and **black** stay available alongsid
|
|
|
137
137
|
|
|
138
138
|
## AI toolchain (Claude Code)
|
|
139
139
|
|
|
140
|
-
OxygenUI ships a built-in MCP server and a set of Claude Code skills and commands that let AI assistants look up component APIs, search by use-case,
|
|
140
|
+
OxygenUI ships a built-in MCP server and a set of Claude Code skills and commands that let AI assistants look up component APIs, search by use-case, generate complete page components, and bootstrap entire apps, all grounded in the real MDX documentation.
|
|
141
|
+
|
|
142
|
+
### Setup
|
|
143
|
+
|
|
144
|
+
The toolchain has two parts: the **MCP server** (does the work) and the **commands + skill** (how you call it from Claude Code). Both can be installed at user scope (available in every project on your machine) or project scope (committed to the repo, shared with the whole team).
|
|
145
|
+
|
|
146
|
+
#### Option A — User scope (recommended for individuals)
|
|
147
|
+
|
|
148
|
+
Installs everything into `~/.claude/`. Available in every Claude Code session from that point on.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 1. Install commands + skill + MCP server config in one shot
|
|
152
|
+
npx --package=@geomak/ui@latest setup-claude
|
|
153
|
+
|
|
154
|
+
# If you already have @geomak/ui installed locally:
|
|
155
|
+
node node_modules/@geomak/ui/scripts/setup-claude.mjs
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Option B — Project scope (recommended for teams)
|
|
159
|
+
|
|
160
|
+
Commits the toolchain into the repo so every developer who clones it gets it automatically.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# 1. Install commands + skill + .mcp.json into the project directory
|
|
164
|
+
npx --package=@geomak/ui@latest setup-claude --scope project
|
|
165
|
+
|
|
166
|
+
# If you already have @geomak/ui installed locally:
|
|
167
|
+
node node_modules/@geomak/ui/scripts/setup-claude.mjs --scope project
|
|
168
|
+
|
|
169
|
+
# 2. Commit the generated files so your team gets them on clone
|
|
170
|
+
git add .claude/ .mcp.json
|
|
171
|
+
git commit -m "chore: add OxygenUI Claude Code toolchain"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Both options write a `.mcp.json` / `~/.claude/mcp.json` entry pointing to `https://oxygenui.com/mcp` alongside the commands and skill, so no manual config is needed. Restart Claude Code (or reload the MCP server list) after running.
|
|
175
|
+
|
|
176
|
+
---
|
|
141
177
|
|
|
142
178
|
### MCP server
|
|
143
179
|
|
|
144
|
-
The server is deployed alongside Storybook
|
|
180
|
+
The server is deployed alongside Storybook at `https://oxygenui.com/mcp` (Netlify Function). It exposes eight tools in two tiers.
|
|
181
|
+
|
|
182
|
+
**Lookup tier** (docs + tokens):
|
|
145
183
|
|
|
146
184
|
| Tool | What it does |
|
|
147
185
|
|---|---|
|
|
148
186
|
| `list_components` | Browse all 100+ components with slug, category, and description |
|
|
149
|
-
| `get_component` |
|
|
187
|
+
| `get_component` | Full props API and usage examples for one component by slug |
|
|
150
188
|
| `find_component` | Keyword search across names, categories, and descriptions |
|
|
151
189
|
| `get_token` | Look up CSS custom properties by name or category prefix |
|
|
152
190
|
|
|
153
|
-
**
|
|
154
|
-
|
|
155
|
-
```jsonc
|
|
156
|
-
// .mcp.json (already in the repo, update the URL after first deploy)
|
|
157
|
-
{
|
|
158
|
-
"mcpServers": {
|
|
159
|
-
"oxygen-ui": {
|
|
160
|
-
"type": "http",
|
|
161
|
-
"url": "https://your-site.netlify.app/mcp"
|
|
162
|
-
},
|
|
163
|
-
"oxygen-ui-local": {
|
|
164
|
-
"type": "http",
|
|
165
|
-
"url": "http://localhost:8888/mcp"
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
```
|
|
191
|
+
**Generation tier** (patterns + code):
|
|
170
192
|
|
|
171
|
-
|
|
193
|
+
| Tool | What it does |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `get_pattern` | Full wired recipe for a page type: components, state, wiring steps, and a complete code example. Patterns: `crud-list`, `dashboard`, `settings-tabs`, `multi-step-form`, `detail-page`, `app-bootstrap` |
|
|
196
|
+
| `get_form_binding` | Correct `useForm` binder for any input (`fieldNative` / `fieldChecked` / `fieldTarget` / `field`), with paste-ready snippet |
|
|
197
|
+
| `compare_components` | Decision guide for similar components: Modal vs Drawer, Table vs DataGrid, Tabs vs SegmentedControl |
|
|
198
|
+
| `get_app_bootstrap` | Full Bootstrap.tsx + AppRoutes.tsx + brandTokens.ts template for a new project |
|
|
172
199
|
|
|
173
200
|
### Claude Code commands
|
|
174
201
|
|
|
175
|
-
|
|
202
|
+
Eight slash commands are available after setup:
|
|
203
|
+
|
|
204
|
+
**Lookup commands:**
|
|
176
205
|
|
|
177
206
|
| Command | Usage |
|
|
178
207
|
|---|---|
|
|
179
|
-
| `/ui-lookup <name>` | Fetch the full docs for a component, e.g. `/ui-lookup wizard` |
|
|
180
208
|
| `/ui-find <query>` | Search by use-case, e.g. `/ui-find virtualized table` |
|
|
181
|
-
| `/ui-
|
|
182
|
-
|
|
209
|
+
| `/ui-lookup <name>` | Full docs for a component, e.g. `/ui-lookup wizard` |
|
|
210
|
+
|
|
211
|
+
**Generation commands** (write real files into your project):
|
|
212
|
+
|
|
213
|
+
| Command | Usage |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `/ui-page <type> [entity]` | Scaffold a complete page file. Types: `crud`, `dashboard`, `settings`, `detail`, `multi-step-form`. Example: `/ui-page crud Vessel` |
|
|
216
|
+
| `/ui-bootstrap [name]` | Generate the four-file app bootstrap (Bootstrap.tsx, AppRoutes.tsx, brandTokens.ts, main.tsx) |
|
|
217
|
+
| `/ui-form <field:type ...>` | Generate a fully-wired Form from a field spec, e.g. `/ui-form name:text role:dropdown active:switch` |
|
|
218
|
+
| `/ui-table <entity> [col:type]` | Typed Table with column renderers + actions, e.g. `/ui-table Vessel name:string flag:string status:badge actions:menu` |
|
|
219
|
+
| `/ui-modal <type>` | Complete state + Modal JSX. Types: `confirm-delete`, `form-in-modal`, `detail-view`, `alert` |
|
|
220
|
+
| `/ui-auth-shell [name] [routes]` | Full SecureLayout + AppShell + Wizard tour shell, e.g. `/ui-auth-shell VesOPS dashboard,vessels,settings` |
|
|
221
|
+
|
|
222
|
+
Two additional library commands (`/ui-scaffold`, `/ui-story`) are available inside the `oxygen-ui` repository itself for contributors.
|
|
183
223
|
|
|
184
224
|
### Context skill
|
|
185
225
|
|
|
186
|
-
The `/oxygen-ui` skill loads the full design system context into any Claude Code session
|
|
226
|
+
The `/oxygen-ui` skill (installed by the setup script) loads the full design system context into any Claude Code session: import patterns, form binder cheat sheet, component decision guide, provider nesting order, React Query integration, and page pattern reference. Invoke it at the start of any session where you plan to build against `@geomak/ui`.
|
|
187
227
|
|
|
188
228
|
### Local development with the MCP server
|
|
189
229
|
|
|
@@ -196,7 +236,7 @@ The function is then available at `http://localhost:8888/mcp`. Claude Code picks
|
|
|
196
236
|
|
|
197
237
|
### How the manifest is built
|
|
198
238
|
|
|
199
|
-
At build time, `scripts/generate-ai-manifest.mjs` scans all
|
|
239
|
+
At build time, `scripts/generate-ai-manifest.mjs` scans all MDX guide files in `src/docs/` and co-located MDX files in `src/components/`, strips Storybook boilerplate, and emits `netlify/_data/ai-manifest.js`. The Netlify Function bundles this via esbuild at deploy time. No database, no runtime file I/O, no cold-start penalty.
|
|
200
240
|
|
|
201
241
|
---
|
|
202
242
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geomak/ui",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.20.0",
|
|
4
4
|
"description": "Oxygen Design System, reusable UI primitives built with Radix UI behaviours and Tailwind CSS styling",
|
|
5
5
|
"author": "G-MAKROGLOU",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,8 +27,14 @@
|
|
|
27
27
|
"main": "./dist/index.cjs",
|
|
28
28
|
"module": "./dist/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
|
+
"bin": {
|
|
31
|
+
"setup-claude": "scripts/setup-claude.mjs"
|
|
32
|
+
},
|
|
30
33
|
"files": [
|
|
31
|
-
"dist"
|
|
34
|
+
"dist",
|
|
35
|
+
".claude/commands",
|
|
36
|
+
".claude/skills",
|
|
37
|
+
"scripts/setup-claude.mjs"
|
|
32
38
|
],
|
|
33
39
|
"sideEffects": [
|
|
34
40
|
"**/*.css"
|
|
@@ -40,6 +46,7 @@
|
|
|
40
46
|
"dev": "tsup --watch",
|
|
41
47
|
"storybook": "storybook dev -p 6006",
|
|
42
48
|
"build-storybook": "storybook build",
|
|
49
|
+
"setup-claude": "node scripts/setup-claude.mjs",
|
|
43
50
|
"generate-manifest": "node scripts/generate-ai-manifest.mjs",
|
|
44
51
|
"mcp:dev": "node scripts/generate-ai-manifest.mjs && netlify dev",
|
|
45
52
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Sets up the OxygenUI Claude Code integration.
|
|
4
|
+
* Copies commands + skill to the target scope, and writes the MCP server entry.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx --package=@geomak/ui@latest setup-claude # user scope (default)
|
|
8
|
+
* npx --package=@geomak/ui@latest setup-claude --scope project
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { join, dirname } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const PACKAGE_ROOT = join(__dirname, '..')
|
|
18
|
+
|
|
19
|
+
// ─── Args ─────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2)
|
|
22
|
+
const scopeIdx = args.indexOf('--scope')
|
|
23
|
+
const scope = scopeIdx !== -1 ? args[scopeIdx + 1] : 'user'
|
|
24
|
+
|
|
25
|
+
if (scope !== 'user' && scope !== 'project') {
|
|
26
|
+
console.error('Error: --scope must be "user" or "project"')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const CONSUMER_COMMANDS = [
|
|
33
|
+
'ui-find',
|
|
34
|
+
'ui-lookup',
|
|
35
|
+
'ui-page',
|
|
36
|
+
'ui-bootstrap',
|
|
37
|
+
'ui-form',
|
|
38
|
+
'ui-table',
|
|
39
|
+
'ui-modal',
|
|
40
|
+
'ui-auth-shell',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const targetBase = scope === 'user' ? homedir() : process.cwd()
|
|
44
|
+
const claudeDir = join(targetBase, '.claude')
|
|
45
|
+
const commandsDir = join(claudeDir, 'commands')
|
|
46
|
+
const skillDir = join(claudeDir, 'skills', 'oxygen-ui')
|
|
47
|
+
const mcpFile = scope === 'user'
|
|
48
|
+
? join(claudeDir, 'mcp.json')
|
|
49
|
+
: join(process.cwd(), '.mcp.json')
|
|
50
|
+
|
|
51
|
+
const srcCommandsDir = join(PACKAGE_ROOT, '.claude', 'commands')
|
|
52
|
+
const srcSkillFile = join(PACKAGE_ROOT, '.claude', 'skills', 'oxygen-ui', 'SKILL.consumer.md')
|
|
53
|
+
const srcSkillFallback = join(PACKAGE_ROOT, '.claude', 'skills', 'oxygen-ui', 'SKILL.md')
|
|
54
|
+
|
|
55
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function readJson(path) {
|
|
58
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')) } catch { return null }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeJson(path, data) {
|
|
62
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ok(msg) { console.log(` \x1b[32m+\x1b[0m ${msg}`) }
|
|
66
|
+
function warn(msg) { console.warn(` \x1b[33m!\x1b[0m ${msg}`) }
|
|
67
|
+
function info(msg) { console.log(` ${msg}`) }
|
|
68
|
+
|
|
69
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
console.log('\nOxygenUI Claude Code setup\n')
|
|
72
|
+
console.log(`Scope: ${scope === 'user' ? 'user (~/.claude)' : `project (${process.cwd()})`}\n`)
|
|
73
|
+
|
|
74
|
+
console.log('Commands:')
|
|
75
|
+
mkdirSync(commandsDir, { recursive: true })
|
|
76
|
+
|
|
77
|
+
for (const name of CONSUMER_COMMANDS) {
|
|
78
|
+
const src = join(srcCommandsDir, `${name}.md`)
|
|
79
|
+
const dest = join(commandsDir, `${name}.md`)
|
|
80
|
+
if (!existsSync(src)) {
|
|
81
|
+
warn(`source not found, skipping: ${name}.md`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
copyFileSync(src, dest)
|
|
85
|
+
ok(`/${name}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Skill ────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
console.log('\nSkill:')
|
|
91
|
+
mkdirSync(skillDir, { recursive: true })
|
|
92
|
+
|
|
93
|
+
const skillSrc = existsSync(srcSkillFile) ? srcSkillFile : srcSkillFallback
|
|
94
|
+
if (!existsSync(skillSrc)) {
|
|
95
|
+
warn('SKILL.md not found in package, skipping skill')
|
|
96
|
+
} else {
|
|
97
|
+
copyFileSync(skillSrc, join(skillDir, 'SKILL.md'))
|
|
98
|
+
ok('/oxygen-ui skill')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── MCP server ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
console.log('\nMCP server:')
|
|
104
|
+
mkdirSync(dirname(mcpFile), { recursive: true })
|
|
105
|
+
|
|
106
|
+
const existing = readJson(mcpFile) ?? {}
|
|
107
|
+
if (!existing.mcpServers) existing.mcpServers = {}
|
|
108
|
+
|
|
109
|
+
if (existing.mcpServers['oxygen-ui']) {
|
|
110
|
+
warn('oxygen-ui entry already exists in ' + mcpFile + ', updating URL')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
existing.mcpServers['oxygen-ui'] = {
|
|
114
|
+
type: 'http',
|
|
115
|
+
url: 'https://oxygenui.com/mcp',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
writeJson(mcpFile, existing)
|
|
119
|
+
ok(`oxygen-ui server -> ${mcpFile}`)
|
|
120
|
+
|
|
121
|
+
// ─── Done ─────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
console.log('\n\x1b[32mDone.\x1b[0m\n')
|
|
124
|
+
|
|
125
|
+
if (scope === 'user') {
|
|
126
|
+
info('The commands, skill, and MCP server are now available in every Claude Code session.')
|
|
127
|
+
info('Restart Claude Code (or reload the MCP server list) to pick up the new server.')
|
|
128
|
+
} else {
|
|
129
|
+
info('Commands and skill are in .claude/ and the MCP server is in .mcp.json.')
|
|
130
|
+
info('Commit both directories so every team member gets the tooling when they clone.')
|
|
131
|
+
info('')
|
|
132
|
+
info('Each developer still needs Claude Code installed:')
|
|
133
|
+
info(' npm install -g @anthropic-ai/claude-code')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log()
|