@xleddyl/443-components-vue 0.16.3 → 0.16.5

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/CATALOG.md CHANGED
@@ -66,7 +66,7 @@ Playground paths in this file are relative to `playground/nuxt/app/pages/compone
66
66
  | Detail or edit panel from the side | `Drawer` (or `useDrawer().open`) |
67
67
  | Dialog with a form or content | `Modal` (or `useModal().open` to get the result as a promise) |
68
68
  | Buttons in a Modal or Drawer header | `#actions` slot of `Modal` / `Drawer` (right side, before the close button) |
69
- | Deep link to a drawer or modal (`?family=fam_1`, `?family=new`) | `useRouteState('family')` + `:open="!!familyId"`; settings: `route` on `SettingsModal`, `settingsRoute` on `DashboardLayout` |
69
+ | Deep link to a drawer or modal (`?family=fam_1`, `#family/fam_1`) | `useRouteState('family')` + `:open="!!familyId"`, `location: 'hash'`; settings: `route`, `settingsRoute` |
70
70
  | Menu opened by a button | `DropdownMenu` |
71
71
  | Right click menu | `ContextMenu` |
72
72
  | Small floating panel with content or a form | `Popover` |
@@ -249,25 +249,25 @@ Mount `OverlayHost` once for the imperative hooks (see [Composables](#composable
249
249
 
250
250
  Templates combine core parts into whole pages and layouts. Start from them for new pages.
251
251
 
252
- | Component | Purpose and when to use it | Main props, models, slots, events | Playground |
253
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
254
- | `DashboardLayout` | Ready app frame: brand, navigation, command search, account menu, settings modal, breadcrumbs, theme switch. Put it in `layouts/default.vue`. | Models: `v-model:collapsed`, `v-model:open`, `v-model:settingsOpen`, `v-model:settingsSection`. Props: `navigation` (`NavItem[]` or `NavSection[]`), `account` (`name`, `email`, `avatar`, `role`, `menu`), `brandName`, `brandIcon`, `brandLogo`, `breadcrumbs`, `search` (true, groups or function), `settingsSections`, `settingsRoute`, `settingsResolvePage`, `settingsHideSession`, `variant`, `content` (`page`, `fill`, `centered`, `bleed`; per page with `definePageMeta({ content })` in `k-dashboard`), `fluid`, `fill`, `onSignOut`. Slots: default, `actions`, `brand`, `sidebar`, `sidebar-footer`, `account`, `topbar`, `search-item`, `settings-account-fields`, `settings-section-<id>`, `settings-actions-<id>`. Event: `accountSelect`. | `templates/dashboard-layout.vue` |
255
- | `AuthLayout` | Login, sign up and reset password frame. Centered card or split with aside. | Props: `variant` (centered, split), `title`, `description`, `footer`, `brandName`, `brandIcon`, `brandLogo`, `asideTitle`, `asideDescription`, `themeSwitch`. Slots: default, `links`, `footer`, `brand`, `aside`, `corner`. | `templates/auth-layout.vue` |
256
- | `ListPage` | Whole list page: header, search, filters, table, pagination, bulk actions, loading, empty, no results and error states. Client or server mode (`total`). | Models: `page`, `pageSize`, `search`, `filter`, `sort`, `selected`. Props: `title`, `rows`, `columns`, `rowKey`, `rowTo`, `selectable`, `loading`, `error`, `total`, `paginate`, `searchable`, `searchKeys`, `filters`, `filterKey`, `filterMultiple`, `emptyTitle`. Slots: `actions`, `toolbar`, `toolbar-end`, `bulk`, `empty`, `empty-actions`, `no-results`, `error`, `cell-<key>`, `table`. Events: `retry`, `clear`, `rowClick`. | `templates/list-page.vue` |
257
- | `DetailPage` | Record page: back link, title, status, actions, tabs, main column and aside with description lists. | Model: `v-model:tab`. Props: `title`, `backTo`, `backLabel`, `status` (`label`, `tone`, `dot`), `tabs`, `sections` (`title`, `items`), `loading`, `error`. Slots: default, `actions`, `badge`, `meta`, `aside`, `aside-before`, `panel-<value>`. | `templates/detail-page.vue` |
258
- | `FormPage` | Form page: header, cards in a `Form`, sticky submit and cancel footer, dirty guard. | Props: `title`, `backTo`, `form` (from `useForm`), `dirty`, `saving`, `guard`, `requireDirty`, `submitLabel`, `cancelLabel`, `cancelTo`, `width`, `sticky`. Slots: default, `actions`, `footer`, `status`. Events: `submit`, `submitted`, `invalid`, `reset`, `cancel`. | `templates/form-page.vue` |
259
- | `DashboardPage` | Overview page: header, stat grid, content grid. | Props: `title`, `eyebrow`, `description`, `stats` (`PageStat[]`), `statsColumns`, `columns`, `loading`. Slots: default, `actions`, `stats`, `sparkline`, `before`, `after`, `eyebrow`. | `templates/dashboard-page.vue` |
260
- | `SettingsPage` | Settings as a page: section nav, panels, floating save bar when `dirty`. | Model: `v-model:section`. Props: `title`, `sections` (`value`, `label`, `icon`, `to`), `dirty`, `saving`, `saveBar`, `form`. Slots: `section-<value>`, `actions`, `nav-footer`. Events: `save`, `discard`. | `templates/settings-page.vue` |
261
- | `SettingsSection` | Card of one settings section with a save footer. | Props: `title`, `description`, `icon`, `hint`, `saveable`, `saving`, `saveDisabled`, `saveLabel`, `form`. Slots: default, `actions`, `footer`, `hint`. Event: `save`. | `templates/settings-page.vue` |
262
- | `SettingsGroup` | Block with label, description and actions inside a settings section. | Props: `label`, `description`, `level`, `row`. Slots: default, `label`, `description`, `actions`. | `templates/settings-modal.vue` |
263
- | `SettingsModal` | Settings dialog with nav, built-in account and appearance sections, your own sections and subpages. | Models: `v-model:open`, `v-model:section`, `v-model:pages`. Props: `sections` (`id`, `label`, `icon`, `group`, `badge`, `to`), `account`, `presets`, `title`, `onSignOut`, `hideSession`, `beforeClose`, `route` (sync with `?settings=<section>/<page>`), `resolvePage(section, id, parents)`. Slots: `section-<id>`, `actions-<id>` (right of the header row, also on a subpage), `account-fields`. | `templates/settings-modal.vue` |
264
- | `AppearanceSettings` | Theme (light, dark, system) and preset cards. Used by `SettingsModal`. Use alone in a settings page. | Props: `presets` (list or false), `themes`, `level`. | `presets.vue` (pages root) |
265
- | `SaveBar` | Floating bar with save and discard for unsaved changes. | Props: `visible`, `saving`, `message`, `saveLabel`, `discardLabel`, `form`. Slots: default, `actions`. Events: `save`, `discard`. | `templates/settings-page.vue` |
266
- | `EmptyPage` | First run screen with illustration, actions and onboarding steps. | Props: `title`, `description`, `icon`, `actionLabel`, `actionTo`, `onAction`, `secondaryLabel`, `steps` (`title`, `description`, `done`, `to`), `fill`. Slots: default, `illustration`, `title`, `description`, `actions`. | `templates/empty-page.vue` |
267
- | `ErrorPage` | Error screen. Reads a Nuxt `error`. Use it in `app/error.vue`. | Props: `error`, `statusCode`, `title`, `message`, `homeTo`, `homeLabel`, `retryLabel`, `brandName`, `fill`, `onHome`, `onRetry`. Slots: default, `title`, `message`, `actions`, `brand`. | `templates/error-page.vue` |
268
- | `NotFoundPage` | 404 screen. | Props: `title`, `message`, `homeTo`, `homeLabel`, `backLabel`, `brandName`, `fill`, `onHome`. Slots: default, `actions`, `brand`. | `templates/error-page.vue` |
269
- | `Brand` | Logo and name block (image URL, inline SVG or component). | Props: `name`, `subtitle`, `icon`, `logo`, `to`, `size`, `accent`, `hideText`, `showName`. Slots: default, `logo`, `mark`, `subtitle`. | `templates/dashboard-layout.vue` |
270
- | `AccountMenu` | Account button with avatar, name and a dropdown menu. Used by `DashboardLayout`. | Props: `name`, `email`, `avatar`, `menu` (menu items), `placement`, `icon`. Event: `select`. | `templates/dashboard-layout.vue` |
252
+ | Component | Purpose and when to use it | Main props, models, slots, events | Playground |
253
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
254
+ | `DashboardLayout` | Ready app frame: brand, navigation, command search, account menu, settings modal, breadcrumbs, theme switch. Put it in `layouts/default.vue`. | Models: `v-model:collapsed`, `v-model:open`, `v-model:settingsOpen`, `v-model:settingsSection`. Props: `navigation` (`NavItem[]` or `NavSection[]`), `account` (`name`, `email`, `avatar`, `role`, `menu`), `brandName`, `brandIcon`, `brandLogo`, `breadcrumbs`, `search` (true, groups or function), `settingsSections`, `settingsRoute`, `settingsRouteLocation`, `settingsAccountSection`, `settingsResolvePage`, `settingsHideSession`, `variant`, `content` (`page`, `fill`, `centered`, `bleed`; per page with `definePageMeta({ content })` in `k-dashboard`), `fluid`, `fill`, `onSignOut`. Slots: default, `actions`, `brand`, `sidebar`, `sidebar-footer`, `account`, `topbar`, `search-item`, `settings-account-fields`, `settings-section-<id>`, `settings-actions-<id>`. Event: `accountSelect`. | `templates/dashboard-layout.vue` |
255
+ | `AuthLayout` | Login, sign up and reset password frame. Centered card or split with aside. | Props: `variant` (centered, split), `title`, `description`, `footer`, `brandName`, `brandIcon`, `brandLogo`, `asideTitle`, `asideDescription`, `themeSwitch`. Slots: default, `links`, `footer`, `brand`, `aside`, `corner`. | `templates/auth-layout.vue` |
256
+ | `ListPage` | Whole list page: header, search, filters, table, pagination, bulk actions, loading, empty, no results and error states. Client or server mode (`total`). | Models: `page`, `pageSize`, `search`, `filter`, `sort`, `selected`. Props: `title`, `rows`, `columns`, `rowKey`, `rowTo`, `selectable`, `loading`, `error`, `total`, `paginate`, `searchable`, `searchKeys`, `filters`, `filterKey`, `filterMultiple`, `emptyTitle`. Slots: `actions`, `toolbar`, `toolbar-end`, `bulk`, `empty`, `empty-actions`, `no-results`, `error`, `cell-<key>`, `table`. Events: `retry`, `clear`, `rowClick`. | `templates/list-page.vue` |
257
+ | `DetailPage` | Record page: back link, title, status, actions, tabs, main column and aside with description lists. | Model: `v-model:tab`. Props: `title`, `backTo`, `backLabel`, `status` (`label`, `tone`, `dot`), `tabs`, `sections` (`title`, `items`), `loading`, `error`. Slots: default, `actions`, `badge`, `meta`, `aside`, `aside-before`, `panel-<value>`. | `templates/detail-page.vue` |
258
+ | `FormPage` | Form page: header, cards in a `Form`, sticky submit and cancel footer, dirty guard. | Props: `title`, `backTo`, `form` (from `useForm`), `dirty`, `saving`, `guard`, `requireDirty`, `submitLabel`, `cancelLabel`, `cancelTo`, `width`, `sticky`. Slots: default, `actions`, `footer`, `status`. Events: `submit`, `submitted`, `invalid`, `reset`, `cancel`. | `templates/form-page.vue` |
259
+ | `DashboardPage` | Overview page: header, stat grid, content grid. | Props: `title`, `eyebrow`, `description`, `stats` (`PageStat[]`), `statsColumns`, `columns`, `loading`. Slots: default, `actions`, `stats`, `sparkline`, `before`, `after`, `eyebrow`. | `templates/dashboard-page.vue` |
260
+ | `SettingsPage` | Settings as a page: section nav, panels, floating save bar when `dirty`. | Model: `v-model:section`. Props: `title`, `sections` (`value`, `label`, `icon`, `to`), `dirty`, `saving`, `saveBar`, `form`. Slots: `section-<value>`, `actions`, `nav-footer`. Events: `save`, `discard`. | `templates/settings-page.vue` |
261
+ | `SettingsSection` | Card of one settings section with a save footer. | Props: `title`, `description`, `icon`, `hint`, `saveable`, `saving`, `saveDisabled`, `saveLabel`, `form`. Slots: default, `actions`, `footer`, `hint`. Event: `save`. | `templates/settings-page.vue` |
262
+ | `SettingsGroup` | Block with label, description and actions inside a settings section. | Props: `label`, `description`, `level`, `row`. Slots: default, `label`, `description`, `actions`. | `templates/settings-modal.vue` |
263
+ | `SettingsModal` | Settings dialog with nav, built-in account and appearance sections, your own sections and subpages. | Models: `v-model:open`, `v-model:section`, `v-model:pages`. Props: `sections` (`id`, `label`, `icon`, `group`, `badge`, `to`), `account`, `presets`, `title`, `onSignOut`, `hideSession`, `beforeClose`, `route` (sync with `?settings=<section>/<page>`), `routeLocation` (`'hash'`: `#settings/<section>/<page>`), `accountSection` (id with the built-in account content, default `account`), `resolvePage(section, id, parents)`. Slots: `section-<id>`, `actions-<id>` (right of the header row, also on a subpage), `account-fields`. | `templates/settings-modal.vue` |
264
+ | `AppearanceSettings` | Theme (light, dark, system) and preset cards. Used by `SettingsModal`. Use alone in a settings page. | Props: `presets` (list or false), `themes`, `level`. | `presets.vue` (pages root) |
265
+ | `SaveBar` | Floating bar with save and discard for unsaved changes. | Props: `visible`, `saving`, `message`, `saveLabel`, `discardLabel`, `form`. Slots: default, `actions`. Events: `save`, `discard`. | `templates/settings-page.vue` |
266
+ | `EmptyPage` | First run screen with illustration, actions and onboarding steps. | Props: `title`, `description`, `icon`, `actionLabel`, `actionTo`, `onAction`, `secondaryLabel`, `steps` (`title`, `description`, `done`, `to`), `fill`. Slots: default, `illustration`, `title`, `description`, `actions`. | `templates/empty-page.vue` |
267
+ | `ErrorPage` | Error screen. Reads a Nuxt `error`. Use it in `app/error.vue`. | Props: `error`, `statusCode`, `title`, `message`, `homeTo`, `homeLabel`, `retryLabel`, `brandName`, `fill`, `onHome`, `onRetry`. Slots: default, `title`, `message`, `actions`, `brand`. | `templates/error-page.vue` |
268
+ | `NotFoundPage` | 404 screen. | Props: `title`, `message`, `homeTo`, `homeLabel`, `backLabel`, `brandName`, `fill`, `onHome`. Slots: default, `actions`, `brand`. | `templates/error-page.vue` |
269
+ | `Brand` | Logo and name block (image URL, inline SVG or component). | Props: `name`, `subtitle`, `icon`, `logo`, `to`, `size`, `accent`, `hideText`, `showName`. Slots: default, `logo`, `mark`, `subtitle`. | `templates/dashboard-layout.vue` |
270
+ | `AccountMenu` | Account button with avatar, name and a dropdown menu. Used by `DashboardLayout`. | Props: `name`, `email`, `avatar`, `menu` (menu items), `placement`, `icon`. Event: `select`. | `templates/dashboard-layout.vue` |
271
271
 
272
272
  Nuxt ready layouts: with `k: { layouts: true }` the module registers the layouts `k-dashboard` and `k-auth`. They read brand, navigation, account and settings from `app.config.ts` (`k` key). `useDashboard()` gives the runtime account, breadcrumbs and badges for `k-dashboard`. See README, section "Nuxt: ready layouts".
273
273
 
@@ -275,12 +275,12 @@ Nuxt ready layouts: with `k: { layouts: true }` the module registers the layouts
275
275
 
276
276
  Import from `@xleddyl/443-components-vue/grid`. It wraps AG Grid Community.
277
277
 
278
- | Component | Purpose and when to use it | Main props, models, slots, events | Playground |
279
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
280
- | `DataGrid` | Big table: virtual scroll, column menu, search, CSV export, selection, server sort and paging, inline editing. For small tables use `Table`. | Models: `v-model:query`, `v-model:page`, `v-model:pageSize`, `v-model:sort` (`{ column, direction }`). Props: `rows`, `columns` (AG Grid `ColDef`), `rowIdKey` or `getRowId`, `storageKey` (columns and page size in the preferences store), `density`, `selectable`, `loading`, `height`, `fill`, `autoHeight`, `toolbar`, `searchable`, `columnMenu`, `exportable`, `serverSort`, `pagination`, `serverPagination`, `total`, `filterable`, `editable`, `spreadsheet`, `gridOptions`, `messages`, `filterCount`. Slots: `actions`, `filters` (`{ close, count }`, hosted in a modal opened by a funnel button next to the search). Events: `filtersReset`, `rowClick`, `selectionChange`, `sortChange`, `filterChange`, `cellsChange`, `cellsInvalid`, `dirtyChange`, `rangeChange`, `copy`. | `grid/data-grid.vue` |
281
- | `DataGrid` with `spreadsheet` | Excel-like editing: cell range selection, copy and paste (TSV), fill down and right, clear, undo and redo, dirty cell tracking. Set `rowIdKey` or `getRowId`. | Template ref methods (`GridSpreadsheetMethods`): `getRange`, `getSelectedCells`, `selectRange`, `selectAll`, `setRangeValue`, `copyRange`, `paste`, `fillDown`, `fillRight`, `clearCells`, `undo`, `redo`, `getDirtyCells`, `clearDirty`. The `actions` slot gets the same methods plus `range`, `selectedCellCount`, `dirtyCount`, `canUndo`, `canRedo`. Use `cellsChange` to save edits, `cellsInvalid` for rejected input. | `grid/spreadsheet.vue` |
282
- | `GridToolbar` | Toolbar of `DataGrid` (search, column menu, export). `DataGrid` already renders it. Use it alone only for a custom grid setup. | Props: `api`, `searchable`, `columnMenu`, `exportable`, `exportFileName`, `debounce`, `filterCount`. Model: `v-model` (query). Slots: default (actions), `filters`. Events: `resetColumns`, `filtersReset`. | - |
283
- | Cell renderers | Use as `cellRenderer` in a column definition, with `cellRendererParams`. | `BadgeCell` (`tones`, `labels`, `defaultTone`, `variant`; array values render one badge each, `max` adds a `+N` badge, `wrap`), `BooleanCell` (`trueLabel`, `falseLabel`), `DateCell` (`locale`, `options`, `relative`), `EmptyCell` (placeholder for empty values), `ImageCell` (`alt`, `rounded`, `fit`, `size`), `LinkCell` (`href`, `label`, `target`), `ButtonCell` (`onClick`, `label`, `ariaLabel`, `emptyLabel`; an inline button that does not trigger the row click). | `grid/data-grid.vue` |
278
+ | Component | Purpose and when to use it | Main props, models, slots, events | Playground |
279
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
280
+ | `DataGrid` | Big table: virtual scroll, column menu, search, CSV export, selection, server sort and paging, inline editing. For small tables use `Table`. | Models: `v-model:query`, `v-model:page`, `v-model:pageSize`, `v-model:sort` (`{ column, direction }`). Props: `rows`, `columns` (AG Grid `ColDef`), `rowIdKey` or `getRowId`, `storageKey` (columns and page size in the preferences store), `density`, `selectable`, `loading`, `height`, `fill`, `autoHeight`, `toolbar`, `searchable`, `columnMenu`, `exportable`, `serverSort`, `pagination`, `serverPagination`, `total`, `filterable`, `editable`, `spreadsheet`, `gridOptions`, `messages`, `filterCount`. Slots: `actions`, `actions-end` (after the column menu and export buttons), `filters` (`{ close, count }`, hosted in a modal opened by a funnel button next to the search). Events: `filtersReset`, `rowClick`, `selectionChange`, `sortChange`, `filterChange`, `cellsChange`, `cellsInvalid`, `dirtyChange`, `rangeChange`, `copy`. | `grid/data-grid.vue` |
281
+ | `DataGrid` with `spreadsheet` | Excel-like editing: cell range selection, copy and paste (TSV), fill down and right, clear, undo and redo, dirty cell tracking. Set `rowIdKey` or `getRowId`. | Template ref methods (`GridSpreadsheetMethods`): `getRange`, `getSelectedCells`, `selectRange`, `selectAll`, `setRangeValue`, `copyRange`, `paste`, `fillDown`, `fillRight`, `clearCells`, `undo`, `redo`, `getDirtyCells`, `clearDirty`. The `actions` slot gets the same methods plus `range`, `selectedCellCount`, `dirtyCount`, `canUndo`, `canRedo`. Use `cellsChange` to save edits, `cellsInvalid` for rejected input. | `grid/spreadsheet.vue` |
282
+ | `GridToolbar` | Toolbar of `DataGrid` (search, column menu, export). `DataGrid` already renders it. Use it alone only for a custom grid setup. | Props: `api`, `searchable`, `columnMenu`, `exportable`, `exportFileName`, `debounce`, `filterCount`. Model: `v-model` (query). Slots: default (actions), `filters`. Events: `resetColumns`, `filtersReset`. | - |
283
+ | Cell renderers | Use as `cellRenderer` in a column definition, with `cellRendererParams`. | `BadgeCell` (`tones`, `labels`, `defaultTone`, `variant`; array values render one badge each, `max` adds a `+N` badge, `wrap`), `BooleanCell` (`trueLabel`, `falseLabel`), `DateCell` (`locale`, `options`, `relative`), `EmptyCell` (placeholder for empty values), `ImageCell` (`alt`, `rounded`, `fit`, `size`), `LinkCell` (`href`, `label`, `target`), `ButtonCell` (`onClick`, `label`, `ariaLabel`, `emptyLabel`; an inline button that does not trigger the row click). | `grid/data-grid.vue` |
284
284
 
285
285
  Grid helpers: `registerGridModules`, `registerGridEditorModules`, `registerGridFilterModules`, `registerGridSpreadsheetModules` (register AG Grid modules by hand), `createGridTheme`, `parseTsv`, `formatTsv`.
286
286
 
@@ -331,7 +331,7 @@ All are exported from `@xleddyl/443-components-vue` and auto-imported in Nuxt.
331
331
  | `usePreset` | Read or set the visual preset. | `presetOptions` lists the presets. | `presets.vue` (pages root) |
332
332
  | `usePreference`, `usePreferences`, `createPreferences`, `providePreferences` | Remember UI choices per user (theme, preset, sidebar collapsed, DataGrid columns and page size, your own keys) in one pluggable store. | `app.use(createPreferences(store))`; a store is `{ get, set, subscribe?, clientOnly? }`. `createRemotePreferenceStore({ initial, save, debounce })` for an API, `createLocalPreferenceStore()` is the default. `usePreference(key, fallback, { validate })` is a writable computed. Keys: `theme`, `preset`, `shell:<storageKey>`, `grid:<storageKey>` (`PREFERENCE_KEYS`). | `core/preferences.vue` |
333
333
  | `useAppShell` | Read the `AppShell` state (collapsed, open) from a child component. | Returns the shell context or `null` outside a shell. | `layout/app-shell.vue` |
334
- | `useRouteState` | Bind a ref to a URL query parameter: deep links to drawers and modals, back button closes them, filters in the URL. | `useRouteState(key, { parse, serialize, default, history: 'push' \| 'replace' })`. Returns a ref; `undefined` or `''` removes the parameter. No-op without a router. | `overlays/drawer.vue` |
334
+ | `useRouteState` | Bind a ref to a URL query parameter or hash entry: deep links to drawers and modals, back button closes them, filters in the URL. | `useRouteState(key, { parse, serialize, default, history: 'push' \| 'replace', location: 'query' \| 'hash' })`. Returns a ref; `undefined` or `''` removes the parameter. Hash entries are `key/value` joined with `&` (`#family/fam_1&variant/var_2`), read after mount. `withRouteStateScroll(scrollBehavior)` for plain vue-router. No-op without a router. | `overlays/drawer.vue` |
335
335
  | `useScrollLock` | Lock the page scroll while something is open. | `useScrollLock(activeRef)`. | - |
336
336
  | `useLocale` | Translate component texts and read the active locale (en, it, de). | `const { locale, t } = useLocale()`. `provideLocale` and `createLocale` set it. Nuxt follows `k.locale`. | `languages.vue` (pages root) |
337
337
  | `useIcons` helpers, `createIcons`, `provideIcons` | Register your own icons by name. | Nuxt: option `k.icons: { dir: '~/assets/icons' }`. | `components/core/icon.vue` |
package/README.md CHANGED
@@ -49,6 +49,7 @@ Every component is registered with the prefix (`Button.vue` becomes `<KButton>`)
49
49
  | `cssLayers` | `'theme, base, k, components, utilities'` | Cascade layer order declared before any other CSS. The library layer `k` sits after the `theme` and `base` layers of Tailwind (so preflight does not reset components) and before `components` and `utilities` (so host classes win). `false` declares nothing. See [Scoping](#scoping). |
50
50
  | `locale` | `'en'` | Language of the built-in copy: `'en'`, `'it'`, `'de'`, or any code completed with `messages`. With `@nuxtjs/i18n` the current i18n locale wins. See [Languages](#languages). |
51
51
  | `messages` | `{}` | Overrides per locale code, for example `{ it: { common: { cancel: 'Indietro' } } }`. |
52
+ | `routeState.location` | `'query'` | Where `useRouteState` and the settings modal `route` keep their value by default: `'query'` (`?family=fam_1`) or `'hash'` (`#family/fam_1`). See [Deep links](#deep-links). |
52
53
  | `composables` | `true` | Auto-imports the composables. |
53
54
  | `global` | `false` | Puts `k-root` and `data-theme` on `<html>` for apps that are dashboard only. |
54
55
  | `theme.cookie` | `'k-theme'` | Cookie that stores the theme preference (SSR safe, no flash). `false` sets no cookie and always renders `theme.default`. |
@@ -557,7 +558,7 @@ const confirmDiscard = () => !dirty.value || confirm({ title: 'Discard changes?'
557
558
 
558
559
  ### Deep links
559
560
 
560
- `useRouteState(key, options?)` binds a ref to one query parameter of the current URL, so any Drawer or Modal can be opened from a link, survives a reload and closes with the browser back button. It reads the router through `$router` (vue-router or Nuxt, no import of either); without a router it is a plain ref.
561
+ `useRouteState(key, options?)` binds a ref to one query parameter (or one hash entry) of the current URL, so any Drawer or Modal can be opened from a link, survives a reload and closes with the browser back button. It reads the router through `$router` (vue-router or Nuxt, no import of either); without a router it is a plain ref.
561
562
 
562
563
  ```ts
563
564
  const familyId = useRouteState('family')
@@ -587,6 +588,8 @@ const archived = useRouteState('archived', {
587
588
  })
588
589
  ```
589
590
 
591
+ - Hash: `location: 'hash'` keeps the value in the hash instead of the query, like `#settings/general`. One entry is `key/value` (the value may contain `/`); several entries are joined with `&` in the order they were opened: `#family/fam_1&variant/var_2`. Query and hash writes in the same tick make one navigation. Set the default for the app with `k.routeState.location` in the Nuxt module, or `app.provide(ROUTE_LOCATION_KEY, 'hash')` in plain Vue; an explicit `location` wins. Use the hash for overlays and the query for page state (filters, tabs). The server never receives the hash, so a hash value is read after mount: the server and hydration render the overlay closed, then it opens.
592
+ - Scroll: a hash made only of `key/value` entries is not an anchor. The Nuxt module wraps the router `scrollBehavior` so that it sees no hash for these routes (no scroll to top when an overlay closes, no lookup of `#settings/general` as an element). With plain vue-router wrap your own: `scrollBehavior: withRouteStateScroll((to, from, saved) => ...)`.
590
593
  - Create forms: use a reserved value (`?family=new`) and branch on it, like above.
591
594
  - Nested views inside one overlay: keep the path in one parameter (`?settings=users/usr_123`, `?family=fam_1/variants`) and split it on `/` in a computed. One parameter keeps every step in one history entry and avoids two refs racing. Encode ids that may contain `/` with `encodeURIComponent` per segment.
592
595
  - Pages that set `definePageMeta({ key: (route) => route.fullPath })` remount on every query change; keep the default page key for pages with deep links.
@@ -627,7 +630,7 @@ Ready defaults to start a dashboard project: an app frame, an auth frame and pag
627
630
  | `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. `brandAlign` (`'start'` or `'center'`) centers the brand in the expanded sidebar, with the collapse button on the right. The collapse button uses the icons `sidebar-collapse` and `sidebar-expand` (chevrons by default), so `k.icons.aliases: { 'sidebar-collapse': 'lucide:panel-left-close', 'sidebar-expand': 'lucide:panel-left-open' }` changes only that button. |
628
631
  | `account` | | `{ name, email?, avatar?, role?, menu? }`. Shown as a row (avatar, name, email, chevron) at the bottom of the sidebar. |
629
632
  | `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. |
630
- | `settingsRoute`, `settingsResolvePage` | `false` | Sync the settings modal with the URL (`?settings=users/usr_123`): `true` uses the key `settings`, a string sets the key. `settingsResolvePage(section, id, parents)` gives the label of a subpage restored from the URL. See SettingsModal. |
633
+ | `settingsRoute`, `settingsResolvePage` | `false` | Sync the settings modal with the URL (`?settings=users/usr_123`): `true` uses the key `settings`, a string sets the key. `settingsRouteLocation: 'hash'` gives `#settings/users/usr_123`. `settingsAccountSection` renames the built-in account section. `settingsResolvePage(section, id, parents)` gives the label of a subpage restored from the URL. See SettingsModal. |
631
634
  | `settingsHideSession` | `false` | Hides the Session group (sign out) in the account section of the settings modal. The Sign out item in the nav stays. |
632
635
  | `breadcrumbs` | | Rendered in the desktop topbar. `#topbar` replaces them, `#actions` adds buttons on the right. |
633
636
  | `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. |
@@ -677,7 +680,8 @@ A large dialog (60rem) with a section nav on the left and the content on the rig
677
680
  - Labels: `title` (accessible name of the dialog, default `Settings`), `closeLabel`, `navLabel`, `signOutLabel`, `accountLabel`, `accountText`, `appearanceLabel`, `appearanceText`, `sessionLabel`, `sessionText`, `themeLabel`, `themeText`, `presetLabel`, `presetText`.
678
681
  - Header actions: `#actions-<id>` renders buttons on the right of the section header row (an "Add user" button), next to the title instead of on a row of its own in the body. It stays there when a subpage is open, so render different actions per page from its `page` prop. When the content is too narrow, the actions wrap under the title. On `DashboardLayout` the slot is `#settings-actions-<id>`.
679
682
  - 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.
680
- - Deep links: `route` (`true` for the key `settings`, or your own query key, default `false`) syncs the open state, the section and the subpage stack with the URL: `?settings=users` for a section, `?settings=users/usr_123` for a subpage, deeper pages add segments. Opening, switching section, opening a subpage, Back and the crumbs push history entries, so the browser back button steps back through them and finally closes the dialog; closing removes the parameter. Loading a URL with the parameter opens the dialog at that section and subpage after mount. A section id that is not selectable falls back to the first section and the URL is corrected with `replace`. The URL holds only page ids, so `resolvePage(section, id, parents)` returns the label of a page restored from the URL: a string, `undefined` (the page and the ones after it are dropped, the URL is corrected) or a promise of either (the page opens at once with an empty label, then the label fills in). Without `resolvePage` the label is the id. Pages opened with `openPage` keep their own label. Link sections (`to`) do not close the dialog themselves when the route is synced: the navigation drops the parameter and closes it. On `DashboardLayout` the props are `settingsRoute` and `settingsResolvePage`; in the `k-dashboard` layout `settings.route` in `app.config`.
683
+ - Account section: `accountSection` (default `'account'`) is the id of the section that shows the built-in account content (profile card, `#account-fields`, session group), so a section `{ id: 'general' }` can hold it.
684
+ - Deep links: `route` (`true` for the key `settings`, or your own query key, default `false`) syncs the open state, the section and the subpage stack with the URL: `?settings=users` for a section, `?settings=users/usr_123` for a subpage, deeper pages add segments. Opening, switching section, opening a subpage, Back and the crumbs push history entries, so the browser back button steps back through them and finally closes the dialog; closing removes the parameter. Loading a URL with the parameter opens the dialog at that section and subpage after mount. A section id that is not selectable falls back to the first section and the URL is corrected with `replace`. The URL holds only page ids, so `resolvePage(section, id, parents)` returns the label of a page restored from the URL: a string, `undefined` (the page and the ones after it are dropped, the URL is corrected) or a promise of either (the page opens at once with an empty label, then the label fills in). Without `resolvePage` the label is the id. Pages opened with `openPage` keep their own label. Link sections (`to`) do not close the dialog themselves when the route is synced: the navigation drops the parameter and closes it. `routeLocation: 'hash'` (default from `k.routeState.location`) keeps the same value in the hash: `#settings/users/usr_123`. On `DashboardLayout` the props are `settingsRoute`, `settingsRouteLocation` and `settingsResolvePage`; in the `k-dashboard` layout `settings.route` in `app.config`.
681
685
  - `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.
682
686
  - 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.
683
687
 
@@ -826,6 +830,7 @@ Accessibility related props and defaults:
826
830
  - `DataGrid` sort is a model: `v-model:sort` (`{ column, direction } | null`) sets the sorted column from outside (server side sort, a sort restored from the URL) and follows header clicks. Leave it unset for an uncontrolled grid. `sortChange` still fires with `serverSort`.
827
831
  - `DataGrid` takes `rowHeight` and `headerHeight` (pixels) for an exact row size on top of `density`, and `headerText: (field, column) => string | undefined` to name every column that has a `field` and no `headerName` (from your i18n).
828
832
  - `DataGrid` `filterable` registers the AG Grid text, number and date filters (`registerGridFilterModules()` does it without the prop) and turns `filter` on for every column. `filterChange(model, event)` fires with `api.getFilterModel()` whenever a column filter changes, also when the filters come from `gridOptions` or your own registered modules.
833
+ - `DataGrid` `#actions-end` slot: same props as `#actions`, rendered at the end of the toolbar, after the column menu and export buttons (a view switch). `GridToolbar` takes it as `#end`.
829
834
  - `DataGrid` `#filters` slot: put your own filter controls (Select, Combobox, DateInput bound with `v-model`) in it and the toolbar shows a square funnel button right after the search input (at the start with `:searchable="false"`), before the `actions`. The button opens a `Modal` titled "Filters" that hosts the slot, so the filters still apply live; its footer has "Reset filters" (emits `filtersReset`, disabled while `filterCount` is 0) and "Show results" (closes it). `filterCount` shows the number of active filters as a badge on the button and in its accessible name ("Filters, 2 active", `grid.filtersActive`). Slot props: `{ close, count }`. Focus returns to the button on close, and the modal becomes a bottom sheet on small screens. `GridToolbar` takes the same slot, prop and event.
830
835
 
831
836
  ```vue
@@ -146,6 +146,8 @@
146
146
  :title="settingsTitleText"
147
147
  :on-sign-out="onSignOut"
148
148
  :route="settingsRoute"
149
+ :route-location="settingsRouteLocation"
150
+ :account-section="settingsAccountSection"
149
151
  :hide-session="settingsHideSession"
150
152
  :resolve-page="settingsResolvePage"
151
153
  >
@@ -191,6 +193,8 @@ const props = defineProps({
191
193
  settingsPresets: { type: [Array, Boolean], required: false, default: void 0 },
192
194
  settingsTitle: { type: String, required: false },
193
195
  settingsRoute: { type: [String, Boolean], required: false, default: false },
196
+ settingsRouteLocation: { type: String, required: false, default: void 0 },
197
+ settingsAccountSection: { type: String, required: false, default: void 0 },
194
198
  settingsHideSession: { type: Boolean, required: false },
195
199
  settingsResolvePage: { type: Function, required: false },
196
200
  onSignOut: { type: Function, required: false },
@@ -1,4 +1,5 @@
1
1
  import type { IconName } from '../../composables/useIcons';
2
+ import type { RouteLocation } from '../../composables/useRouteState';
2
3
  import type { BreadcrumbItem, ShellContent, SidebarBrandAlign } from '../layout/types';
3
4
  import type { CommandGroupLabelSlotProps, CommandItem, MenuItem } from '../overlays/types';
4
5
  import type { ThemePreset } from '../../composables/usePreset';
@@ -22,6 +23,8 @@ declare const __VLS_export: <T = unknown>(__VLS_props: NonNullable<Awaited<typeo
22
23
  settingsPresets?: readonly ThemePreset[] | false;
23
24
  settingsTitle?: string;
24
25
  settingsRoute?: string | boolean;
26
+ settingsRouteLocation?: RouteLocation;
27
+ settingsAccountSection?: string;
25
28
  settingsHideSession?: boolean;
26
29
  settingsResolvePage?: SettingsPageResolver;
27
30
  onSignOut?: () => void;
@@ -146,7 +146,7 @@
146
146
  }"
147
147
  >
148
148
  <slot :name="`section-${active.id}`" v-bind="slotProps(active, close)">
149
- <template v-if="active.id === 'account' && account">
149
+ <template v-if="active.id === accountSection && account">
150
150
  <div class="k-settings-modal-account">
151
151
  <Avatar
152
152
  :name="account.name"
@@ -204,7 +204,11 @@ import { THEME_PRESETS } from "../../composables/usePreset";
204
204
  import { useLocale } from "../../composables/useLocale";
205
205
  import { useLinkResolver } from "../../internal/core/link";
206
206
  import { useScrollEdges } from "../../internal/core/scroll-edges";
207
- import { useQueryValue, writeQueryValue } from "../../internal/layout/query";
207
+ import {
208
+ useDefaultRouteLocation,
209
+ useRouteValue,
210
+ writeRouteValue
211
+ } from "../../internal/layout/query";
208
212
  import { useRouterLike } from "../../internal/layout/route";
209
213
  import Button from "../core/Button.vue";
210
214
  import Icon from "../core/Icon.vue";
@@ -236,6 +240,8 @@ const props = defineProps({
236
240
  hideSession: { type: Boolean, required: false },
237
241
  beforeClose: { type: Function, required: false },
238
242
  route: { type: [String, Boolean], required: false, default: false },
243
+ routeLocation: { type: String, required: false, default: void 0 },
244
+ accountSection: { type: String, required: false, default: "account" },
239
245
  resolvePage: { type: Function, required: false }
240
246
  });
241
247
  defineSlots();
@@ -263,7 +269,7 @@ const sectionList = computed(() => {
263
269
  const list = [];
264
270
  if (props.account)
265
271
  list.push({
266
- id: "account",
272
+ id: props.accountSection,
267
273
  label: props.accountLabel ?? t("account.label"),
268
274
  icon: "user-circle",
269
275
  description: props.accountText ?? t("settings.accountDescription")
@@ -313,7 +319,9 @@ function slotProps(item, close) {
313
319
  }
314
320
  const router = useRouterLike();
315
321
  const routeKey = computed(() => props.route === true ? "settings" : props.route || void 0);
316
- const routeParam = useQueryValue(router, routeKey);
322
+ const defaultLocation = useDefaultRouteLocation();
323
+ const location = computed(() => props.routeLocation ?? defaultLocation);
324
+ const routeParam = useRouteValue(router, routeKey, location);
317
325
  const syncing = computed(() => Boolean(router && routeKey.value));
318
326
  let applying = false;
319
327
  let resolution = 0;
@@ -331,7 +339,7 @@ function decodeSegment(segment) {
331
339
  function writeRoute(value, history) {
332
340
  const key = routeKey.value;
333
341
  if (!router || !key || value === routeParam.value) return;
334
- void writeQueryValue(router, key, value, history);
342
+ void writeRouteValue(router, key, value, history, location.value);
335
343
  }
336
344
  async function settle() {
337
345
  await nextTick();
@@ -1,4 +1,5 @@
1
1
  import type { ThemePreset } from '../../composables/usePreset';
2
+ import type { RouteLocation } from '../../internal/layout/query';
2
3
  import type { DialogBeforeClose } from '../overlays/types';
3
4
  import type { SettingsModalAccount, SettingsModalPage, SettingsModalSection, SettingsPageResolver, SettingsSectionSlotProps } from './types';
4
5
  type __VLS_Slots = {
@@ -33,6 +34,8 @@ declare const __VLS_base: import("vue").DefineComponent<{
33
34
  hideSession?: boolean;
34
35
  beforeClose?: DialogBeforeClose;
35
36
  route?: string | boolean;
37
+ routeLocation?: RouteLocation;
38
+ accountSection?: string;
36
39
  resolvePage?: SettingsPageResolver;
37
40
  open?: boolean | undefined;
38
41
  section?: string;
@@ -277,6 +280,8 @@ declare const __VLS_base: import("vue").DefineComponent<{
277
280
  hideSession?: boolean;
278
281
  beforeClose?: DialogBeforeClose;
279
282
  route?: string | boolean;
283
+ routeLocation?: RouteLocation;
284
+ accountSection?: string;
280
285
  resolvePage?: SettingsPageResolver;
281
286
  open?: boolean | undefined;
282
287
  section?: string;
@@ -506,6 +511,8 @@ declare const __VLS_base: import("vue").DefineComponent<{
506
511
  presetLabel: string;
507
512
  presetText: string;
508
513
  route: string | boolean;
514
+ routeLocation: RouteLocation;
515
+ accountSection: string;
509
516
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
510
517
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
511
518
  declare const _default: typeof __VLS_export;
@@ -1,13 +1,17 @@
1
1
  import type { Ref } from 'vue';
2
- import type { RouteHistory } from '../internal/layout/query';
3
- export type { RouteHistory };
2
+ import type { RouteHistory, RouteLocation } from '../internal/layout/query';
3
+ export type { RouteHistory, RouteLocation };
4
4
  export interface RouteStateOptions<T> {
5
5
  parse?: (value: string) => T | undefined;
6
6
  serialize?: (value: T) => string | undefined;
7
7
  history?: RouteHistory;
8
+ location?: RouteLocation;
8
9
  default?: T;
9
10
  }
10
11
  export declare function useRouteState<T = string>(key: string, options: RouteStateOptions<T> & {
11
12
  default: T;
12
13
  }): Ref<T>;
13
14
  export declare function useRouteState<T = string>(key: string, options?: RouteStateOptions<T>): Ref<T | undefined>;
15
+ export declare function withRouteStateScroll<R extends {
16
+ hash?: string;
17
+ }, A extends unknown[], Result>(scrollBehavior: (to: R, from: R, ...rest: A) => Result): (to: R, from: R, ...rest: A) => Result;
@@ -1,9 +1,15 @@
1
1
  import { computed, shallowRef, watch } from "vue";
2
- import { useQueryValue, writeQueryValue } from "../internal/layout/query.mjs";
2
+ import {
3
+ isRouteStateHash,
4
+ useDefaultRouteLocation,
5
+ useRouteValue,
6
+ writeRouteValue
7
+ } from "../internal/layout/query.mjs";
3
8
  import { useRouterLike } from "../internal/layout/route.mjs";
4
9
  export function useRouteState(key, options = {}) {
5
10
  const router = useRouterLike();
6
- const raw = useQueryValue(router, key);
11
+ const location = options.location ?? useDefaultRouteLocation();
12
+ const raw = useRouteValue(router, key, location);
7
13
  const fallback = options.default;
8
14
  const parse = options.parse ?? ((value) => value);
9
15
  const serialize = options.serialize ?? ((value) => String(value));
@@ -29,9 +35,16 @@ export function useRouteState(key, options = {}) {
29
35
  state.value = value;
30
36
  if (!router) return;
31
37
  const current = ++version;
32
- void writeQueryValue(router, key, toRaw(value), options.history ?? "push").then((ok) => {
38
+ const history = options.history ?? "push";
39
+ void writeRouteValue(router, key, toRaw(value), history, location).then((ok) => {
33
40
  if (!ok && current === version) state.value = fromRaw(raw.value);
34
41
  });
35
42
  }
36
43
  });
37
44
  }
45
+ function withoutStateHash(route) {
46
+ return route.hash && isRouteStateHash(route.hash) ? { ...route, hash: "" } : route;
47
+ }
48
+ export function withRouteStateScroll(scrollBehavior) {
49
+ return (to, from, ...rest) => scrollBehavior(withoutStateHash(to), withoutStateHash(from), ...rest);
50
+ }
@@ -27,6 +27,9 @@
27
27
  @filters-reset="emit('filtersReset')"
28
28
  >
29
29
  <slot name="actions" :api="api" v-bind="sheetSlot" />
30
+ <template v-if="$slots['actions-end']" #end>
31
+ <slot name="actions-end" :api="api" v-bind="sheetSlot" />
32
+ </template>
30
33
  <template v-if="$slots.filters" #filters="scope">
31
34
  <slot name="filters" v-bind="scope" />
32
35
  </template>
@@ -109,6 +109,31 @@ declare const __VLS_export: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_se
109
109
  clearDirty: (rowIds?: string[]) => void;
110
110
  api: GridApi<T> | null;
111
111
  }) => any;
112
+ } & {
113
+ 'actions-end'?: (props: {
114
+ range: GridCellRange | null;
115
+ selectedCellCount: number;
116
+ dirtyCount: number;
117
+ canUndo: boolean;
118
+ canRedo: boolean;
119
+ getRange: () => GridCellRange | null;
120
+ getSelectedCells: () => import("./types").GridSelectedCell<T>[];
121
+ selectRange: (range: import("./types").GridRangeInput) => void;
122
+ selectAll: () => void;
123
+ clearRange: () => void;
124
+ setRangeValue: (value: unknown, options?: import("./types").GridSetRangeValueOptions) => number;
125
+ copyRange: () => Promise<string | null>;
126
+ paste: (text: string) => number;
127
+ fillDown: () => number;
128
+ fillRight: () => number;
129
+ clearCells: () => number;
130
+ undo: () => boolean;
131
+ redo: () => boolean;
132
+ clearHistory: () => void;
133
+ getDirtyCells: () => import("./types").GridDirtyCell[];
134
+ clearDirty: (rowIds?: string[]) => void;
135
+ api: GridApi<T> | null;
136
+ }) => any;
112
137
  } & {
113
138
  filters?: (props: {
114
139
  close: () => void;
@@ -79,6 +79,7 @@
79
79
  :disabled="!api"
80
80
  @click="exportCsv"
81
81
  />
82
+ <slot name="end" />
82
83
  </div>
83
84
  </div>
84
85
  </template>
@@ -3,11 +3,13 @@ declare function closeFilters(): void;
3
3
  declare var __VLS_28: {
4
4
  close: typeof closeFilters;
5
5
  count: number;
6
- }, __VLS_45: {};
6
+ }, __VLS_45: {}, __VLS_65: {};
7
7
  type __VLS_Slots = {} & {
8
8
  filters?: (props: typeof __VLS_28) => any;
9
9
  } & {
10
10
  default?: (props: typeof __VLS_45) => any;
11
+ } & {
12
+ end?: (props: typeof __VLS_65) => any;
11
13
  };
12
14
  declare const __VLS_base: import("vue").DefineComponent<{
13
15
  api?: import("ag-grid-community").GridApi | null;
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ export { builtinLocales, de, defineLocale, en, it } from './locale';
11
11
  export type { DefineLocaleOptions } from './locale';
12
12
  export type * from './locale/types';
13
13
  export type { BuiltinIconName } from './icons/builtin';
14
- export { FIELD_KEY, FORM_CONFIG_KEY, FORM_KEY, ICON_USAGE_KEY, ICONS_KEY, LINK_KEY, LOCALE_KEY, PRESET_KEY, THEME_KEY, } from './internal/keys';
14
+ export { FIELD_KEY, FORM_CONFIG_KEY, FORM_KEY, ICON_USAGE_KEY, ICONS_KEY, LINK_KEY, LOCALE_KEY, PRESET_KEY, ROUTE_LOCATION_KEY, THEME_KEY, } from './internal/keys';
15
15
  export type * from './types/dom';
package/dist/index.mjs CHANGED
@@ -17,5 +17,6 @@ export {
17
17
  LINK_KEY,
18
18
  LOCALE_KEY,
19
19
  PRESET_KEY,
20
+ ROUTE_LOCATION_KEY,
20
21
  THEME_KEY
21
22
  } from "./internal/keys.mjs";
@@ -16,3 +16,4 @@ export declare const ROOT_KEY: InjectionKey<true>;
16
16
  export declare const FORM_KEY: InjectionKey<FormContext>;
17
17
  export declare const FORM_CONFIG_KEY: InjectionKey<FormConfig>;
18
18
  export declare const LOCALE_KEY: InjectionKey<ComputedRef<KLocale>>;
19
+ export declare const ROUTE_LOCATION_KEY: InjectionKey<'query' | 'hash'>;
@@ -9,3 +9,4 @@ export const ROOT_KEY = Symbol("k-root");
9
9
  export const FORM_KEY = Symbol("k-form");
10
10
  export const FORM_CONFIG_KEY = Symbol("k-form-config");
11
11
  export const LOCALE_KEY = Symbol("k-locale");
12
+ export const ROUTE_LOCATION_KEY = Symbol("k-route-location");
@@ -1,6 +1,14 @@
1
1
  import type { MaybeRefOrGetter } from 'vue';
2
2
  import type { RouterLike } from './route';
3
3
  export type RouteHistory = 'push' | 'replace';
4
+ export type RouteLocation = 'query' | 'hash';
5
+ type HashEntry = [key: string, value: string | undefined];
4
6
  export declare function firstQueryValue(value: unknown): string | undefined;
5
- export declare function useQueryValue(router: RouterLike | undefined, key: MaybeRefOrGetter<string | undefined>): import("vue").ComputedRef<string | undefined>;
6
- export declare function writeQueryValue(router: RouterLike, key: string, value: string | undefined, history: RouteHistory): Promise<boolean>;
7
+ export declare function parseRouteHash(hash: string | undefined): HashEntry[];
8
+ export declare function stringifyRouteHash(entries: HashEntry[]): string;
9
+ export declare function isRouteStateHash(hash: string | undefined): boolean;
10
+ export declare function hashParamValue(hash: string | undefined, key: string): string | undefined;
11
+ export declare function useDefaultRouteLocation(): RouteLocation;
12
+ export declare function useRouteValue(router: RouterLike | undefined, key: MaybeRefOrGetter<string | undefined>, location?: MaybeRefOrGetter<RouteLocation>): import("vue").ComputedRef<string | undefined>;
13
+ export declare function writeRouteValue(router: RouterLike, key: string, value: string | undefined, history: RouteHistory, location?: RouteLocation): Promise<boolean>;
14
+ export {};
@@ -1,15 +1,59 @@
1
- import { computed, toValue } from "vue";
1
+ import { computed, getCurrentInstance, inject, onMounted, shallowRef, toValue } from "vue";
2
2
  import { isClient } from "../core/env.mjs";
3
+ import { ROUTE_LOCATION_KEY } from "../keys.mjs";
3
4
  const batches = /* @__PURE__ */ new WeakMap();
4
5
  export function firstQueryValue(value) {
5
6
  if (Array.isArray(value)) return firstQueryValue(value[0]);
6
7
  return typeof value === "string" ? value : void 0;
7
8
  }
8
- export function useQueryValue(router, key) {
9
+ function escapeHashValue(value) {
10
+ return value.replace(/[%&]/g, (char) => char === "%" ? "%25" : "%26");
11
+ }
12
+ function unescapeHashValue(value) {
13
+ return value.replace(/%(25|26)/g, (_, code) => code === "25" ? "%" : "&");
14
+ }
15
+ export function parseRouteHash(hash) {
16
+ const text = (hash ?? "").replace(/^#/, "");
17
+ if (!text) return [];
18
+ return text.split("&").map((part) => {
19
+ const index = part.indexOf("/");
20
+ if (index <= 0) return [part, void 0];
21
+ return [part.slice(0, index), unescapeHashValue(part.slice(index + 1))];
22
+ });
23
+ }
24
+ export function stringifyRouteHash(entries) {
25
+ const text = entries.filter(([key]) => key !== "").map(([key, value]) => value === void 0 ? key : `${key}/${escapeHashValue(value)}`).join("&");
26
+ return text ? `#${text}` : "";
27
+ }
28
+ export function isRouteStateHash(hash) {
29
+ return parseRouteHash(hash).every(([, value]) => value !== void 0);
30
+ }
31
+ export function hashParamValue(hash, key) {
32
+ for (const [name, value] of parseRouteHash(hash)) if (name === key && value) return value;
33
+ return void 0;
34
+ }
35
+ export function useDefaultRouteLocation() {
36
+ if (!getCurrentInstance()) return "query";
37
+ return inject(ROUTE_LOCATION_KEY, "query");
38
+ }
39
+ function useHashReady() {
40
+ const ready = shallowRef(false);
41
+ if (getCurrentInstance())
42
+ onMounted(() => {
43
+ ready.value = true;
44
+ });
45
+ else if (isClient()) ready.value = true;
46
+ return ready;
47
+ }
48
+ export function useRouteValue(router, key, location = "query") {
49
+ const hashReady = useHashReady();
9
50
  return computed(() => {
10
51
  const name = toValue(key);
11
52
  if (!router || !name) return void 0;
12
- return firstQueryValue(router.currentRoute?.value?.query?.[name]);
53
+ const route = router.currentRoute?.value;
54
+ if (toValue(location) === "hash")
55
+ return hashReady.value ? hashParamValue(route?.hash, name) : void 0;
56
+ return firstQueryValue(route?.query?.[name]);
13
57
  });
14
58
  }
15
59
  function isPreviousEntry(router, location) {
@@ -18,12 +62,10 @@ function isPreviousEntry(router, location) {
18
62
  if (typeof back !== "string") return false;
19
63
  return router.resolve(location).fullPath === back;
20
64
  }
21
- async function flush(router, batch) {
22
- batches.delete(router);
23
- const route = router.currentRoute?.value;
24
- const query = { ...route?.query };
65
+ function applyQuery(current, changes) {
66
+ const query = { ...current };
25
67
  let changed = false;
26
- for (const [key, value] of batch.changes) {
68
+ for (const [key, value] of changes) {
27
69
  if (value === void 0) {
28
70
  if (!(key in query)) continue;
29
71
  delete query[key];
@@ -33,8 +75,36 @@ async function flush(router, batch) {
33
75
  changed = true;
34
76
  }
35
77
  }
36
- if (!changed) return true;
37
- const location = { path: route?.path, query, hash: route?.hash };
78
+ return { query, changed };
79
+ }
80
+ function applyHash(current, changes) {
81
+ let entries = parseRouteHash(current);
82
+ let changed = false;
83
+ for (const [key, value] of changes) {
84
+ const index = entries.findIndex(([name, entry]) => name === key && entry !== void 0);
85
+ if (value === void 0) {
86
+ if (index < 0) continue;
87
+ entries = entries.filter((_, position) => position !== index);
88
+ changed = true;
89
+ } else if (index < 0) {
90
+ entries = [...entries, [key, value]];
91
+ changed = true;
92
+ } else if (entries[index][1] !== value) {
93
+ entries = entries.map(
94
+ (entry, position) => position === index ? [key, value] : entry
95
+ );
96
+ changed = true;
97
+ }
98
+ }
99
+ return { hash: changed ? stringifyRouteHash(entries) : current, changed };
100
+ }
101
+ async function flush(router, batch) {
102
+ batches.delete(router);
103
+ const route = router.currentRoute?.value;
104
+ const nextQuery = applyQuery(route?.query ?? {}, batch.query);
105
+ const nextHash = applyHash(route?.hash ?? "", batch.hash);
106
+ if (!nextQuery.changed && !nextHash.changed) return true;
107
+ const location = { path: route?.path, query: nextQuery.query, hash: nextHash.hash };
38
108
  try {
39
109
  if (batch.history === "push" && isPreviousEntry(router, location)) {
40
110
  router.back?.();
@@ -46,19 +116,20 @@ async function flush(router, batch) {
46
116
  return false;
47
117
  }
48
118
  }
49
- export function writeQueryValue(router, key, value, history) {
119
+ export function writeRouteValue(router, key, value, history, location = "query") {
50
120
  if (!isClient()) return Promise.resolve(true);
51
121
  let batch = batches.get(router);
52
122
  if (!batch) {
53
123
  const created = {
54
- changes: /* @__PURE__ */ new Map(),
124
+ query: /* @__PURE__ */ new Map(),
125
+ hash: /* @__PURE__ */ new Map(),
55
126
  history,
56
127
  promise: Promise.resolve().then(() => flush(router, created))
57
128
  };
58
129
  batch = created;
59
130
  batches.set(router, batch);
60
131
  }
61
- batch.changes.set(key, value === "" ? void 0 : value);
132
+ batch[location].set(key, value === "" ? void 0 : value);
62
133
  if (history === "push") batch.history = "push";
63
134
  return batch.promise;
64
135
  }
@@ -41,6 +41,9 @@ export interface ModuleOptions {
41
41
  fonts: boolean | ThemePreset[];
42
42
  locale: string;
43
43
  messages: Record<string, DeepPartial<KMessages>>;
44
+ routeState: {
45
+ location?: 'query' | 'hash';
46
+ };
44
47
  }
45
48
  export interface KRuntimeConfig {
46
49
  global: boolean;
@@ -50,6 +53,7 @@ export interface KRuntimeConfig {
50
53
  presetDefault: ThemePreset;
51
54
  locale: string;
52
55
  messages: Record<string, DeepPartial<KMessages>>;
56
+ routeLocation: 'query' | 'hash';
53
57
  }
54
58
  declare module '@nuxt/schema' {
55
59
  interface PublicRuntimeConfig {
@@ -46,7 +46,10 @@ export default defineNuxtModule({
46
46
  },
47
47
  fonts: true,
48
48
  locale: "en",
49
- messages: {}
49
+ messages: {},
50
+ routeState: {
51
+ location: "query"
52
+ }
50
53
  },
51
54
  async setup(options, nuxt) {
52
55
  const resolver = createResolver(import.meta.url);
@@ -115,12 +118,14 @@ export default defineNuxtModule({
115
118
  presetCookie: options.preset.cookie ?? "k-preset",
116
119
  presetDefault,
117
120
  locale: options.locale,
118
- messages: options.messages
121
+ messages: options.messages,
122
+ routeLocation: options.routeState.location === "hash" ? "hash" : "query"
119
123
  };
120
124
  Object.assign(nuxt.options.runtimeConfig.public, { k: runtimeConfig });
121
125
  addPlugin(resolver.resolve("./runtime/theme"));
122
126
  addPlugin(resolver.resolve("./runtime/storage"));
123
127
  addPlugin(resolver.resolve("./runtime/locale"));
128
+ addPlugin(resolver.resolve("./runtime/route-state"));
124
129
  if (options.composables)
125
130
  addImports({ name: "useDashboard", from: resolver.resolve("./runtime/dashboard") });
126
131
  const layouts = resolveLayouts(options.layouts);
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { defineNuxtPlugin, useRouter, useRuntimeConfig } from "nuxt/app";
2
+ import { withRouteStateScroll } from "../../composables/useRouteState.mjs";
3
+ import { ROUTE_LOCATION_KEY } from "../../internal/keys.mjs";
4
+ export default defineNuxtPlugin({
5
+ name: "k:route-state",
6
+ setup(nuxtApp) {
7
+ const config = useRuntimeConfig().public.k ?? {};
8
+ nuxtApp.vueApp.provide(ROUTE_LOCATION_KEY, config.routeLocation === "hash" ? "hash" : "query");
9
+ if (import.meta.server) return;
10
+ const router = useRouter();
11
+ const scrollBehavior = router.options.scrollBehavior;
12
+ if (scrollBehavior) router.options.scrollBehavior = withRouteStateScroll(scrollBehavior);
13
+ }
14
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xleddyl/443-components-vue",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "description": "Compact, scoped dashboard components for Vue and Nuxt",
5
5
  "license": "MIT",
6
6
  "author": "Edoardo Alberti (https://github.com/xleddyl)",