@jovertical/mise-design-system 1.0.0 → 1.1.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/README.md CHANGED
@@ -92,6 +92,26 @@ Import `src/mise-theme.css` in place of `theme.css`. `emitThemeCss(config)`
92
92
  is exported from `/tokens` too, if you would rather call it in your own build
93
93
  script.
94
94
 
95
+ ## AI agents
96
+
97
+ mise ships an agent skill, `skills/mise/`, that teaches a coding agent in your
98
+ app which mise component does which job, every component's props and
99
+ defaults, setup per stack, theming, and worked recipes. Install it with the
100
+ [`skills` CLI](https://github.com/vercel-labs/skills):
101
+
102
+ ```sh
103
+ npx skills add jovertical/mise-design-system --skill mise
104
+ ```
105
+
106
+ The same skill is in the package at
107
+ `node_modules/@jovertical/mise-design-system/skills/mise/`, matching the
108
+ installed version. `npx skills experimental_sync` links it from there, so it
109
+ updates with mise.
110
+
111
+ The skill reads the app's setup with `npx mise-theme info --json`: the
112
+ installed version, the stack, the theme config and the stylesheet that imports
113
+ mise.
114
+
95
115
  ## Develop
96
116
 
97
117
  ```sh
package/bin/mise-theme.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises'
3
+
2
4
  import { runThemeCli } from '../dist/tokens/cli.js'
3
5
 
4
- const { code, message } = await runThemeCli(process.argv.slice(2))
6
+ const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'))
7
+ const { code, message } = await runThemeCli(process.argv.slice(2), process.cwd(), version)
5
8
 
6
9
  if (code === 0) console.log(message)
7
10
  else console.error(message)
@@ -8,4 +8,4 @@ export interface CliResult {
8
8
  * `dist/styles/`, so the output matches the packaged stylesheet line for line,
9
9
  * apart from the theme.
10
10
  */
11
- export declare function runThemeCli(argv: string[], cwd?: string): Promise<CliResult>;
11
+ export declare function runThemeCli(argv: string[], cwd?: string, version?: string): Promise<CliResult>;
@@ -2,7 +2,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
  import { themeStylesheet } from './emit-web.js';
5
- const USAGE = 'Usage: mise-theme <config> -o <output.css>';
5
+ import { projectInfo } from './info.js';
6
+ const USAGE = 'Usage: mise-theme <config> -o <output.css>\n mise-theme info [--json]';
6
7
  /**
7
8
  * A path rather than a `new URL(…)`, because a global `URL` that is not
8
9
  * Node's (jsdom's, in a test) is rejected by `fs`.
@@ -14,7 +15,9 @@ const COMPONENTS_CSS = join(dirname(fileURLToPath(import.meta.url)), '..', 'styl
14
15
  * `dist/styles/`, so the output matches the packaged stylesheet line for line,
15
16
  * apart from the theme.
16
17
  */
17
- export async function runThemeCli(argv, cwd = process.cwd()) {
18
+ export async function runThemeCli(argv, cwd = process.cwd(), version = '0.0.0') {
19
+ if (argv[0] === 'info')
20
+ return runInfo(argv.includes('--json'), cwd, version);
18
21
  const [configPath, flag, outputPath] = argv;
19
22
  if (!configPath || flag !== '-o' || !outputPath)
20
23
  return { code: 1, message: USAGE };
@@ -35,3 +38,21 @@ export async function runThemeCli(argv, cwd = process.cwd()) {
35
38
  return { code: 1, message: error instanceof Error ? error.message : String(error) };
36
39
  }
37
40
  }
41
+ /**
42
+ * `mise-theme info`: the facts about the app in `cwd` that the consumer skill
43
+ * reads before writing UI. `version` is mise's own, passed in by the binary.
44
+ */
45
+ async function runInfo(json, cwd, version) {
46
+ try {
47
+ const info = await projectInfo(cwd, version);
48
+ const message = json
49
+ ? JSON.stringify(info, null, 2)
50
+ : Object.entries(info)
51
+ .map(([key, value]) => `${key}: ${value ?? '—'}`)
52
+ .join('\n');
53
+ return { code: 0, message };
54
+ }
55
+ catch (error) {
56
+ return { code: 1, message: error instanceof Error ? error.message : String(error) };
57
+ }
58
+ }
@@ -0,0 +1,10 @@
1
+ export type ProjectInfo = {
2
+ version: string;
3
+ stack: 'sveltekit' | 'inertia' | 'vite';
4
+ svelte: string | null;
5
+ tailwind: string | null;
6
+ themeConfig: string | null;
7
+ stylesheet: string | null;
8
+ form: boolean;
9
+ };
10
+ export declare function projectInfo(cwd: string, version: string): Promise<ProjectInfo>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What an agent needs to know about the app it is working in before it writes
3
+ * mise UI: which version, which stack, where the theme and the stylesheet are.
4
+ * Read-only. The consumer skill runs it on load through `mise-theme info --json`.
5
+ */
6
+ import { existsSync } from 'node:fs';
7
+ import { readdir, readFile } from 'node:fs/promises';
8
+ import { join, relative } from 'node:path';
9
+ const THEME_CONFIGS = ['mise.theme.js', 'mise.theme.mjs', 'mise.theme.ts'];
10
+ const SKIPPED = new Set([
11
+ 'node_modules',
12
+ '.git',
13
+ 'vendor',
14
+ 'dist',
15
+ 'build',
16
+ '.svelte-kit',
17
+ 'storage',
18
+ 'public',
19
+ ]);
20
+ const IMPORT = /@import\s+['"]@jovertical\/mise-design-system\//;
21
+ const MAX_DEPTH = 6;
22
+ async function findStylesheet(cwd, dir = cwd, depth = 0) {
23
+ if (depth > MAX_DEPTH)
24
+ return null;
25
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
26
+ for (const entry of entries) {
27
+ const path = join(dir, entry.name);
28
+ if (entry.isFile() &&
29
+ entry.name.endsWith('.css') &&
30
+ IMPORT.test(await readFile(path, 'utf8'))) {
31
+ return relative(cwd, path).split('\\').join('/');
32
+ }
33
+ }
34
+ for (const entry of entries) {
35
+ if (!entry.isDirectory() || SKIPPED.has(entry.name) || entry.name.startsWith('.'))
36
+ continue;
37
+ const found = await findStylesheet(cwd, join(dir, entry.name), depth + 1);
38
+ if (found)
39
+ return found;
40
+ }
41
+ return null;
42
+ }
43
+ export async function projectInfo(cwd, version) {
44
+ const manifestPath = join(cwd, 'package.json');
45
+ if (!existsSync(manifestPath))
46
+ throw new Error(`No package.json in ${cwd}`);
47
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
48
+ const dependencies = {
49
+ ...manifest.dependencies,
50
+ ...manifest.devDependencies,
51
+ };
52
+ const has = (name) => name in dependencies;
53
+ return {
54
+ version,
55
+ stack: has('@sveltejs/kit') ? 'sveltekit' : has('@inertiajs/svelte') ? 'inertia' : 'vite',
56
+ svelte: dependencies.svelte ?? null,
57
+ tailwind: dependencies.tailwindcss ?? null,
58
+ themeConfig: THEME_CONFIGS.find((name) => existsSync(join(cwd, name))) ?? null,
59
+ stylesheet: await findStylesheet(cwd),
60
+ form: has('formsnap') && has('sveltekit-superforms'),
61
+ };
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jovertical/mise-design-system",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Svelte 5 components and a configurable Tailwind v4 theme for operations dashboards",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,7 @@
31
31
  "dist",
32
32
  "bin",
33
33
  "LICENSES",
34
+ "skills",
34
35
  "!dist/**/*.stories.svelte",
35
36
  "!dist/**/*.stories.svelte.d.ts",
36
37
  "!dist/**/*.test.*"
@@ -113,6 +114,7 @@
113
114
  "scripts": {
114
115
  "build": "svelte-kit sync && svelte-package && pnpm build:theme && publint",
115
116
  "build:theme": "tsx scripts/build-theme.ts",
117
+ "build:skill": "tsx scripts/build-skill.ts",
116
118
  "storybook": "pnpm build:theme && storybook dev -p 6006",
117
119
  "build:storybook": "pnpm build:theme && storybook build",
118
120
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: mise
3
+ description: >-
4
+ Builds Svelte 5 UI in an app that uses @jovertical/mise-design-system: picking the mise
5
+ component for a job, its props and defaults, theming, and setting mise up. Activates when
6
+ writing or changing Svelte UI in an app that depends on @jovertical/mise-design-system, or
7
+ setting mise up in a new app; or when the user mentions mise, the design system, a mise
8
+ component, tone, variant, or mise-theme.
9
+ ---
10
+
11
+ # mise
12
+
13
+ mise is a library of Svelte 5 components and a Tailwind v4 theme for operations dashboards:
14
+ locations, shifts, orders, stock. This skill is for an app that depends on it. Before writing
15
+ any UI, find the mise component that does the job; write your own only when none does.
16
+
17
+ ## This app
18
+
19
+ ```json
20
+ !`npx --no-install mise-theme info --json`
21
+ ```
22
+
23
+ `stack` says how the app is wired (`sveltekit`, `inertia` or `vite`), `stylesheet` where mise's
24
+ theme is imported, `themeConfig` whether the app has its own theme, and `form` whether
25
+ `mise/form` can be used. If the command failed, mise is not installed here: follow
26
+ [setup.md](setup.md).
27
+
28
+ ## Which copy to trust
29
+
30
+ This skill may be newer or older than the mise the app has installed. When they disagree, read
31
+ `node_modules/@jovertical/mise-design-system/skills/mise/` instead: that copy ships with the
32
+ installed `version` above, and its `components/` files list exactly the props that version
33
+ takes.
34
+
35
+ ## Rules
36
+
37
+ - **Use a mise component before writing UI.** A badge, a card, a table, an empty state or a
38
+ confirmation dialog almost certainly exists; the index below says which.
39
+ - **Compose before customising.** A card of facts is `Card` with `FactList` in it, not a styled
40
+ `div`. A dashboard is `PageLayout`, `PageHeader`, `CardGrid` and `StatTile`s.
41
+ - **Props before classes.** Colour is `tone`: `neutral`, `primary`, `info`, `success`,
42
+ `warning` or `critical` (never `destructive`, `error` or `danger`). Treatment is `variant`.
43
+ Size is `size`: `sm`, `md` (the default) or `lg`. States are bare adjectives: `loading`,
44
+ `invalid`, `disabled`.
45
+ - **Semantic colours only.** `bg-primary`, `text-muted-foreground`, `border-border`. Never a
46
+ palette class (`bg-blue-500`) or a `dark:` colour override: the theme handles dark mode.
47
+ - **`class` is for layout**: margin, width, grid placement. Do not restyle a component's
48
+ colours, radius or type through `class`; change the theme ([theming.md](theming.md)).
49
+ - **A neutral fill on the page** is `bg-foreground/[0.07]`, the same wash mise's neutral badges
50
+ use. `bg-muted` is the page canvas, so a `bg-muted` chip disappears on it.
51
+ - **The app formats, mise displays.** Money, dates and statuses are formatted by the app.
52
+ `NumberInput` with `format="currency"` takes and reports integer cents.
53
+ - **Every component** takes `class` and spreads other attributes onto its root, and every part
54
+ takes `bind:ref`. Overlays and inputs use `bind:open` and `bind:value`.
55
+ - **For the app's own components**, mise exports `cn` (class merging), the `Tone` and `Size`
56
+ types, `TONES`, and the tone class maps (`TONE_SOFT_CLASS`, `TONE_SOLID_CLASS`,
57
+ `TONE_TEXT_CLASS`, `TONE_FILL_CLASS` and the rest), so a custom badge-like element matches
58
+ mise's colours.
59
+
60
+ ## Which component
61
+
62
+ Each line is a job and the component that does it. The group files list every part, prop,
63
+ type and default.
64
+
65
+ **Actions** ([components/actions.md](components/actions.md))
66
+
67
+ - A button, or a link that looks like one (`href`): `Button`
68
+ - A menu of actions on a row or a page: `DropdownMenu`
69
+ - A searchable list of commands or a palette (`CommandDialog`): `Command`
70
+ - The light and dark switch: `ThemeToggle`, inside a `ThemeProvider`
71
+
72
+ **Forms** ([components/forms.md](components/forms.md))
73
+
74
+ - A label, description and error around any input: `Field` (with `Label`)
75
+ - Text: `Input`; several lines: `Textarea`
76
+ - An input with an icon, a prefix or a button inside it: `InputGroup`
77
+ - A number, a price or a count: `NumberInput`, never `Input type="number"`
78
+ - An amount and its unit (2.5 kg): `QuantityInput`
79
+ - One choice from a few: `RadioGroup`; when each choice needs a description or media:
80
+ `ChoiceRow`
81
+ - One choice from many: `Select`; searchable or several values: `Combobox`
82
+ - On or off: `Switch`; with a label and description in a row: `SwitchField`; a tick:
83
+ `Checkbox`
84
+ - A date: `DatePicker`; a calendar on the page: `Calendar`; a time: `TimePicker`
85
+ - A one-time code: `InputOTP`
86
+ - An image or file upload: `MediaDropzone`
87
+ - Segmented buttons: `ToggleGroup`; one pressed state: `Toggle`; joined buttons and inputs:
88
+ `ButtonGroup`
89
+ - A validated form in SvelteKit: `Form`, from `@jovertical/mise-design-system/form`. It needs
90
+ SvelteKit; in an Inertia or plain Vite app use `Field` with the app's own form state
91
+ ([recipes/form.md](recipes/form.md))
92
+
93
+ **Navigation** ([components/navigation.md](components/navigation.md))
94
+
95
+ - The app's side menu: `Sidebar`
96
+ - Where am I: `Breadcrumb`
97
+ - Sections of one page: `Tabs`
98
+ - The steps of a flow: `Stepper`
99
+
100
+ **Layout** ([components/layout.md](components/layout.md))
101
+
102
+ - The page frame: `PageLayout`, with `PageHeader` for the title, description and actions
103
+ - A surface: `Card`
104
+ - Cards in a grid: `CardGrid`; rows inside one card: `CardList`
105
+ - A collapsible card for a side column: `AsideCard`
106
+ - What one step produced, read-only: `SummaryCard`
107
+ - Show and hide a section: `Collapsible`
108
+ - A rule: `Separator`
109
+
110
+ **Overlays** ([components/overlays.md](components/overlays.md))
111
+
112
+ - "Are you sure?": `AlertDialog`
113
+ - A task in a modal: `Dialog`; in a side panel: `Sheet`; from the bottom on mobile: `Drawer`
114
+ - A small floating panel: `Popover`; a preview on hover: `HoverCard`
115
+ - A label on hover: `Tooltip`; a `?` that explains the thing beside it: `Hint`
116
+
117
+ **Feedback** ([components/feedback.md](components/feedback.md))
118
+
119
+ - A message in the page: `Alert`; a toast: `Toaster` once in the layout, then `toast(…)`
120
+ - Loading: `Spinner` inline, `Skeleton` for the shape of what is coming, `LoadingState` for a
121
+ whole panel
122
+ - Nothing here yet: `EmptyState`; to compose your own: `Empty` and its parts
123
+ - An error page (not found, forbidden, failed): `ErrorState`
124
+ - Progress: `Progress`; a route change: `NavigationProgress`
125
+ - A feature the user cannot use yet: `FeaturePreview` for a whole page, `LockedOverlay` over
126
+ one card
127
+
128
+ **Data display** ([components/data-display.md](components/data-display.md))
129
+
130
+ - A status or a tag: `Badge`; a change up or down: `DeltaBadge`
131
+ - A person's picture or initials: `Avatar`; a named thing with its mark and one line:
132
+ `EntityLabel`; a list row with media, text and actions: `Item`
133
+ - An icon in a tinted square: `IconChip`
134
+ - Label and value pairs: `FactList`
135
+ - The headline number of a dashboard: `StatTile`. One figure per tile, with its delta, unit and
136
+ sparkline
137
+ - One subject and several figures with a bar beneath: `MetricRow`
138
+ - One measure drawn as a bar, in a ranked list: `MetricBar`; sub-groups to compare side by
139
+ side: `MetricGrid`
140
+ - A share of a whole as a ring: `RadialStat`; as one segmented bar: `SegmentedBar`
141
+ - A trend in a cell or a tile: `Sparkline`; a full chart: `ChartContainer` (LayerChart)
142
+ - Rows of data: `Table`; with sorting, filtering, selection or pagination: `DataTable`
143
+ - A history of events: `Timeline`
144
+ - Setup steps: `Checklist`, and `ChecklistCard` for the summary card; what a page still needs
145
+ before going live: `ReadinessChecklist`
146
+ - A code read off one screen and typed into another: `SegmentedCode`
147
+ - A row in a nested list: `TreeLeaf`
148
+
149
+ **Typography** ([components/typography.md](components/typography.md))
150
+
151
+ - A heading: `Heading`; body text with size, muting and tone: `Text`
152
+
153
+ ## Read next
154
+
155
+ - [setup.md](setup.md): install mise and wire it into SvelteKit, Laravel with Inertia, or Vite.
156
+ - [theming.md](theming.md): presets, the brand colour, radius, fonts and dark mode.
157
+ - [recipes/dashboard-page.md](recipes/dashboard-page.md): a dashboard page.
158
+ - [recipes/form.md](recipes/form.md): a form, in Inertia and in SvelteKit.
159
+ - [recipes/data-table.md](recipes/data-table.md): a data table with filters and row actions.
@@ -0,0 +1,200 @@
1
+ <!-- Generated by pnpm build:skill. Do not edit by hand. -->
2
+
3
+ # Actions
4
+
5
+ ## Button
6
+
7
+ `import { Button, buttonVariants, defaultButtonTone } from '@jovertical/mise-design-system'`
8
+
9
+ | Prop | Type | Default | Description |
10
+ | --- | --- | --- | --- |
11
+ | `variant` | `'solid' \| 'soft' \| 'outline' \| 'ghost' \| 'link'` | `'solid'` | |
12
+ | `tone` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'critical'` | | Defaults to `primary` for `solid` and `link`, `neutral` for the rest. |
13
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | |
14
+ | `iconOnly` | `boolean` | `false` | One glyph and no label: square, round, and it needs an `aria-label`. |
15
+ | `children` | `Snippet` | | |
16
+
17
+ Also takes: every `<button>` attribute; every `<a>` attribute.
18
+
19
+ Also exports: `buttonVariants`, `defaultButtonTone`.
20
+
21
+ Types: `ButtonSize` (`'sm' | 'md' | 'lg'`), `ButtonVariant` (`'solid' | 'soft' | 'outline' | 'ghost' | 'link'`).
22
+
23
+ ## Command
24
+
25
+ `import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from '@jovertical/mise-design-system'`
26
+
27
+ A filterable, keyboard-navigable list, grouped by section: the shape a command palette is built on. Bare for an inline picker; `CommandDialog` puts it in a dialog.
28
+
29
+ Also takes: bits-ui `Command.Root` props.
30
+
31
+ ### `CommandDialog`
32
+
33
+ `Command` in a `Dialog`, for a palette that takes over the screen. The title and description are for a screen reader only.
34
+
35
+ | Prop | Type | Default | Description |
36
+ | --- | --- | --- | --- |
37
+ | `title` | `string` | `'Command palette'` | |
38
+ | `description` | `string` | `'Search for a command to run.'` | |
39
+ | `showCloseButton` | `boolean` | `true` | |
40
+ | `class` | `string` | | |
41
+ | `children` (required) | `Snippet` | | |
42
+
43
+ Also takes: bits-ui `Dialog.Root` props except `children`, `child`; bits-ui `Command.Root` props except `children`, `child`.
44
+
45
+ ### `CommandEmpty`
46
+
47
+ Shown when nothing matches what was typed.
48
+
49
+ Also takes: bits-ui `Command.Empty` props.
50
+
51
+ ### `CommandGroup`
52
+
53
+ A section of items, under a small muted heading.
54
+
55
+ | Prop | Type | Default | Description |
56
+ | --- | --- | --- | --- |
57
+ | `heading` | `string` | | |
58
+
59
+ Also takes: bits-ui `Command.Group` props.
60
+
61
+ ### `CommandInput`
62
+
63
+ The search field, with its glyph, ruled off from the list below.
64
+
65
+ Also takes: bits-ui `Command.Input` props.
66
+
67
+ ### `CommandItem`
68
+
69
+ One command. The highlighted row takes the muted wash, as a menu row does.
70
+
71
+ Also takes: bits-ui `Command.Item` props.
72
+
73
+ ### `CommandList`
74
+
75
+ The scrolling list of groups and items.
76
+
77
+ Also takes: bits-ui `Command.List` props.
78
+
79
+ ### `CommandSeparator`
80
+
81
+ A rule between groups.
82
+
83
+ Also takes: bits-ui `Command.Separator` props.
84
+
85
+ ### `CommandShortcut`
86
+
87
+ A keyboard shortcut, muted at the end of the item.
88
+
89
+ Also takes: every `<span>` attribute.
90
+
91
+ ## DropdownMenu
92
+
93
+ `import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '@jovertical/mise-design-system'`
94
+
95
+ Also takes: bits-ui `DropdownMenu.Root` props.
96
+
97
+ ### `DropdownMenuCheckboxItem`
98
+
99
+ | Prop | Type | Default | Description |
100
+ | --- | --- | --- | --- |
101
+ | `children` | `Snippet` | | |
102
+
103
+ Also takes: bits-ui `DropdownMenu.CheckboxItem` props except `children`, `child`.
104
+
105
+ ### `DropdownMenuContent`
106
+
107
+ `min-w-44` (176px) by default, the width menus kept asking for.
108
+
109
+ | Prop | Type | Default | Description |
110
+ | --- | --- | --- | --- |
111
+ | `portalProps` | `DropdownMenuPrimitive.PortalProps` | | |
112
+
113
+ Also takes: bits-ui `DropdownMenu.Content` props.
114
+
115
+ ### `DropdownMenuGroup`
116
+
117
+ Also takes: bits-ui `DropdownMenu.Group` props.
118
+
119
+ ### `DropdownMenuItem`
120
+
121
+ | Prop | Type | Default | Description |
122
+ | --- | --- | --- | --- |
123
+ | `inset` | `boolean` | | |
124
+ | `variant` | `'default' \| 'destructive'` | `'default'` | `destructive` for an action that removes or ends something. |
125
+
126
+ Also takes: bits-ui `DropdownMenu.Item` props.
127
+
128
+ ### `DropdownMenuLabel`
129
+
130
+ | Prop | Type | Default | Description |
131
+ | --- | --- | --- | --- |
132
+ | `inset` | `boolean` | | |
133
+
134
+ Also takes: every `<div>` attribute.
135
+
136
+ ### `DropdownMenuPortal`
137
+
138
+ Also takes: bits-ui `DropdownMenu.Portal` props.
139
+
140
+ ### `DropdownMenuRadioGroup`
141
+
142
+ Also takes: bits-ui `DropdownMenu.RadioGroup` props.
143
+
144
+ ### `DropdownMenuRadioItem`
145
+
146
+ | Prop | Type | Default | Description |
147
+ | --- | --- | --- | --- |
148
+ | `children` | `Snippet` | | |
149
+
150
+ Also takes: bits-ui `DropdownMenu.RadioItem` props except `children`, `child`.
151
+
152
+ ### `DropdownMenuSeparator`
153
+
154
+ Also takes: bits-ui `DropdownMenu.Separator` props.
155
+
156
+ ### `DropdownMenuShortcut`
157
+
158
+ Also takes: every `<span>` attribute.
159
+
160
+ ### `DropdownMenuSub`
161
+
162
+ Also takes: bits-ui `DropdownMenu.Sub` props.
163
+
164
+ ### `DropdownMenuSubContent`
165
+
166
+ Also takes: bits-ui `DropdownMenu.SubContent` props.
167
+
168
+ ### `DropdownMenuSubTrigger`
169
+
170
+ | Prop | Type | Default | Description |
171
+ | --- | --- | --- | --- |
172
+ | `inset` | `boolean` | | |
173
+
174
+ Also takes: bits-ui `DropdownMenu.SubTrigger` props.
175
+
176
+ ### `DropdownMenuTrigger`
177
+
178
+ Also takes: bits-ui `DropdownMenu.Trigger` props.
179
+
180
+ ## ThemeToggle
181
+
182
+ `import { ThemeProvider, ThemeToggle, ThemeState, useTheme } from '@jovertical/mise-design-system'`
183
+
184
+ Icon-only toggle between light and dark, inside a `ThemeProvider`.
185
+
186
+ Also takes: everything `Button` takes except `children`, `onclick`.
187
+
188
+ ### `ThemeProvider`
189
+
190
+ Applies a `light` / `dark` class to `<html>` and persists the choice. The class on the root is what Tailwind's `dark:` variant reads, so this touches the DOM directly rather than passing a value down alone.
191
+
192
+ | Prop | Type | Default | Description |
193
+ | --- | --- | --- | --- |
194
+ | `defaultTheme` | `'dark' \| 'light' \| 'system'` | `'system'` | |
195
+ | `storageKey` | `string` | `'vite-ui-theme'` | localStorage key. Distinct per app, so two apps on one host do not share a theme. |
196
+ | `children` (required) | `Snippet` | | |
197
+
198
+ Also exports: `ThemeState`, `useTheme`.
199
+
200
+ Types: `Theme` (`'dark' | 'light' | 'system'`).