@shwfed/nuxt 0.10.9 → 0.10.11

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.9",
4
+ "version": "0.10.11",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -0,0 +1,29 @@
1
+ import { Effect } from 'effect';
2
+ import type { MenuTabsConfigInput } from './ui/menu-tabs/schema.js';
3
+ export { MenuTabsConfigC, MenuTabsConfigInputC, MenuTabsItemC, MenuTabsItemToC } from './ui/menu-tabs/schema.js';
4
+ export type { MenuTabsConfig, MenuTabsConfigInput, MenuTabsItem } from './ui/menu-tabs/schema.js';
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
7
+ declare const __VLS_export: import("vue").DefineComponent<{
8
+ config?: MenuTabsConfigInput | Effect.Effect<MenuTabsConfigInput | undefined>;
9
+ context?: Record<string, unknown>;
10
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:config": (args_0: Readonly<{
12
+ menus: readonly Readonly<{
13
+ id: string;
14
+ title: import("../utils/coders.js").LocaleValue;
15
+ to: string;
16
+ }>[];
17
+ }>) => any;
18
+ }, string, import("vue").PublicProps, Readonly<{
19
+ config?: MenuTabsConfigInput | Effect.Effect<MenuTabsConfigInput | undefined>;
20
+ context?: Record<string, unknown>;
21
+ }> & Readonly<{
22
+ "onUpdate:config"?: ((args_0: Readonly<{
23
+ menus: readonly Readonly<{
24
+ id: string;
25
+ title: import("../utils/coders.js").LocaleValue;
26
+ to: string;
27
+ }>[];
28
+ }>) => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,40 @@
1
+ <script setup>
2
+ import { Effect } from "effect";
3
+ import UiMenuTabs from "./ui/menu-tabs/MenuTabs.vue";
4
+ defineOptions({
5
+ inheritAttrs: false
6
+ });
7
+ const props = defineProps({
8
+ config: { type: null, required: false },
9
+ context: { type: Object, required: false }
10
+ });
11
+ const emit = defineEmits(["update:config"]);
12
+ const defaultConfig = {
13
+ menus: []
14
+ };
15
+ function isEffectConfig(value) {
16
+ return typeof value === "object" && value !== null && "pipe" in value && typeof Reflect.get(value, "pipe") === "function";
17
+ }
18
+ function resolveConfig() {
19
+ if (isEffectConfig(props.config)) {
20
+ return props.config.pipe(Effect.map((value) => value ?? defaultConfig));
21
+ }
22
+ return Effect.succeed(props.config ?? defaultConfig);
23
+ }
24
+ function handleConfigUpdate(config) {
25
+ emit("update:config", config);
26
+ }
27
+ </script>
28
+
29
+ <script>
30
+ export { MenuTabsConfigC, MenuTabsConfigInputC, MenuTabsItemC, MenuTabsItemToC } from "./ui/menu-tabs/schema";
31
+ </script>
32
+
33
+ <template>
34
+ <UiMenuTabs
35
+ v-bind="$attrs"
36
+ :config="resolveConfig()"
37
+ :context="props.context"
38
+ @update:config="handleConfigUpdate"
39
+ />
40
+ </template>
@@ -0,0 +1,29 @@
1
+ import { Effect } from 'effect';
2
+ import type { MenuTabsConfigInput } from './ui/menu-tabs/schema.js';
3
+ export { MenuTabsConfigC, MenuTabsConfigInputC, MenuTabsItemC, MenuTabsItemToC } from './ui/menu-tabs/schema.js';
4
+ export type { MenuTabsConfig, MenuTabsConfigInput, MenuTabsItem } from './ui/menu-tabs/schema.js';
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
7
+ declare const __VLS_export: import("vue").DefineComponent<{
8
+ config?: MenuTabsConfigInput | Effect.Effect<MenuTabsConfigInput | undefined>;
9
+ context?: Record<string, unknown>;
10
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:config": (args_0: Readonly<{
12
+ menus: readonly Readonly<{
13
+ id: string;
14
+ title: import("../utils/coders.js").LocaleValue;
15
+ to: string;
16
+ }>[];
17
+ }>) => any;
18
+ }, string, import("vue").PublicProps, Readonly<{
19
+ config?: MenuTabsConfigInput | Effect.Effect<MenuTabsConfigInput | undefined>;
20
+ context?: Record<string, unknown>;
21
+ }> & Readonly<{
22
+ "onUpdate:config"?: ((args_0: Readonly<{
23
+ menus: readonly Readonly<{
24
+ id: string;
25
+ title: import("../utils/coders.js").LocaleValue;
26
+ to: string;
27
+ }>[];
28
+ }>) => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -132,6 +132,17 @@ function handleConfiguratorConfirm(nextConfig) {
132
132
  function isDropdownItem(item) {
133
133
  return "items" in item;
134
134
  }
135
+ function hasLeadingPrimaryGap(items, itemIndex) {
136
+ if (itemIndex === 0) {
137
+ return false;
138
+ }
139
+ const item = items[itemIndex];
140
+ const previousItem = items[itemIndex - 1];
141
+ if (!item || !previousItem || isDropdownItem(item) || isDropdownItem(previousItem)) {
142
+ return false;
143
+ }
144
+ return item.variant === "primary" && previousItem.variant === "primary";
145
+ }
135
146
  </script>
136
147
 
137
148
  <script>
@@ -177,7 +188,7 @@ export { ButtonActionC, ButtonConfigC, ButtonConfigInputC, ButtonDropdownC, Butt
177
188
  orientation="horizontal"
178
189
  >
179
190
  <template
180
- v-for="item in group.items"
191
+ v-for="(item, itemIndex) in group.items"
181
192
  :key="item.id"
182
193
  >
183
194
  <Button
@@ -185,6 +196,7 @@ export { ButtonActionC, ButtonConfigC, ButtonConfigInputC, ButtonDropdownC, Butt
185
196
  data-slot="buttons-item"
186
197
  :variant="item.variant"
187
198
  :disabled="isButtonDisabled(item.id)"
199
+ :class="hasLeadingPrimaryGap(group.items, itemIndex) ? 'ml-px' : void 0"
188
200
  :title="item.hideTitle ? getButtonLabel(item) : void 0"
189
201
  @click="void runButton(item.id)"
190
202
  >
@@ -208,6 +220,7 @@ export { ButtonActionC, ButtonConfigC, ButtonConfigInputC, ButtonDropdownC, Butt
208
220
  data-slot="buttons-item"
209
221
  :variant="item.variant"
210
222
  :disabled="isButtonDisabled(item.id)"
223
+ :class="hasLeadingPrimaryGap(group.items, itemIndex) ? 'ml-px' : void 0"
211
224
  :title="item.hideTitle ? getButtonLabel(item) : void 0"
212
225
  @click="void runButton(item.id)"
213
226
  >
@@ -2,14 +2,15 @@
2
2
  import { icons } from "@iconify-json/fluent";
3
3
  import { Icon } from "@iconify/vue";
4
4
  import { useVirtualizer } from "@tanstack/vue-virtual";
5
- import { computed, shallowRef, ref, watch } from "vue";
5
+ import { computed, onBeforeUnmount, onMounted, shallowRef, ref, watch } from "vue";
6
6
  import { cn } from "../../../utils/cn";
7
7
  import { InputGroup, InputGroupAddon, InputGroupInput } from "../input-group";
8
8
  defineOptions({
9
9
  inheritAttrs: false
10
10
  });
11
- const ICON_COLUMNS = 6;
12
- const ICON_ROW_HEIGHT = 60;
11
+ const ICON_BUTTON_SIZE = 40;
12
+ const ICON_GRID_GAP = 8;
13
+ const ICON_GRID_MIN_COLUMN_WIDTH = 52;
13
14
  const props = defineProps({
14
15
  modelValue: { type: String, required: false },
15
16
  placeholder: { type: String, required: false },
@@ -25,6 +26,8 @@ const availableIcons = Object.entries(icons.icons).filter(([name]) => name.endsW
25
26
  const availableIconIds = new Set(availableIcons.map((icon) => icon.id));
26
27
  const searchQuery = ref("");
27
28
  const galleryElement = shallowRef(null);
29
+ const galleryWidth = ref(0);
30
+ let galleryResizeObserver;
28
31
  const selectedIcon = computed(() => props.modelValue && availableIconIds.has(props.modelValue) ? availableIcons.find((icon) => icon.id === props.modelValue) : void 0);
29
32
  const filteredIcons = computed(() => {
30
33
  const term = searchQuery.value.trim().toLowerCase();
@@ -33,11 +36,21 @@ const filteredIcons = computed(() => {
33
36
  }
34
37
  return availableIcons.filter((icon) => icon.id.includes(term));
35
38
  });
36
- const rowCount = computed(() => Math.ceil(filteredIcons.value.length / ICON_COLUMNS));
39
+ const iconColumns = computed(() => {
40
+ if (galleryWidth.value <= 0) {
41
+ return 8;
42
+ }
43
+ return Math.max(
44
+ 1,
45
+ Math.floor((galleryWidth.value + ICON_GRID_GAP) / (ICON_GRID_MIN_COLUMN_WIDTH + ICON_GRID_GAP))
46
+ );
47
+ });
48
+ const iconRowHeight = computed(() => ICON_BUTTON_SIZE + ICON_GRID_GAP);
49
+ const rowCount = computed(() => Math.ceil(filteredIcons.value.length / iconColumns.value));
37
50
  const rowVirtualizer = useVirtualizer(computed(() => ({
38
51
  count: rowCount.value,
39
52
  getScrollElement: () => galleryElement.value,
40
- estimateSize: () => ICON_ROW_HEIGHT,
53
+ estimateSize: () => iconRowHeight.value,
41
54
  overscan: 4
42
55
  })));
43
56
  watch(() => props.modelValue, (value) => {
@@ -45,9 +58,39 @@ watch(() => props.modelValue, (value) => {
45
58
  searchQuery.value = "";
46
59
  }
47
60
  });
61
+ watch(galleryElement, (element) => {
62
+ if (galleryResizeObserver) {
63
+ galleryResizeObserver.disconnect();
64
+ galleryResizeObserver = void 0;
65
+ }
66
+ if (!element || typeof ResizeObserver === "undefined") {
67
+ galleryWidth.value = 0;
68
+ return;
69
+ }
70
+ galleryWidth.value = element.clientWidth;
71
+ galleryResizeObserver = new ResizeObserver((entries) => {
72
+ const entry = entries[0];
73
+ if (!entry) {
74
+ return;
75
+ }
76
+ galleryWidth.value = entry.contentRect.width;
77
+ });
78
+ galleryResizeObserver.observe(element);
79
+ }, {
80
+ flush: "post"
81
+ });
82
+ onMounted(() => {
83
+ if (!galleryElement.value) {
84
+ return;
85
+ }
86
+ galleryWidth.value = galleryElement.value.clientWidth;
87
+ });
88
+ onBeforeUnmount(() => {
89
+ galleryResizeObserver?.disconnect();
90
+ });
48
91
  function iconsForRow(index) {
49
- const start = index * ICON_COLUMNS;
50
- return filteredIcons.value.slice(start, start + ICON_COLUMNS);
92
+ const start = index * iconColumns.value;
93
+ return filteredIcons.value.slice(start, start + iconColumns.value);
51
94
  }
52
95
  function selectIcon(iconId) {
53
96
  if (props.disabled) {
@@ -88,26 +131,28 @@ function handleSearchUpdate(value) {
88
131
  <div
89
132
  data-slot="icon-picker-hero"
90
133
  :data-icon-id="selectedIcon.id"
91
- class="relative flex size-40 items-center justify-center rounded-3xl border border-zinc-200 bg-zinc-50 text-zinc-700 shadow-xs"
134
+ class="relative flex size-28 items-center justify-center rounded-2xl border border-zinc-200 text-zinc-700 shadow-xs"
92
135
  >
93
136
  <button
94
137
  type="button"
95
138
  data-slot="icon-picker-clear"
96
- class="absolute right-3 top-3 flex size-8 items-center justify-center rounded-full border border-zinc-200 bg-white text-zinc-500 transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 disabled:pointer-events-none disabled:opacity-60"
139
+ class="absolute right-2 top-2 flex size-7 items-center justify-center rounded-full border border-zinc-200 bg-white text-zinc-500 shadow-xs transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 disabled:pointer-events-none disabled:opacity-60"
97
140
  :disabled="props.disabled"
98
141
  @click="clearIcon"
99
142
  >
100
- <Icon icon="fluent:dismiss-20-regular" />
143
+ <Icon
144
+ icon="fluent:dismiss-20-regular"
145
+ class="size-4"
146
+ />
101
147
  </button>
102
148
  <Icon
103
149
  :icon="selectedIcon.icon"
104
- class="size-16"
150
+ class="size-14 shrink-0"
105
151
  />
106
152
  </div>
107
-
108
153
  <p
109
154
  data-slot="icon-picker-name"
110
- class="font-mono text-sm text-zinc-500"
155
+ class="max-w-full break-all font-mono text-sm text-zinc-500"
111
156
  >
112
157
  {{ selectedIcon.id }}
113
158
  </p>
@@ -144,9 +189,10 @@ function handleSearchUpdate(value) {
144
189
  <div
145
190
  v-for="row in rowVirtualizer.getVirtualItems()"
146
191
  :key="String(row.key)"
147
- class="absolute left-0 top-0 grid w-full grid-cols-6 gap-2"
192
+ class="absolute left-0 top-0 grid w-full gap-2"
148
193
  :style="{
149
194
  height: `${row.size}px`,
195
+ gridTemplateColumns: `repeat(${iconColumns}, minmax(0, 1fr))`,
150
196
  transform: `translateY(${row.start}px)`
151
197
  }"
152
198
  >
@@ -156,7 +202,7 @@ function handleSearchUpdate(value) {
156
202
  type="button"
157
203
  data-slot="icon-picker-item"
158
204
  :data-icon-id="item.id"
159
- class="flex h-12 items-center justify-center rounded-lg border border-transparent bg-zinc-50 text-zinc-600 transition-colors hover:border-zinc-200 hover:bg-zinc-100 hover:text-zinc-800 disabled:pointer-events-none disabled:opacity-60"
205
+ class="mx-auto flex size-10 items-center justify-center rounded-md text-zinc-600 transition-colors hover:bg-zinc-100 hover:text-zinc-800 disabled:pointer-events-none disabled:opacity-60"
160
206
  :disabled="props.disabled"
161
207
  @click="selectIcon(item.id)"
162
208
  >
@@ -0,0 +1,29 @@
1
+ import { Effect } from 'effect';
2
+ import { type MenuTabsConfigInput } from './schema.js';
3
+ export { MenuTabsConfigC, MenuTabsConfigInputC, MenuTabsItemC, MenuTabsItemToC } from './schema.js';
4
+ export type { MenuTabsConfig, MenuTabsConfigInput, MenuTabsItem } from './schema.js';
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
7
+ declare const __VLS_export: import("vue").DefineComponent<{
8
+ config: Effect.Effect<MenuTabsConfigInput | undefined>;
9
+ context?: Record<string, unknown>;
10
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:config": (args_0: Readonly<{
12
+ menus: readonly Readonly<{
13
+ id: string;
14
+ title: import("../../../utils/coders.js").LocaleValue;
15
+ to: string;
16
+ }>[];
17
+ }>) => any;
18
+ }, string, import("vue").PublicProps, Readonly<{
19
+ config: Effect.Effect<MenuTabsConfigInput | undefined>;
20
+ context?: Record<string, unknown>;
21
+ }> & Readonly<{
22
+ "onUpdate:config"?: ((args_0: Readonly<{
23
+ menus: readonly Readonly<{
24
+ id: string;
25
+ title: import("../../../utils/coders.js").LocaleValue;
26
+ to: string;
27
+ }>[];
28
+ }>) => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,275 @@
1
+ <script setup>
2
+ import { useNuxtApp, useRoute, useRouter } from "#app";
3
+ import { useCheating } from "#imports";
4
+ import { Icon } from "@iconify/vue";
5
+ import { computedAsync } from "@vueuse/core";
6
+ import { Effect } from "effect";
7
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
8
+ import { useI18n } from "vue-i18n";
9
+ import { getLocalizedText } from "../../../utils/coders";
10
+ import { Button } from "../button";
11
+ import MenuTabsConfiguratorDialog from "../menu-tabs-configurator/MenuTabsConfiguratorDialog.vue";
12
+ import { Skeleton } from "../skeleton";
13
+ import { MenuTabsConfigC, normalizeMenuTabsConfigInput } from "./schema";
14
+ defineOptions({
15
+ inheritAttrs: false
16
+ });
17
+ const defaultConfig = {
18
+ menus: []
19
+ };
20
+ const props = defineProps({
21
+ config: { type: null, required: true },
22
+ context: { type: Object, required: false }
23
+ });
24
+ const emit = defineEmits(["update:config"]);
25
+ const { $dsl } = useNuxtApp();
26
+ const route = useRoute();
27
+ const router = useRouter();
28
+ const { locale, t } = useI18n();
29
+ const isCheating = useCheating();
30
+ const resolvedConfig = computedAsync(
31
+ async () => MenuTabsConfigC.parse(normalizeMenuTabsConfigInput(await props.config.pipe(Effect.runPromise)))
32
+ );
33
+ const displayConfig = ref(defaultConfig);
34
+ const isConfiguratorOpen = ref(false);
35
+ const listRef = ref(null);
36
+ const buttonElements = /* @__PURE__ */ new Map();
37
+ const indicatorMetrics = ref();
38
+ let resizeObserver;
39
+ let observedElements = /* @__PURE__ */ new Set();
40
+ let frameId = 0;
41
+ function cleanPath(path) {
42
+ const normalizedPath = path.trim().replace(/\/+$/g, "");
43
+ return normalizedPath.length > 0 ? normalizedPath : "/";
44
+ }
45
+ function getEvaluationContext() {
46
+ return {
47
+ ...props.context ?? {},
48
+ context: props.context ?? {}
49
+ };
50
+ }
51
+ const resolvedMenus = computed(
52
+ () => displayConfig.value.menus.map((menu) => {
53
+ let path;
54
+ try {
55
+ const result = $dsl.evaluate`${menu.to}`(getEvaluationContext());
56
+ if (typeof result === "string" && result.trim().length > 0) {
57
+ path = cleanPath(result);
58
+ }
59
+ } catch {
60
+ path = void 0;
61
+ }
62
+ return {
63
+ id: menu.id,
64
+ path,
65
+ title: getLocalizedText(menu.title, locale.value) ?? t("untitled-menu")
66
+ };
67
+ })
68
+ );
69
+ const activeMenuId = computed(() => {
70
+ const currentPath = cleanPath(route.path);
71
+ return resolvedMenus.value.find((menu) => menu.path === currentPath)?.id;
72
+ });
73
+ const indicatorStyle = computed(() => {
74
+ if (!indicatorMetrics.value) {
75
+ return {
76
+ opacity: 0,
77
+ transform: "translateX(0px)",
78
+ width: "0px"
79
+ };
80
+ }
81
+ return {
82
+ opacity: 1,
83
+ transform: `translateX(${indicatorMetrics.value.left}px)`,
84
+ width: `${indicatorMetrics.value.width}px`
85
+ };
86
+ });
87
+ function setMenuButtonRef(id, value) {
88
+ if (value instanceof HTMLButtonElement) {
89
+ if (buttonElements.get(id) === value) {
90
+ return;
91
+ }
92
+ buttonElements.set(id, value);
93
+ } else {
94
+ if (!buttonElements.has(id)) {
95
+ return;
96
+ }
97
+ buttonElements.delete(id);
98
+ }
99
+ syncObservedElements();
100
+ void scheduleIndicator();
101
+ }
102
+ function updateIndicator() {
103
+ const activeId = activeMenuId.value;
104
+ const activeButton = activeId ? buttonElements.get(activeId) : void 0;
105
+ if (!activeButton) {
106
+ indicatorMetrics.value = void 0;
107
+ return;
108
+ }
109
+ indicatorMetrics.value = {
110
+ left: activeButton.offsetLeft,
111
+ width: activeButton.offsetWidth
112
+ };
113
+ }
114
+ async function scheduleIndicator() {
115
+ if (typeof window !== "undefined") {
116
+ cancelAnimationFrame(frameId);
117
+ frameId = window.requestAnimationFrame(() => {
118
+ updateIndicator();
119
+ });
120
+ return;
121
+ }
122
+ await nextTick();
123
+ updateIndicator();
124
+ }
125
+ function syncObservedElements() {
126
+ if (!resizeObserver) {
127
+ return;
128
+ }
129
+ const nextObservedElements = /* @__PURE__ */ new Set();
130
+ if (listRef.value) {
131
+ nextObservedElements.add(listRef.value);
132
+ }
133
+ for (const button of buttonElements.values()) {
134
+ nextObservedElements.add(button);
135
+ }
136
+ for (const element of observedElements) {
137
+ if (!nextObservedElements.has(element)) {
138
+ resizeObserver.unobserve(element);
139
+ }
140
+ }
141
+ for (const element of nextObservedElements) {
142
+ if (!observedElements.has(element)) {
143
+ resizeObserver.observe(element);
144
+ }
145
+ }
146
+ observedElements = nextObservedElements;
147
+ }
148
+ function handleConfiguratorConfirm(nextConfig) {
149
+ displayConfig.value = nextConfig;
150
+ emit("update:config", nextConfig);
151
+ }
152
+ function handleMenuClick(menu) {
153
+ if (!menu.path) {
154
+ return;
155
+ }
156
+ void router.replace(menu.path);
157
+ }
158
+ watch(resolvedConfig, (value) => {
159
+ if (!value) {
160
+ return;
161
+ }
162
+ displayConfig.value = value;
163
+ }, { immediate: true });
164
+ watch(() => `${route.path}|${resolvedMenus.value.map((menu) => `${menu.id}:${menu.path ?? ""}`).join("|")}`, () => {
165
+ void scheduleIndicator();
166
+ }, { immediate: true });
167
+ onMounted(() => {
168
+ if (typeof ResizeObserver !== "undefined") {
169
+ resizeObserver = new ResizeObserver(() => {
170
+ void scheduleIndicator();
171
+ });
172
+ syncObservedElements();
173
+ }
174
+ void scheduleIndicator();
175
+ });
176
+ onUnmounted(() => {
177
+ cancelAnimationFrame(frameId);
178
+ resizeObserver?.disconnect();
179
+ observedElements = /* @__PURE__ */ new Set();
180
+ });
181
+ </script>
182
+
183
+ <script>
184
+ export { MenuTabsConfigC, MenuTabsConfigInputC, MenuTabsItemC, MenuTabsItemToC } from "./schema";
185
+ </script>
186
+
187
+ <template>
188
+ <div
189
+ v-bind="$attrs"
190
+ data-slot="menu-tabs-root"
191
+ class="relative"
192
+ >
193
+ <Button
194
+ v-if="isCheating"
195
+ data-slot="menu-tabs-configurator-trigger"
196
+ variant="ghost"
197
+ size="sm"
198
+ type="button"
199
+ class="absolute right-2 top-2 z-20 bg-white/90 shadow-xs backdrop-blur-sm hover:bg-white"
200
+ :aria-label="t('menu-tabs-open-configurator')"
201
+ :title="t('menu-tabs-open-configurator')"
202
+ @click="isConfiguratorOpen = true"
203
+ >
204
+ <Icon icon="fluent:settings-20-regular" />
205
+ </Button>
206
+
207
+ <MenuTabsConfiguratorDialog
208
+ v-if="resolvedConfig !== void 0"
209
+ v-model:open="isConfiguratorOpen"
210
+ :config="displayConfig"
211
+ @confirm="handleConfiguratorConfirm"
212
+ />
213
+
214
+ <Skeleton
215
+ v-if="resolvedConfig === void 0"
216
+ data-slot="menu-tabs-skeleton"
217
+ class="absolute inset-0 z-10 h-full w-full"
218
+ />
219
+
220
+ <div
221
+ data-slot="menu-tabs-container"
222
+ class="overflow-x-auto"
223
+ >
224
+ <div
225
+ ref="listRef"
226
+ data-slot="menu-tabs-list"
227
+ class="relative flex min-w-full w-max items-stretch gap-6 border-b-2 border-zinc-200 bg-white"
228
+ >
229
+ <button
230
+ v-for="menu in resolvedMenus"
231
+ :ref="(value) => setMenuButtonRef(menu.id, value)"
232
+ :key="menu.id"
233
+ type="button"
234
+ data-slot="menu-tabs-item"
235
+ :data-menu-id="menu.id"
236
+ :data-active="activeMenuId === menu.id ? 'true' : void 0"
237
+ :data-navigable="menu.path ? 'true' : 'false'"
238
+ :aria-current="activeMenuId === menu.id ? 'page' : void 0"
239
+ :aria-disabled="menu.path ? void 0 : 'true'"
240
+ :class="[
241
+ 'text-sm relative shrink-0 border-b-2 border-transparent py-2 font-semibold transition-colors duration-180',
242
+ activeMenuId === menu.id ? 'text-(--primary)' : 'text-zinc-700',
243
+ menu.path ? 'cursor-pointer hover:text-(--primary)' : 'cursor-default opacity-60'
244
+ ]"
245
+ @click="handleMenuClick(menu)"
246
+ >
247
+ {{ menu.title }}
248
+ </button>
249
+
250
+ <div
251
+ data-slot="menu-tabs-indicator"
252
+ class="pointer-events-none absolute -bottom-0.5 left-0 h-0.5 rounded bg-(--primary) transition-[transform,width,opacity] duration-200 ease-out"
253
+ :style="indicatorStyle"
254
+ />
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </template>
259
+
260
+ <i18n lang="json">
261
+ {
262
+ "zh": {
263
+ "menu-tabs-open-configurator": "打开菜单标签配置",
264
+ "untitled-menu": "未命名菜单"
265
+ },
266
+ "ja": {
267
+ "menu-tabs-open-configurator": "メニュータブ設定を開く",
268
+ "untitled-menu": "名称未設定メニュー"
269
+ },
270
+ "en": {
271
+ "menu-tabs-open-configurator": "Open menu tabs configurator",
272
+ "untitled-menu": "Untitled menu"
273
+ }
274
+ }
275
+ </i18n>
@@ -0,0 +1,29 @@
1
+ import { Effect } from 'effect';
2
+ import { type MenuTabsConfigInput } from './schema.js';
3
+ export { MenuTabsConfigC, MenuTabsConfigInputC, MenuTabsItemC, MenuTabsItemToC } from './schema.js';
4
+ export type { MenuTabsConfig, MenuTabsConfigInput, MenuTabsItem } from './schema.js';
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
7
+ declare const __VLS_export: import("vue").DefineComponent<{
8
+ config: Effect.Effect<MenuTabsConfigInput | undefined>;
9
+ context?: Record<string, unknown>;
10
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:config": (args_0: Readonly<{
12
+ menus: readonly Readonly<{
13
+ id: string;
14
+ title: import("../../../utils/coders.js").LocaleValue;
15
+ to: string;
16
+ }>[];
17
+ }>) => any;
18
+ }, string, import("vue").PublicProps, Readonly<{
19
+ config: Effect.Effect<MenuTabsConfigInput | undefined>;
20
+ context?: Record<string, unknown>;
21
+ }> & Readonly<{
22
+ "onUpdate:config"?: ((args_0: Readonly<{
23
+ menus: readonly Readonly<{
24
+ id: string;
25
+ title: import("../../../utils/coders.js").LocaleValue;
26
+ to: string;
27
+ }>[];
28
+ }>) => any) | undefined;
29
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,58 @@
1
+ import z from 'zod';
2
+ import type { LocaleValue } from '../../../utils/coders.js';
3
+ export declare const MenuTabsItemToC: z.ZodString;
4
+ export declare const MenuTabsItemC: z.ZodReadonly<z.ZodObject<{
5
+ id: z.ZodUUID;
6
+ title: z.ZodReadonly<z.ZodArray<z.ZodObject<{
7
+ locale: z.ZodEnum<{
8
+ zh: "zh";
9
+ ja: "ja";
10
+ en: "en";
11
+ ko: "ko";
12
+ }>;
13
+ message: z.ZodString;
14
+ }, z.core.$strip>>>;
15
+ to: z.ZodString;
16
+ }, z.core.$strict>>;
17
+ export declare const MenuTabsConfigC: z.ZodReadonly<z.ZodObject<{
18
+ menus: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
19
+ id: z.ZodUUID;
20
+ title: z.ZodReadonly<z.ZodArray<z.ZodObject<{
21
+ locale: z.ZodEnum<{
22
+ zh: "zh";
23
+ ja: "ja";
24
+ en: "en";
25
+ ko: "ko";
26
+ }>;
27
+ message: z.ZodString;
28
+ }, z.core.$strip>>>;
29
+ to: z.ZodString;
30
+ }, z.core.$strict>>>>;
31
+ }, z.core.$strict>>;
32
+ export declare const MenuTabsConfigInputC: z.ZodReadonly<z.ZodObject<{
33
+ menus: z.ZodReadonly<z.ZodArray<z.ZodReadonly<z.ZodObject<{
34
+ id: z.ZodUUID;
35
+ title: z.ZodReadonly<z.ZodArray<z.ZodObject<{
36
+ locale: z.ZodEnum<{
37
+ zh: "zh";
38
+ ja: "ja";
39
+ en: "en";
40
+ ko: "ko";
41
+ }>;
42
+ message: z.ZodString;
43
+ }, z.core.$strip>>>;
44
+ to: z.ZodString;
45
+ }, z.core.$strict>>>>;
46
+ }, z.core.$strict>>;
47
+ export type MenuTabsItem = Readonly<{
48
+ id: string;
49
+ title: LocaleValue;
50
+ to: string;
51
+ }>;
52
+ export type MenuTabsConfig = Readonly<{
53
+ menus: ReadonlyArray<MenuTabsItem>;
54
+ }>;
55
+ export type MenuTabsConfigInput = Readonly<{
56
+ menus?: ReadonlyArray<MenuTabsItem>;
57
+ }>;
58
+ export declare function normalizeMenuTabsConfigInput(value: unknown): unknown;
@@ -0,0 +1,24 @@
1
+ import z from "zod";
2
+ import { expressionC, localeC } from "../../../utils/coders.js";
3
+ const menuTabsItemIdC = z.uuid().describe("\u83DC\u5355\u9879\u552F\u4E00\u6807\u8BC6\uFF0C\u5FC5\u987B\u662F UUID");
4
+ export const MenuTabsItemToC = expressionC("string", { context: "dyn" }).describe("\u8FD4\u56DE\u76EE\u6807\u8DEF\u5F84\u7684 CEL \u8868\u8FBE\u5F0F\u3002\u53EF\u7528\u53D8\u91CF\uFF1Acontext\u3002\u5FC5\u987B\u8FD4\u56DE string\u3002");
5
+ export const MenuTabsItemC = z.strictObject({
6
+ id: menuTabsItemIdC,
7
+ title: localeC.describe("\u83DC\u5355\u540D\u79F0\u7684\u672C\u5730\u5316\u663E\u793A\u6587\u672C"),
8
+ to: MenuTabsItemToC
9
+ }).readonly();
10
+ export const MenuTabsConfigC = z.strictObject({
11
+ menus: z.array(MenuTabsItemC).readonly().describe("\u9876\u90E8\u83DC\u5355\u5217\u8868")
12
+ }).readonly();
13
+ export const MenuTabsConfigInputC = MenuTabsConfigC;
14
+ export function normalizeMenuTabsConfigInput(value) {
15
+ if (typeof value !== "object" || value === null) {
16
+ return {
17
+ menus: []
18
+ };
19
+ }
20
+ const menus = Reflect.get(value, "menus");
21
+ return {
22
+ menus: Array.isArray(menus) ? menus : []
23
+ };
24
+ }
@@ -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;
@@ -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.9",
3
+ "version": "0.10.11",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "type": "module",