@shwfed/nuxt 0.10.10 → 0.10.12

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.
@@ -0,0 +1,482 @@
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 {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle
15
+ } from "../dialog";
16
+ import { ExpressionEditor } from "../expression-editor";
17
+ import { Input } from "../input";
18
+ import Locale from "../locale/Locale.vue";
19
+ import { MenuTabsConfigC, MenuTabsItemToC } from "../menu-tabs/schema";
20
+ defineOptions({
21
+ inheritAttrs: false
22
+ });
23
+ const props = defineProps({
24
+ config: { type: Object, required: true }
25
+ });
26
+ const emit = defineEmits(["confirm"]);
27
+ const open = defineModel("open", { type: Boolean, ...{
28
+ default: false
29
+ } });
30
+ const { t } = useI18n();
31
+ const search = ref("");
32
+ const selectedItemId = ref("");
33
+ const draftMenus = ref([]);
34
+ const sortableListRef = ref(null);
35
+ const sortableItemIds = ref([]);
36
+ const toEditor = ref(null);
37
+ function createDraftId() {
38
+ return crypto.randomUUID();
39
+ }
40
+ function cloneLocaleValue(value) {
41
+ return value.map((item) => ({
42
+ locale: item.locale,
43
+ message: item.message
44
+ }));
45
+ }
46
+ function cloneMenu(menu) {
47
+ return {
48
+ id: menu.id,
49
+ title: cloneLocaleValue(menu.title),
50
+ to: menu.to
51
+ };
52
+ }
53
+ function createDraftMenu(menu) {
54
+ return {
55
+ draftId: createDraftId(),
56
+ menu: cloneMenu(menu)
57
+ };
58
+ }
59
+ function cloneMenus(menus) {
60
+ return menus.map(createDraftMenu);
61
+ }
62
+ function createDefaultMenu() {
63
+ return {
64
+ id: crypto.randomUUID(),
65
+ title: [{ locale: "zh", message: "" }],
66
+ to: '"/"'
67
+ };
68
+ }
69
+ function getMenuChineseTitle(menu) {
70
+ const zhTitle = menu.title.find((item) => item.locale === "zh");
71
+ if (!zhTitle) {
72
+ return void 0;
73
+ }
74
+ const message = zhTitle.message.trim();
75
+ return message.length > 0 ? message : void 0;
76
+ }
77
+ function getMenuListLabel(menu) {
78
+ return getMenuChineseTitle(menu) ?? t("menu-tabs-configurator-untitled");
79
+ }
80
+ function applyDraftConfig(config) {
81
+ search.value = "";
82
+ draftMenus.value = cloneMenus(config.menus);
83
+ selectedItemId.value = draftMenus.value[0]?.draftId ?? "";
84
+ }
85
+ function resetDraftConfig() {
86
+ applyDraftConfig(props.config);
87
+ }
88
+ const normalizedSearch = computed(() => search.value.trim().toLocaleLowerCase());
89
+ const menuItems = computed(() => draftMenus.value.map((item) => ({
90
+ itemId: item.draftId,
91
+ label: getMenuListLabel(item.menu),
92
+ id: item.menu.id
93
+ })));
94
+ const filteredMenuItems = computed(() => {
95
+ if (!normalizedSearch.value) {
96
+ return menuItems.value;
97
+ }
98
+ return menuItems.value.filter((item) => {
99
+ const haystack = [item.label, item.id].join(" ").toLocaleLowerCase();
100
+ return haystack.includes(normalizedSearch.value);
101
+ });
102
+ });
103
+ const selectedMenu = computed(() => draftMenus.value.find((item) => item.draftId === selectedItemId.value));
104
+ function syncSortableItemIds() {
105
+ sortableItemIds.value = draftMenus.value.map((item) => item.draftId);
106
+ }
107
+ function moveDraftMenus(menus, oldIndex, newIndex) {
108
+ if (oldIndex < 0 || newIndex < 0 || oldIndex >= menus.length || newIndex >= menus.length) {
109
+ return menus.slice();
110
+ }
111
+ const nextMenus = menus.slice();
112
+ const movedMenu = nextMenus[oldIndex];
113
+ if (!movedMenu) {
114
+ return menus.slice();
115
+ }
116
+ nextMenus.splice(oldIndex, 1);
117
+ nextMenus.splice(newIndex, 0, movedMenu);
118
+ return nextMenus;
119
+ }
120
+ function handleSortableUpdate(event) {
121
+ const oldIndex = event.oldIndex;
122
+ const newIndex = event.newIndex;
123
+ if (oldIndex === void 0 || newIndex === void 0 || oldIndex === newIndex) {
124
+ return;
125
+ }
126
+ draftMenus.value = moveDraftMenus(draftMenus.value, oldIndex, newIndex);
127
+ }
128
+ const sortable = useSortable(sortableListRef, sortableItemIds);
129
+ function configureSortable() {
130
+ sortable.option("animation", 150);
131
+ sortable.option("handle", '[data-slot="menu-tabs-configurator-drag-handle"]');
132
+ sortable.option("onUpdate", handleSortableUpdate);
133
+ }
134
+ async function refreshSortable() {
135
+ sortable.stop();
136
+ if (!open.value || draftMenus.value.length === 0 || normalizedSearch.value.length > 0) {
137
+ return;
138
+ }
139
+ await nextTick();
140
+ sortable.start();
141
+ configureSortable();
142
+ }
143
+ function selectItem(itemId) {
144
+ selectedItemId.value = itemId;
145
+ }
146
+ function updateMenu(draftId, update) {
147
+ draftMenus.value = draftMenus.value.map((item) => item.draftId === draftId ? {
148
+ draftId: item.draftId,
149
+ menu: update(item.menu)
150
+ } : item);
151
+ }
152
+ function updateSelectedTitle(value) {
153
+ if (!selectedMenu.value) {
154
+ return;
155
+ }
156
+ updateMenu(selectedMenu.value.draftId, (menu) => ({
157
+ id: menu.id,
158
+ title: cloneLocaleValue(value),
159
+ to: menu.to
160
+ }));
161
+ }
162
+ function updateSelectedTo(value) {
163
+ if (!selectedMenu.value) {
164
+ return;
165
+ }
166
+ updateMenu(selectedMenu.value.draftId, (menu) => ({
167
+ id: menu.id,
168
+ title: cloneLocaleValue(menu.title),
169
+ to: value ?? ""
170
+ }));
171
+ }
172
+ async function addMenu() {
173
+ const nextMenu = {
174
+ draftId: createDraftId(),
175
+ menu: createDefaultMenu()
176
+ };
177
+ draftMenus.value = [...draftMenus.value, nextMenu];
178
+ selectedItemId.value = nextMenu.draftId;
179
+ await refreshSortable();
180
+ }
181
+ function deleteMenu(draftId) {
182
+ const removedIndex = draftMenus.value.findIndex((item) => item.draftId === draftId);
183
+ draftMenus.value = draftMenus.value.filter((item) => item.draftId !== draftId);
184
+ if (selectedItemId.value !== draftId) {
185
+ return;
186
+ }
187
+ const fallbackMenu = draftMenus.value[removedIndex] ?? draftMenus.value[removedIndex - 1];
188
+ selectedItemId.value = fallbackMenu?.draftId ?? "";
189
+ }
190
+ function buildDraftConfig() {
191
+ const result = MenuTabsConfigC.safeParse({
192
+ menus: draftMenus.value.map((item) => item.menu)
193
+ });
194
+ if (!result.success) {
195
+ return void 0;
196
+ }
197
+ return result.data;
198
+ }
199
+ function handleConfirm() {
200
+ if (selectedMenu.value && !(toEditor.value?.validate() ?? true)) {
201
+ return;
202
+ }
203
+ const nextConfig = buildDraftConfig();
204
+ if (!nextConfig) {
205
+ return;
206
+ }
207
+ emit("confirm", nextConfig);
208
+ open.value = false;
209
+ }
210
+ watch(() => props.config, () => {
211
+ if (!open.value) {
212
+ resetDraftConfig();
213
+ }
214
+ }, { immediate: true });
215
+ watch(draftMenus, () => {
216
+ syncSortableItemIds();
217
+ }, { immediate: true });
218
+ watch(open, async (value) => {
219
+ if (value) {
220
+ resetDraftConfig();
221
+ await refreshSortable();
222
+ return;
223
+ }
224
+ sortable.stop();
225
+ }, { immediate: true });
226
+ watch(menuItems, async (items) => {
227
+ if (!items.some((item) => item.itemId === selectedItemId.value)) {
228
+ selectedItemId.value = items[0]?.itemId ?? "";
229
+ }
230
+ if (open.value) {
231
+ await refreshSortable();
232
+ }
233
+ }, { immediate: true });
234
+ watch(filteredMenuItems, async (items) => {
235
+ if (!normalizedSearch.value) {
236
+ return;
237
+ }
238
+ if (items.some((item) => item.itemId === selectedItemId.value)) {
239
+ return;
240
+ }
241
+ selectedItemId.value = items[0]?.itemId ?? "";
242
+ await refreshSortable();
243
+ }, { immediate: true });
244
+ watch(normalizedSearch, async () => {
245
+ if (!open.value) {
246
+ return;
247
+ }
248
+ await refreshSortable();
249
+ });
250
+ </script>
251
+
252
+ <template>
253
+ <Dialog
254
+ :open="open"
255
+ @update:open="open = $event"
256
+ >
257
+ <DialogContent
258
+ data-slot="menu-tabs-configurator-dialog"
259
+ class="flex h-[min(42rem,calc(100vh-4rem))] w-[calc(100%-2rem)] max-h-[calc(100vh-4rem)] max-w-[calc(100%-2rem)] flex-col overflow-hidden p-0 sm:w-[80vw] sm:max-w-[80vw]"
260
+ >
261
+ <DialogHeader class="gap-1 border-b border-zinc-200 px-6 py-5">
262
+ <DialogTitle class="text-xl font-semibold text-zinc-800">
263
+ {{ t("menu-tabs-configurator-title") }}
264
+ </DialogTitle>
265
+ <DialogDescription class="text-sm text-zinc-500">
266
+ {{ t("menu-tabs-configurator-description") }}
267
+ </DialogDescription>
268
+ </DialogHeader>
269
+
270
+ <div class="grid min-h-0 flex-1 grid-cols-[20rem_minmax(0,1fr)]">
271
+ <section class="flex min-h-0 flex-col border-r border-zinc-200 px-4 py-4">
272
+ <Input
273
+ v-model="search"
274
+ data-slot="menu-tabs-configurator-search"
275
+ :placeholder="t('menu-tabs-configurator-search-placeholder')"
276
+ />
277
+
278
+ <Button
279
+ data-slot="menu-tabs-configurator-add"
280
+ type="button"
281
+ class="mt-3 w-full justify-center"
282
+ @click="void addMenu()"
283
+ >
284
+ <Icon icon="fluent:add-20-regular" />
285
+ {{ t("menu-tabs-configurator-add-menu") }}
286
+ </Button>
287
+
288
+ <div class="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden">
289
+ <div class="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto pr-1">
290
+ <div
291
+ v-if="filteredMenuItems.length > 0"
292
+ ref="sortableListRef"
293
+ data-slot="menu-tabs-configurator-list"
294
+ class="flex flex-col gap-1"
295
+ >
296
+ <div
297
+ v-for="item in filteredMenuItems"
298
+ :key="item.itemId"
299
+ data-slot="menu-tabs-configurator-item"
300
+ :data-item-id="item.itemId"
301
+ :data-menu-id="item.id"
302
+ :data-selected="selectedItemId === item.itemId ? 'true' : 'false'"
303
+ :class="cn(
304
+ 'flex w-full items-center gap-2 rounded-md border p-1 transition-colors',
305
+ 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'
306
+ )"
307
+ >
308
+ <button
309
+ type="button"
310
+ data-slot="menu-tabs-configurator-drag-handle"
311
+ class="flex size-8 shrink-0 cursor-grab items-center justify-center rounded-sm text-zinc-400 active:cursor-grabbing"
312
+ :aria-label="t('menu-tabs-configurator-drag-menu', { menu: item.label })"
313
+ @click.stop
314
+ >
315
+ <Icon icon="fluent:re-order-dots-vertical-20-regular" />
316
+ </button>
317
+
318
+ <button
319
+ type="button"
320
+ data-slot="menu-tabs-configurator-item-select"
321
+ class="min-w-0 flex-1 px-2 py-2 text-left"
322
+ @click="selectItem(item.itemId)"
323
+ >
324
+ <span class="block truncate text-sm font-medium text-zinc-800">
325
+ {{ item.label }}
326
+ </span>
327
+ </button>
328
+
329
+ <button
330
+ type="button"
331
+ data-slot="menu-tabs-configurator-delete"
332
+ class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm text-zinc-400 transition-colors hover:bg-red-50 hover:text-red-600"
333
+ :aria-label="t('menu-tabs-configurator-delete-menu', { menu: item.label })"
334
+ @click.stop="deleteMenu(item.itemId)"
335
+ >
336
+ <Icon icon="fluent:delete-20-regular" />
337
+ </button>
338
+ </div>
339
+ </div>
340
+
341
+ <p
342
+ v-else-if="normalizedSearch"
343
+ data-slot="menu-tabs-configurator-empty"
344
+ class="px-1 pt-2 text-xs text-zinc-400"
345
+ >
346
+ {{ t("menu-tabs-configurator-no-matches") }}
347
+ </p>
348
+
349
+ <p
350
+ v-else
351
+ data-slot="menu-tabs-configurator-empty"
352
+ class="px-1 pt-2 text-xs text-zinc-400"
353
+ >
354
+ {{ t("menu-tabs-configurator-empty") }}
355
+ </p>
356
+ </div>
357
+ </div>
358
+ </section>
359
+
360
+ <section class="flex min-h-0 min-w-0 flex-col overflow-y-auto px-6 py-6">
361
+ <div
362
+ v-if="selectedMenu"
363
+ class="flex min-h-0 flex-1 flex-col gap-5"
364
+ >
365
+ <div>
366
+ <h3 class="text-lg font-semibold text-zinc-800">
367
+ {{ t("menu-tabs-configurator-menu-detail") }}
368
+ </h3>
369
+ </div>
370
+
371
+ <div class="flex flex-col gap-5">
372
+ <Locale
373
+ data-slot="menu-tabs-configurator-title"
374
+ :model-value="selectedMenu.menu.title"
375
+ @update:model-value="updateSelectedTitle"
376
+ />
377
+
378
+ <ExpressionEditor
379
+ ref="toEditor"
380
+ data-slot="menu-tabs-configurator-to"
381
+ :model-value="selectedMenu.menu.to"
382
+ :label="t('menu-tabs-configurator-to-label')"
383
+ :description="t('menu-tabs-configurator-to-description')"
384
+ :placeholder="t('menu-tabs-configurator-to-placeholder')"
385
+ :schema="MenuTabsItemToC"
386
+ @update:model-value="updateSelectedTo"
387
+ />
388
+ </div>
389
+ </div>
390
+
391
+ <div
392
+ v-else
393
+ data-slot="menu-tabs-configurator-detail-empty"
394
+ class="flex min-h-0 flex-1 items-center justify-center text-sm text-zinc-400"
395
+ >
396
+ {{ t("menu-tabs-configurator-select-menu") }}
397
+ </div>
398
+ </section>
399
+ </div>
400
+
401
+ <DialogFooter class="border-t border-zinc-200 px-6 py-4">
402
+ <Button
403
+ data-slot="menu-tabs-configurator-cancel"
404
+ type="button"
405
+ variant="ghost"
406
+ @click="open = false"
407
+ >
408
+ {{ t("cancel") }}
409
+ </Button>
410
+ <Button
411
+ data-slot="menu-tabs-configurator-confirm"
412
+ type="button"
413
+ @click="handleConfirm"
414
+ >
415
+ {{ t("confirm") }}
416
+ </Button>
417
+ </DialogFooter>
418
+ </DialogContent>
419
+ </Dialog>
420
+ </template>
421
+
422
+ <i18n lang="json">
423
+ {
424
+ "zh": {
425
+ "menu-tabs-configurator-title": "菜单标签配置",
426
+ "menu-tabs-configurator-description": "配置顶部菜单标签,并使用拖拽调整顺序。",
427
+ "menu-tabs-configurator-menus": "菜单",
428
+ "menu-tabs-configurator-add-menu": "新增菜单",
429
+ "menu-tabs-configurator-search-placeholder": "搜索菜单名称、路径或 ID",
430
+ "menu-tabs-configurator-empty": "暂无菜单",
431
+ "menu-tabs-configurator-no-matches": "没有匹配的菜单",
432
+ "menu-tabs-configurator-select-menu": "请选择左侧菜单以编辑详情",
433
+ "menu-tabs-configurator-menu-detail": "菜单详情",
434
+ "menu-tabs-configurator-untitled": "未命名菜单",
435
+ "menu-tabs-configurator-drag-menu": "拖拽菜单 {menu}",
436
+ "menu-tabs-configurator-delete-menu": "删除菜单 {menu}",
437
+ "menu-tabs-configurator-to-label": "目标路径表达式",
438
+ "menu-tabs-configurator-to-description": "填写返回路径字符串的 CEL 表达式。",
439
+ "menu-tabs-configurator-to-placeholder": "\"/locale\"",
440
+ "cancel": "取消",
441
+ "confirm": "确认"
442
+ },
443
+ "ja": {
444
+ "menu-tabs-configurator-title": "メニュータブ設定",
445
+ "menu-tabs-configurator-description": "上部メニュータブを設定し、ドラッグで並び順を変更します。",
446
+ "menu-tabs-configurator-menus": "メニュー",
447
+ "menu-tabs-configurator-add-menu": "メニューを追加",
448
+ "menu-tabs-configurator-search-placeholder": "メニュー名、パス、ID を検索",
449
+ "menu-tabs-configurator-empty": "メニューがありません",
450
+ "menu-tabs-configurator-no-matches": "一致するメニューがありません",
451
+ "menu-tabs-configurator-select-menu": "左側のメニューを選択して詳細を編集してください",
452
+ "menu-tabs-configurator-menu-detail": "メニュー詳細",
453
+ "menu-tabs-configurator-untitled": "名称未設定メニュー",
454
+ "menu-tabs-configurator-drag-menu": "{menu} をドラッグ",
455
+ "menu-tabs-configurator-delete-menu": "{menu} を削除",
456
+ "menu-tabs-configurator-to-label": "遷移先パス式",
457
+ "menu-tabs-configurator-to-description": "パス文字列を返す CEL 式を入力してください。",
458
+ "menu-tabs-configurator-to-placeholder": "\"/locale\"",
459
+ "cancel": "キャンセル",
460
+ "confirm": "確認"
461
+ },
462
+ "en": {
463
+ "menu-tabs-configurator-title": "Menu tabs configurator",
464
+ "menu-tabs-configurator-description": "Configure the top menu tabs and drag to reorder them.",
465
+ "menu-tabs-configurator-menus": "Menus",
466
+ "menu-tabs-configurator-add-menu": "Add menu",
467
+ "menu-tabs-configurator-search-placeholder": "Search menu name, path, or ID",
468
+ "menu-tabs-configurator-empty": "No menus yet",
469
+ "menu-tabs-configurator-no-matches": "No matching menus",
470
+ "menu-tabs-configurator-select-menu": "Select a menu on the left to edit its details",
471
+ "menu-tabs-configurator-menu-detail": "Menu details",
472
+ "menu-tabs-configurator-untitled": "Untitled menu",
473
+ "menu-tabs-configurator-drag-menu": "Drag menu {menu}",
474
+ "menu-tabs-configurator-delete-menu": "Delete menu {menu}",
475
+ "menu-tabs-configurator-to-label": "Target path expression",
476
+ "menu-tabs-configurator-to-description": "Enter a CEL expression that returns a path string.",
477
+ "menu-tabs-configurator-to-placeholder": "\"/locale\"",
478
+ "cancel": "Cancel",
479
+ "confirm": "Confirm"
480
+ }
481
+ }
482
+ </i18n>
@@ -0,0 +1,30 @@
1
+ import type { LocaleValue } from '../../../utils/coders.js';
2
+ import { type MenuTabsConfig } from '../menu-tabs/schema.js';
3
+ type __VLS_Props = {
4
+ config: MenuTabsConfig;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ 'open'?: boolean;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:open": (value: boolean) => any;
12
+ confirm: (args_0: Readonly<{
13
+ menus: readonly Readonly<{
14
+ id: string;
15
+ title: LocaleValue;
16
+ to: string;
17
+ }>[];
18
+ }>) => any;
19
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
20
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
21
+ onConfirm?: ((args_0: Readonly<{
22
+ menus: readonly Readonly<{
23
+ id: string;
24
+ title: LocaleValue;
25
+ to: string;
26
+ }>[];
27
+ }>) => any) | undefined;
28
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
+ declare const _default: typeof __VLS_export;
30
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/nuxt",
3
- "version": "0.10.10",
3
+ "version": "0.10.12",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "type": "module",