@shwfed/nuxt 0.10.2 → 0.10.3

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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shwfed/nuxt",
3
3
  "configKey": "shwfed",
4
- "version": "0.10.2",
4
+ "version": "0.10.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -23,7 +23,7 @@ const { filterState } = useCommand();
23
23
  <template>
24
24
  <div
25
25
  data-slot="command-input-wrapper"
26
- class="flex h-12 items-center gap-2 border-b border-zinc-200 px-3"
26
+ class="flex h-10 items-center gap-2 border-b border-zinc-200 px-3"
27
27
  >
28
28
  <Icon
29
29
  icon="fluent:search-20-filled"
@@ -7,15 +7,15 @@ import { Icon } from "@iconify/vue";
7
7
  import { Effect } from "effect";
8
8
  import { format, parse } from "date-fns";
9
9
  import { deleteProperty, getProperty, hasProperty, setProperty } from "dot-prop";
10
- import { ref, toRaw, useId, watch, watchEffect } from "vue";
10
+ import { nextTick, ref, toRaw, useId, watch, watchEffect } from "vue";
11
11
  import { useI18n } from "vue-i18n";
12
12
  import { useCheating } from "#imports";
13
13
  import { Calendar } from "../calendar";
14
14
  import { Button } from "../button";
15
- import { CommandGroup, CommandItem } from "../command";
15
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../command";
16
16
  import { Field, FieldContent, FieldError, FieldLabel } from "../field";
17
17
  import FieldsConfiguratorDialog from "../fields-configurator/FieldsConfiguratorDialog.vue";
18
- import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupCombobox, InputGroupInput, InputGroupNumberField, InputGroupTextarea } from "../input-group";
18
+ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupNumberField, InputGroupTextarea } from "../input-group";
19
19
  import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../popover";
20
20
  import { Skeleton } from "../skeleton";
21
21
  import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip";
@@ -40,6 +40,7 @@ const isConfiguratorOpen = ref(false);
40
40
  const displayConfig = ref(defaultConfig);
41
41
  const validationErrors = ref({});
42
42
  const calendarOpen = ref({});
43
+ const selectOpen = ref({});
43
44
  function cloneConfig(config2) {
44
45
  const nextConfig = {
45
46
  fields: config2.fields.slice()
@@ -109,6 +110,90 @@ function isFieldDisabled(field) {
109
110
  function getFieldValue(field) {
110
111
  return getProperty(modelValue.value, field.path);
111
112
  }
113
+ function stringifySelectValue(value) {
114
+ try {
115
+ return JSON.stringify(value);
116
+ } catch {
117
+ return void 0;
118
+ }
119
+ }
120
+ function isSelectObjectValue(value) {
121
+ return typeof value === "object" && value !== null;
122
+ }
123
+ function isSelectValueEqual(left, right) {
124
+ if (!isSelectObjectValue(left) && !isSelectObjectValue(right)) {
125
+ return Object.is(left, right);
126
+ }
127
+ if (!isSelectObjectValue(left) || !isSelectObjectValue(right)) {
128
+ return false;
129
+ }
130
+ const leftValue = stringifySelectValue(left);
131
+ const rightValue = stringifySelectValue(right);
132
+ if (leftValue === void 0 || rightValue === void 0) {
133
+ return false;
134
+ }
135
+ return leftValue === rightValue;
136
+ }
137
+ function getSelectOptions(field) {
138
+ return $dsl.evaluate`${field.options}`().map((option) => {
139
+ return {
140
+ key: $dsl.evaluate`${field.key}`({ option }),
141
+ label: $dsl.evaluate`${field.label}`({ option }),
142
+ value: $dsl.evaluate`${field.value}`({ option })
143
+ };
144
+ });
145
+ }
146
+ function getSelectFieldState(field) {
147
+ const options = getSelectOptions(field);
148
+ const currentValue = getFieldValue(field);
149
+ const selectedOption = options.find((option) => isSelectValueEqual(option.value, currentValue));
150
+ return {
151
+ options,
152
+ selectedKey: selectedOption?.key
153
+ };
154
+ }
155
+ function getSelectDisplayValue(state, value) {
156
+ if (typeof value !== "string") {
157
+ return "";
158
+ }
159
+ return state.options.find((option) => option.key === value)?.label ?? "";
160
+ }
161
+ function clearSelectField(field) {
162
+ deleteProperty(modelValue.value, field.path);
163
+ }
164
+ function handleSelectValueChange(field, state, value) {
165
+ if (typeof value !== "string") {
166
+ clearSelectField(field);
167
+ return;
168
+ }
169
+ const option = state.options.find((candidate) => candidate.key === value);
170
+ if (!option) {
171
+ clearSelectField(field);
172
+ return;
173
+ }
174
+ setProperty(modelValue.value, field.path, option.value);
175
+ }
176
+ function handleSelectOpenChange(field, open) {
177
+ if (open) {
178
+ selectOpen.value[field.path] = true;
179
+ return;
180
+ }
181
+ Reflect.deleteProperty(selectOpen.value, field.path);
182
+ validateField(field);
183
+ }
184
+ function handleSelectBlur(field) {
185
+ window.setTimeout(() => {
186
+ if (!selectOpen.value[field.path]) {
187
+ validateField(field);
188
+ }
189
+ }, 0);
190
+ }
191
+ function handleSelectCommandValueChange(field, state, value) {
192
+ handleSelectValueChange(field, state, value);
193
+ void nextTick().then(() => {
194
+ handleSelectOpenChange(field, false);
195
+ });
196
+ }
112
197
  function clearFieldValidation(path) {
113
198
  Reflect.deleteProperty(validationErrors.value, path);
114
199
  }
@@ -195,6 +280,11 @@ watchEffect(() => {
195
280
  Reflect.deleteProperty(calendarOpen.value, path);
196
281
  }
197
282
  }
283
+ for (const path of Object.keys(selectOpen.value)) {
284
+ if (!activePaths.has(path)) {
285
+ Reflect.deleteProperty(selectOpen.value, path);
286
+ }
287
+ }
198
288
  });
199
289
  </script>
200
290
 
@@ -265,7 +355,7 @@ export {
265
355
  :orientation="getConfigOrientation(displayConfig)"
266
356
  :style="getFieldStyle(field)"
267
357
  >
268
- <FieldLabel :for="['string', 'textarea', 'number'].includes(field.type) ? `${id}:${field.path}` : void 0">
358
+ <FieldLabel :for="['string', 'textarea', 'number', 'select'].includes(field.type) ? `${id}:${field.path}` : void 0">
269
359
  <span class="inline-flex items-start gap-0.5">
270
360
  <span>{{ getFieldLabel(field) }}</span>
271
361
  <sup
@@ -343,17 +433,132 @@ export {
343
433
  />
344
434
  </PopoverContent>
345
435
  </Popover>
346
- <InputGroup
347
- v-else
348
- :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
349
- :class="field.type === 'textarea' ? 'h-auto flex-col items-stretch' : void 0"
350
- >
351
- <div
352
- v-if="field.type === 'textarea'"
353
- class="flex min-w-0 w-full items-center"
436
+ <template v-else>
437
+ <template v-if="field.type === 'select'">
438
+ <Popover
439
+ v-for="selectState in [getSelectFieldState(field)]"
440
+ :key="`${field.id}:select:${selectState.selectedKey ?? 'empty'}`"
441
+ :open="selectOpen[field.path] === true"
442
+ @update:open="(open) => handleSelectOpenChange(field, open)"
443
+ >
444
+ <PopoverAnchor as-child>
445
+ <InputGroup :data-disabled="isFieldDisabled(field) ? 'true' : void 0">
446
+ <PopoverTrigger as-child>
447
+ <InputGroupInput
448
+ :id="`${id}:${field.path}`"
449
+ :model-value="getSelectDisplayValue(selectState, selectState.selectedKey)"
450
+ :disabled="isFieldDisabled(field)"
451
+ :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
452
+ :placeholder="t('select-placeholder')"
453
+ class="text-left"
454
+ readonly
455
+ @blur="handleSelectBlur(field)"
456
+ />
457
+ </PopoverTrigger>
458
+ <InputGroupAddon v-if="field.icon">
459
+ <Icon
460
+ :icon="field.icon"
461
+ />
462
+ </InputGroupAddon>
463
+ <InputGroupAddon
464
+ v-if="hasProperty(modelValue, field.path)"
465
+ align="inline-end"
466
+ :class="getConfigOrientation(displayConfig) === 'floating' ? 'group-data-[disabled=true]/input-group:hidden' : void 0"
467
+ >
468
+ <Tooltip :delay-duration="800">
469
+ <TooltipTrigger>
470
+ <InputGroupButton as-child>
471
+ <button
472
+ type="button"
473
+ class="text-zinc-300 hover:text-zinc-500 transition-colors"
474
+ :disabled="isFieldDisabled(field)"
475
+ @click="clearSelectField(field)"
476
+ >
477
+ <Icon
478
+ icon="fluent:dismiss-20-regular"
479
+ />
480
+ </button>
481
+ </InputGroupButton>
482
+ </TooltipTrigger>
483
+ <TooltipContent>
484
+ {{ t("clear") }}
485
+ </TooltipContent>
486
+ </Tooltip>
487
+ </InputGroupAddon>
488
+ </InputGroup>
489
+ </PopoverAnchor>
490
+
491
+ <PopoverContent class="w-72 p-0">
492
+ <Command
493
+ :model-value="selectState.selectedKey"
494
+ :disabled="isFieldDisabled(field)"
495
+ selection-behavior="toggle"
496
+ @update:model-value="(value) => handleSelectCommandValueChange(field, selectState, value)"
497
+ >
498
+ <CommandInput :placeholder="t('select-search-placeholder')" />
499
+ <CommandList>
500
+ <CommandEmpty as-child>
501
+ <section class="h-32 flex flex-col text-lg items-center justify-center gap-2 select-none">
502
+ <Icon
503
+ icon="fluent:app-recent-20-regular"
504
+ class="text-zinc-400 text-2xl!"
505
+ />
506
+ <p class="text-zinc-500">
507
+ {{ t("select-empty") }}
508
+ </p>
509
+ </section>
510
+ </CommandEmpty>
511
+ <CommandGroup>
512
+ <CommandItem
513
+ v-for="option in selectState.options"
514
+ :key="option.key"
515
+ data-slot="select-option"
516
+ :value="option.key"
517
+ class="data-highlighted:bg-zinc-50 data-highlighted:text-zinc-700 data-[state=checked]:bg-zinc-100 data-[state=checked]:text-zinc-700 transition cursor-pointer relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none"
518
+ >
519
+ {{ option.label }}
520
+ </CommandItem>
521
+ </CommandGroup>
522
+ </CommandList>
523
+ </Command>
524
+ </PopoverContent>
525
+ </Popover>
526
+ </template>
527
+
528
+ <InputGroup
529
+ v-else
530
+ :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
531
+ :class="field.type === 'textarea' ? 'h-auto flex-col items-stretch' : void 0"
354
532
  >
355
- <InputGroupTextarea
533
+ <div
534
+ v-if="field.type === 'textarea'"
535
+ class="flex min-w-0 w-full items-center"
536
+ >
537
+ <InputGroupTextarea
538
+ :id="`${id}:${field.path}`"
539
+ :model-value="getProperty(modelValue, field.path)"
540
+ :maxlength="field.maxLength ? $dsl.evaluate`${field.maxLength}`() : void 0"
541
+ :disabled="isFieldDisabled(field)"
542
+ :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
543
+ @update:model-value="(value) => {
544
+ if (!value && !field.discardEmptyString) {
545
+ deleteProperty(modelValue, field.path);
546
+ } else {
547
+ setProperty(modelValue, field.path, value);
548
+ }
549
+ }"
550
+ @blur="validateField(field)"
551
+ />
552
+ <InputGroupAddon v-if="field.icon">
553
+ <Icon
554
+ :icon="field.icon"
555
+ />
556
+ </InputGroupAddon>
557
+ </div>
558
+ <InputGroupInput
559
+ v-if="field.type === 'string'"
356
560
  :id="`${id}:${field.path}`"
561
+ :treat-empty-as-different-state-from-null="!!field.discardEmptyString"
357
562
  :model-value="getProperty(modelValue, field.path)"
358
563
  :maxlength="field.maxLength ? $dsl.evaluate`${field.maxLength}`() : void 0"
359
564
  :disabled="isFieldDisabled(field)"
@@ -367,136 +572,97 @@ export {
367
572
  }"
368
573
  @blur="validateField(field)"
369
574
  />
370
- <InputGroupAddon v-if="field.icon">
371
- <Icon
372
- :icon="field.icon"
373
- />
374
- </InputGroupAddon>
375
- </div>
376
- <InputGroupInput
377
- v-if="field.type === 'string'"
378
- :id="`${id}:${field.path}`"
379
- :treat-empty-as-different-state-from-null="!!field.discardEmptyString"
380
- :model-value="getProperty(modelValue, field.path)"
381
- :maxlength="field.maxLength ? $dsl.evaluate`${field.maxLength}`() : void 0"
382
- :disabled="isFieldDisabled(field)"
383
- :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
384
- @update:model-value="(value) => {
385
- if (!value && !field.discardEmptyString) {
386
- deleteProperty(modelValue, field.path);
387
- } else {
388
- setProperty(modelValue, field.path, value);
389
- }
390
- }"
391
- @blur="validateField(field)"
392
- />
393
- <InputGroupNumberField
394
- v-if="field.type === 'number'"
395
- :id="`${id}:${field.path}`"
396
- :model-value="getProperty(modelValue, field.path) ?? null"
397
- :min="field.min ? $dsl.evaluate`${field.min}`() : void 0"
398
- :max="field.max ? $dsl.evaluate`${field.max}`() : void 0"
399
- :step="field.step ? $dsl.evaluate`${field.step}`() : void 0"
400
- :disabled="isFieldDisabled(field)"
401
- :invalid="isFieldInvalid(field)"
402
- @update:model-value="(value) => {
575
+ <InputGroupNumberField
576
+ v-if="field.type === 'number'"
577
+ :id="`${id}:${field.path}`"
578
+ :model-value="getProperty(modelValue, field.path) ?? null"
579
+ :min="field.min ? $dsl.evaluate`${field.min}`() : void 0"
580
+ :max="field.max ? $dsl.evaluate`${field.max}`() : void 0"
581
+ :step="field.step ? $dsl.evaluate`${field.step}`() : void 0"
582
+ :disabled="isFieldDisabled(field)"
583
+ :invalid="isFieldInvalid(field)"
584
+ @update:model-value="(value) => {
403
585
  if (!value && value !== 0) {
404
586
  deleteProperty(modelValue, field.path);
405
587
  } else {
406
588
  setProperty(modelValue, field.path, value);
407
589
  }
408
590
  }"
409
- @blur="validateField(field)"
410
- />
411
- <InputGroupCombobox
412
- v-if="field.type === 'select'"
413
- :disabled="isFieldDisabled(field)"
414
- :invalid="isFieldInvalid(field)"
415
- @blur="validateField(field)"
416
- >
417
- <CommandGroup>
418
- <CommandItem
419
- v-for="option in $dsl.evaluate`${field.options}`()"
420
- :key="$dsl.evaluate`${field.key}`({ option })"
421
- :value="$dsl.evaluate`${field.key}`({ option })"
422
- @select="setProperty(modelValue, field.path, $dsl.evaluate`${field.value}`({ option }))"
423
- >
424
- {{ $dsl.evaluate`${field.label}`({ option }) }}
425
- </CommandItem>
426
- </CommandGroup>
427
- </InputGroupCombobox>
428
- <InputGroupAddon v-if="field.type !== 'textarea' && field.icon">
429
- <Icon
430
- :icon="field.icon"
591
+ @blur="validateField(field)"
431
592
  />
432
- </InputGroupAddon>
433
- <InputGroupAddon
434
- v-if="field.type !== 'textarea' && hasProperty(modelValue, field.path)"
435
- align="inline-end"
436
- :class="getConfigOrientation(displayConfig) === 'floating' ? 'group-data-[disabled=true]/input-group:hidden' : void 0"
437
- >
438
- <Tooltip :delay-duration="800">
439
- <TooltipTrigger>
440
- <InputGroupButton as-child>
441
- <button
442
- type="button"
443
- class="text-zinc-300 hover:text-zinc-500 transition-colors"
444
- :disabled="isFieldDisabled(field)"
445
- @click="deleteProperty(modelValue, field.path)"
446
- >
447
- <Icon
448
- icon="fluent:dismiss-20-regular"
449
- />
450
- </button>
451
- </InputGroupButton>
452
- </TooltipTrigger>
453
- <TooltipContent>
454
- {{ t("clear") }}
455
- </TooltipContent>
456
- </Tooltip>
457
- </InputGroupAddon>
458
- <InputGroupAddon
459
- v-if="field.type === 'string' && field.maxLength && getProperty(modelValue, field.path)"
460
- align="inline-end"
461
- >
462
- <span class="text-xs text-zinc-400 font-mono">
463
- <span class="inline-block text-right">{{ String(getProperty(modelValue, field.path) ?? "").length }}</span>/{{ field.maxLength }}
464
- </span>
465
- </InputGroupAddon>
466
- <InputGroupAddon
467
- v-if="field.type === 'textarea' && (hasProperty(modelValue, field.path) || field.maxLength && getProperty(modelValue, field.path))"
468
- align="block-end"
469
- >
470
- <Tooltip
471
- v-if="hasProperty(modelValue, field.path)"
472
- :delay-duration="800"
593
+ <InputGroupAddon v-if="field.type !== 'textarea' && field.icon">
594
+ <Icon
595
+ :icon="field.icon"
596
+ />
597
+ </InputGroupAddon>
598
+ <InputGroupAddon
599
+ v-if="field.type !== 'textarea' && hasProperty(modelValue, field.path)"
600
+ align="inline-end"
601
+ :class="getConfigOrientation(displayConfig) === 'floating' ? 'group-data-[disabled=true]/input-group:hidden' : void 0"
473
602
  >
474
- <TooltipTrigger>
475
- <InputGroupButton as-child>
476
- <button
477
- type="button"
478
- class="text-zinc-300 hover:text-zinc-500 transition-colors"
479
- :disabled="isFieldDisabled(field)"
480
- @click="deleteProperty(modelValue, field.path)"
481
- >
482
- <Icon
483
- icon="fluent:dismiss-20-regular"
484
- />
485
- </button>
486
- </InputGroupButton>
487
- </TooltipTrigger>
488
- <TooltipContent>
489
- {{ t("clear") }}
490
- </TooltipContent>
491
- </Tooltip>
492
- <span
493
- v-if="field.maxLength && getProperty(modelValue, field.path)"
494
- class="text-xs text-zinc-400 font-mono"
603
+ <Tooltip :delay-duration="800">
604
+ <TooltipTrigger>
605
+ <InputGroupButton as-child>
606
+ <button
607
+ type="button"
608
+ class="text-zinc-300 hover:text-zinc-500 transition-colors"
609
+ :disabled="isFieldDisabled(field)"
610
+ @click="deleteProperty(modelValue, field.path)"
611
+ >
612
+ <Icon
613
+ icon="fluent:dismiss-20-regular"
614
+ />
615
+ </button>
616
+ </InputGroupButton>
617
+ </TooltipTrigger>
618
+ <TooltipContent>
619
+ {{ t("clear") }}
620
+ </TooltipContent>
621
+ </Tooltip>
622
+ </InputGroupAddon>
623
+ <InputGroupAddon
624
+ v-if="field.type === 'string' && field.maxLength && getProperty(modelValue, field.path)"
625
+ align="inline-end"
495
626
  >
496
- <span class="inline-block text-right">{{ String(getProperty(modelValue, field.path) ?? "").length }}</span>/{{ field.maxLength }}
497
- </span>
498
- </InputGroupAddon>
499
- </InputGroup>
627
+ <span class="text-xs text-zinc-400 font-mono">
628
+ <span class="inline-block text-right">{{ String(getProperty(modelValue, field.path) ?? "").length }}</span>/{{ field.maxLength }}
629
+ </span>
630
+ </InputGroupAddon>
631
+ <InputGroupAddon
632
+ v-if="field.type === 'textarea' && (hasProperty(modelValue, field.path) || field.maxLength && getProperty(modelValue, field.path))"
633
+ align="block-end"
634
+ >
635
+ <Tooltip
636
+ v-if="hasProperty(modelValue, field.path)"
637
+ :delay-duration="800"
638
+ >
639
+ <TooltipTrigger>
640
+ <InputGroupButton as-child>
641
+ <button
642
+ type="button"
643
+ class="text-zinc-300 hover:text-zinc-500 transition-colors"
644
+ :disabled="isFieldDisabled(field)"
645
+ @click="deleteProperty(modelValue, field.path)"
646
+ >
647
+ <Icon
648
+ icon="fluent:dismiss-20-regular"
649
+ />
650
+ </button>
651
+ </InputGroupButton>
652
+ </TooltipTrigger>
653
+ <TooltipContent>
654
+ {{ t("clear") }}
655
+ </TooltipContent>
656
+ </Tooltip>
657
+ <span
658
+ v-if="field.maxLength && getProperty(modelValue, field.path)"
659
+ class="text-xs text-zinc-400 font-mono"
660
+ >
661
+ <span class="inline-block text-right">{{ String(getProperty(modelValue, field.path) ?? "").length }}</span>/{{ field.maxLength }}
662
+ </span>
663
+ </InputGroupAddon>
664
+ </InputGroup>
665
+ </template>
500
666
 
501
667
  <FieldError v-if="isFieldInvalid(field)">
502
668
  <span v-html="renderValidationMessage(field)" />
@@ -513,14 +679,20 @@ export {
513
679
  {
514
680
  "zh": {
515
681
  "clear": "清空",
682
+ "select-empty": "无搜索结果",
683
+ "select-search-placeholder": "搜索…",
516
684
  "select-placeholder": "选择…"
517
685
  },
518
686
  "ja": {
519
687
  "clear": "クリア",
688
+ "select-empty": "結果はありません",
689
+ "select-search-placeholder": "検索…",
520
690
  "select-placeholder": "選択…"
521
691
  },
522
692
  "en": {
523
693
  "clear": "Clear",
694
+ "select-empty": "No results",
695
+ "select-search-placeholder": "Search…",
524
696
  "select-placeholder": "Select…"
525
697
  }
526
698
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/nuxt",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "type": "module",