@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 +20 -0
- package/bin/mise-theme.js +4 -1
- package/dist/tokens/cli.d.ts +1 -1
- package/dist/tokens/cli.js +23 -2
- package/dist/tokens/info.d.ts +10 -0
- package/dist/tokens/info.js +62 -0
- package/package.json +3 -1
- package/skills/mise/SKILL.md +159 -0
- package/skills/mise/components/actions.md +200 -0
- package/skills/mise/components/data-display.md +728 -0
- package/skills/mise/components/feedback.md +198 -0
- package/skills/mise/components/forms.md +712 -0
- package/skills/mise/components/layout.md +178 -0
- package/skills/mise/components/navigation.md +250 -0
- package/skills/mise/components/overlays.md +291 -0
- package/skills/mise/components/typography.md +36 -0
- package/skills/mise/recipes/dashboard-page.md +40 -0
- package/skills/mise/recipes/dashboard-page.svelte +132 -0
- package/skills/mise/recipes/data-table.md +39 -0
- package/skills/mise/recipes/data-table.svelte +130 -0
- package/skills/mise/recipes/form-sveltekit.svelte +79 -0
- package/skills/mise/recipes/form.md +82 -0
- package/skills/mise/recipes/form.svelte +158 -0
- package/skills/mise/setup.md +99 -0
- package/skills/mise/theming.md +82 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The same "new stock item" form in a SvelteKit app, through `mise/form`:
|
|
3
|
+
superforms validates on the client and in the form action, and the Form
|
|
4
|
+
parts link each label, description and error to its control. This is a
|
|
5
|
+
page component (+page.svelte); its load and action are in form.md.
|
|
6
|
+
|
|
7
|
+
SvelteKit only: superforms imports `$app/*`. In an Inertia or plain Vite app
|
|
8
|
+
use form.svelte instead.
|
|
9
|
+
-->
|
|
10
|
+
<script lang="ts" module>
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
|
|
13
|
+
/** Shared with +page.server.ts, which validates the same schema on submit. */
|
|
14
|
+
export const stockItemSchema = z.object({
|
|
15
|
+
name: z.string().min(2, 'Name the item.'),
|
|
16
|
+
category: z.enum(['dry', 'dairy', 'produce'], { message: 'Choose a category.' }),
|
|
17
|
+
/** Integer cents. */
|
|
18
|
+
unitCost: z.number().int().min(0).nullable(),
|
|
19
|
+
tracksExpiry: z.boolean().default(false),
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import { Input, NumberInput, SwitchField } from '@jovertical/mise-design-system'
|
|
25
|
+
import {
|
|
26
|
+
Form,
|
|
27
|
+
FormButton,
|
|
28
|
+
FormControl,
|
|
29
|
+
FormDescription,
|
|
30
|
+
FormField,
|
|
31
|
+
FormFieldErrors,
|
|
32
|
+
FormLabel,
|
|
33
|
+
} from '@jovertical/mise-design-system/form'
|
|
34
|
+
import { untrack } from 'svelte'
|
|
35
|
+
import { type Infer, type SuperValidated, superForm } from 'sveltekit-superforms'
|
|
36
|
+
import { zod4 } from 'sveltekit-superforms/adapters'
|
|
37
|
+
|
|
38
|
+
let { data }: { data: { form: SuperValidated<Infer<typeof stockItemSchema>> } } = $props()
|
|
39
|
+
|
|
40
|
+
// Created once for the page; superforms keeps it in step with later loads.
|
|
41
|
+
// `json` posts the bound values as they are, so the switch needs no input
|
|
42
|
+
// name and the cents stay a number.
|
|
43
|
+
const form = superForm(
|
|
44
|
+
untrack(() => data.form),
|
|
45
|
+
{ validators: zod4(stockItemSchema), dataType: 'json' },
|
|
46
|
+
)
|
|
47
|
+
const { form: values } = form
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<Form {form} method="POST" class="flex max-w-xl flex-col gap-6">
|
|
51
|
+
<FormField {form} name="name">
|
|
52
|
+
<FormControl>
|
|
53
|
+
{#snippet children({ props })}
|
|
54
|
+
<FormLabel required>Name</FormLabel>
|
|
55
|
+
<Input {...props} bind:value={$values.name} placeholder="All-purpose flour, 25 kg" />
|
|
56
|
+
{/snippet}
|
|
57
|
+
</FormControl>
|
|
58
|
+
<FormFieldErrors />
|
|
59
|
+
</FormField>
|
|
60
|
+
|
|
61
|
+
<FormField {form} name="unitCost">
|
|
62
|
+
<FormControl>
|
|
63
|
+
{#snippet children({ props })}
|
|
64
|
+
<FormLabel>Unit cost</FormLabel>
|
|
65
|
+
<NumberInput {...props} format="currency" prefix="$" bind:value={$values.unitCost} />
|
|
66
|
+
{/snippet}
|
|
67
|
+
</FormControl>
|
|
68
|
+
<FormDescription>In dollars and cents; stored as cents.</FormDescription>
|
|
69
|
+
<FormFieldErrors />
|
|
70
|
+
</FormField>
|
|
71
|
+
|
|
72
|
+
<SwitchField
|
|
73
|
+
id="tracks-expiry"
|
|
74
|
+
label="Track expiry dates"
|
|
75
|
+
bind:checked={$values.tracksExpiry}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<FormButton class="self-start">Save item</FormButton>
|
|
79
|
+
</Form>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Recipe: a form
|
|
2
|
+
|
|
3
|
+
A "new stock item" form, in two versions: [form.svelte](form.svelte) for Inertia or plain Svelte,
|
|
4
|
+
and [form-sveltekit.svelte](form-sveltekit.svelte) for SvelteKit through `mise/form`. Both
|
|
5
|
+
type-check against the current mise.
|
|
6
|
+
|
|
7
|
+
## What it is made of
|
|
8
|
+
|
|
9
|
+
- `Card` ([layout](../components/layout.md)) as the surface, `FieldGroup` for the spacing.
|
|
10
|
+
- `Field`, `FieldLabel`, `FieldDescription` and `FieldError` ([forms](../components/forms.md))
|
|
11
|
+
around each control: `Input`, `Select`, `QuantityInput`, `NumberInput`.
|
|
12
|
+
- `SwitchField` for an on/off setting with a label and a description in one row.
|
|
13
|
+
- `Button` with a `Spinner` while the form submits.
|
|
14
|
+
|
|
15
|
+
## Inertia (form.svelte)
|
|
16
|
+
|
|
17
|
+
mise does not validate; the app does, and mise shows the result. The component takes the item,
|
|
18
|
+
the errors and the processing flag as props, so with Inertia it is driven by `useForm`:
|
|
19
|
+
|
|
20
|
+
```svelte
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import { useForm } from '@inertiajs/svelte'
|
|
23
|
+
import StockItemForm from './StockItemForm.svelte'
|
|
24
|
+
|
|
25
|
+
const form = useForm({
|
|
26
|
+
name: '',
|
|
27
|
+
category: '',
|
|
28
|
+
unit: 'kg',
|
|
29
|
+
openingStock: null,
|
|
30
|
+
unitCost: null,
|
|
31
|
+
tracksExpiry: false,
|
|
32
|
+
})
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<StockItemForm
|
|
36
|
+
bind:item={$form}
|
|
37
|
+
errors={$form.errors}
|
|
38
|
+
processing={$form.processing}
|
|
39
|
+
onsubmit={() => $form.post('/stock-items')}
|
|
40
|
+
/>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Laravel's validation errors arrive keyed by field name; each `Field` sets `data-invalid` and its
|
|
44
|
+
control `aria-invalid` when its key is present, and `FieldError` shows the message.
|
|
45
|
+
|
|
46
|
+
## SvelteKit (form-sveltekit.svelte)
|
|
47
|
+
|
|
48
|
+
`Form`, `FormField`, `FormControl`, `FormLabel`, `FormDescription`, `FormFieldErrors` and
|
|
49
|
+
`FormButton` come from `@jovertical/mise-design-system/form`. superforms validates in the browser
|
|
50
|
+
with the same schema the server action uses. The page's server file:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// +page.server.ts
|
|
54
|
+
import { fail } from '@sveltejs/kit'
|
|
55
|
+
import { superValidate } from 'sveltekit-superforms'
|
|
56
|
+
import { zod4 } from 'sveltekit-superforms/adapters'
|
|
57
|
+
import { stockItemSchema } from './+page.svelte'
|
|
58
|
+
|
|
59
|
+
export const load = async () => ({ form: await superValidate(zod4(stockItemSchema)) })
|
|
60
|
+
|
|
61
|
+
export const actions = {
|
|
62
|
+
default: async ({ request }) => {
|
|
63
|
+
const form = await superValidate(request, zod4(stockItemSchema))
|
|
64
|
+
if (!form.valid) return fail(400, { form })
|
|
65
|
+
// save form.data
|
|
66
|
+
return { form }
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In a real app, move `stockItemSchema` to its own module (`$lib/schemas/stock-item.ts`) and import
|
|
72
|
+
it from both files.
|
|
73
|
+
|
|
74
|
+
## Decisions worth copying
|
|
75
|
+
|
|
76
|
+
- **Money is integer cents.** `NumberInput format="currency"` shows `$12.50` and binds `1250`, so
|
|
77
|
+
no price is ever a float.
|
|
78
|
+
- **An amount and its unit are one control.** `QuantityInput` makes "2.5" without "kg"
|
|
79
|
+
impossible to submit.
|
|
80
|
+
- **`Select` gets `items`**, so its trigger shows the chosen label while the list is closed.
|
|
81
|
+
- **A required field says so** with `FieldLabel required`: an asterisk for the eye and
|
|
82
|
+
"(required)" for a screen reader.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A "new stock item" form for an Inertia or plain Svelte app. The form state
|
|
3
|
+
is the app's: with Inertia it is `useForm`, and `errors` and `processing`
|
|
4
|
+
come from it (see form.md). mise draws the fields, the labels, the errors
|
|
5
|
+
and the invalid state; it does not validate.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardFooter,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
Field,
|
|
16
|
+
FieldDescription,
|
|
17
|
+
FieldError,
|
|
18
|
+
FieldGroup,
|
|
19
|
+
FieldLabel,
|
|
20
|
+
Input,
|
|
21
|
+
NumberInput,
|
|
22
|
+
QuantityInput,
|
|
23
|
+
type QuantityUnit,
|
|
24
|
+
Select,
|
|
25
|
+
SelectContent,
|
|
26
|
+
SelectItem,
|
|
27
|
+
SelectTrigger,
|
|
28
|
+
SelectValue,
|
|
29
|
+
Spinner,
|
|
30
|
+
SwitchField,
|
|
31
|
+
} from '@jovertical/mise-design-system'
|
|
32
|
+
|
|
33
|
+
type StockItem = {
|
|
34
|
+
name: string
|
|
35
|
+
category: string
|
|
36
|
+
unit: string
|
|
37
|
+
openingStock: number | null
|
|
38
|
+
/** Integer cents: NumberInput's currency format speaks minor units. */
|
|
39
|
+
unitCost: number | null
|
|
40
|
+
tracksExpiry: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
item = $bindable({
|
|
45
|
+
name: '',
|
|
46
|
+
category: '',
|
|
47
|
+
unit: 'kg',
|
|
48
|
+
openingStock: null,
|
|
49
|
+
unitCost: null,
|
|
50
|
+
tracksExpiry: false,
|
|
51
|
+
}),
|
|
52
|
+
errors = {},
|
|
53
|
+
processing = false,
|
|
54
|
+
onsubmit,
|
|
55
|
+
}: {
|
|
56
|
+
item?: StockItem
|
|
57
|
+
/** Field name to message, as Laravel's validation returns them. */
|
|
58
|
+
errors?: Partial<Record<keyof StockItem, string>>
|
|
59
|
+
processing?: boolean
|
|
60
|
+
onsubmit: () => void
|
|
61
|
+
} = $props()
|
|
62
|
+
|
|
63
|
+
const categories = [
|
|
64
|
+
{ value: 'dry', label: 'Dry goods' },
|
|
65
|
+
{ value: 'dairy', label: 'Dairy' },
|
|
66
|
+
{ value: 'produce', label: 'Produce' },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const units: QuantityUnit[] = [
|
|
70
|
+
{ value: 'kg', label: 'Kilogram (kg)', short: 'kg', group: 'Mass' },
|
|
71
|
+
{ value: 'g', label: 'Gram (g)', short: 'g', group: 'Mass' },
|
|
72
|
+
{ value: 'l', label: 'Litre (l)', short: 'l', group: 'Volume' },
|
|
73
|
+
{ value: 'pc', label: 'Piece', short: 'pc', group: 'Count' },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
const errorFor = (field: keyof StockItem) => (errors[field] ? [{ message: errors[field] }] : [])
|
|
77
|
+
|
|
78
|
+
function submit(event: SubmitEvent) {
|
|
79
|
+
event.preventDefault()
|
|
80
|
+
onsubmit()
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<form onsubmit={submit}>
|
|
85
|
+
<Card class="max-w-xl">
|
|
86
|
+
<CardHeader>
|
|
87
|
+
<CardTitle>New stock item</CardTitle>
|
|
88
|
+
</CardHeader>
|
|
89
|
+
|
|
90
|
+
<CardContent>
|
|
91
|
+
<FieldGroup>
|
|
92
|
+
<Field data-invalid={errors.name ? 'true' : undefined}>
|
|
93
|
+
<FieldLabel for="item-name" required>Name</FieldLabel>
|
|
94
|
+
<Input
|
|
95
|
+
id="item-name"
|
|
96
|
+
bind:value={item.name}
|
|
97
|
+
placeholder="All-purpose flour, 25 kg"
|
|
98
|
+
aria-invalid={errors.name ? 'true' : undefined}
|
|
99
|
+
/>
|
|
100
|
+
<FieldError errors={errorFor('name')} />
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
<Field data-invalid={errors.category ? 'true' : undefined}>
|
|
104
|
+
<FieldLabel for="item-category" required>Category</FieldLabel>
|
|
105
|
+
<Select type="single" bind:value={item.category} items={categories}>
|
|
106
|
+
<SelectTrigger id="item-category" aria-invalid={errors.category ? 'true' : undefined}>
|
|
107
|
+
<SelectValue placeholder="Choose a category" />
|
|
108
|
+
</SelectTrigger>
|
|
109
|
+
<SelectContent>
|
|
110
|
+
{#each categories as category (category.value)}
|
|
111
|
+
<SelectItem value={category.value} label={category.label} />
|
|
112
|
+
{/each}
|
|
113
|
+
</SelectContent>
|
|
114
|
+
</Select>
|
|
115
|
+
<FieldError errors={errorFor('category')} />
|
|
116
|
+
</Field>
|
|
117
|
+
|
|
118
|
+
<Field data-invalid={errors.openingStock ? 'true' : undefined}>
|
|
119
|
+
<FieldLabel>Opening stock</FieldLabel>
|
|
120
|
+
<QuantityInput
|
|
121
|
+
{units}
|
|
122
|
+
bind:unit={item.unit}
|
|
123
|
+
bind:quantity={item.openingStock}
|
|
124
|
+
quantityPlaceholder="0"
|
|
125
|
+
/>
|
|
126
|
+
<FieldDescription>What is on the shelf today, counted.</FieldDescription>
|
|
127
|
+
<FieldError errors={errorFor('openingStock')} />
|
|
128
|
+
</Field>
|
|
129
|
+
|
|
130
|
+
<Field data-invalid={errors.unitCost ? 'true' : undefined}>
|
|
131
|
+
<FieldLabel for="item-cost">Unit cost</FieldLabel>
|
|
132
|
+
<NumberInput
|
|
133
|
+
id="item-cost"
|
|
134
|
+
format="currency"
|
|
135
|
+
prefix="$"
|
|
136
|
+
bind:value={item.unitCost}
|
|
137
|
+
aria-invalid={errors.unitCost ? 'true' : undefined}
|
|
138
|
+
/>
|
|
139
|
+
<FieldError errors={errorFor('unitCost')} />
|
|
140
|
+
</Field>
|
|
141
|
+
|
|
142
|
+
<SwitchField
|
|
143
|
+
id="item-expiry"
|
|
144
|
+
label="Track expiry dates"
|
|
145
|
+
description="Ask for a use-by date when this item is received."
|
|
146
|
+
bind:checked={item.tracksExpiry}
|
|
147
|
+
/>
|
|
148
|
+
</FieldGroup>
|
|
149
|
+
</CardContent>
|
|
150
|
+
|
|
151
|
+
<CardFooter class="justify-end">
|
|
152
|
+
<Button type="submit" disabled={processing}>
|
|
153
|
+
{#if processing}<Spinner />{/if}
|
|
154
|
+
Save item
|
|
155
|
+
</Button>
|
|
156
|
+
</CardFooter>
|
|
157
|
+
</Card>
|
|
158
|
+
</form>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Setting mise up
|
|
2
|
+
|
|
3
|
+
mise is a Svelte 5 library styled by Tailwind CSS v4. Setting it up is an install and four lines
|
|
4
|
+
of CSS. No app-wide provider is required: components are imported where they are used, a
|
|
5
|
+
`Tooltip` brings its own provider, and the `Sidebar` family has its own `SidebarProvider` as
|
|
6
|
+
part of the shell. `ThemeProvider` is optional (see Dark mode).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @jovertical/mise-design-system
|
|
12
|
+
pnpm add -D tailwindcss tw-animate-css
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With npm: `npm install @jovertical/mise-design-system` and
|
|
16
|
+
`npm install -D tailwindcss tw-animate-css`. `svelte`, `tailwindcss` and `tw-animate-css` are
|
|
17
|
+
peer dependencies; the app's own stylesheet imports all three, so they resolve from its install.
|
|
18
|
+
|
|
19
|
+
Tailwind v4 runs through its Vite plugin, `@tailwindcss/vite`, in the app's Vite config.
|
|
20
|
+
|
|
21
|
+
## The stylesheet
|
|
22
|
+
|
|
23
|
+
In the app's main stylesheet:
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
@import 'tailwindcss';
|
|
27
|
+
@import 'tw-animate-css';
|
|
28
|
+
@import '@jovertical/mise-design-system/theme.css';
|
|
29
|
+
@source '<relative path>/node_modules/@jovertical/mise-design-system/dist';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `@source` line is required. Tailwind v4 skips `node_modules` when it looks for class names,
|
|
33
|
+
so without it every mise component renders unstyled. The path is relative to the stylesheet;
|
|
34
|
+
the sections below give it for each stack.
|
|
35
|
+
|
|
36
|
+
To use a generated theme ([theming.md](theming.md)), import that file instead of `theme.css`.
|
|
37
|
+
|
|
38
|
+
## SvelteKit
|
|
39
|
+
|
|
40
|
+
- The stylesheet is `src/app.css`, imported once from `src/routes/+layout.svelte`:
|
|
41
|
+
`import '../app.css'`.
|
|
42
|
+
- The `@source` path is `'../node_modules/@jovertical/mise-design-system/dist'`.
|
|
43
|
+
- `Form` is available from `@jovertical/mise-design-system/form` once its optional peers are
|
|
44
|
+
installed: `pnpm add formsnap sveltekit-superforms` (and a validator such as `zod`).
|
|
45
|
+
|
|
46
|
+
## Laravel with Inertia and Svelte
|
|
47
|
+
|
|
48
|
+
- The stylesheet is `resources/css/app.css`, which `laravel({ input: [...] })` in
|
|
49
|
+
`vite.config.js` (or `.ts`) already lists as an entry, with `@tailwindcss/vite` among the
|
|
50
|
+
plugins.
|
|
51
|
+
- The `@source` path is `'../../node_modules/@jovertical/mise-design-system/dist'`.
|
|
52
|
+
- Import components in the pages under `resources/js/Pages/` like any Svelte module.
|
|
53
|
+
- **`mise/form` is not available.** Its `sveltekit-superforms` dependency imports SvelteKit's
|
|
54
|
+
`$app/*` modules, which a plain Vite build cannot resolve. Build forms with `Field` and the
|
|
55
|
+
inputs, and keep the state in Inertia's `useForm`; Laravel's validation errors arrive in
|
|
56
|
+
`form.errors` ([recipes/form.md](recipes/form.md)).
|
|
57
|
+
|
|
58
|
+
## Plain Svelte with Vite
|
|
59
|
+
|
|
60
|
+
- Import the stylesheet from the entry module (`src/main.ts`): `import './app.css'`.
|
|
61
|
+
- The `@source` path is relative to wherever that stylesheet lives, usually
|
|
62
|
+
`'../node_modules/@jovertical/mise-design-system/dist'`.
|
|
63
|
+
- As with Inertia, use `Field` rather than `mise/form`.
|
|
64
|
+
|
|
65
|
+
## Dark mode
|
|
66
|
+
|
|
67
|
+
Dark mode is the `.dark` class on an ancestor, usually `<html>`. Either the app sets it itself,
|
|
68
|
+
or `ThemeProvider` does: it applies `light` or `dark` to `<html>`, follows the system setting by
|
|
69
|
+
default (`defaultTheme="system"`), and stores the choice under `storageKey`. Give each app its
|
|
70
|
+
own `storageKey`, so two apps on one host do not share a theme. `ThemeToggle` is the switch, and
|
|
71
|
+
`useTheme()` reads or sets the theme from code; both need the provider above them.
|
|
72
|
+
|
|
73
|
+
```svelte
|
|
74
|
+
<script lang="ts">
|
|
75
|
+
import { ThemeProvider } from '@jovertical/mise-design-system'
|
|
76
|
+
|
|
77
|
+
let { children } = $props()
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<ThemeProvider storageKey="ops-theme">
|
|
81
|
+
{@render children()}
|
|
82
|
+
</ThemeProvider>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Toasts
|
|
86
|
+
|
|
87
|
+
Render `Toaster` once in the app's root layout, then call `toast('Order saved')` (also
|
|
88
|
+
`toast.success`, `toast.error` and the rest of `svelte-sonner`'s API) from anywhere.
|
|
89
|
+
|
|
90
|
+
## Check it
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npx --no-install mise-theme info --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
It prints the installed version, the stack it detected, and `stylesheet`, the CSS file it found
|
|
97
|
+
importing mise. `"stylesheet": null` means the import above is missing or sits in a file it did
|
|
98
|
+
not search (`node_modules`, `vendor`, `public`, `storage`, `dist`, `build` and dot-folders are
|
|
99
|
+
skipped).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Theming mise
|
|
2
|
+
|
|
3
|
+
A component never carries a colour of its own: it reads the semantic tokens (`--primary`,
|
|
4
|
+
`--muted-foreground`, `--success`) and the tone maps built on them. So the way to change how mise
|
|
5
|
+
looks is to change the tokens, never to override a component's classes. Recolouring `--success`
|
|
6
|
+
recolours every success badge, alert, bar and toast together.
|
|
7
|
+
|
|
8
|
+
There are three levels, each optional. Use the lowest one that does the job.
|
|
9
|
+
|
|
10
|
+
## 1. The default
|
|
11
|
+
|
|
12
|
+
The imports in [setup.md](setup.md) are all it takes: a zinc base, an indigo brand, system
|
|
13
|
+
fonts, and a light and a dark scheme.
|
|
14
|
+
|
|
15
|
+
## 2. Override tokens in CSS
|
|
16
|
+
|
|
17
|
+
For a brand colour, a radius or a font, override the tokens after the import:
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
@import '@jovertical/mise-design-system/theme.css';
|
|
21
|
+
|
|
22
|
+
:root {
|
|
23
|
+
--primary: oklch(0.6 0.15 160);
|
|
24
|
+
--radius: 0.5rem;
|
|
25
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.dark {
|
|
29
|
+
--primary: oklch(0.75 0.13 160);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The tokens are a documented contract, listed in `SEMANTIC_TOKENS` (exported from
|
|
34
|
+
`@jovertical/mise-design-system/tokens`): `--background`, `--foreground`, `--card`,
|
|
35
|
+
`--popover`, `--primary`, `--secondary`, `--muted`, `--accent`, `--destructive`, `--info`,
|
|
36
|
+
`--success`, `--warning`, each `-foreground` pair, `--border`, `--input`, `--ring`,
|
|
37
|
+
`--chart-1` to `--chart-5`, the `--sidebar-*` set and `--color-table-header`. Also `--radius`,
|
|
38
|
+
`--font-sans`, `--font-heading` and `--font-mono`.
|
|
39
|
+
|
|
40
|
+
The `-soft` tints (`bg-success-soft` and the like) are mixed from their ink, so they follow an
|
|
41
|
+
override on their own. `--accent` is the active navigation row only; it is not a second brand
|
|
42
|
+
colour.
|
|
43
|
+
|
|
44
|
+
## 3. A theme from a config
|
|
45
|
+
|
|
46
|
+
For a different base grey, a whole brand ramp, or a different hue for a status, write a config
|
|
47
|
+
and generate a stylesheet from it:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// mise.theme.js
|
|
51
|
+
import { defineTheme } from '@jovertical/mise-design-system/tokens'
|
|
52
|
+
|
|
53
|
+
export default defineTheme({
|
|
54
|
+
base: 'stone', // zinc | slate | gray | neutral | stone
|
|
55
|
+
primary: 'teal', // a Tailwind hue name, or a custom ramp with all of 50 to 950
|
|
56
|
+
status: { warning: 'orange' }, // info | success | warning | critical
|
|
57
|
+
charts: ['teal', 'sky', 'amber', 'rose', 'violet'],
|
|
58
|
+
radius: '0.5rem',
|
|
59
|
+
fonts: { sans: "'Inter', system-ui, sans-serif" }, // sans | heading | mono
|
|
60
|
+
light: { '--sidebar': 'oklch(0.98 0 0)' }, // single tokens, written last
|
|
61
|
+
dark: { '--sidebar': 'oklch(0.2 0 0)' },
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npx mise-theme mise.theme.js -o src/mise-theme.css
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Import the generated file in place of `@jovertical/mise-design-system/theme.css` (keep the
|
|
70
|
+
`@source` line). Re-run the command after changing the config; in a build script,
|
|
71
|
+
`emitThemeCss(config)` from `/tokens` does the same in code. `presets` and `BASES` from
|
|
72
|
+
`/tokens` list the ready-made combinations.
|
|
73
|
+
|
|
74
|
+
`npx --no-install mise-theme info --json` reports the config it finds as `themeConfig`.
|
|
75
|
+
|
|
76
|
+
## Rules
|
|
77
|
+
|
|
78
|
+
- Theme through tokens; never restyle a component's colours, radius or type through `class`.
|
|
79
|
+
- Never write a palette class (`bg-teal-600`) or a `dark:` colour override in the app's UI. Both
|
|
80
|
+
bypass the theme and break the other scheme.
|
|
81
|
+
- Status colour comes from `tone`, which reads `--info`, `--success`, `--warning` and
|
|
82
|
+
`--destructive` (the `critical` tone). Change the status hue in the theme, not per component.
|