@xleddyl/443-components-vue 0.9.2 → 0.10.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
@@ -2,6 +2,8 @@
2
2
 
3
3
  Compact, scoped dashboard components for Vue 3.5 and Nuxt 4. Plain Vue and CSS, no runtime dependency besides `vue`. Every style lives under `.k-root` and inside cascade layers, so the host page is never touched.
4
4
 
5
+ Looking for the right component for a task? Read [CATALOG.md](./CATALOG.md) first: it lists every component and composable with what it is for and when to use it.
6
+
5
7
  ## Install
6
8
 
7
9
  ```sh
@@ -127,21 +129,21 @@ Input tokens, declared only on the outermost root so nested roots inherit them:
127
129
 
128
130
  A few component tokens are unset by default and only exist to be overridden on a root (or any ancestor). Unset, every one keeps the library look. The full list with defaults is in [CONVENTIONS.md](./CONVENTIONS.md#tokens):
129
131
 
130
- | Area | Tokens |
131
- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
132
- | Type | `--k-font-weight` (400), `--k-font-weight-medium` (500), `--k-font-weight-strong` (600), `--k-font-weight-display` (650) |
133
- | Buttons | `--k-btn-highlight`, `--k-btn-shadow` (`0 0 #0000` for a flat button), `--k-btn-border`, `--k-btn-hover-background`, `--k-btn-active-background`, `--k-btn-padding-x`, `--k-btn-font-size`, `--k-btn-icon-size`, `--k-press-scale` (0.97) |
134
- | Fields | `--k-field-padding-x` |
135
- | Overlays | `--k-modal-radius`, `--k-modal-shadow`, `--k-drawer-radius`, `--k-drawer-shadow`, `--k-popover-radius`, `--k-popover-shadow`, `--k-toast-radius`, `--k-toast-shadow`, `--k-scrim-blur` (3px) |
136
- | Tooltip | `--k-tooltip-background`, `--k-tooltip-text`, `--k-tooltip-border`, `--k-tooltip-radius`, `--k-tooltip-shadow`, `--k-tooltip-padding` |
137
- | Shell | `--k-sidebar-width` (15rem), `--k-sidebar-rail-width`, `--k-topbar-height` (3rem), `--k-shell-inset`, `--k-shell-padding`, `--k-shell-content` |
138
- | Switch | `--k-switch-track-width`, `--k-switch-track-height`, `--k-switch-thumb-size` (md size) |
139
- | Separator | `--k-separator-heading-font-size` (0.9375rem), `--k-separator-heading-color` (`--k-text`), `--k-separator-label-font-size` (0.75rem) |
140
- | Calendar | `--k-calendar-day-radius`, `--k-calendar-unavailable-color`, `--k-calendar-unavailable-decoration` (line-through), `--k-calendar-unavailable-opacity` (0.6), `--k-calendar-out-of-range-color`, `--k-calendar-out-of-range-decoration`, `--k-calendar-out-of-range-opacity` (each defaults to its unavailable token) |
141
- | Checkbox group | `--k-checkbox-group-background`, `--k-checkbox-group-padding` (0.75rem), `--k-checkbox-group-radius`, `--k-checkbox-group-border` (`variant="filled"`) |
142
- | Badge | `--k-badge-radius`, `--k-badge-font`, `--k-badge-font-size`, `--k-badge-font-weight`, `--k-badge-text-transform`, `--k-badge-letter-spacing` |
143
- | DataGrid | `--k-grid-header-background`, `--k-grid-header-text`, `--k-grid-header-font`, `--k-grid-header-font-weight`, `--k-grid-header-font-size` (12px), `--k-grid-font-size` (13px), `--k-grid-radius`, `--k-grid-selected-background` |
144
- | Charts | `--k-chart-1` to `--k-chart-8` (series palette) |
132
+ | Area | Tokens |
133
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
+ | Type | `--k-font-weight` (400), `--k-font-weight-medium` (500), `--k-font-weight-strong` (600), `--k-font-weight-display` (650) |
135
+ | Buttons | `--k-btn-highlight`, `--k-btn-shadow` (`0 0 #0000` for a flat button), `--k-btn-border`, `--k-btn-hover-background`, `--k-btn-active-background`, `--k-btn-padding-x`, `--k-btn-font-size`, `--k-btn-icon-size`, `--k-press-scale` (0.97) |
136
+ | Fields | `--k-field-padding-x` |
137
+ | Overlays | `--k-modal-radius`, `--k-modal-shadow`, `--k-drawer-radius`, `--k-drawer-shadow`, `--k-popover-radius`, `--k-popover-shadow`, `--k-toast-radius`, `--k-toast-shadow`, `--k-scrim-blur` (3px) |
138
+ | Tooltip | `--k-tooltip-background`, `--k-tooltip-text`, `--k-tooltip-border`, `--k-tooltip-radius`, `--k-tooltip-shadow`, `--k-tooltip-padding` |
139
+ | Shell | `--k-sidebar-width` (15rem), `--k-sidebar-rail-width`, `--k-topbar-height` (3rem), `--k-shell-inset`, `--k-shell-padding`, `--k-shell-content` |
140
+ | Switch | `--k-switch-track-width`, `--k-switch-track-height`, `--k-switch-thumb-size` (md size) |
141
+ | Separator | `--k-separator-heading-font-size` (0.9375rem), `--k-separator-heading-color` (`--k-text`), `--k-separator-label-font-size` (0.75rem) |
142
+ | Calendar | `--k-calendar-day-radius`, `--k-calendar-unavailable-color`, `--k-calendar-unavailable-decoration` (line-through), `--k-calendar-unavailable-opacity` (0.6), `--k-calendar-out-of-range-color`, `--k-calendar-out-of-range-decoration`, `--k-calendar-out-of-range-opacity` (each defaults to its unavailable token) |
143
+ | Checkbox group | `--k-checkbox-group-background`, `--k-checkbox-group-padding` (0.75rem), `--k-checkbox-group-radius`, `--k-checkbox-group-border` (`variant="filled"`) |
144
+ | Badge | `--k-badge-radius`, `--k-badge-font`, `--k-badge-font-size`, `--k-badge-font-weight`, `--k-badge-text-transform`, `--k-badge-letter-spacing` |
145
+ | DataGrid | `--k-grid-header-background`, `--k-grid-header-text`, `--k-grid-header-font`, `--k-grid-header-font-weight`, `--k-grid-header-font-size` (12px), `--k-grid-font-size` (13px), `--k-grid-radius`, `--k-grid-selected-background`, spreadsheet mode: `--k-grid-range-background`, `--k-grid-range-border`, `--k-grid-dirty-background`, `--k-grid-dirty-marker` |
146
+ | Charts | `--k-chart-1` to `--k-chart-8` (series palette) |
145
147
 
146
148
  Some tokens are meant for one element: set `--k-btn-tone`, `--k-btn-on-tone` and `--k-btn-tone-text` on a button for a brand tone outside `Tone` (a lime fill with green text), and `--k-badge-background`, `--k-badge-color` and `--k-badge-border-color` on a badge for a custom pill color. `variant="subtle"` is a borderless tinted fill (`--k-fill` for the neutral tone). For solid buttons without hover darkening, declare the fill tokens on the button so `var(--k-btn-tone)` resolves per tone: `.k-btn-solid { --k-btn-hover-background: var(--k-btn-tone); --k-btn-active-background: var(--k-btn-tone) }` (the neutral solid button rests on `--k-text`).
147
149
 
@@ -263,6 +265,28 @@ const form = useForm({
263
265
 
264
266
  Issues are keyed by their first path segment and by their full dotted path (`blocks.0.title`), so a nested `FormField name="blocks.0.title"` shows its own error. Pass `issueKey: (path) => string | string[]` to choose the keys yourself. Issues with an empty path become `rootErrors`, which `Form` renders as alerts above the content (`alerts="end"` moves them below, `alerts="false"` hides them, the `#alerts` slot replaces them). `setError('', message)` adds a root error, for example from the server.
265
267
 
268
+ Server errors. `form.setIssues(issues)` takes Standard Schema shaped issues (`FormIssueInput`, `{ message, path? }`), keys them like validator issues (first segment and dotted path) and shows them on their fields; an empty path becomes a root error. Messages go through `translateMessage` (`kind: 'error'`). When `onSubmit` throws, a `submitError` resolver (`FormSubmitErrorResolver`, `(error) => FormSubmitError | null | undefined`, with `FormSubmitError` `{ message?, issues? }`) turns the error into issues: `submit()` then resolves `false` instead of throwing, `Form` focuses the first invalid field (or the alerts), `form.submitError` holds the mapped result, and `message` becomes a root error when no issue landed on a field. Set the resolver once in `createFormConfig` / `provideFormConfig`, or per form in `useForm({ submitError })`, which wins. Without a resolver, or when it returns nothing (or neither a message nor issues), the error is thrown as before. Server errors are stale once the user edits: changing a field drops the server errors of that field and its nested paths, and the next submit drops the rest. `setError` keeps its behavior: its errors stay until the field validates again (after the first submit every edit validates the field, so they clear on edit too; before it they stay).
269
+
270
+ ```ts
271
+ app.use(
272
+ createFormConfig({
273
+ submitError: (error) => {
274
+ const data = (
275
+ error as { data?: { message?: string; data?: { issues?: FormIssueInput[] } } }
276
+ ).data
277
+ return data ? { message: data.message, issues: data.data?.issues } : null
278
+ },
279
+ })
280
+ )
281
+
282
+ const form = useForm({
283
+ values: product,
284
+ onSubmit: (values) => $fetch('/api/products', { method: 'POST', body: values }),
285
+ })
286
+ ```
287
+
288
+ The server answers `422` with `{ message: 'Invalid product', data: { issues: [{ message: 'Taken', path: ['sku'] }] } }` (`createError({ statusCode: 422, message, data: { issues } })` in Nitro), and `$fetch` puts that body on `error.data`.
289
+
266
290
  Built-in messages follow the active language. `messages` overrides them for one form.
267
291
 
268
292
  `schema` also accepts a ref or getter (`MaybeRefOrGetter<FormSchema>`), for one form component reused with different fields, labels or rules per open, mode or language: `schema: () => (mode.value === 'edit' ? editSchema : createSchema)`. Field definitions, labels, `required` and rules follow the source (after the first submit the form validates again when they change). A field that appears starts from its `initial` and is clean; a field that disappears loses its value, error and touched state. With `values` the host owns the values, so only rules and labels change. `form.schema` always returns the current definitions.
@@ -509,7 +533,7 @@ A large dialog (60rem) with a section nav on the left and the content on the rig
509
533
  - Built-in sections, used when no `#section-<id>` slot fills them: `account` (profile card, your `#account-fields`, a sign out button) and `appearance` (`AppearanceSettings`: theme cards light, dark, system and preset cards; `presets` limits or hides them).
510
534
  - `@sign-out` adds a Sign out item at the bottom of the nav and in the account section.
511
535
  - Labels: `title` (accessible name of the dialog, default `Settings`), `closeLabel`, `navLabel`, `signOutLabel`, `accountLabel`, `accountText`, `appearanceLabel`, `appearanceText`, `sessionLabel`, `sessionText`, `themeLabel`, `themeText`, `presetLabel`, `presetText`.
512
- - Subpages: `#section-<id>` and `#actions-<id>` get `page`, `pages`, `openPage({ id, label })` and `back()`. With a page open, a breadcrumb from the section to the page shows below the header (the header keeps the section title and description); a click on a breadcrumb goes back to it. `v-model:pages` binds the stack (it also works unbound). Changing section clears it. Render the detail when `page` is set, the list otherwise.
536
+ - Subpages: `#section-<id>` and `#actions-<id>` get `page`, `pages`, `openPage({ id, label })` and `back()`. With a page open, the header title becomes the path (`Members > Grace Hopper`, same size, only the last part in the text color, earlier parts are buttons that go back to them), the section description stays, and a Back button (`backLabel`, default `common.back`) shows on the right before `#actions-<id>`. `v-model:pages` binds the stack (it also works unbound). Changing section clears it. Render the detail when `page` is set, the list otherwise.
513
537
  - `hide-session` hides the Session group (sign out) in the account section; the Sign out item in the nav stays. `beforeClose` works as on Modal.
514
538
  - Accessibility: native modal `<dialog>` (focus trap, Escape, focus returns to the trigger), the nav is a labelled `nav` of buttons with `aria-current` and `aria-controls`, the content is a `section` labelled by its heading.
515
539
 
@@ -666,6 +690,39 @@ Accessibility related props and defaults:
666
690
  - `DataGrid` registers the AG Grid cell editors (text, large text, number, date, checkbox, select, custom and undo/redo) when `editable` is set or when a column or `gridOptions.defaultColDef` has `editable` at mount. `registerGridEditorModules()` from `/grid` does it by hand, before the first grid mounts, for columns that become editable later.
667
691
  - `ImageCell` takes `fit` (`'cover'` default, or `'contain'` for logos) and `size` (any CSS length, default `1.75rem`) in `cellRendererParams`. `EmptyCell` (the hidden "Empty" dash) is exported from `/grid` for custom renderers.
668
692
  - `BadgeCell` takes `variant` (`BadgeVariant`, `'soft' | 'solid' | 'outline'`) in `cellRendererParams`: set, the cell renders a `KBadge` with that variant and the resolved tone (`size`, `BadgeSize`, default `'sm'`); unset keeps the compact grid pill. `badgeClass` (`string | ((value, data) => string | undefined)`) adds host classes to the badge, to set the per instance badge tokens (`--k-badge-background`, `--k-badge-color`, `--k-badge-border-color`, which the grid pill also reads) for values outside `Tone`. `BadgeCellParams<T>` types the row of `data`.
693
+ - `DataGrid` `spreadsheet` turns on Excel like editing on AG Grid Community (no Enterprise modules): it registers the editor modules and the AG Grid `ScrollApiModule` (`registerGridSpreadsheetModules()` from `/grid` does it by hand) and leaves the normal grid unchanged when it is off. Set `rowIdKey` or `getRowId`: changes, dirty cells and undo refer to row ids, never to indexes.
694
+ - Selection: click selects a cell; drag, Shift+click and Shift+arrows extend a rectangular range (Shift+Cmd/Ctrl+arrow to the edge); Cmd/Ctrl+A selects every cell. The range follows the displayed row order (after sort, filter and quick search) and the displayed column order; the row selection checkbox column is not part of it. Escape collapses the range to the active cell.
695
+ - Keyboard: arrows and Tab move the active cell, Enter, F2 or typing start the AG Grid editor, Escape cancels it. While an editor (or any input) has focus no shortcut fires.
696
+ - Copy: Cmd/Ctrl+C copies the range as TSV (tabs between cells, newlines between rows, cells with a tab, a newline or a leading quote are quoted like Excel). Cells copy their raw value as text (`true`/`false`, dates as `YYYY-MM-DD`); `gridOptions.processCellForClipboard` changes it (it gets `formatValue` and `parseValue` helpers). Right click inside a range copies the range.
697
+ - Paste: Cmd/Ctrl+V reads TSV from Excel, Google Sheets or text (quoted multi line cells, CRLF, trailing newline) and starts at the top left cell of the range. One value fills the whole range; a smaller block that divides the range evenly is tiled; otherwise the block keeps its size and the pasted area becomes the range. Read only cells (`editable` false or a function that returns false) are skipped. Text goes through the column `valueParser` (also the one of `cellDataType`) and then `valueSetter` or `field`; `gridOptions.processCellFromClipboard` replaces the parser. A non empty text that parses to `null`, `undefined`, `NaN` or an invalid date, or a value missing from `cellEditorParams.values` of a select editor, is not written: it is listed in `cellsInvalid` and counted in the toast.
698
+ - Fill: Cmd/Ctrl+D copies the first row of the range down, Cmd/Ctrl+R the first column to the right (a one row or one column range copies from the row above or the column on the left). Drag the small square on the range corner down to repeat the range rows below it. Delete or Backspace clears the editable cells (`null`, or what the column `valueParser` returns for an empty string).
699
+ - Changes: every edit, paste, fill, clear, bulk set, undo and redo is one batch, emitted as `cellsChange` with `{ changes: GridCellChange[], source }` (`GridCellChange` is `{ rowId, colId, field, oldValue, newValue }`, `source` is `'edit' | 'paste' | 'fill' | 'clear' | 'bulk' | 'undo' | 'redo'`). Cmd/Ctrl+Z undoes a batch, Shift+Cmd/Ctrl+Z and Ctrl+Y redo it (100 batches; AG Grid `undoRedoCellEditing` is turned off). The grid writes into the row objects like AG Grid editing does; persist the batch yourself.
700
+ - Dirty cells: a changed cell gets a tint and a corner marker until you clear it; a cell edited back to its first value is clean again. `dirtyChange(count)` fires when the count changes; `getDirtyCells()` returns `{ rowId, colId, field, originalValue }[]` and `clearDirty(rowIds?)` clears all of them or the given rows (after a save).
701
+ - Events: `cellsChange`, `cellsInvalid` (`{ cells: { rowId, colId, field, text }[], source }`), `dirtyChange`, `rangeChange` (`GridCellRange | null`, `{ startRowIndex, endRowIndex, columns, active: { rowIndex, colId } }`), and `copy(text)` for a copied range.
702
+ - Methods (template ref, typed as `GridSpreadsheetMethods<T>`): `getRange()`, `getSelectedCells()` (`{ rowId, rowIndex, colId, field, value, data, editable }[]`), `selectRange({ startRowIndex, endRowIndex?, startColumn, endColumn? })`, `selectAll()`, `clearRange()`, `setRangeValue(value, { parse? })` (writes the value in every editable cell of the range; `parse: true` runs a string through the paste pipeline), `copyRange()`, `paste(text)`, `fillDown()`, `fillRight()`, `clearCells()`, `undo()`, `redo()`, `clearHistory()`, `getDirtyCells()`, `clearDirty(rowIds?)`. Write methods return the number of changed cells.
703
+ - The `actions` slot also gets every method plus `range`, `selectedCellCount`, `dirtyCount`, `canUndo` and `canRedo` (`GridSpreadsheetSlotProps<T>`), for a "set this value on the selected cells" action:
704
+
705
+ ```vue
706
+ <KDataGrid
707
+ ref="grid"
708
+ :rows="products"
709
+ :columns="columns"
710
+ row-id-key="id"
711
+ spreadsheet
712
+ toolbar
713
+ @cells-change="({ changes }) => queue(changes)"
714
+ @dirty-change="(count) => (dirty = count)"
715
+ >
716
+ <template #actions="{ selectedCellCount, setRangeValue, canUndo, undo }">
717
+ <KButton label="Undo" :disabled="!canUndo" @click="undo()" />
718
+ <KButton :label="`Set to Shoes (${selectedCellCount})`" @click="setRangeValue('Shoes')" />
719
+ </template>
720
+ </KDataGrid>
721
+ ```
722
+
723
+ - Rendering stays virtual: the range, active cell and dirty marks are `cellClassRules` (merged with your own) and only the rendered cells of the affected rows refresh. The range tint is `--k-grid-range-background`, its border and the fill handle `--k-grid-range-border`, dirty cells `--k-grid-dirty-background` and `--k-grid-dirty-marker`. `parseTsv` and `formatTsv` are exported from `/grid`.
724
+ - `DataGrid` row classes: `gridOptions.rowClass`, `gridOptions.rowClassRules` (`{ 'is-archived': (params) => params.data?.archived }`) and `gridOptions.getRowClass` reach AG Grid unchanged. Style the classes in host CSS under `.k-data-grid .ag-row.<class>`.
725
+ - `LinkCell` renders internal hrefs (starting with `/`) with the configured link component (`NuxtLink` in Nuxt, `createLinkComponent(RouterLink)` in plain Vue), so they navigate without a reload; other hrefs stay a plain `<a>`.
669
726
  - `DataGrid` translates the AG Grid ARIA strings through the catalog (`grid.*`; your own `localeText` still wins) and announces row count changes.
670
727
 
671
728
  ## Browser support
@@ -99,21 +99,44 @@
99
99
  <header class="k-settings-modal-header">
100
100
  <div class="k-settings-modal-heading">
101
101
  <h2 :id="`${baseId}-title`" class="k-settings-modal-title">
102
- {{ active.label }}
102
+ <template v-if="currentPage">
103
+ <template v-for="(crumb, index) in trail" :key="index">
104
+ <button
105
+ type="button"
106
+ class="k-settings-modal-crumb"
107
+ @click="goTo(index)"
108
+ >
109
+ {{ crumb }}
110
+ </button>
111
+ <Icon name="chevron-right" class="k-settings-modal-crumb-separator" />
112
+ </template>
113
+ <span class="k-settings-modal-crumb is-current">
114
+ {{ currentPage.label }}
115
+ </span>
116
+ </template>
117
+ <template v-else>{{ active.label }}</template>
103
118
  </h2>
104
119
  <p v-if="active.description" class="k-settings-modal-description">
105
120
  {{ active.description }}
106
121
  </p>
107
122
  </div>
108
- <div v-if="$slots[`actions-${active.id}`]" class="k-settings-modal-actions">
123
+ <div
124
+ v-if="currentPage || $slots[`actions-${active.id}`]"
125
+ class="k-settings-modal-actions"
126
+ >
127
+ <Button
128
+ v-if="currentPage"
129
+ variant="ghost"
130
+ tone="neutral"
131
+ size="sm"
132
+ icon="arrow-left"
133
+ @click="back"
134
+ >
135
+ {{ backText }}
136
+ </Button>
109
137
  <slot :name="`actions-${active.id}`" v-bind="slotProps(active, close)" />
110
138
  </div>
111
139
  </header>
112
- <Breadcrumbs
113
- v-if="currentPage"
114
- class="k-settings-modal-breadcrumbs"
115
- :items="breadcrumbItems"
116
- />
117
140
  <div
118
141
  ref="contentBody"
119
142
  class="k-settings-modal-body"
@@ -186,7 +209,6 @@ import Icon from "../core/Icon.vue";
186
209
  import Avatar from "../data/Avatar.vue";
187
210
  import Badge from "../data/Badge.vue";
188
211
  import Modal from "../overlays/Modal.vue";
189
- import Breadcrumbs from "../layout/Breadcrumbs.vue";
190
212
  import AppearanceSettings from "./AppearanceSettings.vue";
191
213
  import SettingsGroup from "./SettingsGroup.vue";
192
214
  const props = defineProps({
@@ -195,6 +217,7 @@ const props = defineProps({
195
217
  presets: { type: [Array, Boolean], required: false, default: () => THEME_PRESETS },
196
218
  title: { type: String, required: false },
197
219
  closeLabel: { type: String, required: false },
220
+ backLabel: { type: String, required: false },
198
221
  navLabel: { type: String, required: false },
199
222
  signOutLabel: { type: String, required: false },
200
223
  sessionLabel: { type: String, required: false },
@@ -219,6 +242,7 @@ const pages = defineModel("pages", { type: Array, ...{ default: () => [] } });
219
242
  const { t } = useLocale();
220
243
  const titleText = computed(() => props.title ?? t("settings.title"));
221
244
  const closeText = computed(() => props.closeLabel ?? t("settings.close"));
245
+ const backText = computed(() => props.backLabel ?? t("common.back"));
222
246
  const navText = computed(() => props.navLabel ?? t("settings.sections"));
223
247
  const signOutText = computed(() => props.signOutLabel ?? t("settings.signOut"));
224
248
  const sessionLabelText = computed(() => props.sessionLabel ?? t("settings.session"));
@@ -276,12 +300,9 @@ function openPage(page) {
276
300
  function back() {
277
301
  goTo(pages.value.length - 1);
278
302
  }
279
- const breadcrumbItems = computed(() => [
280
- { label: active.value?.label ?? "", onClick: () => goTo(0) },
281
- ...pages.value.map((page, index) => ({
282
- label: page.label,
283
- onClick: index < pages.value.length - 1 ? () => goTo(index + 1) : void 0
284
- }))
303
+ const trail = computed(() => [
304
+ active.value?.label ?? "",
305
+ ...pages.value.slice(0, -1).map((page) => page.label)
285
306
  ]);
286
307
  function slotProps(item, close) {
287
308
  return { section: item, close, page: currentPage.value, pages: pages.value, openPage, back };
@@ -16,6 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
16
16
  presets?: readonly ThemePreset[] | false;
17
17
  title?: string | undefined;
18
18
  closeLabel?: string;
19
+ backLabel?: string;
19
20
  navLabel?: string;
20
21
  signOutLabel?: string;
21
22
  sessionLabel?: string;
@@ -257,6 +258,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
257
258
  presets?: readonly ThemePreset[] | false;
258
259
  title?: string | undefined;
259
260
  closeLabel?: string;
261
+ backLabel?: string;
260
262
  navLabel?: string;
261
263
  signOutLabel?: string;
262
264
  sessionLabel?: string;
@@ -50,9 +50,16 @@ export interface FormTranslateContext {
50
50
  params?: MessageParams;
51
51
  }
52
52
  export type FormTranslateMessage = (message: string, context: FormTranslateContext) => string;
53
+ export type FormIssueInput = StandardSchemaIssue;
54
+ export interface FormSubmitError {
55
+ message?: string;
56
+ issues?: ReadonlyArray<FormIssueInput>;
57
+ }
58
+ export type FormSubmitErrorResolver = (error: unknown) => FormSubmitError | undefined | null;
53
59
  export interface FormConfig {
54
60
  messages?: Partial<FormMessages>;
55
61
  translateMessage?: FormTranslateMessage;
62
+ submitError?: FormSubmitErrorResolver;
56
63
  }
57
64
  export type FormValidationTrigger = 'submit' | 'blur' | 'input';
58
65
  export type FormIssueKey = IssueKeyResolver;
@@ -72,6 +79,7 @@ export interface UseFormOptions<S extends FormSchema = FormSchema, V extends obj
72
79
  validateOn?: FormValidationTrigger;
73
80
  onSubmit?: (values: V) => unknown;
74
81
  onInvalid?: (errors: FormErrors<S>) => unknown;
82
+ submitError?: FormSubmitErrorResolver;
75
83
  }
76
84
  export type FormErrors<S extends FormSchema = FormSchema> = Partial<Record<keyof S & string, string>> & FormMessageMap;
77
85
  export interface FormContext {
@@ -105,6 +113,8 @@ export interface UseFormReturn<S extends FormSchema = FormSchema, V extends obje
105
113
  validateField: (name: keyof V & string) => Promise<boolean>;
106
114
  setError: (name: FormPath<V>, message: string | undefined) => void;
107
115
  setErrors: (errors: FormErrors<S>) => void;
116
+ setIssues: (issues: ReadonlyArray<FormIssueInput>) => void;
117
+ submitError: Readonly<Ref<FormSubmitError | null>>;
108
118
  clearErrors: () => void;
109
119
  touch: (name: string) => void;
110
120
  reset: (values?: Partial<V>) => void;
@@ -146,11 +146,14 @@ export function useForm(options) {
146
146
  const touched = reactive({});
147
147
  const submitCount = ref(0);
148
148
  const submitting = ref(false);
149
+ const submitError = shallowRef(null);
149
150
  const ruleErrors = {};
150
151
  const manualErrors = {};
151
152
  let issueErrors = {};
152
153
  let issueRoot = [];
153
154
  let manualRoot = [];
155
+ const serverKeys = /* @__PURE__ */ new Set();
156
+ let serverRoot = [];
154
157
  const readValue = (name) => target()[name];
155
158
  const dirtyFields = computed(() => {
156
159
  void baselineVersion.value;
@@ -172,7 +175,46 @@ export function useForm(options) {
172
175
  }
173
176
  function syncErrors(inScope) {
174
177
  replaceEntries(errors, composeErrors(), inScope);
175
- if (!inScope) rootErrors.value = uniqueMessages(manualRoot, issueRoot);
178
+ if (!inScope) syncRootErrors();
179
+ }
180
+ function syncRootErrors() {
181
+ rootErrors.value = uniqueMessages(manualRoot, serverRoot, issueRoot);
182
+ }
183
+ function dropServerErrors(inScope = () => true) {
184
+ let dropped = false;
185
+ for (const key of serverKeys) {
186
+ if (!inScope(key)) continue;
187
+ delete manualErrors[key];
188
+ serverKeys.delete(key);
189
+ dropped = true;
190
+ }
191
+ return dropped;
192
+ }
193
+ function applyIssues(issues) {
194
+ dropServerErrors();
195
+ const grouped = groupIssues(translateIssues(schemaIssues(issues), "error"), issueKey);
196
+ for (const [key, message] of Object.entries(grouped.fields)) {
197
+ manualErrors[key] = message;
198
+ serverKeys.add(key);
199
+ }
200
+ serverRoot = grouped.root;
201
+ return grouped;
202
+ }
203
+ function setIssues(issues) {
204
+ applyIssues(issues);
205
+ syncErrors();
206
+ }
207
+ function resolveSubmitError(error) {
208
+ const resolver = options.submitError ?? config?.submitError;
209
+ const result = resolver?.(error);
210
+ return result && (result.message || result.issues?.length) ? result : null;
211
+ }
212
+ function applySubmitError(result) {
213
+ const grouped = applyIssues(result.issues ?? []);
214
+ if (result.message && !Object.keys(grouped.fields).length)
215
+ serverRoot = uniqueMessages(serverRoot, [translate(result.message, { kind: "error" })]);
216
+ submitError.value = result;
217
+ syncErrors();
176
218
  }
177
219
  function evaluateValidator() {
178
220
  const validator = toValue(options.validator);
@@ -274,6 +316,8 @@ export function useForm(options) {
274
316
  fields.forEach((name, index) => ruleErrors[name] = results[index]);
275
317
  for (const key of Object.keys(manualErrors)) delete manualErrors[key];
276
318
  manualRoot = [];
319
+ serverKeys.clear();
320
+ serverRoot = [];
277
321
  syncErrors();
278
322
  }
279
323
  function isValidResult(issues, results) {
@@ -309,9 +353,10 @@ export function useForm(options) {
309
353
  function setError(name, message) {
310
354
  if (name === "") {
311
355
  manualRoot = message ? [message] : [];
312
- rootErrors.value = uniqueMessages(manualRoot, issueRoot);
356
+ syncRootErrors();
313
357
  return;
314
358
  }
359
+ serverKeys.delete(name);
315
360
  manualErrors[name] = message;
316
361
  if (!message) {
317
362
  delete ruleErrors[name];
@@ -323,6 +368,8 @@ export function useForm(options) {
323
368
  for (const key of Object.keys(manualErrors)) delete manualErrors[key];
324
369
  for (const key of Object.keys(ruleErrors)) delete ruleErrors[key];
325
370
  issueErrors = {};
371
+ serverKeys.clear();
372
+ serverRoot = [];
326
373
  for (const [key, message] of Object.entries(next)) {
327
374
  if (key === "") manualRoot = message ? [message] : [];
328
375
  else manualErrors[key] = message;
@@ -335,6 +382,9 @@ export function useForm(options) {
335
382
  issueErrors = {};
336
383
  issueRoot = [];
337
384
  manualRoot = [];
385
+ serverKeys.clear();
386
+ serverRoot = [];
387
+ submitError.value = null;
338
388
  syncErrors();
339
389
  }
340
390
  function touch(name) {
@@ -374,6 +424,7 @@ export function useForm(options) {
374
424
  async function submit() {
375
425
  if (submitting.value) return false;
376
426
  submitCount.value++;
427
+ submitError.value = null;
377
428
  const ok = await validate();
378
429
  if (!ok) {
379
430
  await options.onInvalid?.({ ...errors });
@@ -382,6 +433,11 @@ export function useForm(options) {
382
433
  submitting.value = true;
383
434
  try {
384
435
  await options.onSubmit?.(cloneValue(target()));
436
+ } catch (error) {
437
+ const result = resolveSubmitError(error);
438
+ if (!result) throw error;
439
+ applySubmitError(result);
440
+ return false;
385
441
  } finally {
386
442
  submitting.value = false;
387
443
  }
@@ -394,6 +450,8 @@ export function useForm(options) {
394
450
  const current = readValue(name);
395
451
  if (snapshots.has(name) && isEqualValue(current, snapshots.get(name))) continue;
396
452
  snapshots.set(name, cloneValue(current));
453
+ const inField = (key) => isWithin(key, name);
454
+ if (dropServerErrors(inField)) syncErrors(inField);
397
455
  if (validateOn === "input" || submitCount.value > 0 || validateOn === "blur" && touched[name])
398
456
  void validateField(name);
399
457
  }
@@ -406,6 +464,7 @@ export function useForm(options) {
406
464
  if (isWithin(key, name)) delete source[key];
407
465
  }
408
466
  }
467
+ dropServerErrors((key) => isWithin(key, name));
409
468
  }
410
469
  watch(
411
470
  schemaNames,
@@ -500,6 +559,8 @@ export function useForm(options) {
500
559
  validateField,
501
560
  setError,
502
561
  setErrors,
562
+ setIssues,
563
+ submitError: readonly(submitError),
503
564
  clearErrors,
504
565
  touch,
505
566
  isRequired: isFieldRequired,
@@ -3,7 +3,13 @@
3
3
  class="k-data-grid"
4
4
  :class="[
5
5
  `k-data-grid-${density}`,
6
- { 'is-loading': loading, 'is-auto-height': autoHeight, 'is-fill': fill && !autoHeight }
6
+ {
7
+ 'is-loading': loading,
8
+ 'is-auto-height': autoHeight,
9
+ 'is-fill': fill && !autoHeight,
10
+ 'is-spreadsheet': spreadsheet,
11
+ 'is-filling': spreadsheet && sheetFilling
12
+ }
7
13
  ]"
8
14
  >
9
15
  <GridToolbar
@@ -18,15 +24,20 @@
18
24
  :debounce="searchDebounce"
19
25
  @reset-columns="clearStoredState"
20
26
  >
21
- <slot name="actions" :api="api" />
27
+ <slot name="actions" :api="api" v-bind="sheetSlot" />
22
28
  </GridToolbar>
23
29
 
24
30
  <div
31
+ ref="bodyElement"
25
32
  class="k-data-grid-body"
26
33
  role="region"
27
34
  :aria-label="label ?? text.label"
28
35
  :aria-busy="loading ? 'true' : void 0"
29
36
  :style="autoHeight || fill ? void 0 : { height }"
37
+ @keydown.capture="sheet.onKeyDown"
38
+ @pointerdown.capture="sheet.onPointerDown"
39
+ @mousedown.capture="sheet.onMouseDown"
40
+ @pointerover="sheet.onPointerOver"
30
41
  >
31
42
  <AgGridVue
32
43
  v-if="mounted"
@@ -54,12 +65,15 @@
54
65
  :header-height="headerHeight ?? gridOptions?.headerHeight"
55
66
  :tooltip-show-delay="gridOptions?.tooltipShowDelay ?? 600"
56
67
  :suppress-cell-focus="false"
68
+ :undo-redo-cell-editing="spreadsheet ? false : gridOptions?.undoRedoCellEditing"
57
69
  v-on="gridEvents"
58
70
  />
59
71
  <div v-else class="k-data-grid-placeholder" aria-hidden="true">
60
72
  <Spinner decorative />
61
73
  </div>
62
- <p v-if="copied" class="k-data-grid-toast" aria-hidden="true">{{ text.copied }}</p>
74
+ <p v-if="notice || copied" class="k-data-grid-toast" aria-hidden="true">
75
+ {{ notice || text.copied }}
76
+ </p>
63
77
  <LiveRegion :message="loading || !mounted ? text.loading : live.message.value" />
64
78
  </div>
65
79
 
@@ -99,7 +113,7 @@
99
113
 
100
114
  <script setup>
101
115
  import { AgGridVue } from "ag-grid-vue3";
102
- import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
116
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
103
117
  import Pagination from "../components/data/Pagination.vue";
104
118
  import Spinner from "../components/core/Spinner.vue";
105
119
  import { useCopy } from "../composables/useCopy";
@@ -113,8 +127,10 @@ import { escapeHtml } from "../internal/core/text";
113
127
  import {
114
128
  registerGridEditorModules,
115
129
  registerGridFilterModules,
116
- registerGridModules
130
+ registerGridModules,
131
+ registerGridSpreadsheetModules
117
132
  } from "./modules";
133
+ import { createGridSpreadsheet } from "./spreadsheet";
118
134
  import { getGridTheme } from "./theme";
119
135
  registerGridModules();
120
136
  const props = defineProps({
@@ -150,25 +166,34 @@ const props = defineProps({
150
166
  headerHeight: { type: Number, required: false },
151
167
  headerText: { type: Function, required: false },
152
168
  filterable: { type: Boolean, required: false },
153
- editable: { type: Boolean, required: false }
169
+ editable: { type: Boolean, required: false },
170
+ spreadsheet: { type: Boolean, required: false }
154
171
  });
155
- const emit = defineEmits(["rowClick", "selectionChange", "sortChange", "filterChange", "copy"]);
172
+ const emit = defineEmits(["rowClick", "selectionChange", "sortChange", "filterChange", "copy", "cellsChange", "cellsInvalid", "dirtyChange", "rangeChange"]);
156
173
  const query = defineModel("query", { type: String, ...{ default: "" } });
157
174
  const page = defineModel("page", { type: Number, ...{ default: 1 } });
158
175
  const pageSize = defineModel("pageSize", { type: Number, ...{ default: 25 } });
159
176
  const sort = defineModel("sort", { type: null, ...{ default: void 0 } });
160
177
  if (props.filterable) registerGridFilterModules();
161
178
  function hasEditableColumn() {
162
- return props.editable || Boolean(props.gridOptions?.defaultColDef?.editable) || props.columns.some((column) => Boolean(column.editable));
179
+ return props.editable || props.spreadsheet || Boolean(props.gridOptions?.defaultColDef?.editable) || props.columns.some((column) => Boolean(column.editable));
163
180
  }
164
- if (hasEditableColumn()) registerGridEditorModules();
181
+ if (props.spreadsheet) registerGridSpreadsheetModules();
182
+ else if (hasEditableColumn()) registerGridEditorModules();
165
183
  const columnDefs = computed(() => {
166
184
  const headerText = props.headerText;
167
- if (!headerText) return props.columns;
185
+ const rules = props.spreadsheet ? sheet.classRules : void 0;
186
+ if (!headerText && !rules) return props.columns;
187
+ const defaultRules = props.gridOptions?.defaultColDef?.cellClassRules;
168
188
  return props.columns.map((column) => {
169
- if (column.headerName !== void 0 || !column.field) return column;
170
- const headerName = headerText(String(column.field), column);
171
- return headerName === void 0 ? column : { ...column, headerName };
189
+ let next = column;
190
+ if (headerText && column.headerName === void 0 && column.field) {
191
+ const headerName = headerText(String(column.field), column);
192
+ if (headerName !== void 0) next = { ...next, headerName };
193
+ }
194
+ if (rules)
195
+ next = { ...next, cellClassRules: { ...defaultRules, ...column.cellClassRules, ...rules } };
196
+ return next;
172
197
  });
173
198
  });
174
199
  const { locale, t } = useLocale();
@@ -212,6 +237,52 @@ const selectedCount = ref(0);
212
237
  const clientTotal = ref(0);
213
238
  const clientPageCount = ref(1);
214
239
  const { copied, copy } = useCopy();
240
+ const bodyElement = ref(null);
241
+ const notice = ref("");
242
+ let noticeTimer;
243
+ const noticeMessages = {
244
+ copied: "cellsCopied",
245
+ pasted: "cellsPasted",
246
+ filled: "cellsFilled",
247
+ cleared: "cellsCleared",
248
+ updated: "cellsUpdated",
249
+ undone: "changesUndone",
250
+ redone: "changesRedone"
251
+ };
252
+ function formatCount(template, count) {
253
+ return formatLocaleMessage(locale.value.code, template, { count });
254
+ }
255
+ function showNotice(kind, count, invalid) {
256
+ if (!count && !invalid) return;
257
+ const message = formatCount(text.value[noticeMessages[kind]], count);
258
+ notice.value = invalid ? `${message}, ${formatCount(text.value.invalidValues, invalid)}` : message;
259
+ live.announce(notice.value);
260
+ clearTimeout(noticeTimer);
261
+ noticeTimer = setTimeout(() => notice.value = "", 1600);
262
+ }
263
+ const sheet = createGridSpreadsheet({
264
+ enabled: () => Boolean(props.spreadsheet),
265
+ body: () => bodyElement.value,
266
+ gridOptions: () => props.gridOptions,
267
+ onChange: (event) => emit("cellsChange", event),
268
+ onInvalid: (event) => emit("cellsInvalid", event),
269
+ onDirty: (count) => emit("dirtyChange", count),
270
+ onRange: (range) => emit("rangeChange", range),
271
+ onCopy: (copiedText) => emit("copy", copiedText),
272
+ onSelect: (count) => live.announce(formatCount(text.value.cellsSelected, count)),
273
+ notify: showNotice
274
+ });
275
+ const { filling: sheetFilling, slotProps: sheetSlot } = sheet;
276
+ watch(
277
+ () => props.spreadsheet,
278
+ (enabled) => {
279
+ if (!enabled) sheet.disable();
280
+ }
281
+ );
282
+ onBeforeUnmount(() => {
283
+ clearTimeout(noticeTimer);
284
+ sheet.detach();
285
+ });
215
286
  const paginationEnabled = computed(() => props.pagination || props.serverPagination);
216
287
  const paginated = computed(() => Boolean(props.pagination) && !props.serverPagination);
217
288
  const showFooter = computed(() => paginationEnabled.value || Boolean(props.selectable));
@@ -293,6 +364,7 @@ function persistState(event) {
293
364
  }
294
365
  function onGridReady(event) {
295
366
  api.value = event.api;
367
+ sheet.attach(event.api);
296
368
  const stored = readStoredState();
297
369
  if (stored?.length) event.api.applyColumnState({ state: stored, applyOrder: true });
298
370
  applySort();
@@ -369,6 +441,10 @@ function onFilterChanged(event) {
369
441
  async function onCellContextMenu(event) {
370
442
  if (!event.node || !event.column) return;
371
443
  event.event?.preventDefault();
444
+ if (props.spreadsheet && sheet.containsCell(event.node.rowIndex, event.column.getColId())) {
445
+ await sheet.copyRange();
446
+ return;
447
+ }
372
448
  const value = event.api.getCellValue({
373
449
  rowNode: event.node,
374
450
  colKey: event.column,
@@ -399,6 +475,7 @@ onMounted(() => {
399
475
  mounted.value = true;
400
476
  });
401
477
  defineExpose({
478
+ ...sheet.methods,
402
479
  api,
403
480
  getSelectedRows: () => api.value?.getSelectedRows() ?? [],
404
481
  deselectAll: () => api.value?.deselectAll(),