@xleddyl/443-components-vue 0.3.0 → 0.4.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 +52 -25
- package/dist/components/core/FieldBoundary.vue +14 -0
- package/dist/components/core/FieldBoundary.vue.d.ts +15 -0
- package/dist/components/core/Form.vue +91 -12
- package/dist/components/core/Form.vue.d.ts +27 -2
- package/dist/components/core/FormAlerts.vue +30 -0
- package/dist/{internal/forms → components/core}/FormAlerts.vue.d.ts +4 -4
- package/dist/components/core/FormField.vue +18 -3
- package/dist/components/core/index.d.ts +2 -0
- package/dist/components/core/index.mjs +2 -0
- package/dist/components/data/FilterPills.vue +82 -37
- package/dist/components/data/FilterPills.vue.d.ts +46 -427
- package/dist/components/data/SelectionBar.vue +7 -4
- package/dist/components/data/Tree.vue +9 -4
- package/dist/components/data/Tree.vue.d.ts +4 -1
- package/dist/components/data/types.d.ts +7 -2
- package/dist/components/forms/CheckboxGroup.vue +2 -1
- package/dist/components/forms/FileInput.vue +98 -26
- package/dist/components/forms/FileInput.vue.d.ts +28 -10
- package/dist/components/forms/RadioGroup.vue +2 -1
- package/dist/components/forms/SegmentedControl.vue +2 -1
- package/dist/components/forms/types.d.ts +3 -0
- package/dist/components/layout/Accordion.vue.d.ts +4 -4
- package/dist/components/layout/AppShell.vue +1 -1
- package/dist/components/layout/AppShell.vue.d.ts +2 -2
- package/dist/components/layout/Tabs.vue +10 -1
- package/dist/components/layout/Tabs.vue.d.ts +4 -0
- package/dist/components/layout/types.d.ts +1 -0
- package/dist/components/overlays/CommandPalette.vue +10 -4
- package/dist/components/overlays/CommandPalette.vue.d.ts +273 -511
- package/dist/components/overlays/HoverCard.vue +1 -1
- package/dist/components/overlays/HoverCard.vue.d.ts +2 -2
- package/dist/components/overlays/Popover.vue +1 -1
- package/dist/components/overlays/Popover.vue.d.ts +2 -2
- package/dist/components/overlays/types.d.ts +7 -4
- package/dist/components/templates/DashboardLayout.vue +21 -18
- package/dist/components/templates/DashboardLayout.vue.d.ts +332 -565
- package/dist/components/templates/ListPage.vue +1 -1
- package/dist/components/templates/ListPage.vue.d.ts +3 -3
- package/dist/components/templates/types.d.ts +4 -4
- package/dist/composables/useField.d.ts +6 -1
- package/dist/composables/useField.mjs +40 -12
- package/dist/composables/useFieldBoundary.d.ts +1 -0
- package/dist/composables/useFieldBoundary.mjs +5 -1
- package/dist/composables/useFileDrop.d.ts +14 -1
- package/dist/composables/useFileDrop.mjs +21 -20
- package/dist/composables/useForm.d.ts +4 -0
- package/dist/composables/useForm.mjs +62 -22
- package/dist/editor/RichTextEditor.vue +2 -1
- package/dist/internal/core/ButtonElement.d.ts +2 -0
- package/dist/internal/core/ButtonElement.mjs +8 -0
- package/dist/internal/forms/files.d.ts +7 -0
- package/dist/internal/forms/files.mjs +24 -0
- package/dist/internal/keys.d.ts +2 -1
- package/dist/internal/keys.mjs +1 -0
- package/dist/locale/en.mjs +2 -0
- package/dist/locale/it.mjs +2 -0
- package/dist/locale/types.d.ts +2 -0
- package/dist/styles/core/button.css +1 -13
- package/dist/styles/data/avatar.css +1 -1
- package/dist/styles/data/filter-pills.css +0 -1
- package/dist/styles/data/pagination.css +0 -1
- package/dist/styles/data/selection-bar.css +4 -0
- package/dist/styles/data/stat.css +1 -6
- package/dist/styles/data/table.css +1 -3
- package/dist/styles/data/tag.css +1 -4
- package/dist/styles/data/tree.css +8 -2
- package/dist/styles/feedback/alert.css +0 -1
- package/dist/styles/feedback/empty-state.css +0 -1
- package/dist/styles/forms/color-input.css +1 -1
- package/dist/styles/forms/combobox.css +0 -1
- package/dist/styles/forms/date-picker.css +1 -1
- package/dist/styles/forms/file-input.css +9 -1
- package/dist/styles/forms/number-input.css +1 -1
- package/dist/styles/forms/password-input.css +1 -5
- package/dist/styles/forms/switch.css +0 -1
- package/dist/styles/layout/accordion.css +6 -1
- package/dist/styles/layout/card.css +1 -5
- package/dist/styles/layout/copy-button.css +2 -2
- package/dist/styles/layout/nav-link.css +1 -2
- package/dist/styles/layout/sidebar-nav.css +1 -1
- package/dist/styles/layout/sidebar.css +1 -1
- package/dist/styles/layout/tabs.css +22 -2
- package/dist/styles/layout/theme-switch.css +1 -3
- package/dist/styles/overlays/command.css +1 -1
- package/dist/styles/overlays/confirm.css +0 -1
- package/dist/styles/overlays/menu.css +2 -4
- package/dist/styles/overlays/toast.css +0 -1
- package/dist/styles/templates/auth-layout.css +0 -1
- package/dist/styles/templates/empty-page.css +3 -3
- package/dist/styles/templates/form-page.css +1 -1
- package/dist/styles/templates/page.css +1 -1
- package/dist/styles/templates/settings-modal.css +0 -1
- package/dist/styles/tokens.css +1 -1
- package/package.json +1 -1
- package/dist/internal/forms/FormAlerts.vue +0 -25
- package/dist/internal/overlays/FieldBoundary.d.ts +0 -4
- package/dist/internal/overlays/FieldBoundary.mjs +0 -9
package/README.md
CHANGED
|
@@ -244,11 +244,38 @@ const form = useForm({
|
|
|
244
244
|
|
|
245
245
|
A control can report its own error (a JSON field that fails to parse) through the surrounding `FormField`: `const control = useFieldControl()` then `control.setControlError(message | undefined)`. The form error wins over it, and it clears when the control unmounts. `FieldMessage` renders the same inline error, warning or hint line outside a `FormField` (`tone="danger" | "warning" | "neutral"`, `icon`, `id` for `aria-describedby`).
|
|
246
246
|
|
|
247
|
+
`validate()` is async. When every rule, `validate` function and Standard Schema is synchronous, `validateSync()` answers immediately and fills `form.errors` in the same tick, so a caller can reveal the first error without parsing again (it throws when it meets a Promise):
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
if (!form.validateSync()) revealField(Object.keys(form.errors).find((key) => form.errors[key]))
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Nested forms. Overlays stay in the DOM, so a `Form` inside a Modal opened from another `Form` would nest `<form>` elements. Render the inner one with `as="div"`: it submits on Enter in its inputs and through its own submit and reset buttons, and keeps those events from reaching the outer form. An outer `Form` also ignores submit events whose target is not its own form element. A component that renders fields without `Form` gives them the form with `provideForm(form)` and shows the root alerts with `<KFormAlerts />` (it reads `rootErrors` and `rootWarnings` from the provided form, or takes `errors` and `warnings` props). `Form` exposes `submit()`, `reset()` and `el`.
|
|
254
|
+
|
|
255
|
+
```vue
|
|
256
|
+
<KForm :form="entry">
|
|
257
|
+
<KModal v-model:open="open" title="New tag">
|
|
258
|
+
<KForm as="div" :form="tag">...</KForm>
|
|
259
|
+
</KModal>
|
|
260
|
+
</KForm>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Several controls in one `FormField`. Only the primary control takes the field id, name, description and invalid state. Wrap secondary controls (a search input above a Tree) in `<KFieldBoundary auxiliary>`, or call `useFieldControl({ role: 'auxiliary' })` in your own control: they keep their own id and only follow `disabled`. `<KFieldBoundary>` without `auxiliary` cuts the field context completely, like the overlays do. A primary control that is not labelable (a tree, a group, a contenteditable editor) passes `useFieldControl({ labelable: false })`; `FormField` then drops `for` from its label and the control uses `aria-labelledby` with `field.labelId` instead. `Tree`, `CheckboxGroup`, `RadioGroup`, `SegmentedControl` and `RichTextEditor` already do.
|
|
264
|
+
|
|
265
|
+
```vue
|
|
266
|
+
<KFormField label="Folder">
|
|
267
|
+
<KFieldBoundary auxiliary>
|
|
268
|
+
<KInput v-model="query" aria-label="Find a folder" />
|
|
269
|
+
</KFieldBoundary>
|
|
270
|
+
<KTree v-model:selected="folder" :nodes="nodes" />
|
|
271
|
+
</KFormField>
|
|
272
|
+
```
|
|
273
|
+
|
|
247
274
|
## Components
|
|
248
275
|
|
|
249
276
|
| Group | Components |
|
|
250
277
|
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
-
| core | Root, Icon, Button, Spinner, Input, Textarea, FormField, FieldMessage, Form, Kbd, VisuallyHidden
|
|
278
|
+
| core | Root, Icon, Button, Spinner, Input, Textarea, FormField, FieldMessage, FieldBoundary, Form, FormAlerts, Kbd, VisuallyHidden |
|
|
252
279
|
| forms | Calendar, Checkbox, CheckboxGroup, ColorInput, Combobox, DateInput, DatePicker, FileInput, InputAddon, InputGroup, NumberInput, PasswordInput, PinInput, Radio, RadioGroup, SegmentedControl, Select, Slider, Switch, TimeInput |
|
|
253
280
|
| overlays | CommandPalette, ConfirmDialog, ConfirmHost, ContextMenu, Drawer, DropdownMenu, HoverCard, Modal, OverlayHost, Popover, Toaster, Tooltip |
|
|
254
281
|
| data | Avatar, AvatarGroup, Badge, DescriptionList, FilterPill, FilterPills, Pagination, SelectionBar, Stat, Table, Tag, Timeline, Tree |
|
|
@@ -259,17 +286,17 @@ A control can report its own error (a JSON field that fails to parse) through th
|
|
|
259
286
|
| editor | RichTextEditor |
|
|
260
287
|
| chart | ChartPanel, ChartLegend, LineChart, BarChart, DoughnutChart |
|
|
261
288
|
|
|
262
|
-
Composables: `useTheme`, `usePreset`, `useForm`, `useField`, `useFieldControl`, `useToast` (and `toast`), `useConfirm`, `useAlert`, `usePrompt`, `useModal`, `useDrawer`, `useOverlay`, `useModalContext`, `useLoading` (and `withLoading`), `useLoadingState`, `useHotkey`, `useCopy`, `useFileDrop` (with `readDroppedFiles`), `usePasswordStrength`, `useTableSort`, `useAppShell`, `useScrollLock`, `useDirtyGuard`, `useIcons` helpers and `useLinkComponent`, `provideFieldBoundary`, `useLocale`. With the Nuxt module also `useDashboard()` (runtime account, breadcrumbs and badges for the `k-dashboard` layout).
|
|
289
|
+
Composables: `useTheme`, `usePreset`, `useForm`, `useField`, `useFieldControl`, `useToast` (and `toast`), `useConfirm`, `useAlert`, `usePrompt`, `useModal`, `useDrawer`, `useOverlay`, `useModalContext`, `useLoading` (and `withLoading`), `useLoadingState`, `useHotkey`, `useCopy`, `useFileDrop` (with `readDroppedFiles`), `usePasswordStrength`, `useTableSort`, `useAppShell`, `useScrollLock`, `useDirtyGuard`, `useIcons` helpers and `useLinkComponent`, `provideFieldBoundary`, `provideAuxiliaryControls`, `provideForm`, `useLocale`. With the Nuxt module also `useDashboard()` (runtime account, breadcrumbs and badges for the `k-dashboard` layout).
|
|
263
290
|
|
|
264
291
|
### Lists, trees and uploads
|
|
265
292
|
|
|
266
|
-
- `Tree` takes `size="sm"` for dense rows, `checkmark`, `emptyText` (or `#empty`) and works inside a `FormField` (labelled and described by the field). With `reorderable` rows get a drag handle and Alt with the up and down arrows moves them; `nestable` also lets rows change level (drop in the middle of a row, Alt with left and right). The `move` event carries `{ node, target, position, from: { parent, index }, to: { parent, index } }`, `to.index` being the final index, and `applyTreeMove(nodes, move)` returns the updated tree. `canDrop(move)` vetoes a move. Moves are announced to screen readers.
|
|
267
|
-
- `SelectionBar` shows a plural aware selected count (announced in a live region), select all (`total` plus `@select-all`) and clear (`@clear`) actions, an `#actions` slot and `placement` `inline`, `sticky` or `floating`. `ListPage` renders it for `#bulk`.
|
|
268
|
-
- `FilterPills`
|
|
293
|
+
- `Tree` takes `size="sm"` for dense rows, `checkmark`, `descriptionVariant` (`mono`, the default for paths and slugs, or `text`; a node can override it with its own `descriptionVariant`), `emptyText` (or `#empty`) and works inside a `FormField` (labelled and described by the field). With `reorderable` rows get a drag handle and Alt with the up and down arrows moves them; `nestable` also lets rows change level (drop in the middle of a row, Alt with left and right). The `move` event carries `{ node, target, position, from: { parent, index }, to: { parent, index } }`, `to.index` being the final index, and `applyTreeMove(nodes, move)` returns the updated tree. `canDrop(move)` vetoes a move. Moves are announced to screen readers.
|
|
294
|
+
- `SelectionBar` shows a plural aware selected count (announced in a live region), select all (`total` plus `@select-all`) and clear (`@clear`) actions, an `#actions` slot and `placement` `inline`, `sticky` or `floating`. With `count` 0 it renders no box (only its live region), so it can sit in a flex toolbar without adding a gap; the first count after mount is announced too. `ListPage` renders it for `#bulk`.
|
|
295
|
+
- `FilterPills` is generic: the value type comes from the options, `multiple` makes the model an array and `required` drops `null` from it (like `SegmentedControl`). Options take `menu` items (a dropdown trigger showing `valueLabel`) or `panel: true`, which opens a popover with the `#panel-<value>` slot (`{ option, active, toggle, close }`, `panelLabel` names it). The `#pill` slot renders any trigger and its scope has `defaultPill`, a component that renders the built-in pill for that option, so one custom pill does not mean rewriting the others: `<component :is="defaultPill" v-else />`. `FilterPill` is exported so custom triggers match the other pills.
|
|
269
296
|
- `SegmentedControl` with `required` narrows `v-model` to a non null value. Options take `title` (tooltip and description) and `muted`, and the `#option` slot replaces the content of each option.
|
|
270
|
-
- `FileInput` takes `directory` (folder picker; dropped folders are walked, every file keeps its relative path), `list: false` (no file list: picks and drops only emit `add`) and `
|
|
271
|
-
- `useFileDrop` returns `{ over, reset }` and takes `ignore(event)`. A drop handled by a nested drop zone is skipped by the outer ones, and the state resets after any drop, even when a child stops propagation. `readDroppedFiles(dataTransfer)` walks dropped folders (call it synchronously in the drop handler).
|
|
272
|
-
- `Tabs` items take `badgeTone` and `badgeLabel` (the accessible name of the badge); the `#panel="{ item }"` slot renders a single panel for the active tab. `Breadcrumbs` items take `attrs` and `onClick` (an item without a link renders a button, handy for drop targets), `#item` customizes the content, and `ancestors` links every item and ends with a separator (`breadcrumbsAncestors` on `PageHeader`). `Accordion` items take `description`, and `contentVariant="neutral"` drops the content padding and text styling.
|
|
297
|
+
- `FileInput` takes `directory` (folder picker; dropped folders are walked, every file keeps its relative path) or `directory="drop"` (the click opens a file picker, dropped folders are still walked), `directoryAction` (an "Upload a folder" button under the dropzone, `directoryActionLabel` renames it) or the `#actions` slot (`{ browse, browseDirectory, disabled }`), `skipHidden` (ignores dot files and dot folders), `list: false` (no file list: picks and drops only emit `add`), `loading` and the `#hint` slot. `add` receives `(files, { entries: [{ file, path }], directories, source, event, dataTransfer })`; `directories` lists every folder path of a folder pick or drop, empty folders included, and `add` also fires for a drop of empty folders. `invalid` fires per file and `reject` once with every rejected file (`(events, detail)`). `acceptMode="hint"` only passes `accept` to the picker and leaves filtering to the host. The component exposes `browse()`, `browseDirectory()`, `addFiles()` and `focus()`.
|
|
298
|
+
- `useFileDrop` returns `{ over, reset }` and takes `ignore(event)`. A drop handled by a nested drop zone is skipped by the outer ones, and the state resets after any drop, even when a child stops propagation. `readDroppedFiles(dataTransfer)` walks dropped folders (call it synchronously in the drop handler) and resolves to `[{ file, path }]`; `readDroppedFiles(dataTransfer, { directories: true })` resolves to `{ files, directories }` with every folder path, empty ones included, and `skipHidden: true` leaves out dot entries.
|
|
299
|
+
- `Tabs` items take `badgeTone` and `badgeLabel` (the accessible name of the badge); the `#panel="{ item }"` slot renders a single panel for the active tab. `stretch` makes the tabs fill the remaining height of a flex column and the active panel grow with it (`flex: 1; min-height: 0`), and `inset` spaces the tab list from the container edges with the panel flush below it, for inspectors and side panes. `Breadcrumbs` items take `attrs` and `onClick` (an item without a link renders a button, handy for drop targets), `#item` customizes the content, and `ancestors` links every item and ends with a separator (`breadcrumbsAncestors` on `PageHeader`). `Accordion` takes `variant` `card` (default), `outline` (the same card without a shadow), `separated` or `plain`; items take `description`, and `contentVariant="neutral"` drops the content padding and text styling.
|
|
273
300
|
- `RichTextEditor` labels heading buttons by their real level (`headingLabel="relative"` numbers them from 1) and `toolbar` picks and orders the buttons: `['headings', '|', 'bold', 'italic', 'link']`.
|
|
274
301
|
|
|
275
302
|
## Imperative hooks
|
|
@@ -337,7 +364,7 @@ const confirmDiscard = () => !dirty.value || confirm({ title: 'Discard changes?'
|
|
|
337
364
|
- `size="fullscreen"` on a Modal fills the viewport edge to edge (no radius, no outer padding, safe area insets inside the panel) and never turns into a bottom sheet.
|
|
338
365
|
- Overlays reset the field context: controls inside a Modal, Drawer, CommandPalette, menu, Popover or HoverCard panel do not inherit the id, description or invalid state of a `FormField` that contains the trigger. Call `provideFieldBoundary()` in your own overlay components to do the same.
|
|
339
366
|
|
|
340
|
-
`CommandPalette`:
|
|
367
|
+
`CommandPalette`: items take typed `data` (`CommandItem<T>`, `CommandGroup<T>`), which reaches the `#item` slot and the `select` event typed; `emptyText` also takes `(query) => string`. Groups take a `description` (shown after the label), the `#group-label` slot (`{ group, label, description, recent }`) replaces the label, and `notice` or the `#after-results` slot (`{ query, count }`) adds a line under the results, for example "Showing the first results".
|
|
341
368
|
|
|
342
369
|
- `Brand`: `logo` takes an image URL, trusted inline SVG markup (`'<svg ...>'`) or a component; the `#logo` slot replaces the boxed mark; `show-name` keeps the name visible next to a logo.
|
|
343
370
|
- `Card`: `selected` gives a pressed look (`aria-pressed` on `as="button"`, `aria-selected` with `role="option"` and similar roles); `:animated="false"` turns off the entrance animation.
|
|
@@ -364,22 +391,22 @@ Ready defaults to start a dashboard project: an app frame, an auth frame and pag
|
|
|
364
391
|
</KDashboardLayout>
|
|
365
392
|
```
|
|
366
393
|
|
|
367
|
-
| Prop | Default | Notes
|
|
368
|
-
| ---------------------------------------------------------------------------------- | ------------------------------- |
|
|
369
|
-
| `variant` | `'inset'` | `inset` (canvas sidebar, inset content panel), `sidebar` (bordered sidebar, full bleed content), `floating` (raised sidebar card on the canvas).
|
|
370
|
-
| `navigation` | | `NavItem[]` or `NavSection[]` (`{ label?, items }`). Items: `label`, `to`/`href`, `icon`, `badge`, `exact`, `active`, `external`, `disabled`, `children`.
|
|
371
|
-
| `currentPath` | router path | Active state is computed from the router (`$route`) with prefix matching (`/` and `exact` items match exactly).
|
|
372
|
-
| `brandName`, `brandSubtitle`, `brandIcon`, `brandLogo`, `brandShowName`, `brandTo` | `brandTo: '/'` | `brandLogo` takes an image URL, inline SVG markup or a component; the name is hidden next to a logo unless `brandShowName`. Or the `#brand` slot.
|
|
373
|
-
| `account` | | `{ name, email?, avatar?, role?, menu? }`. Shown as a row (avatar, name, email, chevron) at the bottom of the sidebar.
|
|
374
|
-
| `accountAction` | `'settings'` | `settings`: the row opens a `SettingsModal` (`settingsSections`, `settingsPresets`, `v-model:settings-open`, `v-model:settings-section`, `@sign-out`). `menu`: a DropdownMenu with `account.menu`, selections emit `account-select`. Or replace it with the `#account` slot.
|
|
375
|
-
| `breadcrumbs` | | Rendered in the desktop topbar. `#topbar` replaces them, `#actions` adds buttons on the right.
|
|
376
|
-
| `search` | `false` | `true` adds a CommandPalette with every navigation item, an array adds your own command groups, an async function `(query, signal) => CommandGroup[]` searches on your server (matching navigation items come first unless `searchNavigation` is `false`). `#search` replaces it.
|
|
377
|
-
| `searchProps` | | Options for the built-in CommandPalette: `minQueryLength`, `debounce`, `placeholder`, `label`, `emptyText
|
|
378
|
-
| `themeSwitch` | `'sidebar'` | `'sidebar'`, `'topbar'` or `false`.
|
|
379
|
-
| `collapsible`, `v-model:collapsed`, `storageKey` | `true`, `'k-sidebar-collapsed'` | Icon rail on desktop. Persisted in a cookie under Nuxt (rendered on the server, no flash) or `localStorage` elsewhere. `false` disables persistence.
|
|
380
|
-
| `shortcut` | `'mod+b'` | Toggles the rail on desktop and the drawer on mobile. `searchShortcut` defaults to `mod+k`.
|
|
381
|
-
| `v-model:open` | `false` | Mobile drawer (below 768px of shell width): scrim, focus moves into the drawer, the content is `inert`, Escape and the scrim close it and focus returns to the burger.
|
|
382
|
-
| `fluid`, `viewTransition` | `false`, `true` | Full width content; `view-transition-name` on sidebar and panel so page transitions keep the frame still.
|
|
394
|
+
| Prop | Default | Notes |
|
|
395
|
+
| ---------------------------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
396
|
+
| `variant` | `'inset'` | `inset` (canvas sidebar, inset content panel), `sidebar` (bordered sidebar, full bleed content), `floating` (raised sidebar card on the canvas). |
|
|
397
|
+
| `navigation` | | `NavItem[]` or `NavSection[]` (`{ label?, items }`). Items: `label`, `to`/`href`, `icon`, `badge`, `exact`, `active`, `external`, `disabled`, `children`. |
|
|
398
|
+
| `currentPath` | router path | Active state is computed from the router (`$route`) with prefix matching (`/` and `exact` items match exactly). |
|
|
399
|
+
| `brandName`, `brandSubtitle`, `brandIcon`, `brandLogo`, `brandShowName`, `brandTo` | `brandTo: '/'` | `brandLogo` takes an image URL, inline SVG markup or a component; the name is hidden next to a logo unless `brandShowName`. Or the `#brand` slot. |
|
|
400
|
+
| `account` | | `{ name, email?, avatar?, role?, menu? }`. Shown as a row (avatar, name, email, chevron) at the bottom of the sidebar. |
|
|
401
|
+
| `accountAction` | `'settings'` | `settings`: the row opens a `SettingsModal` (`settingsSections`, `settingsPresets`, `v-model:settings-open`, `v-model:settings-section`, `@sign-out`). `menu`: a DropdownMenu with `account.menu`, selections emit `account-select`. Or replace it with the `#account` slot. |
|
|
402
|
+
| `breadcrumbs` | | Rendered in the desktop topbar. `#topbar` replaces them, `#actions` adds buttons on the right. |
|
|
403
|
+
| `search` | `false` | `true` adds a CommandPalette with every navigation item, an array adds your own command groups, an async function `(query, signal) => CommandGroup<T>[]` searches on your server (item `data` reaches `#search-item` typed) (matching navigation items come first unless `searchNavigation` is `false`). `#search` replaces it. |
|
|
404
|
+
| `searchProps` | | Options for the built-in CommandPalette: `minQueryLength`, `debounce`, `placeholder`, `label`, `emptyText` (a string or `(query) => string`), `minQueryText`, `errorText`, `loadingText`, `resultsText`, `notice`, `recentKey`, footer labels. Slots `#search-item`, `#search-empty`, `#search-group-label` and `#search-after-results` reach the palette. |
|
|
405
|
+
| `themeSwitch` | `'sidebar'` | `'sidebar'`, `'topbar'` or `false`. |
|
|
406
|
+
| `collapsible`, `v-model:collapsed`, `storageKey` | `true`, `'k-sidebar-collapsed'` | Icon rail on desktop. Persisted in a cookie under Nuxt (rendered on the server, no flash) or `localStorage` elsewhere. `false` disables persistence. |
|
|
407
|
+
| `shortcut` | `'mod+b'` | Toggles the rail on desktop and the drawer on mobile. `searchShortcut` defaults to `mod+k`. |
|
|
408
|
+
| `v-model:open` | `false` | Mobile drawer (below 768px of shell width): scrim, focus moves into the drawer, the content is `inert`, Escape and the scrim close it and focus returns to the burger. |
|
|
409
|
+
| `fluid`, `viewTransition` | `false`, `true` | Full width content; `view-transition-name` on sidebar and panel so page transitions keep the frame still. |
|
|
383
410
|
|
|
384
411
|
Slots: `brand`, `search`, `sidebar` (replaces the navigation), `sidebar-footer`, `account`, `topbar-brand`, `topbar`, `actions`, default, plus `settings-section-<id>`, `settings-actions-<id>` and `settings-account-fields`, forwarded to the SettingsModal. The exposed `openSettings(id?)` opens it on a section. Landmarks: skip link, `aside` and `nav` with labels, `main` with an id, `aria-current="page"` on the active link. `SidebarNav` (layout group) renders the same navigation data inside your own `Sidebar`.
|
|
385
412
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<slot />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { provideAuxiliaryControls, provideFieldBoundary } from "../../composables/useFieldBoundary";
|
|
7
|
+
defineOptions({ inheritAttrs: false });
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
auxiliary: { type: Boolean, required: false }
|
|
10
|
+
});
|
|
11
|
+
if (props.auxiliary) provideAuxiliaryControls();
|
|
12
|
+
else provideFieldBoundary();
|
|
13
|
+
defineSlots();
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
auxiliary?: boolean;
|
|
3
|
+
};
|
|
4
|
+
type __VLS_Slots = {
|
|
5
|
+
default?: () => unknown;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
9
|
+
declare const _default: typeof __VLS_export;
|
|
10
|
+
export default _default;
|
|
11
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
12
|
+
new (): {
|
|
13
|
+
$slots: S;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<form
|
|
3
|
+
v-if="as === 'form'"
|
|
3
4
|
ref="formRef"
|
|
4
5
|
class="k-form"
|
|
5
6
|
novalidate
|
|
6
7
|
:aria-busy="form.submitting.value ? 'true' : void 0"
|
|
7
|
-
@submit
|
|
8
|
-
@reset
|
|
8
|
+
@submit="onSubmitEvent"
|
|
9
|
+
@reset="onResetEvent"
|
|
9
10
|
>
|
|
10
11
|
<template v-if="alerts === 'start'">
|
|
11
12
|
<slot name="alerts" :errors="rootErrors" :warnings="rootWarnings">
|
|
@@ -27,37 +28,115 @@
|
|
|
27
28
|
</slot>
|
|
28
29
|
</template>
|
|
29
30
|
</form>
|
|
31
|
+
<div
|
|
32
|
+
v-else
|
|
33
|
+
ref="divRef"
|
|
34
|
+
class="k-form"
|
|
35
|
+
:aria-busy="form.submitting.value ? 'true' : void 0"
|
|
36
|
+
@keydown="onKeydown"
|
|
37
|
+
@click="onClick"
|
|
38
|
+
>
|
|
39
|
+
<template v-if="alerts === 'start'">
|
|
40
|
+
<slot name="alerts" :errors="rootErrors" :warnings="rootWarnings">
|
|
41
|
+
<FormAlerts :errors="rootErrors" :warnings="rootWarnings" />
|
|
42
|
+
</slot>
|
|
43
|
+
</template>
|
|
44
|
+
<slot
|
|
45
|
+
:values="form.values"
|
|
46
|
+
:errors="form.errors"
|
|
47
|
+
:warnings="form.warnings"
|
|
48
|
+
:root-errors="rootErrors"
|
|
49
|
+
:root-warnings="rootWarnings"
|
|
50
|
+
:dirty="form.dirty.value"
|
|
51
|
+
:submitting="form.submitting.value"
|
|
52
|
+
/>
|
|
53
|
+
<template v-if="alerts === 'end'">
|
|
54
|
+
<slot name="alerts" :errors="rootErrors" :warnings="rootWarnings">
|
|
55
|
+
<FormAlerts :errors="rootErrors" :warnings="rootWarnings" />
|
|
56
|
+
</slot>
|
|
57
|
+
</template>
|
|
58
|
+
</div>
|
|
30
59
|
</template>
|
|
31
60
|
|
|
32
61
|
<script setup>
|
|
33
|
-
import { computed, nextTick,
|
|
34
|
-
import {
|
|
35
|
-
import FormAlerts from "
|
|
62
|
+
import { computed, nextTick, useTemplateRef } from "vue";
|
|
63
|
+
import { provideForm } from "../../composables/useForm";
|
|
64
|
+
import FormAlerts from "./FormAlerts.vue";
|
|
36
65
|
const props = defineProps({
|
|
37
66
|
form: { type: Object, required: true },
|
|
67
|
+
as: { type: String, required: false, default: "form" },
|
|
38
68
|
focusOnError: { type: Boolean, required: false, default: true },
|
|
39
69
|
alerts: { type: [String, Boolean], required: false, default: "start" }
|
|
40
70
|
});
|
|
41
71
|
const emit = defineEmits(["submitted", "invalid", "reset"]);
|
|
42
72
|
const formRef = useTemplateRef("formRef");
|
|
73
|
+
const divRef = useTemplateRef("divRef");
|
|
74
|
+
const rootRef = computed(() => formRef.value ?? divRef.value);
|
|
43
75
|
const rootErrors = computed(() => props.form.rootErrors?.value ?? []);
|
|
44
76
|
const rootWarnings = computed(() => props.form.rootWarnings?.value ?? []);
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
provideForm(props.form);
|
|
78
|
+
const IMPLICIT_SUBMIT_SKIP = /* @__PURE__ */ new Set([
|
|
79
|
+
"button",
|
|
80
|
+
"submit",
|
|
81
|
+
"reset",
|
|
82
|
+
"image",
|
|
83
|
+
"checkbox",
|
|
84
|
+
"radio",
|
|
85
|
+
"file",
|
|
86
|
+
"color",
|
|
87
|
+
"range",
|
|
88
|
+
"hidden"
|
|
89
|
+
]);
|
|
90
|
+
function ownsTarget(target) {
|
|
91
|
+
if (!(target instanceof Element)) return false;
|
|
92
|
+
return target.closest("form, .k-form") === rootRef.value;
|
|
93
|
+
}
|
|
94
|
+
async function submit() {
|
|
47
95
|
const ok = await props.form.submit();
|
|
48
96
|
if (ok) {
|
|
49
97
|
emit("submitted");
|
|
50
|
-
return;
|
|
98
|
+
return true;
|
|
51
99
|
}
|
|
52
100
|
emit("invalid");
|
|
53
|
-
if (!props.focusOnError) return;
|
|
101
|
+
if (!props.focusOnError) return false;
|
|
54
102
|
await nextTick();
|
|
55
|
-
const control =
|
|
103
|
+
const control = rootRef.value?.querySelector('[aria-invalid="true"]');
|
|
56
104
|
if (control) control.focus();
|
|
57
|
-
else
|
|
105
|
+
else rootRef.value?.querySelector(".k-form-alerts")?.focus();
|
|
106
|
+
return false;
|
|
58
107
|
}
|
|
59
|
-
function
|
|
108
|
+
function reset() {
|
|
60
109
|
props.form.reset();
|
|
61
110
|
emit("reset");
|
|
62
111
|
}
|
|
112
|
+
function onSubmitEvent(event) {
|
|
113
|
+
if (event.target !== formRef.value) return;
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
void submit();
|
|
116
|
+
}
|
|
117
|
+
function onResetEvent(event) {
|
|
118
|
+
if (event.target !== formRef.value) return;
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
reset();
|
|
121
|
+
}
|
|
122
|
+
function onKeydown(event) {
|
|
123
|
+
if (event.key !== "Enter" || event.defaultPrevented) return;
|
|
124
|
+
if (event.isComposing || event.shiftKey || event.altKey) return;
|
|
125
|
+
const target = event.target;
|
|
126
|
+
if (!(target instanceof HTMLInputElement) || IMPLICIT_SUBMIT_SKIP.has(target.type)) return;
|
|
127
|
+
if (!ownsTarget(target)) return;
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
void submit();
|
|
130
|
+
}
|
|
131
|
+
function onClick(event) {
|
|
132
|
+
if (event.defaultPrevented || !(event.target instanceof Element)) return;
|
|
133
|
+
const button = event.target.closest(
|
|
134
|
+
'button[type="submit"], button:not([type]), input[type="submit"], button[type="reset"], input[type="reset"]'
|
|
135
|
+
);
|
|
136
|
+
if (!button || button.disabled || !ownsTarget(button)) return;
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
if (button.type === "reset") reset();
|
|
139
|
+
else void submit();
|
|
140
|
+
}
|
|
141
|
+
defineExpose({ submit, reset, el: rootRef });
|
|
63
142
|
</script>
|
|
@@ -2,16 +2,21 @@ import type { FormSchema, UseFormReturn } from '../../composables/useForm';
|
|
|
2
2
|
declare const __VLS_export: <S extends FormSchema, V extends object>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
3
3
|
props: import("vue").PublicProps & __VLS_PrettifyLocal<{
|
|
4
4
|
form: UseFormReturn<S, V>;
|
|
5
|
+
as?: "form" | "div";
|
|
5
6
|
focusOnError?: boolean;
|
|
6
7
|
alerts?: "start" | "end" | false;
|
|
7
8
|
} & {
|
|
8
9
|
onInvalid?: (() => any) | undefined;
|
|
9
10
|
onReset?: (() => any) | undefined;
|
|
10
11
|
onSubmitted?: (() => any) | undefined;
|
|
11
|
-
} & Partial<import("vue").FormHTMLAttributes & import("vue").ReservedProps>> & (typeof globalThis extends {
|
|
12
|
+
} & Partial<import("vue").FormHTMLAttributes & import("vue").ReservedProps & import("vue").HTMLAttributes>> & (typeof globalThis extends {
|
|
12
13
|
__VLS_PROPS_FALLBACK: infer P;
|
|
13
14
|
} ? P : {});
|
|
14
|
-
expose: (exposed: {
|
|
15
|
+
expose: (exposed: import("vue").ShallowUnwrapRef<{
|
|
16
|
+
submit: () => Promise<boolean>;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
el: import("vue").ComputedRef<HTMLElement | null>;
|
|
19
|
+
}>) => void;
|
|
15
20
|
attrs: any;
|
|
16
21
|
slots: {
|
|
17
22
|
alerts?: (props: {
|
|
@@ -33,6 +38,26 @@ declare const __VLS_export: <S extends FormSchema, V extends object>(__VLS_props
|
|
|
33
38
|
errors: readonly string[];
|
|
34
39
|
warnings: readonly string[];
|
|
35
40
|
}) => any;
|
|
41
|
+
} & {
|
|
42
|
+
alerts?: (props: {
|
|
43
|
+
errors: readonly string[];
|
|
44
|
+
warnings: readonly string[];
|
|
45
|
+
}) => any;
|
|
46
|
+
} & {
|
|
47
|
+
default?: (props: {
|
|
48
|
+
values: import("vue").UnwrapRef<V>;
|
|
49
|
+
errors: import("../../index.ts").FormErrors<S>;
|
|
50
|
+
warnings: Readonly<import("../../index.ts").FormMessageMap>;
|
|
51
|
+
rootErrors: readonly string[];
|
|
52
|
+
rootWarnings: readonly string[];
|
|
53
|
+
dirty: boolean;
|
|
54
|
+
submitting: boolean;
|
|
55
|
+
}) => any;
|
|
56
|
+
} & {
|
|
57
|
+
alerts?: (props: {
|
|
58
|
+
errors: readonly string[];
|
|
59
|
+
warnings: readonly string[];
|
|
60
|
+
}) => any;
|
|
36
61
|
};
|
|
37
62
|
emit: ((evt: "invalid") => void) & ((evt: "reset") => void) & ((evt: "submitted") => void);
|
|
38
63
|
}>) => import("vue").VNode & {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="errorList.length || warningList.length" class="k-form-alerts" tabindex="-1">
|
|
3
|
+
<Alert
|
|
4
|
+
v-for="message in errorList"
|
|
5
|
+
:key="`error:${message}`"
|
|
6
|
+
tone="danger"
|
|
7
|
+
:description="message"
|
|
8
|
+
/>
|
|
9
|
+
<Alert
|
|
10
|
+
v-for="message in warningList"
|
|
11
|
+
:key="`warning:${message}`"
|
|
12
|
+
tone="warning"
|
|
13
|
+
role="status"
|
|
14
|
+
:description="message"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup>
|
|
20
|
+
import { computed } from "vue";
|
|
21
|
+
import { useFormContext } from "../../composables/useForm";
|
|
22
|
+
import Alert from "../feedback/Alert.vue";
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
errors: { type: Array, required: false },
|
|
25
|
+
warnings: { type: Array, required: false }
|
|
26
|
+
});
|
|
27
|
+
const form = useFormContext();
|
|
28
|
+
const errorList = computed(() => props.errors ?? form?.rootErrors?.value ?? []);
|
|
29
|
+
const warningList = computed(() => props.warnings ?? form?.rootWarnings?.value ?? []);
|
|
30
|
+
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
declare const __VLS_export: import("vue").DefineComponent<{
|
|
2
|
-
errors
|
|
3
|
-
warnings
|
|
2
|
+
errors?: readonly string[];
|
|
3
|
+
warnings?: readonly string[];
|
|
4
4
|
innerHTML?: string | undefined | undefined;
|
|
5
5
|
class?: import("vue").ClassValue;
|
|
6
6
|
style?: import("vue").StyleValue;
|
|
@@ -194,8 +194,8 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
194
194
|
ref_for?: boolean | undefined | undefined;
|
|
195
195
|
ref_key?: string | undefined | undefined;
|
|
196
196
|
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
|
|
197
|
-
errors
|
|
198
|
-
warnings
|
|
197
|
+
errors?: readonly string[];
|
|
198
|
+
warnings?: readonly string[];
|
|
199
199
|
innerHTML?: string | undefined | undefined;
|
|
200
200
|
class?: import("vue").ClassValue;
|
|
201
201
|
style?: import("vue").StyleValue;
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
@animationend="onAnimationEnd"
|
|
12
12
|
>
|
|
13
13
|
<div v-if="hasLabelRow" class="k-form-field-label-row">
|
|
14
|
-
<label
|
|
14
|
+
<label
|
|
15
|
+
v-if="label || $slots.label"
|
|
16
|
+
:id="labelId"
|
|
17
|
+
:for="labelFor"
|
|
18
|
+
class="k-form-field-label"
|
|
19
|
+
>
|
|
15
20
|
<Icon v-if="icon" :name="icon" class="k-form-field-label-icon" />
|
|
16
21
|
<span
|
|
17
22
|
><slot name="label">{{ label }}</slot></span
|
|
@@ -53,7 +58,7 @@
|
|
|
53
58
|
<script setup>
|
|
54
59
|
import { computed, provide, ref, shallowReactive, useId, useSlots, watch } from "vue";
|
|
55
60
|
import { useFormContext } from "../../composables/useForm";
|
|
56
|
-
import { FIELD_KEY } from "../../internal/keys";
|
|
61
|
+
import { FIELD_KEY, FIELD_ROLE_KEY } from "../../internal/keys";
|
|
57
62
|
import FieldMessage from "./FieldMessage.vue";
|
|
58
63
|
import Icon from "./Icon.vue";
|
|
59
64
|
const props = defineProps({
|
|
@@ -83,6 +88,14 @@ function setControlError(message, source = fieldSource) {
|
|
|
83
88
|
else controlErrors.delete(source);
|
|
84
89
|
}
|
|
85
90
|
const controlError = computed(() => controlErrors.values().next().value);
|
|
91
|
+
const labelables = shallowReactive(/* @__PURE__ */ new Map());
|
|
92
|
+
function setLabelable(labelable, source) {
|
|
93
|
+
if (labelable === void 0) labelables.delete(source);
|
|
94
|
+
else labelables.set(source, labelable);
|
|
95
|
+
}
|
|
96
|
+
const labelFor = computed(
|
|
97
|
+
() => [...labelables.values()].some((labelable) => !labelable) ? void 0 : id.value
|
|
98
|
+
);
|
|
86
99
|
const errorMessage = computed(() => {
|
|
87
100
|
if (props.error) return props.error;
|
|
88
101
|
const formError = props.error !== false && props.name ? form?.errors[props.name] : void 0;
|
|
@@ -116,9 +129,11 @@ const context = {
|
|
|
116
129
|
invalid: computed(() => Boolean(errorMessage.value)),
|
|
117
130
|
required: isRequired,
|
|
118
131
|
disabled: computed(() => Boolean(props.disabled)),
|
|
119
|
-
setControlError
|
|
132
|
+
setControlError,
|
|
133
|
+
setLabelable
|
|
120
134
|
};
|
|
121
135
|
provide(FIELD_KEY, context);
|
|
136
|
+
provide(FIELD_ROLE_KEY, "primary");
|
|
122
137
|
const shaking = ref(false);
|
|
123
138
|
watch(
|
|
124
139
|
() => [errorMessage.value, form?.submitCount.value],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default as Button } from './Button.vue';
|
|
2
|
+
export { default as FieldBoundary } from './FieldBoundary.vue';
|
|
2
3
|
export { default as FieldMessage } from './FieldMessage.vue';
|
|
3
4
|
export { default as Form } from './Form.vue';
|
|
5
|
+
export { default as FormAlerts } from './FormAlerts.vue';
|
|
4
6
|
export { default as FormField } from './FormField.vue';
|
|
5
7
|
export { default as Icon } from './Icon.vue';
|
|
6
8
|
export { default as Input } from './Input.vue';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default as Button } from "./Button.vue";
|
|
2
|
+
export { default as FieldBoundary } from "./FieldBoundary.vue";
|
|
2
3
|
export { default as FieldMessage } from "./FieldMessage.vue";
|
|
3
4
|
export { default as Form } from "./Form.vue";
|
|
5
|
+
export { default as FormAlerts } from "./FormAlerts.vue";
|
|
4
6
|
export { default as FormField } from "./FormField.vue";
|
|
5
7
|
export { default as Icon } from "./Icon.vue";
|
|
6
8
|
export { default as Input } from "./Input.vue";
|