nuxt-ui-tools 1.21.0 → 1.22.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.21.0",
8
+ "version": "1.22.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -13,11 +13,14 @@ type __VLS_Props = {
13
13
  avatarSize?: string;
14
14
  /** Classes of the box the chip dot is centered in. */
15
15
  chipClass?: string;
16
+ /** Nuxt UI chip size: `sm` for the small text of a token. */
17
+ chipSize?: 'sm' | 'md';
16
18
  };
17
19
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
18
20
  iconClass: string;
19
21
  avatarSize: string;
20
22
  chipClass: string;
23
+ chipSize: "sm" | "md";
21
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
25
  declare const _default: typeof __VLS_export;
23
26
  export default _default;
@@ -8,7 +8,8 @@ const props = defineProps({
8
8
  option: { type: Object, required: false },
9
9
  iconClass: { type: String, required: false, default: "size-4 text-muted" },
10
10
  avatarSize: { type: String, required: false, default: "xs" },
11
- chipClass: { type: String, required: false, default: "size-4" }
11
+ chipClass: { type: String, required: false, default: "size-4" },
12
+ chipSize: { type: String, required: false, default: "md" }
12
13
  });
13
14
  const size = computed(() => AVATAR_SIZES.find((entry) => entry === props.avatarSize) ?? "xs");
14
15
  </script>
@@ -36,6 +37,6 @@ const size = computed(() => AVATAR_SIZES.find((entry) => entry === props.avatarS
36
37
  aria-hidden="true"
37
38
  data-form-option-leading="chip"
38
39
  >
39
- <UChip standalone inset size="md" :color="option.chip.color" />
40
+ <UChip standalone inset :size="chipSize" :color="option.chip.color" />
40
41
  </span>
41
42
  </template>
@@ -13,11 +13,14 @@ type __VLS_Props = {
13
13
  avatarSize?: string;
14
14
  /** Classes of the box the chip dot is centered in. */
15
15
  chipClass?: string;
16
+ /** Nuxt UI chip size: `sm` for the small text of a token. */
17
+ chipSize?: 'sm' | 'md';
16
18
  };
17
19
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
18
20
  iconClass: string;
19
21
  avatarSize: string;
20
22
  chipClass: string;
23
+ chipSize: "sm" | "md";
21
24
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
25
  declare const _default: typeof __VLS_export;
23
26
  export default _default;
@@ -0,0 +1,15 @@
1
+ import type { VNodeChild } from 'vue';
2
+ import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
3
+ /**
4
+ * One chosen option inside a token or a tag: its icon, avatar or chip, then its label, both sized
5
+ * for the small text of a token. A `renderValue` function draws the content instead.
6
+ */
7
+ type __VLS_Props = {
8
+ option: ResolvedFormOption;
9
+ render?: (context: FormOptionValueRenderContext) => VNodeChild;
10
+ /** Shown instead of the label, such as the path of a hierarchy option. */
11
+ label?: string;
12
+ };
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,29 @@
1
+ <script setup>
2
+ import FormOptionLeading from "./form-option-leading.vue";
3
+ import FormOptionRender from "./form-option-render";
4
+ const props = defineProps({
5
+ option: { type: Object, required: true },
6
+ render: { type: Function, required: false },
7
+ label: { type: String, required: false }
8
+ });
9
+ function content() {
10
+ const { item } = props.option;
11
+ return item === void 0 ? null : props.render?.({ item, option: props.option });
12
+ }
13
+ </script>
14
+
15
+ <template>
16
+ <span class="inline-flex max-w-full min-w-0 items-center gap-1" data-form-option-token-content>
17
+ <FormOptionRender v-if="render && option.item !== void 0" :content="content" />
18
+ <template v-else>
19
+ <FormOptionLeading
20
+ :option="option"
21
+ icon-class="size-3.5 text-muted"
22
+ avatar-size="3xs"
23
+ chip-class="size-3"
24
+ chip-size="sm"
25
+ />
26
+ <span class="min-w-0 truncate">{{ label ?? option.label }}</span>
27
+ </template>
28
+ </span>
29
+ </template>
@@ -0,0 +1,15 @@
1
+ import type { VNodeChild } from 'vue';
2
+ import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
3
+ /**
4
+ * One chosen option inside a token or a tag: its icon, avatar or chip, then its label, both sized
5
+ * for the small text of a token. A `renderValue` function draws the content instead.
6
+ */
7
+ type __VLS_Props = {
8
+ option: ResolvedFormOption;
9
+ render?: (context: FormOptionValueRenderContext) => VNodeChild;
10
+ /** Shown instead of the label, such as the path of a hierarchy option. */
11
+ label?: string;
12
+ };
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import type { VNodeChild } from 'vue';
2
+ import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
3
+ /**
4
+ * The chosen options of a multiple selection as its closed control shows them: one token each,
5
+ * with the option's icon, avatar or chip before its label, wrapping onto more lines when they do
6
+ * not fit. A token is no taller than a line of text, so one row keeps the control as high as the
7
+ * inputs beside it. The tokens ignore the pointer, so a click still opens the control. They look
8
+ * like the tags of a multiple auto-complete, so both read alike. A visually hidden comma between
9
+ * them keeps the value reading as a list ("Euro, Dollar") for assistive technology.
10
+ */
11
+ type __VLS_Props = {
12
+ options: readonly ResolvedFormOption[];
13
+ render?: (context: FormOptionValueRenderContext) => VNodeChild;
14
+ /** Shown instead of each label, such as the path of a hierarchy option. */
15
+ labelOf?: (option: ResolvedFormOption) => string;
16
+ /** Classes of each token. */
17
+ tokenClass?: string;
18
+ };
19
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
@@ -0,0 +1,26 @@
1
+ <script setup>
2
+ import { formOptionKey } from "../../utils/options";
3
+ import { mergeFormUiClass } from "../../utils/ui";
4
+ import FormOptionToken from "./form-option-token.vue";
5
+ defineProps({
6
+ options: { type: Array, required: true },
7
+ render: { type: Function, required: false },
8
+ labelOf: { type: Function, required: false },
9
+ tokenClass: { type: String, required: false }
10
+ });
11
+ const TOKEN = "inline-flex max-w-full min-w-0 items-center rounded-sm bg-elevated px-1.5 py-0.5 text-xs font-medium text-default ring ring-accented ring-inset";
12
+ </script>
13
+
14
+ <template>
15
+ <span
16
+ class="pointer-events-none flex min-w-0 flex-1 flex-wrap gap-1 text-start"
17
+ data-form-option-tokens
18
+ >
19
+ <template v-for="(option, index) in options" :key="formOptionKey(option.value)">
20
+ <span :class="mergeFormUiClass(TOKEN, tokenClass)" data-form-option-token>
21
+ <FormOptionToken :option="option" :render="render" :label="labelOf?.(option)" />
22
+ </span>
23
+ <span v-if="index < options.length - 1" class="sr-only">, </span>
24
+ </template>
25
+ </span>
26
+ </template>
@@ -0,0 +1,21 @@
1
+ import type { VNodeChild } from 'vue';
2
+ import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
3
+ /**
4
+ * The chosen options of a multiple selection as its closed control shows them: one token each,
5
+ * with the option's icon, avatar or chip before its label, wrapping onto more lines when they do
6
+ * not fit. A token is no taller than a line of text, so one row keeps the control as high as the
7
+ * inputs beside it. The tokens ignore the pointer, so a click still opens the control. They look
8
+ * like the tags of a multiple auto-complete, so both read alike. A visually hidden comma between
9
+ * them keeps the value reading as a list ("Euro, Dollar") for assistive technology.
10
+ */
11
+ type __VLS_Props = {
12
+ options: readonly ResolvedFormOption[];
13
+ render?: (context: FormOptionValueRenderContext) => VNodeChild;
14
+ /** Shown instead of each label, such as the path of a hierarchy option. */
15
+ labelOf?: (option: ResolvedFormOption) => string;
16
+ /** Classes of each token. */
17
+ tokenClass?: string;
18
+ };
19
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
@@ -7,10 +7,11 @@ import FormOptionItemLabel from "../../components/utils/form-option-item-label.v
7
7
  import FormOptionLeading from "../../components/utils/form-option-leading.vue";
8
8
  import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
9
9
  import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
10
+ import FormOptionToken from "../../components/utils/form-option-token.vue";
10
11
  import FormOptionValueOverlay from "../../components/utils/form-option-value-overlay.vue";
11
12
  import { useFieldControl } from "../../composables/use-field-control";
12
13
  import { useOptionMenu } from "../../composables/use-option-menu";
13
- import { leadingVisualPadding } from "../../utils/options";
14
+ import { formOptionKey, leadingVisualPadding } from "../../utils/options";
14
15
  import { isBoolean, isNumber, isString } from "../../utils/predicate";
15
16
  import { mergeFormUiClass } from "../../utils/ui";
16
17
  const props = defineProps({
@@ -102,6 +103,14 @@ async function handleNativeCreate(label) {
102
103
  }
103
104
  await handleCreate(normalizedLabel);
104
105
  }
106
+ function tagOption(value) {
107
+ const optionValue = typeof value === "number" || typeof value === "boolean" ? value : String(value);
108
+ const key = formOptionKey(optionValue);
109
+ return options.selectedItems.value.find((option) => formOptionKey(option.value) === key) ?? {
110
+ label: String(value),
111
+ value: optionValue
112
+ };
113
+ }
105
114
  function isOptionValue(value) {
106
115
  return isString(value) || isNumber(value) || isBoolean(value);
107
116
  }
@@ -151,6 +160,10 @@ function isOptionValue(value) {
151
160
  />
152
161
  </template>
153
162
 
163
+ <template v-if="fieldProps.multiple" #tags-item-text="{ item }">
164
+ <FormOptionToken :option="tagOption(item)" :render="menu.renderValue.value" />
165
+ </template>
166
+
154
167
  <template #item-leading="{ item }">
155
168
  <FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
156
169
  </template>
@@ -10,10 +10,12 @@ import { computed, ref, watch } from "vue";
10
10
  import { useUiToolsLocale } from "../../../i18n/use-locale";
11
11
  import FormCompositeControl from "../../components/renderer/form-composite-control.vue";
12
12
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
13
+ import FormOptionLeading from "../../components/utils/form-option-leading.vue";
13
14
  import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
15
+ import FormOptionTokens from "../../components/utils/form-option-tokens.vue";
14
16
  import { useFieldControl } from "../../composables/use-field-control";
15
17
  import { useFormUi } from "../../composables/use-form-ui";
16
- import { formOptionKey } from "../../utils/options";
18
+ import { formOptionKey, leadingVisualPadding, resolveOptionLeading } from "../../utils/options";
17
19
  import { isBoolean, isNumber, isString } from "../../utils/predicate";
18
20
  import { resolveFormText } from "../../utils/text";
19
21
  import { mergeFormUiClass } from "../../utils/ui";
@@ -147,8 +149,43 @@ const treeSelectLabel = computed(() => {
147
149
  return props.field.type === "tree-select" && fieldProps.value.showPath ? selected.pathLabel : String(selected.label);
148
150
  });
149
151
  const fieldLabel = computed(() => resolveFormText(props.field.label));
152
+ const optionEntries = computed(() => {
153
+ const entries = /* @__PURE__ */ new Map();
154
+ const walk = (items, labels) => {
155
+ for (const item of items) {
156
+ const path = [...labels, item.label];
157
+ entries.set(formOptionKey(item.value), { option: item, path: path.join(separator.value) });
158
+ walk(item.children ?? [], path);
159
+ }
160
+ };
161
+ walk(options.items.value, []);
162
+ return entries;
163
+ });
164
+ const showsPath = computed(() => props.field.type === "cascader" || hasShowPath(props.field));
165
+ function optionOf(item) {
166
+ return optionEntries.value.get(formOptionKey(item.value))?.option;
167
+ }
168
+ function tokenLabel(option) {
169
+ return showsPath.value ? optionEntries.value.get(formOptionKey(option.value))?.path ?? option.label : option.label;
170
+ }
171
+ const chosenOptions = computed(() => {
172
+ const values = Array.isArray(model.value) ? model.value : [model.value];
173
+ return values.flatMap((value) => {
174
+ const option = value === null ? void 0 : optionEntries.value.get(formOptionKey(value));
175
+ return option ? [option.option] : [];
176
+ });
177
+ });
178
+ const leadingOption = computed(() => {
179
+ const [option] = chosenOptions.value;
180
+ const multiple = usesTree.value ? treeMultiple.value : fieldProps.value.multiple === true;
181
+ return !multiple && option && resolveOptionLeading(option) ? option : void 0;
182
+ });
150
183
  const ownedControlUi = computed(() => ({
151
184
  ...controlProps.value.ui,
185
+ base: mergeFormUiClass(
186
+ controlProps.value.ui?.base,
187
+ leadingOption.value ? leadingVisualPadding(controlProps.value.size) : void 0
188
+ ),
152
189
  content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value)
153
190
  }));
154
191
  watch(treeSelectOpen, (open) => {
@@ -402,6 +439,7 @@ function isOptionValue(value) {
402
439
  @click.stop
403
440
  @select="selectRadioTreeItem(item)"
404
441
  />
442
+ <FormOptionLeading :option="optionOf(item)" />
405
443
  <span :class="ui.linkLabel()">{{ item.label }}</span>
406
444
  <button
407
445
  v-if="isExpandable(item)"
@@ -460,7 +498,25 @@ function isOptionValue(value) {
460
498
  }"
461
499
  @blur="handleBlur"
462
500
  >
463
- {{ treeSelectLabel }}
501
+ <FormOptionTokens
502
+ v-if="treeMultiple && chosenOptions.length"
503
+ :options="chosenOptions"
504
+ :label-of="tokenLabel"
505
+ />
506
+ <span
507
+ v-else
508
+ :class="
509
+ mergeFormUiClass(
510
+ 'flex min-w-0 flex-1 items-center gap-1.5 text-left',
511
+ formUi.ui.value.treeSelect?.ui?.triggerLabel
512
+ )
513
+ "
514
+ >
515
+ <FormOptionLeading v-if="leadingOption" :option="leadingOption" />
516
+ <span :class="chosenOptions.length ? 'truncate' : 'truncate text-dimmed'">
517
+ {{ treeSelectLabel }}
518
+ </span>
519
+ </span>
464
520
  </UButton>
465
521
 
466
522
  <template #content>
@@ -538,6 +594,7 @@ function isOptionValue(value) {
538
594
  @click.stop
539
595
  @select="selectRadioTreeItem(item)"
540
596
  />
597
+ <FormOptionLeading :option="optionOf(item)" />
541
598
  <span :class="ui.linkLabel()">{{ item.label }}</span>
542
599
  <button
543
600
  v-if="isExpandable(item)"
@@ -610,7 +667,19 @@ function isOptionValue(value) {
610
667
  :loading="validationPending || options.loading.value"
611
668
  :ui="ownedControlUi"
612
669
  @blur="handleBlur"
613
- />
670
+ >
671
+ <template v-if="leadingOption" #leading="{ ui }">
672
+ <FormOptionLeading
673
+ :option="leadingOption"
674
+ :icon-class="ui.leadingIcon()"
675
+ :avatar-size="ui.itemLeadingAvatarSize()"
676
+ chip-class="size-5"
677
+ />
678
+ </template>
679
+ <template v-if="fieldProps.multiple && chosenOptions.length" #default>
680
+ <FormOptionTokens :options="chosenOptions" :label-of="tokenLabel" />
681
+ </template>
682
+ </USelectMenu>
614
683
  </FormCompositeControl>
615
684
  </FormFieldShell>
616
685
  </template>
@@ -9,9 +9,10 @@ import FormOptionItemLabel from "../../components/utils/form-option-item-label.v
9
9
  import FormOptionLeading from "../../components/utils/form-option-leading.vue";
10
10
  import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
11
11
  import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
12
+ import FormOptionTokens from "../../components/utils/form-option-tokens.vue";
12
13
  import { useFieldControl } from "../../composables/use-field-control";
13
14
  import { useOptionMenu } from "../../composables/use-option-menu";
14
- import { leadingVisualPadding } from "../../utils/options";
15
+ import { formOptionKey, leadingVisualPadding } from "../../utils/options";
15
16
  import { isBoolean, isNumber, isString } from "../../utils/predicate";
16
17
  import { mergeFormUiClass } from "../../utils/ui";
17
18
  const props = defineProps({
@@ -76,6 +77,17 @@ const controlUi = computed(() => ({
76
77
  controlProps.value.ui?.itemDescription
77
78
  )
78
79
  }));
80
+ const tokenOptions = computed(() => {
81
+ if (!fieldProps.value.multiple || !Array.isArray(model.value)) {
82
+ return [];
83
+ }
84
+ const chosen = new Map(
85
+ options.selectedItems.value.map((option) => [formOptionKey(option.value), option])
86
+ );
87
+ return model.value.map(
88
+ (value) => chosen.get(formOptionKey(value)) ?? { label: String(value), value }
89
+ );
90
+ });
79
91
  const createItem = computed(
80
92
  () => options.creatable.value && fieldProps.value.createItem ? fieldProps.value.createItem : false
81
93
  );
@@ -166,6 +178,9 @@ function isOptionValue(value) {
166
178
  />
167
179
  </span>
168
180
  </template>
181
+ <template v-else-if="tokenOptions.length" #default>
182
+ <FormOptionTokens :options="tokenOptions" />
183
+ </template>
169
184
 
170
185
  <template #item-leading="{ item }">
171
186
  <FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
@@ -2,6 +2,7 @@
2
2
  import USwitch from "@nuxt/ui/components/Switch.vue";
3
3
  import { computed, useId } from "vue";
4
4
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
5
+ import FormOptionChoiceLabel from "../../components/utils/form-option-choice-label.vue";
5
6
  import { useFieldControl } from "../../composables/use-field-control";
6
7
  import { formOptionKey } from "../../utils/options";
7
8
  import { isBoolean, isNumber, isString } from "../../utils/predicate";
@@ -40,7 +41,7 @@ function isOptionValue(value) {
40
41
  <div
41
42
  role="group"
42
43
  class="grid gap-3"
43
- :class="fieldProps.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap' : ''"
44
+ :class="fieldProps.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap sm:gap-x-7' : ''"
44
45
  >
45
46
  <USwitch
46
47
  v-for="item in items"
@@ -56,7 +57,11 @@ function isOptionValue(value) {
56
57
  :unchecked-icon="fieldProps.uncheckedIcon"
57
58
  @update:model-value="toggleOption(item.value, $event === true)"
58
59
  @blur="handleBlur"
59
- />
60
+ >
61
+ <template #label>
62
+ <FormOptionChoiceLabel :option="item" :selected="model.includes(item.value)" />
63
+ </template>
64
+ </USwitch>
60
65
  </div>
61
66
  </FormFieldShell>
62
67
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.21.0",
3
+ "version": "1.22.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",