@shwfed/nuxt 0.10.3 → 0.10.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.
Files changed (23) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/button.d.vue.ts +95 -0
  3. package/dist/runtime/components/button.vue +39 -0
  4. package/dist/runtime/components/button.vue.d.ts +95 -0
  5. package/dist/runtime/components/fields.d.vue.ts +2 -2
  6. package/dist/runtime/components/fields.vue.d.ts +2 -2
  7. package/dist/runtime/components/ui/button-configurator/ButtonConfiguratorDialog.d.vue.ts +97 -0
  8. package/dist/runtime/components/ui/button-configurator/ButtonConfiguratorDialog.vue +1078 -0
  9. package/dist/runtime/components/ui/button-configurator/ButtonConfiguratorDialog.vue.d.ts +97 -0
  10. package/dist/runtime/components/ui/button-configurator/menu.d.ts +48 -0
  11. package/dist/runtime/components/ui/button-configurator/menu.js +381 -0
  12. package/dist/runtime/components/ui/buttons/Buttons.d.vue.ts +95 -0
  13. package/dist/runtime/components/ui/buttons/Buttons.vue +330 -0
  14. package/dist/runtime/components/ui/buttons/Buttons.vue.d.ts +95 -0
  15. package/dist/runtime/components/ui/buttons/schema.d.ts +374 -0
  16. package/dist/runtime/components/ui/buttons/schema.js +58 -0
  17. package/dist/runtime/components/ui/fields/Fields.d.vue.ts +4 -4
  18. package/dist/runtime/components/ui/fields/Fields.vue.d.ts +4 -4
  19. package/dist/runtime/components/ui/fields/schema.d.ts +3 -3
  20. package/dist/runtime/components/ui/fields-configurator/FieldsConfiguratorDialog.d.vue.ts +2 -2
  21. package/dist/runtime/components/ui/fields-configurator/FieldsConfiguratorDialog.vue.d.ts +2 -2
  22. package/dist/runtime/components/ui/table/Table.vue +11 -10
  23. package/package.json +1 -1
@@ -0,0 +1,1078 @@
1
+ <script setup>
2
+ import { useSortable } from "@vueuse/integrations/useSortable";
3
+ import { Icon } from "@iconify/vue";
4
+ import { computed, nextTick, ref, watch } from "vue";
5
+ import { useI18n } from "vue-i18n";
6
+ import { cn } from "../../../utils/cn";
7
+ import { Button } from "../button";
8
+ import { ButtonConfigC, ButtonsStyleC } from "../buttons/schema";
9
+ import { Checkbox } from "../checkbox";
10
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../dialog";
11
+ import { IconPicker } from "../icon-picker";
12
+ import { Input } from "../input";
13
+ import Locale from "../locale/Locale.vue";
14
+ import { NativeSelect, NativeSelectOption } from "../native-select";
15
+ import { Textarea } from "../textarea";
16
+ import {
17
+ buildButtonConfiguratorTree,
18
+ createButtonConfiguratorButtonNode,
19
+ createButtonConfiguratorDropdownNode,
20
+ createButtonConfiguratorGroupNode,
21
+ flattenButtonConfiguratorTree,
22
+ getButtonConfiguratorNode,
23
+ insertButtonConfiguratorChildNode,
24
+ insertButtonConfiguratorRootNode,
25
+ materializeButtonConfiguratorTree,
26
+ moveButtonConfiguratorNodeToParent,
27
+ moveButtonConfiguratorSibling,
28
+ removeButtonConfiguratorSubtree,
29
+ updateButtonConfiguratorNode
30
+ } from "./menu";
31
+ defineOptions({
32
+ inheritAttrs: false
33
+ });
34
+ const props = defineProps({
35
+ config: { type: null, required: true }
36
+ });
37
+ const emit = defineEmits(["confirm"]);
38
+ const open = defineModel("open", { type: Boolean, ...{
39
+ default: false
40
+ } });
41
+ const { t } = useI18n();
42
+ const search = ref("");
43
+ const draftGap = ref(props.config.gap ?? 12);
44
+ const draftStyle = ref(props.config.style);
45
+ const selectedItemId = ref("general");
46
+ const draftTree = ref(buildButtonConfiguratorTree(props.config.groups));
47
+ const sortableListRef = ref(null);
48
+ const sortableItemIds = ref([]);
49
+ const validationErrors = ref({});
50
+ const sortable = useSortable(sortableListRef, sortableItemIds);
51
+ function handleSortableUpdate(event) {
52
+ if (event.oldIndex === void 0 || event.newIndex === void 0 || event.oldIndex === event.newIndex) {
53
+ return;
54
+ }
55
+ draftTree.value = moveButtonConfiguratorSibling(
56
+ draftTree.value,
57
+ selectedItemId.value === "general" ? void 0 : selectedItemId.value,
58
+ event.oldIndex,
59
+ event.newIndex
60
+ );
61
+ }
62
+ function configureSortable() {
63
+ sortable.option("handle", '[data-slot="button-configurator-drag-handle"]');
64
+ sortable.option("animation", 150);
65
+ sortable.option("onUpdate", handleSortableUpdate);
66
+ }
67
+ async function refreshSortable() {
68
+ sortable.stop();
69
+ if (!open.value || selectedContainerItemIds.value.length === 0) {
70
+ return;
71
+ }
72
+ await nextTick();
73
+ sortable.start();
74
+ configureSortable();
75
+ }
76
+ const normalizedSearch = computed(() => search.value.trim().toLocaleLowerCase());
77
+ const generalItem = computed(() => ({
78
+ id: "general",
79
+ label: t("general")
80
+ }));
81
+ const selectedNode = computed(() => {
82
+ if (selectedItemId.value === "general") {
83
+ return void 0;
84
+ }
85
+ return getButtonConfiguratorNode(draftTree.value, selectedItemId.value);
86
+ });
87
+ const selectedGroup = computed(() => selectedNode.value?.item.type === "group" ? selectedNode.value.item : void 0);
88
+ const selectedButton = computed(() => selectedNode.value?.item.type === "button" ? selectedNode.value.item : void 0);
89
+ const selectedDropdown = computed(() => selectedNode.value?.item.type === "dropdown" ? selectedNode.value.item : void 0);
90
+ const selectedNodeItemId = computed(() => selectedNode.value?.itemId ?? "");
91
+ const selectedParentNodeId = computed(() => {
92
+ const parentItemId = selectedNode.value?.parentItemId;
93
+ if (!parentItemId) {
94
+ return "";
95
+ }
96
+ return getButtonConfiguratorNode(draftTree.value, parentItemId)?.item.id ?? "";
97
+ });
98
+ const selectedButtonIsInGroup = computed(() => {
99
+ const parentItemId = selectedNode.value?.parentItemId;
100
+ if (!parentItemId) {
101
+ return false;
102
+ }
103
+ return getButtonConfiguratorNode(draftTree.value, parentItemId)?.item.type === "group";
104
+ });
105
+ const canAddGroup = computed(() => selectedItemId.value === "general");
106
+ const canAddGroupButton = computed(() => selectedNode.value?.item.type === "group");
107
+ const canAddGroupDropdown = computed(() => selectedNode.value?.item.type === "group");
108
+ const canAddDropdownChildButton = computed(() => selectedNode.value?.item.type === "dropdown");
109
+ const flattenedItems = computed(() => flattenButtonConfiguratorTree(draftTree.value).map((item) => ({
110
+ itemId: item.itemId,
111
+ depth: item.depth,
112
+ label: getNodeLabel(item.item),
113
+ type: item.item.type,
114
+ id: item.item.id
115
+ })));
116
+ const filteredItems = computed(() => {
117
+ if (!normalizedSearch.value) {
118
+ return flattenedItems.value;
119
+ }
120
+ return flattenedItems.value.filter((item) => {
121
+ const haystack = `${item.label} ${item.id}`.toLocaleLowerCase();
122
+ return haystack.includes(normalizedSearch.value);
123
+ });
124
+ });
125
+ const selectedItemLabel = computed(() => {
126
+ if (!selectedNode.value) {
127
+ return generalItem.value.label;
128
+ }
129
+ return getNodeLabel(selectedNode.value.item);
130
+ });
131
+ const selectedContainerItemIds = computed(() => {
132
+ if (selectedItemId.value === "general") {
133
+ return draftTree.value.rootItemIds;
134
+ }
135
+ if (!selectedNode.value) {
136
+ return [];
137
+ }
138
+ if (selectedNode.value.item.type === "button") {
139
+ return [];
140
+ }
141
+ return selectedNode.value.childItemIds;
142
+ });
143
+ const childItemsTitle = computed(() => {
144
+ if (selectedItemId.value === "general") {
145
+ return t("groups");
146
+ }
147
+ if (selectedGroup.value) {
148
+ return t("group-items");
149
+ }
150
+ return t("children");
151
+ });
152
+ const childItems = computed(() => selectedContainerItemIds.value.flatMap((itemId) => {
153
+ const node = getButtonConfiguratorNode(draftTree.value, itemId);
154
+ if (!node) {
155
+ return [];
156
+ }
157
+ return [{
158
+ itemId: node.itemId,
159
+ label: getNodeLabel(node.item),
160
+ type: node.item.type,
161
+ id: node.item.id
162
+ }];
163
+ }));
164
+ const buttonParentOptions = computed(() => draftTree.value.nodes.flatMap((node) => {
165
+ if (node.item.type === "group") {
166
+ return [{
167
+ id: node.item.id,
168
+ itemId: node.itemId,
169
+ label: getNodeLabel(node.item),
170
+ type: "group"
171
+ }];
172
+ }
173
+ if (node.item.type === "dropdown") {
174
+ return [{
175
+ id: node.item.id,
176
+ itemId: node.itemId,
177
+ label: getNodeLabel(node.item),
178
+ type: "dropdown"
179
+ }];
180
+ }
181
+ return [];
182
+ }));
183
+ const dropdownParentOptions = computed(() => draftTree.value.nodes.flatMap((node) => node.item.type === "group" ? [{
184
+ id: node.item.id,
185
+ itemId: node.itemId,
186
+ label: getNodeLabel(node.item)
187
+ }] : []));
188
+ watch(() => props.config, (config) => {
189
+ applyDraftConfig(config);
190
+ }, { immediate: true });
191
+ watch([selectedContainerItemIds, selectedItemId], () => {
192
+ sortableItemIds.value = selectedContainerItemIds.value.slice();
193
+ sortable.option("disabled", selectedContainerItemIds.value.length === 0);
194
+ });
195
+ watch([open, selectedItemId, selectedContainerItemIds], async () => {
196
+ await refreshSortable();
197
+ }, { immediate: true });
198
+ watch(filteredItems, (items) => {
199
+ if (selectedItemId.value === "general") {
200
+ return;
201
+ }
202
+ if (items.some((item) => item.itemId === selectedItemId.value)) {
203
+ return;
204
+ }
205
+ selectedItemId.value = items[0]?.itemId ?? "general";
206
+ }, { immediate: true });
207
+ function getZhText(value) {
208
+ if (!value) {
209
+ return void 0;
210
+ }
211
+ const zhValue = value.find((item) => item.locale === "zh");
212
+ if (!zhValue) {
213
+ return void 0;
214
+ }
215
+ const message = zhValue.message.trim();
216
+ return message.length > 0 ? message : void 0;
217
+ }
218
+ function normalizeOptionalString(value) {
219
+ const normalized = value.trim();
220
+ return normalized.length > 0 ? normalized : void 0;
221
+ }
222
+ function getGeneralErrorKey(field) {
223
+ return `general.${field}`;
224
+ }
225
+ function clearError(key) {
226
+ const nextErrors = {};
227
+ for (const [errorKey, errorValue] of Object.entries(validationErrors.value)) {
228
+ if (errorKey !== key) {
229
+ Reflect.set(nextErrors, errorKey, errorValue);
230
+ }
231
+ }
232
+ validationErrors.value = nextErrors;
233
+ }
234
+ function isCopyableNode(node) {
235
+ if (!node) {
236
+ return false;
237
+ }
238
+ return node.item.type === "button" || node.item.type === "dropdown";
239
+ }
240
+ function getNodeLabel(item) {
241
+ if (item.type === "group") {
242
+ return t("group-label");
243
+ }
244
+ return getZhText(item.title) ?? t(item.type === "button" ? "untitled-button" : "untitled-dropdown");
245
+ }
246
+ function applyDraftConfig(config) {
247
+ draftGap.value = config.gap ?? 12;
248
+ draftStyle.value = config.style;
249
+ draftTree.value = buildButtonConfiguratorTree(config.groups);
250
+ validationErrors.value = {};
251
+ selectedItemId.value = "general";
252
+ sortableItemIds.value = draftTree.value.rootItemIds.slice();
253
+ }
254
+ function selectGeneral() {
255
+ selectedItemId.value = "general";
256
+ }
257
+ function selectItem(itemId) {
258
+ selectedItemId.value = itemId;
259
+ }
260
+ function addGroup() {
261
+ const node = createButtonConfiguratorGroupNode();
262
+ draftTree.value = insertButtonConfiguratorRootNode(draftTree.value, node);
263
+ selectedItemId.value = node.itemId;
264
+ }
265
+ function addButton() {
266
+ const node = createButtonConfiguratorButtonNode();
267
+ if (selectedNode.value?.item.type === "group" || selectedNode.value?.item.type === "dropdown") {
268
+ draftTree.value = insertButtonConfiguratorChildNode(draftTree.value, selectedNode.value.itemId, node);
269
+ selectedItemId.value = node.itemId;
270
+ }
271
+ }
272
+ function addDropdown() {
273
+ const node = createButtonConfiguratorDropdownNode();
274
+ if (selectedNode.value?.item.type === "group") {
275
+ draftTree.value = insertButtonConfiguratorChildNode(draftTree.value, selectedNode.value.itemId, node);
276
+ selectedItemId.value = node.itemId;
277
+ }
278
+ }
279
+ function deleteItem(itemId) {
280
+ draftTree.value = removeButtonConfiguratorSubtree(draftTree.value, itemId);
281
+ if (selectedItemId.value === itemId) {
282
+ selectedItemId.value = "general";
283
+ }
284
+ }
285
+ function updateGap(value) {
286
+ const parsed = Number(value);
287
+ if (Number.isFinite(parsed) && parsed >= 0) {
288
+ draftGap.value = parsed;
289
+ }
290
+ }
291
+ function updateDraftStyle(value) {
292
+ clearError(getGeneralErrorKey("style"));
293
+ draftStyle.value = normalizeOptionalString(String(value));
294
+ }
295
+ function updateSelectedTitle(value) {
296
+ const node = selectedNode.value;
297
+ if (!node) {
298
+ return;
299
+ }
300
+ draftTree.value = updateButtonConfiguratorNode(draftTree.value, node.itemId, (item) => {
301
+ if (item.type === "group") {
302
+ return item;
303
+ }
304
+ return {
305
+ ...item,
306
+ title: value
307
+ };
308
+ });
309
+ }
310
+ function updateSelectedTooltip(value) {
311
+ const node = selectedNode.value;
312
+ if (!node || node.item.type !== "button") {
313
+ return;
314
+ }
315
+ draftTree.value = updateButtonConfiguratorNode(draftTree.value, node.itemId, (item) => {
316
+ if (item.type !== "button") {
317
+ return item;
318
+ }
319
+ return {
320
+ ...item,
321
+ tooltip: value
322
+ };
323
+ });
324
+ }
325
+ function updateSelectedIcon(value) {
326
+ const node = selectedNode.value;
327
+ if (!node) {
328
+ return;
329
+ }
330
+ draftTree.value = updateButtonConfiguratorNode(draftTree.value, node.itemId, (item) => {
331
+ if (item.type === "group") {
332
+ return item;
333
+ }
334
+ if (item.type === "button") {
335
+ const nextItem2 = {
336
+ type: "button",
337
+ id: item.id,
338
+ title: item.title
339
+ };
340
+ if (item.variant) {
341
+ nextItem2.variant = item.variant;
342
+ }
343
+ if (item.hideTitle) {
344
+ nextItem2.hideTitle = item.hideTitle;
345
+ }
346
+ if (item.tooltip) {
347
+ nextItem2.tooltip = item.tooltip;
348
+ }
349
+ if (value) {
350
+ nextItem2.icon = value;
351
+ }
352
+ return nextItem2;
353
+ }
354
+ const nextItem = {
355
+ type: "dropdown",
356
+ id: item.id,
357
+ title: item.title
358
+ };
359
+ if (value) {
360
+ nextItem.icon = value;
361
+ }
362
+ return nextItem;
363
+ });
364
+ }
365
+ function updateSelectedVariant(value) {
366
+ const node = selectedNode.value;
367
+ if (!node || node.item.type !== "button" || typeof value !== "string") {
368
+ return;
369
+ }
370
+ draftTree.value = updateButtonConfiguratorNode(draftTree.value, node.itemId, (item) => {
371
+ if (item.type !== "button") {
372
+ return item;
373
+ }
374
+ return {
375
+ ...item,
376
+ variant: value === "default" || value === "primary" || value === "destructive" || value === "ghost" ? value : void 0
377
+ };
378
+ });
379
+ }
380
+ function updateSelectedHideTitle(value) {
381
+ const node = selectedNode.value;
382
+ if (!node || node.item.type !== "button" || !selectedButtonIsInGroup.value || typeof value !== "boolean") {
383
+ return;
384
+ }
385
+ draftTree.value = updateButtonConfiguratorNode(draftTree.value, node.itemId, (item) => {
386
+ if (item.type !== "button") {
387
+ return item;
388
+ }
389
+ if (!value) {
390
+ return {
391
+ type: "button",
392
+ id: item.id,
393
+ title: item.title,
394
+ tooltip: item.tooltip,
395
+ icon: item.icon,
396
+ variant: item.variant
397
+ };
398
+ }
399
+ return {
400
+ ...item,
401
+ hideTitle: true
402
+ };
403
+ });
404
+ }
405
+ function moveSelectedButtonParent(value) {
406
+ const node = selectedNode.value;
407
+ if (!node || node.item.type !== "button" || typeof value !== "string") {
408
+ return;
409
+ }
410
+ const targetParent = buttonParentOptions.value.find((option) => option.id === value);
411
+ if (!targetParent) {
412
+ return;
413
+ }
414
+ draftTree.value = moveButtonConfiguratorNodeToParent(draftTree.value, node.itemId, targetParent.itemId);
415
+ }
416
+ function moveSelectedDropdownParent(value) {
417
+ const node = selectedNode.value;
418
+ if (!node || node.item.type !== "dropdown" || typeof value !== "string") {
419
+ return;
420
+ }
421
+ const targetParent = dropdownParentOptions.value.find((option) => option.id === value);
422
+ if (!targetParent) {
423
+ return;
424
+ }
425
+ draftTree.value = moveButtonConfiguratorNodeToParent(draftTree.value, node.itemId, targetParent.itemId);
426
+ }
427
+ function handleSelectedVariantUpdate(value) {
428
+ if (typeof value === "string") {
429
+ updateSelectedVariant(value);
430
+ }
431
+ }
432
+ function handleSelectedButtonParentUpdate(value) {
433
+ if (typeof value === "string") {
434
+ moveSelectedButtonParent(value);
435
+ }
436
+ }
437
+ function handleSelectedDropdownParentUpdate(value) {
438
+ if (typeof value === "string") {
439
+ moveSelectedDropdownParent(value);
440
+ }
441
+ }
442
+ async function copySelectedId() {
443
+ const node = selectedNode.value;
444
+ if (!node || !isCopyableNode(node)) {
445
+ return;
446
+ }
447
+ try {
448
+ await navigator.clipboard.writeText(node.item.id);
449
+ } catch {
450
+ return;
451
+ }
452
+ }
453
+ function confirmChanges() {
454
+ const generalStyleResult = ButtonsStyleC.safeParse(draftStyle.value);
455
+ if (!generalStyleResult.success) {
456
+ validationErrors.value = {
457
+ ...validationErrors.value,
458
+ [getGeneralErrorKey("style")]: generalStyleResult.error.issues[0]?.message ?? t("general-style-invalid")
459
+ };
460
+ selectedItemId.value = "general";
461
+ return;
462
+ }
463
+ const result = ButtonConfigC.safeParse({
464
+ gap: draftGap.value,
465
+ style: generalStyleResult.data,
466
+ groups: materializeButtonConfiguratorTree(draftTree.value)
467
+ });
468
+ if (!result.success) {
469
+ return;
470
+ }
471
+ emit("confirm", result.data);
472
+ open.value = false;
473
+ }
474
+ </script>
475
+
476
+ <template>
477
+ <Dialog
478
+ :open="open"
479
+ @update:open="open = $event"
480
+ >
481
+ <DialogContent class="flex h-[min(40rem,calc(100vh-4rem))] w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] flex-col overflow-hidden p-0 sm:w-[72rem] sm:max-w-[72rem]">
482
+ <DialogHeader class="gap-1 border-b border-zinc-200 px-6 py-5">
483
+ <DialogTitle class="text-xl font-semibold text-zinc-800">
484
+ {{ t("configure-buttons") }}
485
+ </DialogTitle>
486
+ <DialogDescription class="text-sm text-zinc-500">
487
+ {{ t("configure-buttons-description") }}
488
+ </DialogDescription>
489
+ </DialogHeader>
490
+
491
+ <div class="grid min-h-0 flex-1 grid-cols-[18rem_minmax(0,1fr)]">
492
+ <section class="flex min-h-0 flex-col border-r border-zinc-200 px-4 py-4">
493
+ <Input
494
+ v-model="search"
495
+ data-slot="button-configurator-search"
496
+ :placeholder="t('search')"
497
+ />
498
+
499
+ <div class="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden">
500
+ <div class="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto pr-1">
501
+ <button
502
+ type="button"
503
+ data-slot="button-configurator-item"
504
+ data-item-id="general"
505
+ :data-selected="selectedItemId === 'general' ? 'true' : 'false'"
506
+ :class="cn(
507
+ 'flex w-full items-center rounded-md border p-1 text-left transition-colors',
508
+ selectedItemId === 'general' ? 'border-(--primary)/25 bg-[color-mix(in_srgb,var(--primary)_10%,white)]' : 'border-transparent hover:border-zinc-200 hover:bg-zinc-50'
509
+ )"
510
+ @click="selectGeneral"
511
+ >
512
+ <span class="truncate px-2 py-2 text-sm font-medium text-zinc-800">
513
+ {{ generalItem.label }}
514
+ </span>
515
+ </button>
516
+
517
+ <div
518
+ v-for="item in filteredItems"
519
+ :key="item.itemId"
520
+ data-slot="button-configurator-item"
521
+ :data-item-id="item.itemId"
522
+ :data-node-id="item.id"
523
+ :data-selected="selectedItemId === item.itemId ? 'true' : 'false'"
524
+ :class="cn(
525
+ 'flex w-full items-center gap-2 rounded-md border p-1 text-left transition-colors',
526
+ selectedItemId === item.itemId ? 'border-(--primary)/25 bg-[color-mix(in_srgb,var(--primary)_10%,white)]' : 'border-transparent hover:border-zinc-200 hover:bg-zinc-50'
527
+ )"
528
+ :style="{ paddingInlineStart: `${item.depth * 14 + 8}px` }"
529
+ >
530
+ <button
531
+ type="button"
532
+ data-slot="button-configurator-item-select"
533
+ class="min-w-0 flex-1 text-left"
534
+ @click="selectItem(item.itemId)"
535
+ >
536
+ <span
537
+ data-slot="button-configurator-item-label"
538
+ class="block truncate px-2 py-2 text-sm font-medium text-zinc-800"
539
+ >
540
+ {{ item.label }}
541
+ </span>
542
+ </button>
543
+
544
+ <button
545
+ type="button"
546
+ data-slot="button-configurator-item-delete"
547
+ class="flex size-8 shrink-0 items-center justify-center rounded-sm text-zinc-400 transition-colors hover:bg-red-50 hover:text-red-600"
548
+ @click.stop="deleteItem(item.itemId)"
549
+ >
550
+ <Icon icon="fluent:delete-20-regular" />
551
+ </button>
552
+ </div>
553
+
554
+ <p
555
+ v-if="filteredItems.length === 0 && normalizedSearch"
556
+ data-slot="button-configurator-empty"
557
+ class="px-2 pt-2 text-xs text-zinc-400"
558
+ >
559
+ {{ t("no-matches") }}
560
+ </p>
561
+ </div>
562
+ </div>
563
+ </section>
564
+
565
+ <section class="flex min-h-0 flex-col overflow-y-auto px-6 py-6">
566
+ <div class="flex items-center gap-2">
567
+ <h3
568
+ data-slot="button-configurator-detail-title"
569
+ class="text-lg font-semibold text-zinc-800"
570
+ >
571
+ {{ selectedItemLabel }}
572
+ </h3>
573
+ <Button
574
+ v-if="isCopyableNode(selectedNode)"
575
+ type="button"
576
+ variant="ghost"
577
+ size="sm"
578
+ data-slot="button-configurator-copy-id"
579
+ class="size-7 p-0 text-zinc-400 hover:text-zinc-700"
580
+ :title="selectedNode?.item.id"
581
+ @click="void copySelectedId()"
582
+ >
583
+ <Icon icon="fluent:copy-20-regular" />
584
+ </Button>
585
+ </div>
586
+
587
+ <p class="mt-2 text-sm text-zinc-500">
588
+ {{ selectedItemId === "general" ? t("general-description") : t("detail-description") }}
589
+ </p>
590
+
591
+ <div class="mt-6 flex flex-wrap gap-2">
592
+ <Button
593
+ v-if="canAddGroup"
594
+ type="button"
595
+ data-slot="button-configurator-add-group"
596
+ @click="addGroup"
597
+ >
598
+ <Icon icon="fluent:add-20-regular" />
599
+ {{ t("add-group") }}
600
+ </Button>
601
+
602
+ <Button
603
+ v-if="canAddGroupButton || canAddDropdownChildButton"
604
+ type="button"
605
+ data-slot="button-configurator-add-button"
606
+ @click="addButton"
607
+ >
608
+ <Icon icon="fluent:add-20-regular" />
609
+ {{ t("add-button") }}
610
+ </Button>
611
+
612
+ <Button
613
+ v-if="canAddGroupDropdown"
614
+ type="button"
615
+ data-slot="button-configurator-add-dropdown"
616
+ @click="addDropdown"
617
+ >
618
+ <Icon icon="fluent:add-20-regular" />
619
+ {{ t("add-dropdown") }}
620
+ </Button>
621
+ </div>
622
+
623
+ <section
624
+ v-if="selectedItemId === 'general'"
625
+ data-slot="button-configurator-general"
626
+ class="mt-6 grid gap-4"
627
+ >
628
+ <label class="flex flex-col gap-2">
629
+ <span class="text-xs font-medium text-zinc-500">{{ t("group-gap") }}</span>
630
+ <Input
631
+ data-slot="button-configurator-gap"
632
+ type="number"
633
+ min="0"
634
+ :model-value="`${draftGap}`"
635
+ @update:model-value="updateGap"
636
+ />
637
+ </label>
638
+
639
+ <label
640
+ data-slot="button-configurator-general-style-section"
641
+ class="flex flex-col gap-2"
642
+ >
643
+ <span class="text-xs font-medium text-zinc-500">{{ t("general-style") }}</span>
644
+ <Textarea
645
+ data-slot="button-configurator-general-style-input"
646
+ :model-value="draftStyle ?? ''"
647
+ :aria-invalid="validationErrors[getGeneralErrorKey('style')] ? 'true' : void 0"
648
+ :placeholder="t('general-style-placeholder')"
649
+ class="min-h-20 font-mono text-sm"
650
+ @update:model-value="updateDraftStyle"
651
+ />
652
+ <p
653
+ v-if="validationErrors[getGeneralErrorKey('style')]"
654
+ data-slot="button-configurator-general-style-error"
655
+ class="text-xs text-red-500"
656
+ >
657
+ {{ validationErrors[getGeneralErrorKey("style")] }}
658
+ </p>
659
+ </label>
660
+ </section>
661
+
662
+ <section
663
+ v-else-if="selectedGroup"
664
+ data-slot="button-configurator-group"
665
+ class="mt-6 grid gap-4"
666
+ >
667
+ <Button
668
+ type="button"
669
+ variant="ghost"
670
+ data-slot="button-configurator-delete-selected"
671
+ class="justify-start text-red-600 hover:bg-red-50 hover:text-red-700"
672
+ @click="deleteItem(selectedNodeItemId)"
673
+ >
674
+ <Icon icon="fluent:delete-20-regular" />
675
+ {{ t("delete-group") }}
676
+ </Button>
677
+ </section>
678
+
679
+ <section
680
+ v-else-if="selectedButton"
681
+ data-slot="button-configurator-button"
682
+ class="mt-6 grid gap-4"
683
+ >
684
+ <label class="flex flex-col gap-2">
685
+ <span class="text-xs font-medium text-zinc-500">{{ t("button-title") }}</span>
686
+ <Locale
687
+ data-slot="button-configurator-button-title"
688
+ :model-value="selectedButton.title"
689
+ @update:model-value="updateSelectedTitle"
690
+ />
691
+ </label>
692
+
693
+ <label class="flex flex-col gap-2">
694
+ <span class="text-xs font-medium text-zinc-500">{{ t("button-icon") }}</span>
695
+ <IconPicker
696
+ data-slot="button-configurator-button-icon"
697
+ :model-value="selectedButton.icon"
698
+ @update:model-value="updateSelectedIcon"
699
+ />
700
+ </label>
701
+
702
+ <label class="flex flex-col gap-2">
703
+ <span class="text-xs font-medium text-zinc-500">{{ t("tooltip") }}</span>
704
+ <Locale
705
+ data-slot="button-configurator-button-tooltip"
706
+ :model-value="selectedButton.tooltip"
707
+ @update:model-value="updateSelectedTooltip"
708
+ />
709
+ </label>
710
+
711
+ <label class="flex flex-col gap-2">
712
+ <span class="text-xs font-medium text-zinc-500">{{ t("button-variant") }}</span>
713
+ <NativeSelect
714
+ data-slot="button-configurator-button-variant"
715
+ :model-value="selectedButton.variant ?? ''"
716
+ @update:model-value="handleSelectedVariantUpdate"
717
+ >
718
+ <NativeSelectOption value="">
719
+ {{ t("button-variant-default") }}
720
+ </NativeSelectOption>
721
+ <NativeSelectOption value="default">
722
+ {{ t("button-variant-default-option") }}
723
+ </NativeSelectOption>
724
+ <NativeSelectOption value="primary">
725
+ {{ t("button-variant-primary") }}
726
+ </NativeSelectOption>
727
+ <NativeSelectOption value="destructive">
728
+ {{ t("button-variant-destructive") }}
729
+ </NativeSelectOption>
730
+ <NativeSelectOption value="ghost">
731
+ {{ t("button-variant-ghost") }}
732
+ </NativeSelectOption>
733
+ </NativeSelect>
734
+ </label>
735
+
736
+ <label class="flex flex-col gap-2">
737
+ <span class="text-xs font-medium text-zinc-500">{{ t("button-parent") }}</span>
738
+ <NativeSelect
739
+ data-slot="button-configurator-button-parent"
740
+ :model-value="selectedParentNodeId"
741
+ @update:model-value="handleSelectedButtonParentUpdate"
742
+ >
743
+ <NativeSelectOption
744
+ v-for="option in buttonParentOptions"
745
+ :key="option.itemId"
746
+ :value="option.id"
747
+ >
748
+ {{ option.type === "group" ? `${t("group-label")} \xB7 ${option.label}` : `${t("dropdown-label")} \xB7 ${option.label}` }}
749
+ </NativeSelectOption>
750
+ </NativeSelect>
751
+ </label>
752
+
753
+ <label
754
+ v-if="selectedButtonIsInGroup"
755
+ class="flex items-center gap-3 rounded-md border border-zinc-200 px-3 py-2"
756
+ >
757
+ <Checkbox
758
+ data-slot="button-configurator-button-hide-title"
759
+ :model-value="selectedButton.hideTitle ?? false"
760
+ @update:model-value="updateSelectedHideTitle"
761
+ />
762
+ <span class="text-sm text-zinc-700">{{ t("button-hide-title") }}</span>
763
+ </label>
764
+
765
+ <Button
766
+ type="button"
767
+ variant="ghost"
768
+ data-slot="button-configurator-delete-selected"
769
+ class="justify-start text-red-600 hover:bg-red-50 hover:text-red-700"
770
+ @click="deleteItem(selectedNodeItemId)"
771
+ >
772
+ <Icon icon="fluent:delete-20-regular" />
773
+ {{ t("delete-button") }}
774
+ </Button>
775
+ </section>
776
+
777
+ <section
778
+ v-else-if="selectedDropdown"
779
+ data-slot="button-configurator-dropdown"
780
+ class="mt-6 grid gap-4"
781
+ >
782
+ <label class="flex flex-col gap-2">
783
+ <span class="text-xs font-medium text-zinc-500">{{ t("dropdown-title") }}</span>
784
+ <Locale
785
+ data-slot="button-configurator-dropdown-title"
786
+ :model-value="selectedDropdown.title"
787
+ @update:model-value="updateSelectedTitle"
788
+ />
789
+ </label>
790
+
791
+ <label class="flex flex-col gap-2">
792
+ <span class="text-xs font-medium text-zinc-500">{{ t("dropdown-icon") }}</span>
793
+ <IconPicker
794
+ data-slot="button-configurator-dropdown-icon"
795
+ :model-value="selectedDropdown.icon"
796
+ @update:model-value="updateSelectedIcon"
797
+ />
798
+ </label>
799
+
800
+ <label class="flex flex-col gap-2">
801
+ <span class="text-xs font-medium text-zinc-500">{{ t("dropdown-parent") }}</span>
802
+ <NativeSelect
803
+ data-slot="button-configurator-dropdown-parent"
804
+ :model-value="selectedParentNodeId"
805
+ @update:model-value="handleSelectedDropdownParentUpdate"
806
+ >
807
+ <NativeSelectOption
808
+ v-for="option in dropdownParentOptions"
809
+ :key="option.itemId"
810
+ :value="option.id"
811
+ >
812
+ {{ `${t("group-label")} \xB7 ${option.label}` }}
813
+ </NativeSelectOption>
814
+ </NativeSelect>
815
+ </label>
816
+
817
+ <Button
818
+ type="button"
819
+ variant="ghost"
820
+ data-slot="button-configurator-delete-selected"
821
+ class="justify-start text-red-600 hover:bg-red-50 hover:text-red-700"
822
+ @click="deleteItem(selectedNodeItemId)"
823
+ >
824
+ <Icon icon="fluent:delete-20-regular" />
825
+ {{ t("delete-dropdown") }}
826
+ </Button>
827
+ </section>
828
+
829
+ <section
830
+ v-if="childItems.length > 0"
831
+ class="mt-6 flex min-h-0 flex-1 flex-col"
832
+ >
833
+ <div class="mb-2 flex items-center justify-between">
834
+ <span class="text-xs font-medium text-zinc-500">{{ childItemsTitle }}</span>
835
+ </div>
836
+
837
+ <div
838
+ ref="sortableListRef"
839
+ data-slot="button-configurator-child-list"
840
+ class="flex flex-col gap-1"
841
+ >
842
+ <div
843
+ v-for="item in childItems"
844
+ :key="item.itemId"
845
+ data-slot="button-configurator-child-item"
846
+ :data-item-id="item.itemId"
847
+ :data-node-id="item.id"
848
+ class="flex items-center gap-2 rounded-md border border-zinc-200 p-1"
849
+ >
850
+ <button
851
+ type="button"
852
+ data-slot="button-configurator-drag-handle"
853
+ class="flex size-8 shrink-0 cursor-grab items-center justify-center rounded-sm text-zinc-400 active:cursor-grabbing"
854
+ >
855
+ <Icon icon="fluent:re-order-dots-vertical-20-regular" />
856
+ </button>
857
+
858
+ <button
859
+ type="button"
860
+ data-slot="button-configurator-child-select"
861
+ class="min-w-0 flex-1 px-2 py-2 text-left"
862
+ @click="selectItem(item.itemId)"
863
+ >
864
+ <span class="block truncate text-sm font-medium text-zinc-800">
865
+ {{ item.label }}
866
+ </span>
867
+ </button>
868
+
869
+ <button
870
+ type="button"
871
+ data-slot="button-configurator-child-delete"
872
+ class="flex size-8 shrink-0 items-center justify-center rounded-sm text-zinc-400 transition-colors hover:bg-red-50 hover:text-red-600"
873
+ @click="deleteItem(item.itemId)"
874
+ >
875
+ <Icon icon="fluent:delete-20-regular" />
876
+ </button>
877
+ </div>
878
+ </div>
879
+ </section>
880
+ </section>
881
+ </div>
882
+
883
+ <DialogFooter class="border-t border-zinc-200 px-6 py-4">
884
+ <Button
885
+ type="button"
886
+ variant="default"
887
+ data-slot="button-configurator-cancel"
888
+ @click="open = false"
889
+ >
890
+ <Icon icon="fluent:dismiss-20-regular" />
891
+ {{ t("cancel") }}
892
+ </Button>
893
+ <Button
894
+ type="button"
895
+ variant="primary"
896
+ data-slot="button-configurator-confirm"
897
+ @click="confirmChanges"
898
+ >
899
+ <Icon icon="fluent:checkmark-20-regular" />
900
+ {{ t("confirm") }}
901
+ </Button>
902
+ </DialogFooter>
903
+ </DialogContent>
904
+ </Dialog>
905
+ </template>
906
+
907
+ <i18n lang="json">
908
+ {
909
+ "zh": {
910
+ "configure-buttons": "配置按钮",
911
+ "configure-buttons-description": "管理多个按钮组、组间距和下拉按钮结构。",
912
+ "general": "总览",
913
+ "general-description": "在这里管理按钮组和按钮组之间的间距。",
914
+ "detail-description": "编辑当前选中节点,或调整它的子项顺序。",
915
+ "search": "搜索按钮",
916
+ "no-matches": "没有匹配的按钮。",
917
+ "group-label": "按钮组",
918
+ "groups": "按钮组",
919
+ "group-items": "组内按钮",
920
+ "dropdown-label": "下拉按钮",
921
+ "untitled-button": "未命名按钮",
922
+ "untitled-dropdown": "未命名下拉按钮",
923
+ "add-group": "新增按钮组",
924
+ "add-button": "新增按钮",
925
+ "add-dropdown": "新增下拉按钮",
926
+ "group-gap": "按钮组间距",
927
+ "general-style": "通用样式表达式",
928
+ "general-style-placeholder": "例如返回一个 style map,例如 display: flex",
929
+ "general-style-invalid": "样式表达式无效",
930
+ "button-title": "按钮名称",
931
+ "tooltip": "提示",
932
+ "button-icon": "按钮图标",
933
+ "button-variant": "按钮变体",
934
+ "button-variant-default": "默认",
935
+ "button-variant-default-option": "默认按钮",
936
+ "button-variant-primary": "主按钮",
937
+ "button-variant-destructive": "危险按钮",
938
+ "button-variant-ghost": "幽灵按钮",
939
+ "button-parent": "所属容器",
940
+ "button-hide-title": "隐藏文字,仅显示图标",
941
+ "dropdown-title": "下拉名称",
942
+ "dropdown-icon": "下拉图标",
943
+ "dropdown-parent": "所属按钮组",
944
+ "children": "子项",
945
+ "delete-group": "删除按钮组",
946
+ "delete-button": "删除按钮",
947
+ "delete-dropdown": "删除下拉按钮",
948
+ "cancel": "取消",
949
+ "confirm": "确认"
950
+ },
951
+ "en": {
952
+ "configure-buttons": "Configure buttons",
953
+ "configure-buttons-description": "Manage button groups, group gaps, and dropdown structure.",
954
+ "general": "General",
955
+ "general-description": "Manage button groups and the gap between them.",
956
+ "detail-description": "Edit the selected node and reorder its children.",
957
+ "search": "Search buttons",
958
+ "no-matches": "No matching buttons.",
959
+ "group-label": "Group",
960
+ "groups": "Groups",
961
+ "group-items": "Group items",
962
+ "dropdown-label": "Dropdown",
963
+ "untitled-button": "Untitled button",
964
+ "untitled-dropdown": "Untitled dropdown",
965
+ "add-group": "Add group",
966
+ "add-button": "Add button",
967
+ "add-dropdown": "Add dropdown",
968
+ "group-gap": "Group gap",
969
+ "general-style": "Shared style expression",
970
+ "general-style-placeholder": "Return a style map, for example display: flex",
971
+ "general-style-invalid": "The style expression is invalid",
972
+ "button-title": "Button title",
973
+ "tooltip": "Tooltip",
974
+ "button-icon": "Button icon",
975
+ "button-variant": "Button variant",
976
+ "button-variant-default": "Default",
977
+ "button-variant-default-option": "Default",
978
+ "button-variant-primary": "Primary",
979
+ "button-variant-destructive": "Destructive",
980
+ "button-variant-ghost": "Ghost",
981
+ "button-parent": "Parent container",
982
+ "button-hide-title": "Hide text and show icon only",
983
+ "dropdown-title": "Dropdown title",
984
+ "dropdown-icon": "Dropdown icon",
985
+ "dropdown-parent": "Parent group",
986
+ "children": "Children",
987
+ "delete-group": "Delete group",
988
+ "delete-button": "Delete button",
989
+ "delete-dropdown": "Delete dropdown",
990
+ "cancel": "Cancel",
991
+ "confirm": "Confirm"
992
+ },
993
+ "ja": {
994
+ "configure-buttons": "ボタン設定",
995
+ "configure-buttons-description": "複数のボタングループとドロップダウン構造を管理します。",
996
+ "general": "全体",
997
+ "general-description": "ボタングループとその間隔を管理します。",
998
+ "detail-description": "選択中ノードを編集し、子項目を並び替えます。",
999
+ "search": "ボタンを検索",
1000
+ "no-matches": "一致するボタンがありません。",
1001
+ "group-label": "グループ",
1002
+ "groups": "グループ",
1003
+ "group-items": "グループ内項目",
1004
+ "dropdown-label": "ドロップダウン",
1005
+ "untitled-button": "未命名ボタン",
1006
+ "untitled-dropdown": "未命名ドロップダウン",
1007
+ "add-group": "グループを追加",
1008
+ "add-button": "ボタンを追加",
1009
+ "add-dropdown": "ドロップダウンを追加",
1010
+ "group-gap": "グループ間隔",
1011
+ "general-style": "共通スタイル式",
1012
+ "general-style-placeholder": "例: style map を返す式。例: display: flex",
1013
+ "general-style-invalid": "スタイル式が無効です",
1014
+ "button-title": "ボタン名",
1015
+ "tooltip": "ツールチップ",
1016
+ "button-icon": "ボタンアイコン",
1017
+ "button-variant": "ボタンバリアント",
1018
+ "button-variant-default": "デフォルト",
1019
+ "button-variant-default-option": "通常",
1020
+ "button-variant-primary": "プライマリ",
1021
+ "button-variant-destructive": "危険",
1022
+ "button-variant-ghost": "ゴースト",
1023
+ "button-parent": "親コンテナ",
1024
+ "button-hide-title": "テキストを隠してアイコンのみ表示",
1025
+ "dropdown-title": "ドロップダウン名",
1026
+ "dropdown-icon": "ドロップダウンアイコン",
1027
+ "dropdown-parent": "親グループ",
1028
+ "children": "子項目",
1029
+ "delete-group": "グループを削除",
1030
+ "delete-button": "ボタンを削除",
1031
+ "delete-dropdown": "ドロップダウンを削除",
1032
+ "cancel": "キャンセル",
1033
+ "confirm": "確認"
1034
+ },
1035
+ "ko": {
1036
+ "configure-buttons": "버튼 설정",
1037
+ "configure-buttons-description": "여러 버튼 그룹과 드롭다운 구조를 관리합니다.",
1038
+ "general": "전체",
1039
+ "general-description": "버튼 그룹과 그룹 간 간격을 관리합니다.",
1040
+ "detail-description": "선택한 노드를 편집하고 하위 항목 순서를 조정합니다.",
1041
+ "search": "버튼 검색",
1042
+ "no-matches": "일치하는 버튼이 없습니다.",
1043
+ "group-label": "그룹",
1044
+ "groups": "그룹",
1045
+ "group-items": "그룹 항목",
1046
+ "dropdown-label": "드롭다운",
1047
+ "untitled-button": "이름 없는 버튼",
1048
+ "untitled-dropdown": "이름 없는 드롭다운",
1049
+ "add-group": "그룹 추가",
1050
+ "add-button": "버튼 추가",
1051
+ "add-dropdown": "드롭다운 추가",
1052
+ "group-gap": "그룹 간격",
1053
+ "general-style": "공통 스타일 식",
1054
+ "general-style-placeholder": "예: style map 을 반환하는 식. 예: display: flex",
1055
+ "general-style-invalid": "스타일 식이 올바르지 않습니다",
1056
+ "button-title": "버튼 이름",
1057
+ "tooltip": "툴팁",
1058
+ "button-icon": "버튼 아이콘",
1059
+ "button-variant": "버튼 변형",
1060
+ "button-variant-default": "기본값",
1061
+ "button-variant-default-option": "기본",
1062
+ "button-variant-primary": "주 버튼",
1063
+ "button-variant-destructive": "위험",
1064
+ "button-variant-ghost": "고스트",
1065
+ "button-parent": "부모 컨테이너",
1066
+ "button-hide-title": "텍스트를 숨기고 아이콘만 표시",
1067
+ "dropdown-title": "드롭다운 이름",
1068
+ "dropdown-icon": "드롭다운 아이콘",
1069
+ "dropdown-parent": "부모 그룹",
1070
+ "children": "하위 항목",
1071
+ "delete-group": "그룹 삭제",
1072
+ "delete-button": "버튼 삭제",
1073
+ "delete-dropdown": "드롭다운 삭제",
1074
+ "cancel": "취소",
1075
+ "confirm": "확인"
1076
+ }
1077
+ }
1078
+ </i18n>