nuxt-ui-tools 1.29.0 → 1.30.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/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.29.0",
8
+ "version": "1.30.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -47,6 +47,11 @@ function getPublicComponents(runtimeDir, options) {
47
47
  global: options.global,
48
48
  name: `${prefix}DataList${part}`
49
49
  })),
50
+ {
51
+ filePath: `${runtimeDir}/table/components/group-header.vue`,
52
+ global: options.global,
53
+ name: `${prefix}GroupHeader`
54
+ },
50
55
  // Spreadsheet
51
56
  {
52
57
  filePath: `${runtimeDir}/spreadsheet/components/spreadsheet-import.vue`,
@@ -177,7 +177,10 @@ onBeforeUnmount(() => unregister?.());
177
177
  </header>
178
178
  <div
179
179
  :class="
180
- mergeFormUiClass(`min-w-0 ${page.surfaceClasses.value.sectionBody}`, pageUi?.sectionBody)
180
+ mergeFormUiClass(
181
+ entry.section.flush ? 'min-w-0' : `min-w-0 ${page.surfaceClasses.value.sectionBody}`,
182
+ pageUi?.sectionBody
183
+ )
181
184
  "
182
185
  :style="grid.style.value"
183
186
  >
@@ -40,6 +40,12 @@ export interface FormPageSection<TContext = NonNullable<unknown>, TFields extend
40
40
  label: FormText;
41
41
  /** Supporting copy next to the title. */
42
42
  description?: FormText;
43
+ /**
44
+ * Lets the section's content reach the card's edges: the body under the heading has no padding.
45
+ * For a block that draws its own rows, such as a price grid. Only panel surfaces pad a section's
46
+ * body, so this changes nothing on a plain one.
47
+ */
48
+ flush?: boolean;
43
49
  /**
44
50
  * Iconify icon drawn before the title, so each section of a long page reads at a glance
45
51
  * (`'i-lucide-map-pin'` for an address). Style it with `ui.page.ui.sectionIcon`.
@@ -0,0 +1,25 @@
1
+ import type { MaybeRefOrGetter } from 'vue';
2
+ /** The attribute a cell's input carries: its column, so up and down stay in it. */
3
+ export declare const SHEET_COLUMN_ATTRIBUTE = "data-sheet-col";
4
+ /**
5
+ * Where a key press moves in a grid of inputs, or `undefined` to leave it to the input. Up, down
6
+ * and Enter stay in the column; left and right follow the reading order, and only once the caret
7
+ * has nothing left to cross, so editing inside a value still works.
8
+ */
9
+ export declare function nextSheetInput<TInput>(params: {
10
+ inputs: readonly TInput[];
11
+ current: TInput;
12
+ key: string;
13
+ shiftKey?: boolean;
14
+ columnOf: (input: TInput) => string | null;
15
+ caret: {
16
+ atStart: boolean;
17
+ atEnd: boolean;
18
+ };
19
+ }): TInput | undefined;
20
+ /**
21
+ * Makes the inputs under `root` walk like a spreadsheet. Mark each one with
22
+ * `data-sheet-col="<column>"`: arrows and Enter move between them, and focusing one selects its
23
+ * value, so typing replaces it.
24
+ */
25
+ export declare function useSheetNavigation(root: MaybeRefOrGetter<HTMLElement | null | undefined>): void;
@@ -0,0 +1,41 @@
1
+ import { useEventListener } from "@vueuse/core";
2
+ export const SHEET_COLUMN_ATTRIBUTE = "data-sheet-col";
3
+ const SELECTOR = `input[${SHEET_COLUMN_ATTRIBUTE}]:not(:disabled)`;
4
+ export function nextSheetInput(params) {
5
+ const { inputs, current, key, caret } = params;
6
+ const across = inputs.indexOf(current);
7
+ const column = inputs.filter((input) => params.columnOf(input) === params.columnOf(current));
8
+ const along = column.indexOf(current);
9
+ if (key === "ArrowDown" || key === "Enter" && !params.shiftKey) return column[along + 1];
10
+ if (key === "ArrowUp" || key === "Enter" && params.shiftKey) return column[along - 1];
11
+ if (key === "ArrowRight" && caret.atEnd) return inputs[across + 1];
12
+ if (key === "ArrowLeft" && caret.atStart) return inputs[across - 1];
13
+ return void 0;
14
+ }
15
+ export function useSheetNavigation(root) {
16
+ useEventListener(root, "focusin", (event) => {
17
+ if (event.target instanceof HTMLInputElement && event.target.matches(SELECTOR)) {
18
+ event.target.select();
19
+ }
20
+ });
21
+ useEventListener(root, "keydown", (event) => {
22
+ const { currentTarget, target } = event;
23
+ if (!(target instanceof HTMLInputElement) || !(currentTarget instanceof HTMLElement)) return;
24
+ if (!target.matches(SELECTOR)) return;
25
+ const next = nextSheetInput({
26
+ caret: {
27
+ atEnd: target.selectionEnd === target.value.length,
28
+ atStart: target.selectionStart === 0
29
+ },
30
+ columnOf: (input) => input.getAttribute(SHEET_COLUMN_ATTRIBUTE),
31
+ current: target,
32
+ inputs: [...currentTarget.querySelectorAll(SELECTOR)],
33
+ key: event.key,
34
+ shiftKey: event.shiftKey
35
+ });
36
+ if (next) {
37
+ event.preventDefault();
38
+ next.focus();
39
+ }
40
+ });
41
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './composables/use-responsive-value.js';
2
2
  export * from './composables/use-range-select.js';
3
+ export * from './composables/use-sheet-navigation.js';
3
4
  export { defineRemoteOptions } from './utils/define-remote-options.js';
4
5
  export * from './types/responsive.js';
5
6
  export * from './types/utils.js';
@@ -1,5 +1,6 @@
1
1
  export * from "./composables/use-responsive-value.js";
2
2
  export * from "./composables/use-range-select.js";
3
+ export * from "./composables/use-sheet-navigation.js";
3
4
  export { defineRemoteOptions } from "./utils/define-remote-options.js";
4
5
  export * from "./types/responsive.js";
5
6
  export * from "./types/utils.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The fold header of a group of rows: chevron, label and count, with room for a checkbox before
3
+ * them. It holds no state, so a table's row groups and a hand-built grid read the same.
4
+ */
5
+ type __VLS_Props = {
6
+ collapsed?: boolean;
7
+ /** Names the chevron for assistive technology. */
8
+ toggleLabel: string;
9
+ };
10
+ type __VLS_Slots = {
11
+ /** Sits before the chevron, usually a checkbox. A click in it does not fold the group. */
12
+ leading?: () => unknown;
13
+ /** The group's name. */
14
+ default?: () => unknown;
15
+ /** Shown after the name, usually a count. */
16
+ summary?: () => unknown;
17
+ };
18
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ toggle: () => any;
20
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onToggle?: (() => any) | undefined;
22
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
26
+ type __VLS_WithSlots<T, S> = T & {
27
+ new (): {
28
+ $slots: S;
29
+ };
30
+ };
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ defineProps({
3
+ collapsed: { type: Boolean, required: false },
4
+ toggleLabel: { type: String, required: true }
5
+ });
6
+ defineSlots();
7
+ const emit = defineEmits(["toggle"]);
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ class="nut-dl-group"
13
+ :data-collapsed="collapsed || void 0"
14
+ :data-selectable="$slots.leading ? true : void 0"
15
+ @click="emit('toggle')"
16
+ >
17
+ <span v-if="$slots.leading" class="nut-dl-group__check"><slot name="leading" /></span>
18
+ <button
19
+ type="button"
20
+ class="nut-dl-group__toggle"
21
+ :aria-expanded="!collapsed"
22
+ :aria-label="toggleLabel"
23
+ @click.stop="emit('toggle')"
24
+ >
25
+ <svg
26
+ viewBox="0 0 16 16"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="1.75"
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ aria-hidden="true"
33
+ >
34
+ <path d="m4 6 4 4 4-4" />
35
+ </svg>
36
+ </button>
37
+ <span class="nut-dl-group__label"><slot /></span>
38
+ <span v-if="$slots.summary" class="nut-dl-group__summary"><slot name="summary" /></span>
39
+ </div>
40
+ </template>
41
+
42
+ <style>
43
+ .nut-dl-group{--nut-dl-group-gutter:var(--nut-dl-gutter,0px);box-sizing:border-box;color:var(--ui-text-highlighted);cursor:pointer;font-size:13px;font-weight:600;gap:var(--nut-group-gap,6px);height:100%;left:0;max-width:100%;min-height:32px;padding-inline:var(--nut-dl-group-gutter);position:sticky;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.nut-dl-group,.nut-dl-group__check{align-items:center;display:inline-flex}.nut-dl-group__check{flex:none;margin-right:-6px;width:calc(var(--nut-dl-select-w, 44px) - var(--nut-dl-group-gutter) + var(--nut-dl-cell-x, 0px))}.nut-dl-group__toggle{background:transparent;border:0;border-radius:calc(var(--ui-radius)*1.5);color:var(--ui-text-muted);cursor:pointer;display:grid;flex:none;height:22px;outline:none;padding:0;place-items:center;width:var(--nut-group-toggle,22px)}.nut-dl-group__toggle:hover{background:var(--nut-dl-toggle-hover,var(--ui-bg-accented))}.nut-dl-group__toggle:focus-visible{box-shadow:0 0 0 2px var(--ui-primary)}.nut-dl-group__toggle svg{height:16px;transition:transform .16s ease;width:16px}.nut-dl-group[data-collapsed] .nut-dl-group__toggle svg{transform:rotate(-90deg)}.nut-dl-group__label{min-width:0;overflow:hidden;text-overflow:ellipsis}.nut-dl-group__summary{color:var(--ui-text-muted);flex:none;font-size:12px;font-variant-numeric:tabular-nums;font-weight:500}@media (prefers-reduced-motion:reduce){.nut-dl-group__toggle svg{transition:none}}
44
+ </style>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The fold header of a group of rows: chevron, label and count, with room for a checkbox before
3
+ * them. It holds no state, so a table's row groups and a hand-built grid read the same.
4
+ */
5
+ type __VLS_Props = {
6
+ collapsed?: boolean;
7
+ /** Names the chevron for assistive technology. */
8
+ toggleLabel: string;
9
+ };
10
+ type __VLS_Slots = {
11
+ /** Sits before the chevron, usually a checkbox. A click in it does not fold the group. */
12
+ leading?: () => unknown;
13
+ /** The group's name. */
14
+ default?: () => unknown;
15
+ /** Shown after the name, usually a count. */
16
+ summary?: () => unknown;
17
+ };
18
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ toggle: () => any;
20
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onToggle?: (() => any) | undefined;
22
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
26
+ type __VLS_WithSlots<T, S> = T & {
27
+ new (): {
28
+ $slots: S;
29
+ };
30
+ };
@@ -2,6 +2,7 @@
2
2
  import { computed } from "vue";
3
3
  import { useUiToolsLocale } from "#ui-tools/i18n";
4
4
  import { useTableInternals } from "../../composables/use-table-internals";
5
+ import GroupHeader from "../group-header.vue";
5
6
  import TableCell from "./table-cell";
6
7
  import TableSelectionControl from "./table-selection-control.vue";
7
8
  const props = defineProps({
@@ -31,47 +32,25 @@ function toggleSelection() {
31
32
  </script>
32
33
 
33
34
  <template>
34
- <div
35
- class="nut-dl-group"
36
- :data-collapsed="collapsed || void 0"
37
- :data-selectable="showCheckbox || void 0"
38
- @click="groups.toggle(group)"
35
+ <GroupHeader
36
+ :collapsed="collapsed"
37
+ :toggle-label="t('table.groups.toggle')"
38
+ @toggle="groups.toggle(group)"
39
39
  >
40
- <span v-if="showCheckbox" class="nut-dl-group__check">
40
+ <template v-if="showCheckbox" #leading>
41
41
  <TableSelectionControl
42
42
  :model-value="checked"
43
43
  :label="t('table.groups.select')"
44
44
  @toggle="toggleSelection"
45
45
  />
46
- </span>
47
- <button
48
- type="button"
49
- class="nut-dl-group__toggle"
50
- :aria-expanded="!collapsed"
51
- :aria-label="t('table.groups.toggle')"
52
- @click.stop="groups.toggle(group)"
53
- >
54
- <svg
55
- viewBox="0 0 16 16"
56
- fill="none"
57
- stroke="currentColor"
58
- stroke-width="1.75"
59
- stroke-linecap="round"
60
- stroke-linejoin="round"
61
- aria-hidden="true"
62
- >
63
- <path d="m4 6 4 4 4-4" />
64
- </svg>
65
- </button>
66
- <span class="nut-dl-group__label">
67
- <TableCell :index="0" :render="renderLabel" :row="group" />
68
- </span>
69
- <span class="nut-dl-group__summary">
46
+ </template>
47
+ <TableCell :index="0" :render="renderLabel" :row="group" />
48
+ <template #summary>
70
49
  <TableCell :index="0" :render="renderSummary" :row="group" />
71
- </span>
72
- </div>
50
+ </template>
51
+ </GroupHeader>
73
52
  </template>
74
53
 
75
54
  <style>
76
- .nut-dl-group{--nut-dl-group-gutter:var(--nut-dl-gutter,0px);box-sizing:border-box;color:var(--ui-text-highlighted);cursor:pointer;font-size:13px;font-weight:600;gap:6px;height:100%;left:0;max-width:100%;min-height:32px;padding-inline:var(--nut-dl-group-gutter);position:sticky;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.nut-dl-group,.nut-dl-group__check{align-items:center;display:inline-flex}.nut-dl-group__check{flex:none;margin-right:-6px;width:calc(var(--nut-dl-select-w, 44px) - var(--nut-dl-group-gutter) + var(--nut-dl-cell-x, 0px))}.nut-dl-group__toggle{background:transparent;border:0;border-radius:calc(var(--ui-radius)*1.5);color:var(--ui-text-muted);cursor:pointer;display:grid;flex:none;height:22px;outline:none;padding:0;place-items:center;width:22px}.nut-dl-group__toggle:hover{background:var(--nut-dl-toggle-hover,var(--ui-bg-accented))}.nut-dl-group__toggle:focus-visible{box-shadow:0 0 0 2px var(--ui-primary)}.nut-dl-group__toggle svg{height:16px;transition:transform .16s ease;width:16px}.nut-dl-group[data-collapsed] .nut-dl-group__toggle svg{transform:rotate(-90deg)}.nut-dl-group__label{min-width:0;overflow:hidden;text-overflow:ellipsis}.nut-dl-group__summary{color:var(--ui-text-muted);flex:none;font-size:12px;font-variant-numeric:tabular-nums;font-weight:500}.nut-dl-row.nut-dl-row--group{background:var(--nut-dl-head-bg)}.nut-dl-row.nut-dl-row--group:hover{background:color-mix(in srgb,var(--nut-dl-head-fg) 7%,var(--nut-dl-head-bg))}.nut-dl-group-cell{background:inherit;border-bottom:1px solid var(--nut-dl-line);height:var(--nut-dl-head-h);padding:0}.nut-dl-grid__group{border-bottom:1px solid var(--ui-border)}@media (prefers-reduced-motion:reduce){.nut-dl-group__toggle svg{transition:none}}
55
+ .nut-dl-row.nut-dl-row--group{background:var(--nut-dl-head-bg)}.nut-dl-row.nut-dl-row--group:hover{background:color-mix(in srgb,var(--nut-dl-head-fg) 7%,var(--nut-dl-head-bg))}.nut-dl-group-cell{background:inherit;border-bottom:1px solid var(--nut-dl-line);height:var(--nut-dl-head-h);padding:0}.nut-dl-grid__group{border-bottom:1px solid var(--ui-border)}
77
56
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
5
5
  "homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
6
6
  "bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",