@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.
@@ -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.