@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.
@@ -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, and scaffold new components, all grounded in the real MDX documentation.
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 on Netlify as a Netlify Function at `/mcp`. It exposes four tools:
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` | Fetch the full props API and usage examples for one component by slug |
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
- **Connect it:**
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
- Or add it from the CLI: `claude mcp add oxygen-ui --transport http https://your-site.netlify.app/mcp`
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
- Four slash commands are registered in `.claude/commands/`:
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-scaffold <name>` | Scaffold a new component with source + story + MDX guide |
182
- | `/ui-story <name>` | Add story coverage for an existing component |
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, import patterns, token usage, Tailwind utilities, form API, and repository conventions. Invoke it at the start of any session where you plan to build against `@geomak/ui`.
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 101 MDX guide files in `src/docs/` and the co-located MDX files in `src/components/`, strips Storybook boilerplate, and emits `netlify/functions/ai-manifest.json`. The Netlify Function bundles this JSON via esbuild, no database, no runtime file I/O, no cold-start penalty.
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.19.0",
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()