@astryx-vue/lab 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4016 @@
1
+ import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, h, mergeModels, mergeProps, nextTick, normalizeClass, normalizeStyle, onScopeDispose, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useAttrs, useId, useModel, useTemplateRef, watch, withCtx } from "vue";
2
+ import "../styles/circular-progress.css";
3
+ import { AlertDialog, BottomSheet, Button, CheckboxIndicator, ComplexSelector, EmptyState, Field, FieldStatus, Icon, IconButton, Item, List, Text, TextInput, Token, Tooltip, VisuallyHidden, useAnnounce, useTranslator } from "@astryx-vue/core";
4
+ import "../styles/info-tip.css";
5
+ import "../styles/list-input.css";
6
+ import "../styles/log-stream.css";
7
+ import "../styles/mobile-tokenizer.css";
8
+ import "../styles/stat.css";
9
+ import "../styles/svg-icon.css";
10
+ import "../styles/transfer-list.css";
11
+ import "../styles/transfer-list-selector.css";
12
+ //#region src/CircularProgress/types.ts
13
+ /** Ring geometry per size: outer diameter and stroke width in px. */
14
+ const SIZE_CONFIG = {
15
+ sm: {
16
+ diameter: 32,
17
+ strokeWidth: 3
18
+ },
19
+ md: {
20
+ diameter: 48,
21
+ strokeWidth: 4
22
+ },
23
+ lg: {
24
+ diameter: 64,
25
+ strokeWidth: 5
26
+ }
27
+ };
28
+ /** Default center label: the rounded percentage of the filled range. */
29
+ function defaultFormatValueLabel(value, max) {
30
+ return `${max > 0 ? Math.round(value / max * 100) : 0}%`;
31
+ }
32
+ //#endregion
33
+ //#region src/CircularProgress/CircularProgress.vue
34
+ const _hoisted_1$11 = ["data-size", "data-variant"];
35
+ const _hoisted_2$10 = { class: "astryx-circular-progress__ring" };
36
+ const _hoisted_3$9 = [
37
+ "aria-valuenow",
38
+ "aria-valuemin",
39
+ "aria-valuemax",
40
+ "aria-valuetext",
41
+ "width",
42
+ "height",
43
+ "viewBox"
44
+ ];
45
+ const _hoisted_4$8 = [
46
+ "data-variant",
47
+ "cx",
48
+ "cy",
49
+ "r",
50
+ "stroke-width"
51
+ ];
52
+ const _hoisted_5$7 = [
53
+ "data-variant",
54
+ "cx",
55
+ "cy",
56
+ "r",
57
+ "stroke-width"
58
+ ];
59
+ const _hoisted_6$6 = [
60
+ "data-variant",
61
+ "cx",
62
+ "cy",
63
+ "r",
64
+ "stroke-width",
65
+ "stroke-dasharray",
66
+ "stroke-dashoffset"
67
+ ];
68
+ const _hoisted_7$4 = {
69
+ key: 0,
70
+ class: "astryx-circular-progress__center"
71
+ };
72
+ const _sfc_main = /*@__PURE__*/ defineComponent({
73
+ name: "AstryxCircularProgress",
74
+ inheritAttrs: false,
75
+ __name: "CircularProgress",
76
+ props: {
77
+ value: { default: 0 },
78
+ max: { default: 100 },
79
+ label: {},
80
+ isLabelHidden: {
81
+ type: Boolean,
82
+ default: true
83
+ },
84
+ hasValueLabel: {
85
+ type: Boolean,
86
+ default: false
87
+ },
88
+ formatValueLabel: {
89
+ type: Function,
90
+ default: defaultFormatValueLabel
91
+ },
92
+ size: { default: "md" },
93
+ variant: { default: "accent" },
94
+ isIndeterminate: {
95
+ type: Boolean,
96
+ default: false
97
+ },
98
+ isDisabled: {
99
+ type: Boolean,
100
+ default: false
101
+ }
102
+ },
103
+ setup(__props) {
104
+ const props = __props;
105
+ const attrs = useAttrs();
106
+ const labelId = `astryx-circular-progress-${useId()}`;
107
+ const geometry = computed(() => {
108
+ const { diameter, strokeWidth } = SIZE_CONFIG[props.size];
109
+ const radius = (diameter - strokeWidth) / 2;
110
+ return {
111
+ diameter,
112
+ strokeWidth,
113
+ radius,
114
+ circumference: 2 * Math.PI * radius,
115
+ center: diameter / 2
116
+ };
117
+ });
118
+ const safeValue = computed(() => Number.isFinite(props.value) ? props.value : 0);
119
+ const safeMax = computed(() => Number.isFinite(props.max) ? props.max : 0);
120
+ const clampedValue = computed(() => Math.min(Math.max(0, safeValue.value), safeMax.value));
121
+ const percentage = computed(() => safeMax.value > 0 ? clampedValue.value / safeMax.value : 0);
122
+ const dashoffset = computed(() => geometry.value.circumference * (1 - percentage.value));
123
+ const valueText = computed(() => props.formatValueLabel(clampedValue.value, safeMax.value));
124
+ const showLabel = computed(() => !props.isLabelHidden);
125
+ const showValueLabel = computed(() => props.hasValueLabel && !props.isIndeterminate);
126
+ const hasCenterContent = computed(() => showValueLabel.value);
127
+ const fillVariant = computed(() => props.isDisabled ? "disabled" : props.variant);
128
+ const trackVariant = computed(() => props.isDisabled ? "neutral" : props.variant);
129
+ return (_ctx, _cache) => {
130
+ return openBlock(), createElementBlock("div", mergeProps(unref(attrs), {
131
+ class: ["astryx-circular-progress", [_ctx.$attrs.class, { "astryx-circular-progress--labelled": showLabel.value }]],
132
+ "data-size": __props.size,
133
+ "data-variant": __props.variant
134
+ }), [createElementVNode("span", {
135
+ id: labelId,
136
+ class: normalizeClass(["astryx-circular-progress__label", {
137
+ "astryx-circular-progress__visually-hidden": !showLabel.value,
138
+ "astryx-circular-progress__label--disabled": showLabel.value && __props.isDisabled
139
+ }])
140
+ }, toDisplayString(__props.label), 3), createElementVNode("div", _hoisted_2$10, [(openBlock(), createElementBlock("svg", {
141
+ class: normalizeClass(["astryx-circular-progress__svg", { "astryx-circular-progress__svg--indeterminate": __props.isIndeterminate }]),
142
+ role: "progressbar",
143
+ "aria-labelledby": labelId,
144
+ "aria-valuenow": __props.isIndeterminate ? void 0 : clampedValue.value,
145
+ "aria-valuemin": __props.isIndeterminate ? void 0 : 0,
146
+ "aria-valuemax": __props.isIndeterminate ? void 0 : safeMax.value,
147
+ "aria-valuetext": __props.isIndeterminate ? void 0 : valueText.value,
148
+ width: geometry.value.diameter,
149
+ height: geometry.value.diameter,
150
+ viewBox: `0 0 ${geometry.value.diameter} ${geometry.value.diameter}`
151
+ }, [createElementVNode("circle", {
152
+ class: "astryx-circular-progress__track",
153
+ "data-variant": trackVariant.value,
154
+ cx: geometry.value.center,
155
+ cy: geometry.value.center,
156
+ r: geometry.value.radius,
157
+ "stroke-width": geometry.value.strokeWidth
158
+ }, null, 8, _hoisted_4$8), __props.isIndeterminate ? (openBlock(), createElementBlock("circle", {
159
+ key: 0,
160
+ class: "astryx-circular-progress__fill astryx-circular-progress__fill--indeterminate",
161
+ "data-variant": fillVariant.value,
162
+ cx: geometry.value.center,
163
+ cy: geometry.value.center,
164
+ r: geometry.value.radius,
165
+ "stroke-width": geometry.value.strokeWidth
166
+ }, null, 8, _hoisted_5$7)) : (openBlock(), createElementBlock("circle", {
167
+ key: 1,
168
+ class: "astryx-circular-progress__fill",
169
+ "data-variant": fillVariant.value,
170
+ cx: geometry.value.center,
171
+ cy: geometry.value.center,
172
+ r: geometry.value.radius,
173
+ "stroke-width": geometry.value.strokeWidth,
174
+ "stroke-dasharray": geometry.value.circumference,
175
+ "stroke-dashoffset": dashoffset.value
176
+ }, null, 8, _hoisted_6$6))], 10, _hoisted_3$9)), _ctx.$slots.default || hasCenterContent.value ? (openBlock(), createElementBlock("div", _hoisted_7$4, [renderSlot(_ctx.$slots, "default", {}, () => [hasCenterContent.value ? (openBlock(), createElementBlock("span", {
177
+ key: 0,
178
+ class: normalizeClass(["astryx-circular-progress__value", { "astryx-circular-progress__value--disabled": __props.isDisabled }])
179
+ }, toDisplayString(valueText.value), 3)) : createCommentVNode("v-if", true)])])) : createCommentVNode("v-if", true)])], 16, _hoisted_1$11);
180
+ };
181
+ }
182
+ });
183
+ //#endregion
184
+ //#region src/InfoTip/InfoTip.vue
185
+ const _hoisted_1$10 = ["aria-label"];
186
+ const _sfc_main$1 = /*@__PURE__*/ defineComponent({
187
+ name: "AstryxInfoTip",
188
+ __name: "InfoTip",
189
+ props: {
190
+ content: { default: void 0 },
191
+ label: { default: "More information" },
192
+ size: { default: "sm" }
193
+ },
194
+ setup(__props) {
195
+ return (_ctx, _cache) => {
196
+ return openBlock(), createElementBlock(Fragment, null, [createCommentVNode("\n 触发器是个真按钮,Tooltip 的 `auto` 触摸规则会把点击让给控件、不弹气泡。\n 这里的按钮唯一目的就是弹气泡,所以用 `tap` 让点击总能打开,\n 否则手机上的用户永远看不到说明。\n "), createVNode(unref(Tooltip), {
197
+ content: __props.content,
198
+ "touch-trigger": "tap"
199
+ }, createSlots({
200
+ default: withCtx(() => [createElementVNode("button", {
201
+ type: "button",
202
+ class: "astryx-info-tip",
203
+ "aria-label": __props.label
204
+ }, [createVNode(unref(Icon), {
205
+ icon: "info",
206
+ size: __props.size
207
+ }, null, 8, ["size"])], 8, _hoisted_1$10)]),
208
+ _: 2
209
+ }, [_ctx.$slots.content ? {
210
+ name: "content",
211
+ fn: withCtx(() => [renderSlot(_ctx.$slots, "content")]),
212
+ key: "0"
213
+ } : void 0]), 1032, ["content"])], 2112);
214
+ };
215
+ }
216
+ });
217
+ //#endregion
218
+ //#region src/ListInput/columns.ts
219
+ /** 比例列没有指定最小宽度时的兜底值。 */
220
+ const DEFAULT_MIN_COLUMN_WIDTH_PX = 140;
221
+ /** 把列宽配置翻成一条 CSS grid 轨道。 */
222
+ function resolveColumnTrack(width) {
223
+ if (width?.type === "pixel") return `${width.value}px`;
224
+ const proportion = width?.value ?? 1;
225
+ return `minmax(${width?.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH_PX}px, ${proportion}fr)`;
226
+ }
227
+ /** 所有列拼成 `grid-template-columns`。 */
228
+ function resolveGridTemplate(columns) {
229
+ return columns.map((column) => resolveColumnTrack(column.width)).join(" ");
230
+ }
231
+ /** 把 `fromIndex` 那一项挪到 `toIndex`,返回新数组。 */
232
+ function moveItem$1(value, fromIndex, toIndex) {
233
+ if (fromIndex === toIndex) return [...value];
234
+ const nextValue = [...value];
235
+ const [item] = nextValue.splice(fromIndex, 1);
236
+ nextValue.splice(toIndex, 0, item);
237
+ return nextValue;
238
+ }
239
+ //#endregion
240
+ //#region src/TransferList/icons.ts
241
+ /**
242
+ * 加号。语义图标表里没有 `add`,所以自己画一个,
243
+ * 仍然交给 core `Icon` 渲染,尺寸和颜色走同一条管线。
244
+ */
245
+ const PlusIcon = () => h("svg", {
246
+ "viewBox": "0 0 24 24",
247
+ "fill": "none",
248
+ "stroke": "currentColor",
249
+ "stroke-width": "1.5",
250
+ "stroke-linecap": "round",
251
+ "stroke-linejoin": "round"
252
+ }, [h("path", { d: "M5 12h14" }), h("path", { d: "M12 5v14" })]);
253
+ /** 拖动手柄。语义图标表里没有 `grip`,同样自己画。 */
254
+ const GripVerticalIcon = () => h("svg", {
255
+ "viewBox": "0 0 24 24",
256
+ "fill": "none",
257
+ "stroke": "currentColor",
258
+ "stroke-width": "1.5",
259
+ "stroke-linecap": "round",
260
+ "stroke-linejoin": "round"
261
+ }, [
262
+ 5,
263
+ 12,
264
+ 19
265
+ ].flatMap((cy) => [9, 15].map((cx) => h("circle", {
266
+ cx,
267
+ cy,
268
+ r: 1
269
+ }))));
270
+ //#endregion
271
+ //#region src/ListInput/ListInputCell.ts
272
+ /**
273
+ * 把调用方给的 `renderInput` 函数当组件渲染。
274
+ *
275
+ * 渲染函数必须在渲染阶段调用,所以这里包一层:单元格的壳子留在模板里,
276
+ * 壳子里只剩这一层转发。
277
+ */
278
+ var ListInputCell_default = defineComponent({
279
+ name: "AstryxListInputCell",
280
+ props: {
281
+ render: {
282
+ type: Function,
283
+ required: true
284
+ },
285
+ context: {
286
+ type: Object,
287
+ required: true
288
+ }
289
+ },
290
+ setup(props) {
291
+ return () => props.render(props.context);
292
+ }
293
+ });
294
+ //#endregion
295
+ //#region src/ListInput/ListInputRow.vue
296
+ const _hoisted_1$9 = [
297
+ "data-list-input-motion-key",
298
+ "data-list-input-drop-target",
299
+ "aria-describedby",
300
+ "aria-invalid",
301
+ "aria-posinset",
302
+ "aria-setsize"
303
+ ];
304
+ const _hoisted_2$9 = ["data-list-input-reorder-source", "data-list-input-row"];
305
+ const _hoisted_3$8 = ["data-list-input-cell"];
306
+ const _hoisted_4$7 = ["data-list-input-column-label", "data-list-input-repeated"];
307
+ const _hoisted_5$6 = { class: "astryx-list-input__cell-content" };
308
+ const _hoisted_6$5 = { class: "astryx-list-input__control-cell" };
309
+ const _hoisted_7$3 = {
310
+ key: 0,
311
+ class: "astryx-list-input__control-cell astryx-list-input__control-cell--reorder"
312
+ };
313
+ const _sfc_main$11 = /*@__PURE__*/ defineComponent({
314
+ name: "AstryxListInputRow",
315
+ __name: "ListInputRow",
316
+ props: {
317
+ item: {},
318
+ index: {},
319
+ total: {},
320
+ columns: {},
321
+ gridTemplate: {},
322
+ itemKey: {},
323
+ itemName: {},
324
+ isDisabled: { type: Boolean },
325
+ isLoading: { type: Boolean },
326
+ isReorderable: { type: Boolean },
327
+ isReorderSource: { type: Boolean },
328
+ isPointerSource: { type: Boolean },
329
+ isKeyboardActive: { type: Boolean },
330
+ dropPosition: {},
331
+ fieldStatus: { type: Function },
332
+ itemStatus: {},
333
+ itemStatusId: {},
334
+ reorderInstructionsId: {},
335
+ labels: {},
336
+ handlers: {}
337
+ },
338
+ setup(__props) {
339
+ const props = __props;
340
+ /** 每条记录的单元格上下文。行堆叠时列名要带上这是第几条。 */
341
+ function cellContext(column, columnIndex) {
342
+ const label = props.index === 0 ? column.header : props.labels.fieldLabel(column.header, props.index + 1, props.total);
343
+ return {
344
+ item: props.item,
345
+ index: props.index,
346
+ label,
347
+ isLabelHidden: true,
348
+ status: props.fieldStatus?.(column.key),
349
+ statusVariant: "tooltip",
350
+ isDisabled: props.isDisabled || props.isLoading,
351
+ isLoading: props.isLoading,
352
+ updateItem: (nextItem, columnKey) => props.handlers.updateItem(props.item, props.index, nextItem, columnKey ?? props.columns[columnIndex]?.key)
353
+ };
354
+ }
355
+ return (_ctx, _cache) => {
356
+ return openBlock(), createElementBlock("li", {
357
+ "data-list-input-motion-key": `item:${__props.itemKey}`,
358
+ "data-list-input-drop-target": __props.dropPosition ?? void 0,
359
+ "aria-describedby": __props.itemStatusId,
360
+ "aria-invalid": __props.itemStatus?.type === "error" || void 0,
361
+ "aria-posinset": __props.index + 1,
362
+ "aria-setsize": __props.total,
363
+ class: "astryx-list-input__item"
364
+ }, [createElementVNode("div", {
365
+ "data-list-input-reorder-source": __props.isReorderSource || void 0,
366
+ "data-list-input-row": __props.itemKey,
367
+ class: normalizeClass(["astryx-list-input__row", { "astryx-list-input__row--dragging": __props.isPointerSource }])
368
+ }, [
369
+ createElementVNode("div", {
370
+ class: "astryx-list-input__fields",
371
+ style: normalizeStyle({ gridTemplateColumns: __props.gridTemplate })
372
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (column, columnIndex) => {
373
+ return openBlock(), createElementBlock("div", {
374
+ key: column.key,
375
+ class: "astryx-list-input__cell",
376
+ "data-list-input-cell": String(columnIndex)
377
+ }, [createElementVNode("span", {
378
+ class: "astryx-list-input__column-label",
379
+ "aria-hidden": "true",
380
+ "data-list-input-column-label": __props.index === 0 ? "primary" : "responsive",
381
+ "data-list-input-repeated": __props.index === 0 ? void 0 : ""
382
+ }, toDisplayString(column.header), 9, _hoisted_4$7), createElementVNode("div", _hoisted_5$6, [createVNode(unref(ListInputCell_default), {
383
+ render: column.renderInput,
384
+ context: cellContext(column, columnIndex)
385
+ }, null, 8, ["render", "context"])])], 8, _hoisted_3$8);
386
+ }), 128))], 4),
387
+ createElementVNode("div", _hoisted_6$5, [createVNode(unref(IconButton), {
388
+ label: __props.labels.remove(__props.index + 1),
389
+ tooltip: __props.isDisabled || __props.isLoading ? __props.labels.removeUnavailable() : __props.labels.remove(__props.index + 1),
390
+ size: "md",
391
+ variant: "ghost",
392
+ "is-disabled": __props.isDisabled || __props.isLoading,
393
+ "data-list-input-remove": "",
394
+ onClick: _cache[0] || (_cache[0] = ($event) => __props.handlers.remove(__props.item))
395
+ }, {
396
+ default: withCtx(() => [createVNode(unref(Icon), {
397
+ icon: "close",
398
+ size: "sm"
399
+ })]),
400
+ _: 1
401
+ }, 8, [
402
+ "label",
403
+ "tooltip",
404
+ "is-disabled"
405
+ ])]),
406
+ __props.isReorderable ? (openBlock(), createElementBlock("div", _hoisted_7$3, [createVNode(unref(IconButton), {
407
+ class: "astryx-list-input__reorder-handle",
408
+ label: __props.labels.reorder(__props.index + 1),
409
+ "aria-describedby": __props.reorderInstructionsId,
410
+ "aria-pressed": __props.isKeyboardActive || void 0,
411
+ size: "md",
412
+ variant: "ghost",
413
+ "is-disabled": __props.isDisabled || __props.isLoading,
414
+ "data-list-input-reorder": "",
415
+ onKeydown: _cache[1] || (_cache[1] = ($event) => __props.handlers.keydown($event, __props.item, __props.index)),
416
+ onBlur: _cache[2] || (_cache[2] = ($event) => __props.handlers.blur(__props.item)),
417
+ onPointerdown: _cache[3] || (_cache[3] = ($event) => __props.handlers.pointerdown($event, __props.item, __props.index)),
418
+ onPointermove: _cache[4] || (_cache[4] = ($event) => __props.handlers.pointermove($event)),
419
+ onPointerup: _cache[5] || (_cache[5] = ($event) => __props.handlers.pointerup($event)),
420
+ onPointercancel: _cache[6] || (_cache[6] = ($event) => __props.handlers.pointercancel($event)),
421
+ onLostpointercapture: _cache[7] || (_cache[7] = ($event) => __props.handlers.lostpointercapture($event))
422
+ }, {
423
+ default: withCtx(() => [createVNode(unref(Icon), {
424
+ icon: unref(GripVerticalIcon),
425
+ size: "sm"
426
+ }, null, 8, ["icon"])]),
427
+ _: 1
428
+ }, 8, [
429
+ "label",
430
+ "aria-describedby",
431
+ "aria-pressed",
432
+ "is-disabled"
433
+ ])])) : createCommentVNode("v-if", true),
434
+ __props.itemStatus?.message ? (openBlock(), createBlock(unref(FieldStatus), {
435
+ key: 1,
436
+ id: __props.itemStatusId,
437
+ type: __props.itemStatus.type,
438
+ message: __props.itemStatus.message,
439
+ variant: "detached",
440
+ "data-list-input-item-status": "",
441
+ class: "astryx-list-input__item-status"
442
+ }, null, 8, [
443
+ "id",
444
+ "type",
445
+ "message"
446
+ ])) : createCommentVNode("v-if", true)
447
+ ], 10, _hoisted_2$9)], 8, _hoisted_1$9);
448
+ };
449
+ }
450
+ });
451
+ //#endregion
452
+ //#region src/ListInput/useListReorder.ts
453
+ /** 指针要移动这么远才算真的在拖。 */
454
+ const POINTER_DRAG_THRESHOLD_PX$1 = 5;
455
+ /**
456
+ * 记录列表的重排状态机。
457
+ *
458
+ * 两种起点、两种提交时机,和上游一致:
459
+ * - 手柄没拿起时按上下方向键:立刻挪一位,马上提交,读屏能听到新位置。
460
+ * - 空格或回车先拿起,之后方向键只改预览,再按空格或回车才提交,Escape 放弃。
461
+ * - 指针拖动过程中不动数据,松手才提交一次。
462
+ */
463
+ function useListReorder(options) {
464
+ const state = ref(null);
465
+ function setState(next) {
466
+ state.value = next;
467
+ }
468
+ const displayValue = computed(() => {
469
+ const current = state.value;
470
+ if (current == null || current.mode === "pointer") return options.currentValue();
471
+ return current.fromIndex === current.toIndex ? current.originalValue : moveItem$1(current.originalValue, current.fromIndex, current.toIndex);
472
+ });
473
+ const sourceKey = computed(() => state.value?.mode === "pointer" ? state.value.itemKey : null);
474
+ const keyboardKey = computed(() => state.value?.mode === "keyboard" ? state.value.itemKey : null);
475
+ const pointerDrop = computed(() => {
476
+ const current = state.value;
477
+ if (current?.mode !== "pointer" || !current.hasPointerMoved || current.toIndex === current.fromIndex) return null;
478
+ const remaining = current.originalValue.filter((_, index) => index !== current.fromIndex);
479
+ const before = remaining[current.toIndex];
480
+ if (before != null) return {
481
+ itemKey: String(options.getItemKey(before)),
482
+ position: "before"
483
+ };
484
+ const after = remaining.at(-1);
485
+ return after == null ? null : {
486
+ itemKey: String(options.getItemKey(after)),
487
+ position: "after"
488
+ };
489
+ });
490
+ const previewStyle = computed(() => {
491
+ const current = state.value;
492
+ if (current?.mode !== "pointer" || current.pointerClientX == null || current.pointerClientY == null || current.pointerOffsetX == null || current.pointerOffsetY == null || current.previewWidth == null) return null;
493
+ return {
494
+ top: `${current.pointerClientY - current.pointerOffsetY}px`,
495
+ left: `${current.pointerClientX - current.pointerOffsetX}px`,
496
+ width: `${current.previewWidth}px`
497
+ };
498
+ });
499
+ function startReorder(item, index, mode, geometry) {
500
+ if (options.isBlocked() || !options.isReorderable()) return;
501
+ setState({
502
+ itemKey: String(options.getItemKey(item)),
503
+ fromIndex: index,
504
+ toIndex: index,
505
+ originalValue: [...options.currentValue()],
506
+ mode,
507
+ ...geometry,
508
+ hasPointerMoved: false
509
+ });
510
+ options.announce(options.messages.grabbed(index + 1));
511
+ }
512
+ function previewReorder(toIndex) {
513
+ const current = state.value;
514
+ if (current == null) return;
515
+ const bounded = Math.max(0, Math.min(toIndex, current.originalValue.length - 1));
516
+ if (bounded === current.toIndex) return;
517
+ setState({
518
+ ...current,
519
+ toIndex: bounded
520
+ });
521
+ options.announce(options.messages.movedToPosition(bounded + 1, current.originalValue.length));
522
+ }
523
+ function cancelReorder() {
524
+ if (state.value == null) return;
525
+ setState(null);
526
+ options.announce(options.messages.reorderCancelled());
527
+ }
528
+ function commitReorder() {
529
+ const current = state.value;
530
+ if (current == null) return;
531
+ if (current.mode === "pointer" && !current.hasPointerMoved) {
532
+ setState(null);
533
+ return;
534
+ }
535
+ const nextValue = moveItem$1(current.originalValue, current.fromIndex, current.toIndex);
536
+ const item = current.originalValue[current.fromIndex];
537
+ const hasChanged = current.fromIndex !== current.toIndex;
538
+ setState(null);
539
+ if (!hasChanged) {
540
+ options.announce(options.messages.returnedToPosition(current.fromIndex + 1));
541
+ return;
542
+ }
543
+ options.emitReorder(nextValue, {
544
+ type: "reorder",
545
+ item,
546
+ fromIndex: current.fromIndex,
547
+ toIndex: current.toIndex
548
+ });
549
+ options.announce(options.messages.dropped(current.toIndex + 1, current.originalValue.length));
550
+ }
551
+ function moveWithArrowKey(item, fromIndex, offset) {
552
+ const value = options.currentValue();
553
+ const toIndex = Math.max(0, Math.min(fromIndex + offset, value.length - 1));
554
+ if (toIndex === fromIndex) {
555
+ options.announce(options.messages.alreadyAtBoundary(offset < 0 ? "first" : "last"));
556
+ return;
557
+ }
558
+ const nextValue = moveItem$1(value, fromIndex, toIndex);
559
+ options.emitReorder(nextValue, {
560
+ type: "reorder",
561
+ item,
562
+ fromIndex,
563
+ toIndex
564
+ });
565
+ options.announce(options.messages.movedToPosition(toIndex + 1, value.length));
566
+ }
567
+ function onHandleKeydown(event, item, index) {
568
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
569
+ const current = state.value;
570
+ const isActivationKey = event.key === " " || event.key === "Enter";
571
+ if (current == null) {
572
+ if (event.key === "ArrowUp") {
573
+ event.preventDefault();
574
+ moveWithArrowKey(item, index, -1);
575
+ return;
576
+ }
577
+ if (event.key === "ArrowDown") {
578
+ event.preventDefault();
579
+ moveWithArrowKey(item, index, 1);
580
+ return;
581
+ }
582
+ if (isActivationKey) {
583
+ event.preventDefault();
584
+ startReorder(item, index, "keyboard");
585
+ }
586
+ return;
587
+ }
588
+ if (current.itemKey !== String(options.getItemKey(item))) return;
589
+ if (isActivationKey) {
590
+ event.preventDefault();
591
+ commitReorder();
592
+ } else if (event.key === "Escape") {
593
+ event.preventDefault();
594
+ cancelReorder();
595
+ } else if (event.key === "ArrowUp") {
596
+ event.preventDefault();
597
+ previewReorder(current.toIndex - 1);
598
+ } else if (event.key === "ArrowDown") {
599
+ event.preventDefault();
600
+ previewReorder(current.toIndex + 1);
601
+ } else if (event.key === "Home") {
602
+ event.preventDefault();
603
+ previewReorder(0);
604
+ } else if (event.key === "End") {
605
+ event.preventDefault();
606
+ previewReorder(current.originalValue.length - 1);
607
+ }
608
+ }
609
+ function onHandlePointerDown(event, item, index) {
610
+ if (event.button !== 0 || options.isBlocked()) return;
611
+ const itemKey = String(options.getItemKey(item));
612
+ const sourceRow = options.rowElement(itemKey);
613
+ if (sourceRow == null) return;
614
+ const bounds = sourceRow.getBoundingClientRect();
615
+ event.currentTarget?.setPointerCapture?.(event.pointerId);
616
+ startReorder(item, index, "pointer", {
617
+ pointerId: event.pointerId,
618
+ pointerStartX: event.clientX,
619
+ pointerStartY: event.clientY,
620
+ pointerOffsetX: event.clientX - bounds.left,
621
+ pointerOffsetY: event.clientY - bounds.top,
622
+ pointerClientX: event.clientX,
623
+ pointerClientY: event.clientY,
624
+ previewWidth: bounds.width
625
+ });
626
+ }
627
+ function onHandlePointerMove(event) {
628
+ const current = state.value;
629
+ if (current?.mode !== "pointer" || current.pointerId !== event.pointerId) return;
630
+ const horizontalDistance = event.clientX - (current.pointerStartX ?? event.clientX);
631
+ const verticalDistance = event.clientY - (current.pointerStartY ?? event.clientY);
632
+ const hasCrossedThreshold = current.hasPointerMoved === true || Math.hypot(horizontalDistance, verticalDistance) >= POINTER_DRAG_THRESHOLD_PX$1;
633
+ const remaining = current.originalValue.filter((_, index) => index !== current.fromIndex);
634
+ let targetIndex = current.fromIndex;
635
+ if (hasCrossedThreshold) {
636
+ event.preventDefault();
637
+ targetIndex = remaining.length;
638
+ for (let index = 0; index < remaining.length; index += 1) {
639
+ const row = options.rowElement(String(options.getItemKey(remaining[index])));
640
+ if (row == null) continue;
641
+ const bounds = row.getBoundingClientRect();
642
+ if (event.clientY < bounds.top + bounds.height / 2) {
643
+ targetIndex = index;
644
+ break;
645
+ }
646
+ }
647
+ }
648
+ const nextState = {
649
+ ...current,
650
+ pointerClientX: event.clientX,
651
+ pointerClientY: event.clientY,
652
+ hasPointerMoved: hasCrossedThreshold,
653
+ toIndex: targetIndex
654
+ };
655
+ if (current.pointerClientX === nextState.pointerClientX && current.pointerClientY === nextState.pointerClientY && current.hasPointerMoved === nextState.hasPointerMoved && current.toIndex === nextState.toIndex) return;
656
+ setState(nextState);
657
+ if (hasCrossedThreshold && current.toIndex !== targetIndex) options.announce(options.messages.movedToPosition(targetIndex + 1, current.originalValue.length));
658
+ }
659
+ function onHandlePointerUp(event) {
660
+ const current = state.value;
661
+ if (current?.mode !== "pointer" || current.pointerId !== event.pointerId) return;
662
+ commitReorder();
663
+ const target = event.currentTarget;
664
+ if (target?.hasPointerCapture?.(event.pointerId)) target.releasePointerCapture?.(event.pointerId);
665
+ }
666
+ function onHandlePointerCancel(event) {
667
+ const current = state.value;
668
+ if (current?.mode !== "pointer" || current.pointerId !== event.pointerId) return;
669
+ cancelReorder();
670
+ const target = event.currentTarget;
671
+ if (target?.hasPointerCapture?.(event.pointerId)) target.releasePointerCapture?.(event.pointerId);
672
+ }
673
+ function onHandleLostPointerCapture(event) {
674
+ const current = state.value;
675
+ if (current?.mode === "pointer" && current.pointerId === event.pointerId) cancelReorder();
676
+ }
677
+ /** 键盘拿起时焦点跑掉就放弃,避免留下一个看不见的预览顺序。 */
678
+ function onHandleBlur(item) {
679
+ const current = state.value;
680
+ if (current?.mode === "keyboard" && current.itemKey === String(options.getItemKey(item))) cancelReorder();
681
+ }
682
+ watch(() => state.value?.mode === "pointer" ? state.value.itemKey : null, async (itemKey) => {
683
+ if (itemKey == null) {
684
+ options.previewHost.value?.replaceChildren();
685
+ return;
686
+ }
687
+ await nextTick();
688
+ const host = options.previewHost.value;
689
+ if (host == null) return;
690
+ const sourceRow = options.rowElement(itemKey);
691
+ if (sourceRow == null) return;
692
+ const clone = sourceRow.cloneNode(true);
693
+ host.style.direction = getComputedStyle(sourceRow).direction;
694
+ clone.style.opacity = "1";
695
+ clone.removeAttribute("data-list-input-reorder-source");
696
+ clone.removeAttribute("data-list-input-drop-target");
697
+ clone.removeAttribute("data-list-input-reorder");
698
+ clone.removeAttribute("data-list-input-remove");
699
+ clone.removeAttribute("data-list-input-row");
700
+ clone.setAttribute("aria-hidden", "true");
701
+ for (const element of [clone, ...clone.querySelectorAll("*")]) {
702
+ element.removeAttribute("id");
703
+ element.removeAttribute("aria-describedby");
704
+ element.removeAttribute("aria-labelledby");
705
+ element.removeAttribute("name");
706
+ element.removeAttribute("data-list-input-reorder-source");
707
+ element.removeAttribute("data-list-input-drop-target");
708
+ element.removeAttribute("data-list-input-reorder");
709
+ element.removeAttribute("data-list-input-remove");
710
+ element.setAttribute("tabindex", "-1");
711
+ }
712
+ host.replaceChildren(clone);
713
+ });
714
+ return {
715
+ state,
716
+ sourceKey,
717
+ keyboardKey,
718
+ pointerDrop,
719
+ previewStyle,
720
+ displayValue,
721
+ startReorder,
722
+ cancelReorder,
723
+ commitReorder,
724
+ moveWithArrowKey,
725
+ previewReorder,
726
+ onHandleKeydown,
727
+ onHandlePointerDown,
728
+ onHandlePointerMove,
729
+ onHandlePointerUp,
730
+ onHandlePointerCancel,
731
+ onHandleLostPointerCapture,
732
+ onHandleBlur
733
+ };
734
+ }
735
+ //#endregion
736
+ //#region src/ListInput/ListInput.vue
737
+ const _hoisted_1$8 = [
738
+ "aria-describedby",
739
+ "aria-disabled",
740
+ "aria-busy",
741
+ "data-reorderable",
742
+ "data-state"
743
+ ];
744
+ const _hoisted_2$8 = ["aria-describedby"];
745
+ const _hoisted_3$7 = {
746
+ key: 0,
747
+ "data-list-input-motion-key": "state:empty",
748
+ class: "astryx-list-input__empty-item"
749
+ };
750
+ const _hoisted_4$6 = {
751
+ "data-list-input-motion-key": "action:add",
752
+ class: "astryx-list-input__action-row"
753
+ };
754
+ const _hoisted_5$5 = {
755
+ "data-list-input-add-content": "",
756
+ class: "astryx-list-input__action-content"
757
+ };
758
+ const focusableFieldSelector = "[data-list-input-cell] input, [data-list-input-cell] select, [data-list-input-cell] textarea, [data-list-input-cell] button, [data-list-input-cell] [tabindex]:not([tabindex=\"-1\"])";
759
+ const _sfc_main$2 = /*@__PURE__*/ defineComponent({
760
+ name: "AstryxListInput",
761
+ inheritAttrs: false,
762
+ __name: "ListInput",
763
+ props: {
764
+ label: {},
765
+ description: { default: void 0 },
766
+ modelValue: { default: () => [] },
767
+ getItemKey: {},
768
+ createItem: {},
769
+ columns: {},
770
+ itemName: { default: "item" },
771
+ status: { default: void 0 },
772
+ getItemStatus: {
773
+ type: Function,
774
+ default: void 0
775
+ },
776
+ getFieldStatus: {
777
+ type: Function,
778
+ default: void 0
779
+ },
780
+ isReorderable: {
781
+ type: Boolean,
782
+ default: false
783
+ },
784
+ isDisabled: {
785
+ type: Boolean,
786
+ default: false
787
+ },
788
+ isLoading: {
789
+ type: Boolean,
790
+ default: false
791
+ },
792
+ maxItems: { default: void 0 },
793
+ isLabelHidden: {
794
+ type: Boolean,
795
+ default: false
796
+ },
797
+ isOptional: {
798
+ type: Boolean,
799
+ default: false
800
+ },
801
+ isRequired: {
802
+ type: Boolean,
803
+ default: false
804
+ }
805
+ },
806
+ emits: ["update:modelValue", "change"],
807
+ setup(__props, { emit: __emit }) {
808
+ const props = __props;
809
+ const emit = __emit;
810
+ const { t } = useTranslator();
811
+ const announce = useAnnounce();
812
+ const groupId = `list-input-${useId()}`;
813
+ const labelId = `${groupId}-label`;
814
+ const descriptionId = computed(() => props.description ? `${groupId}-description` : void 0);
815
+ const statusId = computed(() => props.status?.message ? `${groupId}-status` : void 0);
816
+ const reorderInstructionsId = `${groupId}-reorder-instructions`;
817
+ const groupRef = useTemplateRef("groupRef");
818
+ const addButtonRef = useTemplateRef("addButtonRef");
819
+ const previewHost = ref(null);
820
+ const itemKeyOf = (item) => String(props.getItemKey(item));
821
+ const mutationsDisabled = computed(() => props.isDisabled || props.isLoading);
822
+ const hasReachedMax = computed(() => props.maxItems != null && props.modelValue.length >= props.maxItems);
823
+ const gridTemplate = computed(() => resolveGridTemplate(props.columns));
824
+ /** 提交之后 props 才会回来,中间的连点读不到最新值,这里留一份同步的顺序。 */
825
+ const liveValue = ref([...props.modelValue]);
826
+ watch(() => props.modelValue, (next) => {
827
+ liveValue.value = [...next];
828
+ });
829
+ const announceMessages = {
830
+ grabbed: (position) => t("@astryx.listInput.announceGrabbed", {
831
+ itemName: props.itemName,
832
+ position
833
+ }),
834
+ movedToPosition: (position, total) => t("@astryx.listInput.announceMovedToPosition", {
835
+ itemName: props.itemName,
836
+ position,
837
+ total
838
+ }),
839
+ reorderCancelled: () => t("@astryx.listInput.announceReorderCancelled"),
840
+ returnedToPosition: (position) => t("@astryx.listInput.announceReturnedToPosition", {
841
+ itemName: props.itemName,
842
+ position
843
+ }),
844
+ dropped: (position, total) => t("@astryx.listInput.announceDropped", {
845
+ itemName: props.itemName,
846
+ position,
847
+ total
848
+ }),
849
+ alreadyAtBoundary: (boundary) => t("@astryx.listInput.announceAlreadyAtBoundary", {
850
+ itemName: props.itemName,
851
+ boundary
852
+ })
853
+ };
854
+ const labels = {
855
+ remove: (position) => t("@astryx.listInput.removeItem", {
856
+ itemName: props.itemName,
857
+ position
858
+ }),
859
+ removeUnavailable: () => t("@astryx.listInput.removeUnavailable", { itemName: props.itemName }),
860
+ reorder: (position) => t("@astryx.listInput.reorderItem", {
861
+ itemName: props.itemName,
862
+ position
863
+ }),
864
+ fieldLabel: (header, position, total) => t("@astryx.listInput.fieldLabelWithPosition", {
865
+ header,
866
+ itemName: props.itemName,
867
+ position,
868
+ total
869
+ })
870
+ };
871
+ function emitValue(nextValue, change) {
872
+ liveValue.value = [...nextValue];
873
+ emit("update:modelValue", nextValue);
874
+ emit("change", nextValue, change);
875
+ }
876
+ function rowElement(itemKey) {
877
+ return groupRef.value?.querySelector(`[data-list-input-row="${itemKey}"]`) ?? null;
878
+ }
879
+ const reorder = useListReorder({
880
+ currentValue: () => liveValue.value,
881
+ getItemKey: (item) => props.getItemKey(item),
882
+ isReorderable: () => props.isReorderable,
883
+ isBlocked: () => mutationsDisabled.value,
884
+ rowElement,
885
+ previewHost,
886
+ emitReorder: (nextValue, change) => emitValue(nextValue, change),
887
+ announce,
888
+ messages: announceMessages
889
+ });
890
+ watch(mutationsDisabled, (blocked) => {
891
+ if (blocked && reorder.state.value != null) {
892
+ reorder.state.value = null;
893
+ announce(announceMessages.reorderCancelled());
894
+ }
895
+ });
896
+ let pendingFocus = null;
897
+ watch(() => props.modelValue, () => {
898
+ const pending = pendingFocus;
899
+ if (pending == null) return;
900
+ pendingFocus = null;
901
+ let target = null;
902
+ if (pending.target === "add") target = addButtonRef.value?.element ?? null;
903
+ else if (pending.itemKey != null) {
904
+ const row = rowElement(pending.itemKey);
905
+ const selector = pending.target === "remove" ? "[data-list-input-remove]" : focusableFieldSelector;
906
+ target = row?.querySelector(selector) ?? null;
907
+ }
908
+ target?.focus();
909
+ }, { flush: "post" });
910
+ function handleAdd() {
911
+ if (mutationsDisabled.value || hasReachedMax.value) return;
912
+ const item = props.createItem();
913
+ const nextValue = [...props.modelValue, item];
914
+ pendingFocus = {
915
+ itemKey: itemKeyOf(item),
916
+ target: "first-field"
917
+ };
918
+ emitValue(nextValue, {
919
+ type: "add",
920
+ item,
921
+ index: props.modelValue.length
922
+ });
923
+ announce(t("@astryx.listInput.announceAdded", {
924
+ itemName: props.itemName,
925
+ position: props.modelValue.length + 1
926
+ }));
927
+ }
928
+ function handleUpdate(item, index, nextItem, columnKey) {
929
+ if (mutationsDisabled.value) return;
930
+ const nextValue = [...props.modelValue];
931
+ nextValue[index] = nextItem;
932
+ emitValue(nextValue, {
933
+ type: "update",
934
+ item: nextItem,
935
+ previousItem: item,
936
+ index,
937
+ columnKey
938
+ });
939
+ }
940
+ function handleRemove(item) {
941
+ if (mutationsDisabled.value) return;
942
+ const itemKey = itemKeyOf(item);
943
+ const index = props.modelValue.findIndex((candidate) => itemKeyOf(candidate) === itemKey);
944
+ if (index < 0) return;
945
+ const nextValue = props.modelValue.filter((_, itemIndex) => itemIndex !== index);
946
+ const nextFocusItem = nextValue[Math.min(index, nextValue.length - 1)];
947
+ pendingFocus = nextFocusItem == null ? { target: "add" } : {
948
+ itemKey: itemKeyOf(nextFocusItem),
949
+ target: "remove"
950
+ };
951
+ emitValue(nextValue, {
952
+ type: "remove",
953
+ item: props.modelValue[index],
954
+ index
955
+ });
956
+ announce(t("@astryx.listInput.announceRemoved", {
957
+ itemName: props.itemName,
958
+ position: index + 1
959
+ }));
960
+ }
961
+ const handlers = {
962
+ updateItem: (item, index, nextItem, columnKey) => handleUpdate(item, index, nextItem, columnKey),
963
+ remove: (item) => handleRemove(item),
964
+ keydown: (event, item, index) => reorder.onHandleKeydown(event, item, index),
965
+ blur: (item) => reorder.onHandleBlur(item),
966
+ pointerdown: (event, item, index) => reorder.onHandlePointerDown(event, item, index),
967
+ pointermove: (event) => reorder.onHandlePointerMove(event),
968
+ pointerup: (event) => reorder.onHandlePointerUp(event),
969
+ pointercancel: (event) => reorder.onHandlePointerCancel(event),
970
+ lostpointercapture: (event) => reorder.onHandleLostPointerCapture(event)
971
+ };
972
+ const fieldStatusFor = (item, index) => (columnKey) => props.getFieldStatus?.(item, columnKey, index);
973
+ const describedBy = computed(() => [descriptionId.value, statusId.value].filter(Boolean).join(" ") || void 0);
974
+ watch([
975
+ () => props.modelValue,
976
+ () => props.columns,
977
+ () => props.getItemKey
978
+ ], () => {
979
+ const itemKeys = /* @__PURE__ */ new Set();
980
+ for (const item of props.modelValue) {
981
+ const key = itemKeyOf(item);
982
+ if (itemKeys.has(key)) {
983
+ console.warn(`ListInput: getItemKey returned duplicate key "${key}". Keys must be unique and stable.`);
984
+ break;
985
+ }
986
+ itemKeys.add(key);
987
+ }
988
+ const columnKeys = /* @__PURE__ */ new Set();
989
+ for (const column of props.columns) {
990
+ if (columnKeys.has(column.key)) {
991
+ console.warn(`ListInput: columns contains duplicate key "${column.key}". Column keys must be unique.`);
992
+ break;
993
+ }
994
+ columnKeys.add(column.key);
995
+ }
996
+ }, { immediate: true });
997
+ return (_ctx, _cache) => {
998
+ return openBlock(), createBlock(unref(Field), {
999
+ label: __props.label,
1000
+ description: __props.description,
1001
+ "input-id": groupId,
1002
+ "label-id": labelId,
1003
+ "description-id": descriptionId.value,
1004
+ "is-group-label": "",
1005
+ "is-label-hidden": __props.isLabelHidden,
1006
+ "is-optional": __props.isOptional,
1007
+ "is-required": __props.isRequired,
1008
+ "is-disabled": __props.isDisabled,
1009
+ status: __props.status == null ? void 0 : {
1010
+ ...__props.status,
1011
+ messageId: statusId.value
1012
+ },
1013
+ "status-variant": "detached"
1014
+ }, {
1015
+ default: withCtx(() => [createElementVNode("div", {
1016
+ id: groupId,
1017
+ ref_key: "groupRef",
1018
+ ref: groupRef,
1019
+ role: "group",
1020
+ "aria-labelledby": labelId,
1021
+ "aria-describedby": describedBy.value,
1022
+ "aria-disabled": __props.isDisabled || void 0,
1023
+ "aria-busy": __props.isLoading || void 0,
1024
+ class: "astryx-list-input",
1025
+ "data-reorderable": __props.isReorderable ? "true" : void 0,
1026
+ "data-state": __props.isLoading ? "loading" : __props.isDisabled ? "disabled" : void 0
1027
+ }, [
1028
+ createElementVNode("ol", {
1029
+ "aria-labelledby": labelId,
1030
+ "aria-describedby": describedBy.value,
1031
+ class: "astryx-list-input__list"
1032
+ }, [unref(reorder).displayValue.value.length === 0 ? (openBlock(), createElementBlock("li", _hoisted_3$7, [createVNode(unref(EmptyState), {
1033
+ title: unref(t)("@astryx.listInput.emptyTitle", { itemName: __props.itemName }),
1034
+ description: unref(t)("@astryx.listInput.emptyDescription", { itemName: __props.itemName }),
1035
+ "is-compact": ""
1036
+ }, null, 8, ["title", "description"])])) : createCommentVNode("v-if", true), (openBlock(true), createElementBlock(Fragment, null, renderList(unref(reorder).displayValue.value, (item, index) => {
1037
+ return openBlock(), createBlock(_sfc_main$11, {
1038
+ key: itemKeyOf(item),
1039
+ item,
1040
+ index,
1041
+ total: unref(reorder).displayValue.value.length,
1042
+ columns: __props.columns,
1043
+ "grid-template": gridTemplate.value,
1044
+ "item-key": itemKeyOf(item),
1045
+ "item-name": __props.itemName,
1046
+ "is-disabled": __props.isDisabled,
1047
+ "is-loading": __props.isLoading,
1048
+ "is-reorderable": __props.isReorderable,
1049
+ "is-reorder-source": unref(reorder).state.value?.itemKey === itemKeyOf(item),
1050
+ "is-pointer-source": unref(reorder).sourceKey.value === itemKeyOf(item),
1051
+ "is-keyboard-active": unref(reorder).keyboardKey.value === itemKeyOf(item),
1052
+ "drop-position": unref(reorder).pointerDrop.value?.itemKey === itemKeyOf(item) ? unref(reorder).pointerDrop.value.position : null,
1053
+ "field-status": fieldStatusFor(item, index),
1054
+ "item-status": __props.getItemStatus?.(item, index),
1055
+ "item-status-id": __props.getItemStatus?.(item, index)?.message ? `${groupId}-item-${index}-status` : void 0,
1056
+ "reorder-instructions-id": reorderInstructionsId,
1057
+ labels,
1058
+ handlers
1059
+ }, null, 8, [
1060
+ "item",
1061
+ "index",
1062
+ "total",
1063
+ "columns",
1064
+ "grid-template",
1065
+ "item-key",
1066
+ "item-name",
1067
+ "is-disabled",
1068
+ "is-loading",
1069
+ "is-reorderable",
1070
+ "is-reorder-source",
1071
+ "is-pointer-source",
1072
+ "is-keyboard-active",
1073
+ "drop-position",
1074
+ "field-status",
1075
+ "item-status",
1076
+ "item-status-id",
1077
+ "handlers"
1078
+ ]);
1079
+ }), 128))], 8, _hoisted_2$8),
1080
+ createElementVNode("div", _hoisted_4$6, [createElementVNode("div", _hoisted_5$5, [createVNode(unref(Button), {
1081
+ ref_key: "addButtonRef",
1082
+ ref: addButtonRef,
1083
+ label: unref(t)("@astryx.listInput.addItem", { itemName: __props.itemName }),
1084
+ variant: "secondary",
1085
+ size: "md",
1086
+ width: "100%",
1087
+ "is-disabled": __props.isDisabled || hasReachedMax.value,
1088
+ "is-loading": __props.isLoading,
1089
+ onClick: handleAdd
1090
+ }, null, 8, [
1091
+ "label",
1092
+ "is-disabled",
1093
+ "is-loading"
1094
+ ])])]),
1095
+ __props.isReorderable ? (openBlock(), createBlock(unref(VisuallyHidden), {
1096
+ key: 0,
1097
+ id: reorderInstructionsId,
1098
+ as: "div"
1099
+ }, {
1100
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("@astryx.listInput.reorderInstructions")), 1)]),
1101
+ _: 1
1102
+ })) : createCommentVNode("v-if", true)
1103
+ ], 8, _hoisted_1$8), (openBlock(), createBlock(Teleport, { to: "body" }, [unref(reorder).previewStyle.value != null ? (openBlock(), createElementBlock("div", {
1104
+ key: 0,
1105
+ ref_key: "previewHost",
1106
+ ref: previewHost,
1107
+ class: "astryx-list-input__drag-preview",
1108
+ "data-list-input-drag-preview": "",
1109
+ "aria-hidden": "true",
1110
+ style: normalizeStyle(unref(reorder).previewStyle.value)
1111
+ }, null, 4)) : createCommentVNode("v-if", true)]))]),
1112
+ _: 1
1113
+ }, 8, [
1114
+ "label",
1115
+ "description",
1116
+ "description-id",
1117
+ "is-label-hidden",
1118
+ "is-optional",
1119
+ "is-required",
1120
+ "is-disabled",
1121
+ "status"
1122
+ ]);
1123
+ };
1124
+ }
1125
+ });
1126
+ //#endregion
1127
+ //#region src/LogStream/LogStreamRow.vue
1128
+ const _hoisted_1$7 = ["data-level", "data-cols"];
1129
+ const _hoisted_2$7 = {
1130
+ key: 0,
1131
+ class: "astryx-log-stream__time"
1132
+ };
1133
+ const _hoisted_3$6 = { class: "astryx-log-stream__level" };
1134
+ const _hoisted_4$5 = ["title"];
1135
+ const _hoisted_5$4 = { class: "astryx-log-stream__message" };
1136
+ const _hoisted_6$4 = [
1137
+ "data-level",
1138
+ "data-cols",
1139
+ "aria-expanded"
1140
+ ];
1141
+ const _hoisted_7$2 = {
1142
+ key: 0,
1143
+ class: "astryx-log-stream__time"
1144
+ };
1145
+ const _hoisted_8$2 = { class: "astryx-log-stream__level" };
1146
+ const _hoisted_9$2 = ["title"];
1147
+ const _hoisted_10$2 = { class: "astryx-log-stream__message" };
1148
+ const _hoisted_11 = {
1149
+ key: 0,
1150
+ class: "astryx-log-stream__detail"
1151
+ };
1152
+ const _sfc_main$10 = /*@__PURE__*/ defineComponent({
1153
+ name: "AstryxLogStreamRow",
1154
+ __name: "LogStreamRow",
1155
+ props: {
1156
+ entry: {},
1157
+ hasTimestamps: { type: Boolean },
1158
+ hasSource: { type: Boolean },
1159
+ isExpanded: { type: Boolean }
1160
+ },
1161
+ emits: ["toggle"],
1162
+ setup(__props, { emit: __emit }) {
1163
+ const props = __props;
1164
+ const emit = __emit;
1165
+ const isExpandable = computed(() => props.entry.detail != null);
1166
+ const cols = computed(() => {
1167
+ if (props.hasTimestamps) return props.hasSource ? "full" : "no-source";
1168
+ return props.hasSource ? "no-timestamp" : "message-only";
1169
+ });
1170
+ const detailText = computed(() => props.entry.detail);
1171
+ return (_ctx, _cache) => {
1172
+ return !isExpandable.value ? (openBlock(), createElementBlock("div", {
1173
+ key: 0,
1174
+ class: "astryx-log-stream__row",
1175
+ "data-level": __props.entry.level,
1176
+ "data-cols": cols.value
1177
+ }, [
1178
+ __props.hasTimestamps ? (openBlock(), createElementBlock("span", _hoisted_2$7, toDisplayString(__props.entry.timestamp), 1)) : createCommentVNode("v-if", true),
1179
+ createElementVNode("span", _hoisted_3$6, toDisplayString(__props.entry.level), 1),
1180
+ __props.hasSource ? (openBlock(), createElementBlock("span", {
1181
+ key: 1,
1182
+ class: "astryx-log-stream__source",
1183
+ title: __props.entry.source
1184
+ }, toDisplayString(__props.entry.source), 9, _hoisted_4$5)) : createCommentVNode("v-if", true),
1185
+ createElementVNode("span", _hoisted_5$4, toDisplayString(__props.entry.message), 1)
1186
+ ], 8, _hoisted_1$7)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createElementVNode("button", {
1187
+ type: "button",
1188
+ class: "astryx-log-stream__row astryx-log-stream__row--button",
1189
+ "data-level": __props.entry.level,
1190
+ "data-cols": cols.value,
1191
+ "aria-expanded": __props.isExpanded,
1192
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("toggle"))
1193
+ }, [
1194
+ __props.hasTimestamps ? (openBlock(), createElementBlock("span", _hoisted_7$2, toDisplayString(__props.entry.timestamp), 1)) : createCommentVNode("v-if", true),
1195
+ createElementVNode("span", _hoisted_8$2, toDisplayString(__props.entry.level), 1),
1196
+ __props.hasSource ? (openBlock(), createElementBlock("span", {
1197
+ key: 1,
1198
+ class: "astryx-log-stream__source",
1199
+ title: __props.entry.source
1200
+ }, toDisplayString(__props.entry.source), 9, _hoisted_9$2)) : createCommentVNode("v-if", true),
1201
+ createElementVNode("span", _hoisted_10$2, toDisplayString(__props.entry.message), 1)
1202
+ ], 8, _hoisted_6$4), __props.isExpanded ? (openBlock(), createElementBlock("div", _hoisted_11, [renderSlot(_ctx.$slots, "detail", { entry: __props.entry }, () => [createTextVNode(toDisplayString(detailText.value), 1)])])) : createCommentVNode("v-if", true)], 64));
1203
+ };
1204
+ }
1205
+ });
1206
+ //#endregion
1207
+ //#region src/LogStream/useLogFollow.ts
1208
+ /** 离底部多远以内算「还在跟随最新一行」。 */
1209
+ const FOLLOW_THRESHOLD_PX = 24;
1210
+ /**
1211
+ * 日志跟滚:跟随最新一行、用户往回滚就取消跟随、点「跳到最新」再跟上。
1212
+ *
1213
+ * 跟随状态下直接写 `scrollTop`(不用平滑滚动),这样连续追加的行不会堆起
1214
+ * 互相打断的动画。取消跟随只在视口离开底部时发生,程序化滚动落到底部时
1215
+ * 不会触发,所以不需要「这是不是我自己滚的」这类保护。
1216
+ */
1217
+ function useLogFollow(options) {
1218
+ const scrollerRef = ref(null);
1219
+ const isAtBottom = ref(true);
1220
+ function setFollowing(next) {
1221
+ if (next === options.following.value) return;
1222
+ options.following.value = next;
1223
+ options.onFollowChange(next);
1224
+ }
1225
+ function pinToBottom() {
1226
+ const el = scrollerRef.value;
1227
+ if (el == null) return;
1228
+ el.scrollTop = el.scrollHeight;
1229
+ isAtBottom.value = true;
1230
+ }
1231
+ watch([options.following, options.entryCount], ([isFollowing]) => {
1232
+ if (isFollowing) nextTick(pinToBottom);
1233
+ });
1234
+ function handleScroll() {
1235
+ const el = scrollerRef.value;
1236
+ if (el == null) return;
1237
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= FOLLOW_THRESHOLD_PX;
1238
+ isAtBottom.value = atBottom;
1239
+ if (options.following.value && !atBottom) setFollowing(false);
1240
+ }
1241
+ function jumpToLatest() {
1242
+ setFollowing(true);
1243
+ pinToBottom();
1244
+ }
1245
+ return {
1246
+ scrollerRef,
1247
+ isAtBottom,
1248
+ handleScroll,
1249
+ jumpToLatest
1250
+ };
1251
+ }
1252
+ //#endregion
1253
+ //#region src/LogStream/LogStream.vue
1254
+ const _hoisted_1$6 = ["data-variant"];
1255
+ const _hoisted_2$6 = ["aria-label", "aria-live"];
1256
+ const _sfc_main$3 = /*@__PURE__*/ defineComponent({
1257
+ name: "AstryxLogStream",
1258
+ inheritAttrs: false,
1259
+ __name: "LogStream",
1260
+ props: /*@__PURE__*/ mergeModels({
1261
+ entries: {},
1262
+ variant: { default: "default" },
1263
+ isFollowing: { type: Boolean },
1264
+ maxHeight: { default: void 0 },
1265
+ hasTimestamps: {
1266
+ type: Boolean,
1267
+ default: true
1268
+ },
1269
+ label: { default: "Log stream" }
1270
+ }, {
1271
+ "isFollowing": {
1272
+ type: Boolean,
1273
+ default: false
1274
+ },
1275
+ "isFollowingModifiers": {}
1276
+ }),
1277
+ emits: /*@__PURE__*/ mergeModels(["followChange"], ["update:isFollowing"]),
1278
+ setup(__props, { emit: __emit }) {
1279
+ const props = __props;
1280
+ const emit = __emit;
1281
+ const attrs = useAttrs();
1282
+ const following = useModel(__props, "isFollowing");
1283
+ const hasSource = computed(() => props.entries.some((entry) => entry.source != null));
1284
+ const scrollerStyle = computed(() => props.maxHeight == null ? void 0 : { maxHeight: typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight });
1285
+ const rootAttrs = computed(() => {
1286
+ const { class: _class, style: _style, ...rest } = attrs;
1287
+ return rest;
1288
+ });
1289
+ const rootClass = computed(() => attrs.class);
1290
+ const rootStyle = computed(() => attrs.style);
1291
+ const { scrollerRef, isAtBottom, handleScroll, jumpToLatest } = useLogFollow({
1292
+ following,
1293
+ onFollowChange: (next) => emit("followChange", next),
1294
+ entryCount: () => props.entries.length
1295
+ });
1296
+ function setScroller(el) {
1297
+ scrollerRef.value = el instanceof HTMLElement ? el : null;
1298
+ }
1299
+ const expandedIds = ref(/* @__PURE__ */ new Set());
1300
+ function toggleExpanded(id) {
1301
+ const next = new Set(expandedIds.value);
1302
+ if (next.has(id)) next.delete(id);
1303
+ else next.add(id);
1304
+ expandedIds.value = next;
1305
+ }
1306
+ return (_ctx, _cache) => {
1307
+ return openBlock(), createElementBlock("div", mergeProps(rootAttrs.value, {
1308
+ class: ["astryx-log-stream", rootClass.value],
1309
+ "data-variant": __props.variant,
1310
+ style: rootStyle.value
1311
+ }), [
1312
+ createCommentVNode("\n role=\"log\" 隐含 aria-live=\"polite\":繁忙的日志流每追加一行都会念给读屏软件,\n 很吵(WCAG 2.2.2 / 4.1.3)。跟随状态顺便当静音开关——用户不在尾部时\n 改成 aria-live=\"off\",点「跳到最新」恢复念。\n "),
1313
+ createElementVNode("div", {
1314
+ ref: setScroller,
1315
+ class: "astryx-log-stream__scroller",
1316
+ role: "log",
1317
+ "aria-label": __props.label,
1318
+ "aria-live": following.value ? "polite" : "off",
1319
+ style: normalizeStyle(scrollerStyle.value),
1320
+ onScrollPassive: _cache[0] || (_cache[0] = (...args) => unref(handleScroll) && unref(handleScroll)(...args))
1321
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.entries, (entry) => {
1322
+ return renderSlot(_ctx.$slots, "entry", { entry }, () => [createVNode(_sfc_main$10, {
1323
+ entry,
1324
+ "has-timestamps": __props.hasTimestamps,
1325
+ "has-source": hasSource.value,
1326
+ "is-expanded": expandedIds.value.has(entry.id),
1327
+ onToggle: ($event) => toggleExpanded(entry.id)
1328
+ }, createSlots({ _: 2 }, [_ctx.$slots.detail ? {
1329
+ name: "detail",
1330
+ fn: withCtx((slotProps) => [renderSlot(_ctx.$slots, "detail", mergeProps({ ref_for: true }, slotProps))]),
1331
+ key: "0"
1332
+ } : void 0]), 1032, [
1333
+ "entry",
1334
+ "has-timestamps",
1335
+ "has-source",
1336
+ "is-expanded",
1337
+ "onToggle"
1338
+ ])], void 0, entry.id);
1339
+ }), 128))], 44, _hoisted_2$6),
1340
+ !following.value && !unref(isAtBottom) && __props.entries.length > 0 ? (openBlock(), createElementBlock("button", {
1341
+ key: 0,
1342
+ type: "button",
1343
+ class: "astryx-log-stream__jump",
1344
+ onClick: _cache[1] || (_cache[1] = (...args) => unref(jumpToLatest) && unref(jumpToLatest)(...args))
1345
+ }, " Jump to latest ↓ ")) : createCommentVNode("v-if", true)
1346
+ ], 16, _hoisted_1$6);
1347
+ };
1348
+ }
1349
+ });
1350
+ //#endregion
1351
+ //#region src/MobileTokenizer/icons.ts
1352
+ /** 搜索框前面的放大镜。core 的 `TextInput.startIcon` 要的是组件,不是名字。 */
1353
+ const SearchIcon = () => h(Icon, { icon: "search" });
1354
+ /** 空列表时用的项目符号图标,语义图标表里没有对应的名字。 */
1355
+ const ListBulletIcon = () => h("svg", {
1356
+ viewBox: "0 0 24 24",
1357
+ fill: "none"
1358
+ }, [h("path", {
1359
+ "d": "M8.5 6h11M8.5 12h11M8.5 18h11",
1360
+ "stroke": "currentColor",
1361
+ "stroke-width": "1.5",
1362
+ "stroke-linecap": "round"
1363
+ }), ...[
1364
+ 6,
1365
+ 12,
1366
+ 18
1367
+ ].map((cy) => h("circle", {
1368
+ cx: 4.5,
1369
+ cy,
1370
+ r: 1,
1371
+ fill: "currentColor"
1372
+ }))]);
1373
+ function useSheetSearch(options) {
1374
+ const results = ref([]);
1375
+ const isSearching = ref(false);
1376
+ const renderedCount = ref(50);
1377
+ let sequence = 0;
1378
+ let timer;
1379
+ let lastLoadScrollHeight = null;
1380
+ function resetWindow() {
1381
+ lastLoadScrollHeight = null;
1382
+ renderedCount.value = 50;
1383
+ }
1384
+ const visibleItems = computed(() => {
1385
+ const normalized = options.query.value.trim().toLowerCase();
1386
+ const matches = (item) => normalized === "" || item.label.toLowerCase().includes(normalized);
1387
+ const selectedAtOpenMatches = options.selectedAtOpen().filter(matches);
1388
+ const selectedAtOpenIds = new Set(selectedAtOpenMatches.map((item) => item.id));
1389
+ const resultIds = new Set(results.value.map((item) => item.id));
1390
+ const selectedMissingFromResults = options.selectedNow().filter((item) => !selectedAtOpenIds.has(item.id) && !resultIds.has(item.id) && matches(item));
1391
+ return [
1392
+ ...selectedAtOpenMatches,
1393
+ ...selectedMissingFromResults,
1394
+ ...results.value.filter((item) => !selectedAtOpenIds.has(item.id))
1395
+ ];
1396
+ });
1397
+ const renderedItems = computed(() => visibleItems.value.slice(0, renderedCount.value));
1398
+ function search() {
1399
+ if (!options.isOpen()) return;
1400
+ const current = ++sequence;
1401
+ const run = async () => {
1402
+ isSearching.value = true;
1403
+ try {
1404
+ const source = options.searchSource();
1405
+ const trimmed = options.query.value.trim();
1406
+ let items;
1407
+ if (trimmed === "") items = source.bootstrap ? await source.bootstrap() : await source.search("");
1408
+ else if (trimmed.length < options.minQueryLength()) items = [];
1409
+ else items = await source.search(options.query.value);
1410
+ if (sequence === current) results.value = items.slice(0, options.maxMenuItems());
1411
+ } finally {
1412
+ if (sequence === current) isSearching.value = false;
1413
+ }
1414
+ };
1415
+ if (timer != null) clearTimeout(timer);
1416
+ const delay = options.debounceMs();
1417
+ if (delay > 0) timer = setTimeout(() => void run(), delay);
1418
+ else run();
1419
+ }
1420
+ watch([
1421
+ options.isOpen,
1422
+ options.query,
1423
+ options.searchSource,
1424
+ options.minQueryLength,
1425
+ options.maxMenuItems,
1426
+ options.debounceMs
1427
+ ], () => {
1428
+ resetWindow();
1429
+ search();
1430
+ }, { immediate: true });
1431
+ onScopeDispose(() => {
1432
+ if (timer != null) clearTimeout(timer);
1433
+ });
1434
+ /** 滚到离底部 200px 以内就多画一批;同一个 scrollHeight 只触发一次。 */
1435
+ function handleScroll(event) {
1436
+ const list = event.currentTarget;
1437
+ if (list == null) return;
1438
+ if (list.scrollHeight - list.scrollTop - list.clientHeight > 200 || lastLoadScrollHeight === list.scrollHeight) return;
1439
+ lastLoadScrollHeight = list.scrollHeight;
1440
+ renderedCount.value = Math.min(renderedCount.value + 50, visibleItems.value.length);
1441
+ }
1442
+ return {
1443
+ results,
1444
+ isSearching,
1445
+ visibleItems,
1446
+ renderedItems,
1447
+ renderedCount,
1448
+ resetWindow,
1449
+ handleScroll
1450
+ };
1451
+ }
1452
+ //#endregion
1453
+ //#region src/MobileTokenizer/MobileTokenizer.vue
1454
+ const _hoisted_1$5 = [
1455
+ "aria-expanded",
1456
+ "aria-label",
1457
+ "disabled"
1458
+ ];
1459
+ const _hoisted_2$5 = {
1460
+ key: 1,
1461
+ class: "astryx-mobile-tokenizer__trigger-placeholder"
1462
+ };
1463
+ const _hoisted_3$5 = { class: "astryx-mobile-tokenizer__sheet-body" };
1464
+ const _hoisted_4$4 = { class: "astryx-mobile-tokenizer__filter-row" };
1465
+ const _hoisted_5$3 = [
1466
+ "aria-label",
1467
+ "data-rendered-count",
1468
+ "data-total-count"
1469
+ ];
1470
+ const _hoisted_6$3 = {
1471
+ key: 0,
1472
+ class: "astryx-mobile-tokenizer__create-row",
1473
+ "data-testid": "mobile-tokenizer-create-row"
1474
+ };
1475
+ const _hoisted_7$1 = { class: "astryx-mobile-tokenizer__create-label" };
1476
+ const _hoisted_8$1 = [
1477
+ "aria-checked",
1478
+ "aria-label",
1479
+ "disabled",
1480
+ "onClick"
1481
+ ];
1482
+ const _hoisted_9$1 = { class: "astryx-mobile-tokenizer__row-label" };
1483
+ const _hoisted_10$1 = {
1484
+ key: 0,
1485
+ class: "astryx-mobile-tokenizer__footer"
1486
+ };
1487
+ const CREATABLE_ID_PREFIX = "__xds_create__";
1488
+ const _sfc_main$4 = /*@__PURE__*/ defineComponent({
1489
+ name: "AstryxMobileTokenizer",
1490
+ inheritAttrs: false,
1491
+ __name: "MobileTokenizer",
1492
+ props: {
1493
+ label: {},
1494
+ searchSource: {},
1495
+ modelValue: {},
1496
+ placeholder: {},
1497
+ description: {},
1498
+ isOptional: { type: Boolean },
1499
+ isDisabled: {
1500
+ type: Boolean,
1501
+ default: false
1502
+ },
1503
+ hasCreate: {
1504
+ type: Boolean,
1505
+ default: false
1506
+ },
1507
+ maxEntries: {},
1508
+ maxMenuItems: { default: 10 },
1509
+ minQueryLength: { default: 1 },
1510
+ debounceMs: { default: 150 },
1511
+ emptySearchResultsText: { default: "No results found" }
1512
+ },
1513
+ emits: [
1514
+ "update:modelValue",
1515
+ "change",
1516
+ "update:query",
1517
+ "changeQuery",
1518
+ "openChange"
1519
+ ],
1520
+ setup(__props, { emit: __emit }) {
1521
+ const props = __props;
1522
+ const emit = __emit;
1523
+ const triggerId = `astryx-mobile-tokenizer-${useId()}`;
1524
+ const isOpen = ref(false);
1525
+ const isClearConfirmationOpen = ref(false);
1526
+ const query = ref("");
1527
+ /** 打开那一刻的选中项,决定列表开头那一段的顺序。 */
1528
+ const selectedAtOpen = ref([]);
1529
+ const selected = computed(() => props.modelValue);
1530
+ const selectedIds = computed(() => new Set(selected.value.map((item) => item.id)));
1531
+ const isAtMax = computed(() => props.maxEntries != null && selected.value.length >= props.maxEntries);
1532
+ const sheet = useSheetSearch({
1533
+ isOpen: () => isOpen.value,
1534
+ query,
1535
+ searchSource: () => props.searchSource,
1536
+ minQueryLength: () => props.minQueryLength,
1537
+ maxMenuItems: () => props.maxMenuItems,
1538
+ debounceMs: () => props.debounceMs,
1539
+ selectedAtOpen: () => selectedAtOpen.value,
1540
+ selectedNow: () => selected.value
1541
+ });
1542
+ const visibleItems = sheet.visibleItems;
1543
+ const renderedItems = sheet.renderedItems;
1544
+ /** 输入的文字还不在任何选项里时,给一行 `Create "…"` 和后面的加一项按钮。 */
1545
+ const creatableItem = computed(() => {
1546
+ const trimmed = query.value.trim();
1547
+ if (!props.hasCreate || trimmed === "" || isAtMax.value) return null;
1548
+ const lowered = trimmed.toLowerCase();
1549
+ if (selectedIds.value.has(trimmed) || selected.value.some((item) => item.label.toLowerCase() === lowered) || sheet.results.value.some((item) => item.label.toLowerCase() === lowered)) return null;
1550
+ return {
1551
+ id: `${CREATABLE_ID_PREFIX}${trimmed}`,
1552
+ label: `Create "${trimmed}"`
1553
+ };
1554
+ });
1555
+ function commit(items, change) {
1556
+ emit("update:modelValue", items);
1557
+ emit("change", items, change);
1558
+ }
1559
+ function handleAdd(item) {
1560
+ if (isAtMax.value || selectedIds.value.has(item.id)) return;
1561
+ if (props.hasCreate && item.id.startsWith(CREATABLE_ID_PREFIX)) {
1562
+ const created = item.id.slice(14);
1563
+ if (selectedIds.value.has(created)) return;
1564
+ const createdItem = {
1565
+ id: created,
1566
+ label: created
1567
+ };
1568
+ commit([...selected.value, createdItem], {
1569
+ item: createdItem,
1570
+ type: "create"
1571
+ });
1572
+ return;
1573
+ }
1574
+ commit([...selected.value, item], {
1575
+ item,
1576
+ type: "add"
1577
+ });
1578
+ }
1579
+ function handleRemove(item) {
1580
+ commit(selected.value.filter((candidate) => candidate.id !== item.id), {
1581
+ item,
1582
+ type: "remove"
1583
+ });
1584
+ }
1585
+ function handleClearAll() {
1586
+ if (selected.value.length === 0) return;
1587
+ commit([], {
1588
+ item: selected.value[selected.value.length - 1],
1589
+ type: "remove"
1590
+ });
1591
+ }
1592
+ function openSheet() {
1593
+ selectedAtOpen.value = [...selected.value];
1594
+ sheet.resetWindow();
1595
+ isOpen.value = true;
1596
+ emit("openChange", true);
1597
+ }
1598
+ function handleOpenChange(next) {
1599
+ isOpen.value = next;
1600
+ emit("openChange", next);
1601
+ }
1602
+ function handleQueryInput(next) {
1603
+ query.value = next;
1604
+ emit("update:query", next);
1605
+ emit("changeQuery", next);
1606
+ }
1607
+ function handleCreate() {
1608
+ const item = creatableItem.value;
1609
+ if (item == null) return;
1610
+ handleAdd(item);
1611
+ query.value = "";
1612
+ sheet.resetWindow();
1613
+ }
1614
+ function handleDone() {
1615
+ query.value = "";
1616
+ handleOpenChange(false);
1617
+ }
1618
+ function confirmClear() {
1619
+ handleClearAll();
1620
+ isClearConfirmationOpen.value = false;
1621
+ }
1622
+ return (_ctx, _cache) => {
1623
+ return openBlock(), createBlock(unref(Field), {
1624
+ label: __props.label,
1625
+ description: __props.description,
1626
+ "is-optional": __props.isOptional,
1627
+ "is-disabled": __props.isDisabled,
1628
+ "input-id": triggerId
1629
+ }, {
1630
+ default: withCtx(() => [createElementVNode("button", {
1631
+ id: triggerId,
1632
+ type: "button",
1633
+ class: "astryx-mobile-tokenizer__trigger",
1634
+ "aria-haspopup": "dialog",
1635
+ "aria-expanded": isOpen.value,
1636
+ "aria-label": __props.label,
1637
+ disabled: __props.isDisabled,
1638
+ onClick: openSheet
1639
+ }, [selected.value.length > 0 ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(selected.value, (item) => {
1640
+ return openBlock(), createBlock(unref(Token), {
1641
+ key: item.id,
1642
+ label: item.label,
1643
+ disabled: __props.isDisabled
1644
+ }, null, 8, ["label", "disabled"]);
1645
+ }), 128)) : (openBlock(), createElementBlock("span", _hoisted_2$5, toDisplayString(__props.placeholder ?? ""), 1))], 8, _hoisted_1$5), !__props.isDisabled ? (openBlock(), createBlock(unref(BottomSheet), {
1646
+ key: 0,
1647
+ "is-open": isOpen.value,
1648
+ label: __props.label,
1649
+ height: "tall",
1650
+ padding: 0,
1651
+ onOpenChange: handleOpenChange
1652
+ }, {
1653
+ default: withCtx(() => [createElementVNode("div", _hoisted_3$5, [
1654
+ createElementVNode("div", _hoisted_4$4, [createVNode(unref(TextInput), {
1655
+ "model-value": query.value,
1656
+ label: `Search ${__props.label}`,
1657
+ "is-label-hidden": "",
1658
+ placeholder: "Search...",
1659
+ "start-icon": unref(SearchIcon),
1660
+ size: "lg",
1661
+ width: "100%",
1662
+ "has-clear": "",
1663
+ "control-class": "astryx-mobile-tokenizer__filter-input",
1664
+ "onUpdate:modelValue": handleQueryInput
1665
+ }, null, 8, [
1666
+ "model-value",
1667
+ "label",
1668
+ "start-icon"
1669
+ ])]),
1670
+ createElementVNode("div", {
1671
+ class: "astryx-mobile-tokenizer__list",
1672
+ role: "group",
1673
+ "aria-label": __props.label,
1674
+ "data-testid": "mobile-tokenizer-list",
1675
+ "data-rendered-count": unref(renderedItems).length,
1676
+ "data-total-count": unref(visibleItems).length,
1677
+ onScroll: _cache[0] || (_cache[0] = (...args) => unref(sheet).handleScroll && unref(sheet).handleScroll(...args))
1678
+ }, [creatableItem.value != null ? (openBlock(), createElementBlock("div", _hoisted_6$3, [createElementVNode("span", _hoisted_7$1, [createVNode(unref(Text), { type: "body" }, {
1679
+ default: withCtx(() => [createTextVNode(toDisplayString(creatableItem.value.label), 1)]),
1680
+ _: 1
1681
+ })]), createVNode(unref(Button), {
1682
+ label: `Add ${query.value.trim()}`,
1683
+ variant: "secondary",
1684
+ size: "lg",
1685
+ onClick: handleCreate
1686
+ }, null, 8, ["label"])])) : createCommentVNode("v-if", true), !unref(sheet).isSearching.value && unref(visibleItems).length === 0 && creatableItem.value == null ? (openBlock(), createBlock(unref(EmptyState), {
1687
+ key: 1,
1688
+ class: "astryx-mobile-tokenizer__empty",
1689
+ "is-compact": "",
1690
+ title: query.value.trim() === "" ? "No items available" : __props.emptySearchResultsText,
1691
+ description: query.value.trim() === "" ? "There are no items to choose from." : "Try a different search."
1692
+ }, {
1693
+ icon: withCtx(() => [createVNode(unref(Icon), {
1694
+ icon: query.value.trim() === "" ? unref(ListBulletIcon) : "search",
1695
+ size: "lg",
1696
+ color: "secondary"
1697
+ }, null, 8, ["icon"])]),
1698
+ _: 1
1699
+ }, 8, ["title", "description"])) : (openBlock(true), createElementBlock(Fragment, { key: 2 }, renderList(unref(renderedItems), (item) => {
1700
+ return openBlock(), createElementBlock("button", {
1701
+ key: item.id,
1702
+ type: "button",
1703
+ role: "checkbox",
1704
+ class: "astryx-mobile-tokenizer__row",
1705
+ "aria-checked": selectedIds.value.has(item.id),
1706
+ "aria-label": item.label,
1707
+ disabled: !selectedIds.value.has(item.id) && isAtMax.value,
1708
+ onClick: ($event) => selectedIds.value.has(item.id) ? handleRemove(item) : handleAdd(item)
1709
+ }, [createElementVNode("span", _hoisted_9$1, [createVNode(unref(Text), { type: "body" }, {
1710
+ default: withCtx(() => [createTextVNode(toDisplayString(item.label), 1)]),
1711
+ _: 2
1712
+ }, 1024)]), createVNode(unref(CheckboxIndicator), {
1713
+ state: selectedIds.value.has(item.id) ? "checked" : "unchecked",
1714
+ size: "md",
1715
+ "is-disabled": !selectedIds.value.has(item.id) && isAtMax.value
1716
+ }, null, 8, ["state", "is-disabled"])], 8, _hoisted_8$1);
1717
+ }), 128))], 40, _hoisted_5$3),
1718
+ query.value.trim() === "" ? (openBlock(), createElementBlock("div", _hoisted_10$1, [createVNode(unref(Button), {
1719
+ class: "astryx-mobile-tokenizer__footer-action",
1720
+ label: "Clear all",
1721
+ variant: "secondary",
1722
+ size: "lg",
1723
+ "is-disabled": selected.value.length === 0,
1724
+ onClick: _cache[1] || (_cache[1] = ($event) => isClearConfirmationOpen.value = true)
1725
+ }, null, 8, ["is-disabled"]), createVNode(unref(Button), {
1726
+ class: "astryx-mobile-tokenizer__footer-action",
1727
+ label: "Done",
1728
+ variant: "primary",
1729
+ size: "lg",
1730
+ onClick: handleDone
1731
+ })])) : createCommentVNode("v-if", true),
1732
+ createVNode(unref(AlertDialog), {
1733
+ "is-open": isClearConfirmationOpen.value,
1734
+ title: "Clear all selected items?",
1735
+ description: `This removes all selected items from ${__props.label}.`,
1736
+ "action-label": "Clear selection",
1737
+ onOpenChange: _cache[2] || (_cache[2] = ($event) => isClearConfirmationOpen.value = $event),
1738
+ onAction: confirmClear
1739
+ }, null, 8, ["is-open", "description"])
1740
+ ])]),
1741
+ _: 1
1742
+ }, 8, ["is-open", "label"])) : createCommentVNode("v-if", true)]),
1743
+ _: 1
1744
+ }, 8, [
1745
+ "label",
1746
+ "description",
1747
+ "is-optional",
1748
+ "is-disabled"
1749
+ ]);
1750
+ };
1751
+ }
1752
+ });
1753
+ //#endregion
1754
+ //#region src/Stat/types.ts
1755
+ /** Default direction-to-sentiment mapping (up=good, down=bad). */
1756
+ const DIRECTION_SENTIMENT = {
1757
+ up: "positive",
1758
+ down: "negative",
1759
+ flat: "neutral"
1760
+ };
1761
+ /** Screen-reader text announced after the delta value. */
1762
+ const DIRECTION_TEXT = {
1763
+ up: "trending up",
1764
+ down: "trending down",
1765
+ flat: "flat"
1766
+ };
1767
+ /** Inline glyph paths (12x12 viewBox): up-right arrow, down-right arrow, dash. */
1768
+ const DELTA_GLYPH_PATHS = {
1769
+ up: "M3.5 8.5L8.5 3.5M8.5 3.5H4.75M8.5 3.5V7.25",
1770
+ down: "M3.5 3.5L8.5 8.5M8.5 8.5H4.75M8.5 8.5V4.75",
1771
+ flat: "M2.5 6H9.5"
1772
+ };
1773
+ //#endregion
1774
+ //#region src/Stat/Stat.vue
1775
+ const _hoisted_1$4 = ["data-size"];
1776
+ const _hoisted_2$4 = { class: "astryx-stat__label" };
1777
+ const _hoisted_3$4 = { class: "astryx-stat__value-row" };
1778
+ const _hoisted_4$3 = { class: "astryx-stat__value" };
1779
+ const _hoisted_5$2 = ["data-sentiment"];
1780
+ const _hoisted_6$2 = {
1781
+ class: "astryx-stat__delta-glyph",
1782
+ "aria-hidden": "true",
1783
+ viewBox: "0 0 12 12",
1784
+ width: "12",
1785
+ height: "12",
1786
+ fill: "none",
1787
+ stroke: "currentColor",
1788
+ "stroke-width": 1.5,
1789
+ "stroke-linecap": "round",
1790
+ "stroke-linejoin": "round"
1791
+ };
1792
+ const _hoisted_7 = ["d"];
1793
+ const _hoisted_8 = { class: "astryx-stat__sr-only" };
1794
+ const _hoisted_9 = {
1795
+ key: 0,
1796
+ class: "astryx-stat__description"
1797
+ };
1798
+ const _hoisted_10 = {
1799
+ key: 1,
1800
+ class: "astryx-stat__media"
1801
+ };
1802
+ const _sfc_main$6 = /*@__PURE__*/ defineComponent({
1803
+ name: "AstryxStat",
1804
+ inheritAttrs: false,
1805
+ __name: "Stat",
1806
+ props: {
1807
+ label: {},
1808
+ value: { default: void 0 },
1809
+ delta: { default: void 0 },
1810
+ description: { default: void 0 },
1811
+ size: { default: "md" }
1812
+ },
1813
+ setup(__props) {
1814
+ const props = __props;
1815
+ const attrs = useAttrs();
1816
+ const sentiment = computed(() => {
1817
+ if (props.delta == null) return null;
1818
+ return props.delta.sentiment ?? DIRECTION_SENTIMENT[props.delta.direction];
1819
+ });
1820
+ return (_ctx, _cache) => {
1821
+ return openBlock(), createElementBlock("div", mergeProps(unref(attrs), {
1822
+ class: ["astryx-stat", unref(attrs).class],
1823
+ "data-size": __props.size
1824
+ }), [
1825
+ createElementVNode("span", _hoisted_2$4, toDisplayString(__props.label), 1),
1826
+ createElementVNode("span", _hoisted_3$4, [createElementVNode("span", _hoisted_4$3, [renderSlot(_ctx.$slots, "default", {}, () => [createTextVNode(toDisplayString(__props.value), 1)])]), __props.delta != null && sentiment.value != null ? (openBlock(), createElementBlock("span", {
1827
+ key: 0,
1828
+ class: "astryx-stat__delta",
1829
+ "data-sentiment": sentiment.value
1830
+ }, [
1831
+ (openBlock(), createElementBlock("svg", _hoisted_6$2, [createElementVNode("path", { d: unref(DELTA_GLYPH_PATHS)[__props.delta.direction] }, null, 8, _hoisted_7)])),
1832
+ createTextVNode(" " + toDisplayString(__props.delta.value) + " ", 1),
1833
+ createElementVNode("span", _hoisted_8, "(" + toDisplayString(unref(DIRECTION_TEXT)[__props.delta.direction]) + ")", 1)
1834
+ ], 8, _hoisted_5$2)) : createCommentVNode("v-if", true)]),
1835
+ __props.description != null ? (openBlock(), createElementBlock("span", _hoisted_9, toDisplayString(__props.description), 1)) : createCommentVNode("v-if", true),
1836
+ _ctx.$slots.media ? (openBlock(), createElementBlock("div", _hoisted_10, [renderSlot(_ctx.$slots, "media")])) : createCommentVNode("v-if", true)
1837
+ ], 16, _hoisted_1$4);
1838
+ };
1839
+ }
1840
+ });
1841
+ //#endregion
1842
+ //#region src/SVGIcon/icons.ts
1843
+ /** Simple stroke-only: X / Close — both lines are stroke-role */
1844
+ const xIcon = {
1845
+ name: "X",
1846
+ primary: [{
1847
+ type: "line",
1848
+ attrs: {
1849
+ x1: "18",
1850
+ y1: "6",
1851
+ x2: "6",
1852
+ y2: "18"
1853
+ },
1854
+ role: "stroke"
1855
+ }, {
1856
+ type: "line",
1857
+ attrs: {
1858
+ x1: "6",
1859
+ y1: "6",
1860
+ x2: "18",
1861
+ y2: "18"
1862
+ },
1863
+ role: "stroke"
1864
+ }]
1865
+ };
1866
+ /** Simple stroke-only: Check */
1867
+ const checkIcon = {
1868
+ name: "Check",
1869
+ primary: [{
1870
+ type: "path",
1871
+ attrs: { d: "M20 6 9 17l-5-5" },
1872
+ role: "stroke"
1873
+ }]
1874
+ };
1875
+ /** Two-layer with mixed roles: Bell body is fill, clapper is stroke */
1876
+ const bellIcon = {
1877
+ name: "Bell",
1878
+ primary: [{
1879
+ type: "path",
1880
+ attrs: { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" },
1881
+ role: "fill"
1882
+ }],
1883
+ secondary: [{
1884
+ type: "path",
1885
+ attrs: { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" },
1886
+ role: "stroke"
1887
+ }]
1888
+ };
1889
+ /**
1890
+ * Two-layer: Home — house outline is fill, door is fill (secondary).
1891
+ * In bold mode the door gets masked out of the house body.
1892
+ */
1893
+ const homeIcon = {
1894
+ name: "Home",
1895
+ primary: [{
1896
+ type: "path",
1897
+ attrs: { d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" },
1898
+ role: "fill"
1899
+ }],
1900
+ secondary: [{
1901
+ type: "path",
1902
+ attrs: { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" },
1903
+ role: "fill"
1904
+ }]
1905
+ };
1906
+ /** Complex: Settings — gear body is fill, center circle is fill (secondary, masked in bold) */
1907
+ const settingsIcon = {
1908
+ name: "Settings",
1909
+ primary: [{
1910
+ type: "path",
1911
+ attrs: { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" },
1912
+ role: "fill"
1913
+ }],
1914
+ secondary: [{
1915
+ type: "circle",
1916
+ attrs: {
1917
+ cx: "12",
1918
+ cy: "12",
1919
+ r: "3"
1920
+ },
1921
+ role: "fill"
1922
+ }]
1923
+ };
1924
+ /** Mixed roles: Calendar frame is fill, pegs are stroke, divider line is stroke */
1925
+ const calendarIcon = {
1926
+ name: "Calendar",
1927
+ primary: [
1928
+ {
1929
+ type: "rect",
1930
+ attrs: {
1931
+ width: "18",
1932
+ height: "18",
1933
+ x: "3",
1934
+ y: "4",
1935
+ rx: "2"
1936
+ },
1937
+ role: "fill"
1938
+ },
1939
+ {
1940
+ type: "line",
1941
+ attrs: {
1942
+ x1: "16",
1943
+ y1: "2",
1944
+ x2: "16",
1945
+ y2: "6"
1946
+ },
1947
+ role: "stroke"
1948
+ },
1949
+ {
1950
+ type: "line",
1951
+ attrs: {
1952
+ x1: "8",
1953
+ y1: "2",
1954
+ x2: "8",
1955
+ y2: "6"
1956
+ },
1957
+ role: "stroke"
1958
+ }
1959
+ ],
1960
+ secondary: [{
1961
+ type: "line",
1962
+ attrs: {
1963
+ x1: "3",
1964
+ y1: "10",
1965
+ x2: "21",
1966
+ y2: "10"
1967
+ },
1968
+ role: "stroke"
1969
+ }]
1970
+ };
1971
+ /** Stroke-only: Menu / Hamburger — all lines are stroke-role */
1972
+ const menuIcon = {
1973
+ name: "Menu",
1974
+ primary: [
1975
+ {
1976
+ type: "line",
1977
+ attrs: {
1978
+ x1: "4",
1979
+ y1: "6",
1980
+ x2: "20",
1981
+ y2: "6"
1982
+ },
1983
+ role: "stroke"
1984
+ },
1985
+ {
1986
+ type: "line",
1987
+ attrs: {
1988
+ x1: "4",
1989
+ y1: "12",
1990
+ x2: "20",
1991
+ y2: "12"
1992
+ },
1993
+ role: "stroke"
1994
+ },
1995
+ {
1996
+ type: "line",
1997
+ attrs: {
1998
+ x1: "4",
1999
+ y1: "18",
2000
+ x2: "20",
2001
+ y2: "18"
2002
+ },
2003
+ role: "stroke"
2004
+ }
2005
+ ]
2006
+ };
2007
+ /** Single curved path: Heart */
2008
+ const heartIcon = {
2009
+ name: "Heart",
2010
+ primary: [{
2011
+ type: "path",
2012
+ attrs: { d: "M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" },
2013
+ role: "fill"
2014
+ }]
2015
+ };
2016
+ /** Nested shapes: Eye — outer shape (fill) + pupil (fill, secondary) */
2017
+ const eyeIcon = {
2018
+ name: "Eye",
2019
+ primary: [{
2020
+ type: "path",
2021
+ attrs: { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" },
2022
+ role: "fill"
2023
+ }],
2024
+ secondary: [{
2025
+ type: "circle",
2026
+ attrs: {
2027
+ cx: "12",
2028
+ cy: "12",
2029
+ r: "3"
2030
+ },
2031
+ role: "fill"
2032
+ }]
2033
+ };
2034
+ /** Complex single path: Star */
2035
+ const starIcon = {
2036
+ name: "Star",
2037
+ primary: [{
2038
+ type: "path",
2039
+ attrs: { d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" },
2040
+ role: "fill"
2041
+ }]
2042
+ };
2043
+ /** Single panel: Folder */
2044
+ const folderIcon = {
2045
+ name: "Folder",
2046
+ primary: [{
2047
+ type: "path",
2048
+ attrs: { d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" },
2049
+ role: "fill"
2050
+ }]
2051
+ };
2052
+ /** Single path: Shield */
2053
+ const shieldIcon = {
2054
+ name: "Shield",
2055
+ primary: [{
2056
+ type: "path",
2057
+ attrs: { d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" },
2058
+ role: "fill"
2059
+ }]
2060
+ };
2061
+ /** Mixed: Search — glass (fill) + handle (stroke, secondary) */
2062
+ const searchIcon = {
2063
+ name: "Search",
2064
+ primary: [{
2065
+ type: "circle",
2066
+ attrs: {
2067
+ cx: "11",
2068
+ cy: "11",
2069
+ r: "8"
2070
+ },
2071
+ role: "stroke"
2072
+ }, {
2073
+ type: "path",
2074
+ attrs: { d: "m21 21-4.34-4.34" },
2075
+ role: "stroke"
2076
+ }]
2077
+ };
2078
+ /** Mixed: Mail — body (fill) + flap line (stroke, secondary) */
2079
+ const mailIcon = {
2080
+ name: "Mail",
2081
+ primary: [{
2082
+ type: "rect",
2083
+ attrs: {
2084
+ x: "2",
2085
+ y: "4",
2086
+ width: "20",
2087
+ height: "16",
2088
+ rx: "2"
2089
+ },
2090
+ role: "fill"
2091
+ }],
2092
+ secondary: [{
2093
+ type: "path",
2094
+ attrs: { d: "m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" },
2095
+ role: "stroke"
2096
+ }]
2097
+ };
2098
+ /** Mixed: Lock — body (fill) + shackle (stroke) */
2099
+ const lockIcon = {
2100
+ name: "Lock",
2101
+ primary: [{
2102
+ type: "rect",
2103
+ attrs: {
2104
+ width: "18",
2105
+ height: "11",
2106
+ x: "3",
2107
+ y: "11",
2108
+ rx: "2"
2109
+ },
2110
+ role: "fill"
2111
+ }, {
2112
+ type: "path",
2113
+ attrs: { d: "M7 11V7a5 5 0 0 1 10 0v4" },
2114
+ role: "stroke"
2115
+ }]
2116
+ };
2117
+ /** All starter icons for convenience */
2118
+ const starterIcons = [
2119
+ xIcon,
2120
+ checkIcon,
2121
+ bellIcon,
2122
+ homeIcon,
2123
+ settingsIcon,
2124
+ calendarIcon,
2125
+ menuIcon,
2126
+ heartIcon,
2127
+ eyeIcon,
2128
+ starIcon,
2129
+ folderIcon,
2130
+ shieldIcon,
2131
+ searchIcon,
2132
+ mailIcon,
2133
+ lockIcon
2134
+ ];
2135
+ //#endregion
2136
+ //#region src/SVGIcon/pathTransforms/parse.ts
2137
+ /**
2138
+ * Parse an SVG path d-string into an array of absolute commands.
2139
+ * Normalizes H/V → L for uniform processing.
2140
+ */
2141
+ function parsePath(d) {
2142
+ const commands = [];
2143
+ const re = /([MLHVCSQTAZ])([^MLHVCSQTAZ]*)/gi;
2144
+ let cx = 0;
2145
+ let cy = 0;
2146
+ let sx = 0;
2147
+ let sy = 0;
2148
+ for (let match = re.exec(d); match !== null; match = re.exec(d)) {
2149
+ const cmd = match[1];
2150
+ const args = (match[2].match(/-?[\d.]+(?:e[+-]?\d+)?/gi) || []).map(Number);
2151
+ switch (cmd) {
2152
+ case "M":
2153
+ cx = args[0];
2154
+ cy = args[1];
2155
+ sx = cx;
2156
+ sy = cy;
2157
+ commands.push({
2158
+ type: "M",
2159
+ x: cx,
2160
+ y: cy
2161
+ });
2162
+ for (let i = 2; i < args.length; i += 2) {
2163
+ cx = args[i];
2164
+ cy = args[i + 1];
2165
+ commands.push({
2166
+ type: "L",
2167
+ x: cx,
2168
+ y: cy
2169
+ });
2170
+ }
2171
+ break;
2172
+ case "m":
2173
+ cx += args[0];
2174
+ cy += args[1];
2175
+ sx = cx;
2176
+ sy = cy;
2177
+ commands.push({
2178
+ type: "M",
2179
+ x: cx,
2180
+ y: cy
2181
+ });
2182
+ for (let i = 2; i < args.length; i += 2) {
2183
+ cx += args[i];
2184
+ cy += args[i + 1];
2185
+ commands.push({
2186
+ type: "L",
2187
+ x: cx,
2188
+ y: cy
2189
+ });
2190
+ }
2191
+ break;
2192
+ case "L":
2193
+ for (let i = 0; i < args.length; i += 2) {
2194
+ cx = args[i];
2195
+ cy = args[i + 1];
2196
+ commands.push({
2197
+ type: "L",
2198
+ x: cx,
2199
+ y: cy
2200
+ });
2201
+ }
2202
+ break;
2203
+ case "l":
2204
+ for (let i = 0; i < args.length; i += 2) {
2205
+ cx += args[i];
2206
+ cy += args[i + 1];
2207
+ commands.push({
2208
+ type: "L",
2209
+ x: cx,
2210
+ y: cy
2211
+ });
2212
+ }
2213
+ break;
2214
+ case "H":
2215
+ for (let i = 0; i < args.length; i++) {
2216
+ cx = args[i];
2217
+ commands.push({
2218
+ type: "L",
2219
+ x: cx,
2220
+ y: cy
2221
+ });
2222
+ }
2223
+ break;
2224
+ case "h":
2225
+ for (let i = 0; i < args.length; i++) {
2226
+ cx += args[i];
2227
+ commands.push({
2228
+ type: "L",
2229
+ x: cx,
2230
+ y: cy
2231
+ });
2232
+ }
2233
+ break;
2234
+ case "V":
2235
+ for (let i = 0; i < args.length; i++) {
2236
+ cy = args[i];
2237
+ commands.push({
2238
+ type: "L",
2239
+ x: cx,
2240
+ y: cy
2241
+ });
2242
+ }
2243
+ break;
2244
+ case "v":
2245
+ for (let i = 0; i < args.length; i++) {
2246
+ cy += args[i];
2247
+ commands.push({
2248
+ type: "L",
2249
+ x: cx,
2250
+ y: cy
2251
+ });
2252
+ }
2253
+ break;
2254
+ case "C":
2255
+ for (let i = 0; i < args.length; i += 6) {
2256
+ commands.push({
2257
+ type: "C",
2258
+ x1: args[i],
2259
+ y1: args[i + 1],
2260
+ x2: args[i + 2],
2261
+ y2: args[i + 3],
2262
+ x: args[i + 4],
2263
+ y: args[i + 5]
2264
+ });
2265
+ cx = args[i + 4];
2266
+ cy = args[i + 5];
2267
+ }
2268
+ break;
2269
+ case "c":
2270
+ for (let i = 0; i < args.length; i += 6) {
2271
+ commands.push({
2272
+ type: "C",
2273
+ x1: cx + args[i],
2274
+ y1: cy + args[i + 1],
2275
+ x2: cx + args[i + 2],
2276
+ y2: cy + args[i + 3],
2277
+ x: cx + args[i + 4],
2278
+ y: cy + args[i + 5]
2279
+ });
2280
+ cx += args[i + 4];
2281
+ cy += args[i + 5];
2282
+ }
2283
+ break;
2284
+ case "Q":
2285
+ for (let i = 0; i < args.length; i += 4) {
2286
+ commands.push({
2287
+ type: "Q",
2288
+ cx: args[i],
2289
+ cy: args[i + 1],
2290
+ x: args[i + 2],
2291
+ y: args[i + 3]
2292
+ });
2293
+ cx = args[i + 2];
2294
+ cy = args[i + 3];
2295
+ }
2296
+ break;
2297
+ case "q":
2298
+ for (let i = 0; i < args.length; i += 4) {
2299
+ commands.push({
2300
+ type: "Q",
2301
+ cx: cx + args[i],
2302
+ cy: cy + args[i + 1],
2303
+ x: cx + args[i + 2],
2304
+ y: cy + args[i + 3]
2305
+ });
2306
+ cx += args[i + 2];
2307
+ cy += args[i + 3];
2308
+ }
2309
+ break;
2310
+ case "A":
2311
+ case "a": {
2312
+ const rel = cmd === "a";
2313
+ for (let i = 0; i < args.length; i += 7) {
2314
+ const nx = rel ? cx + args[i + 5] : args[i + 5];
2315
+ const ny = rel ? cy + args[i + 6] : args[i + 6];
2316
+ commands.push({
2317
+ type: "A",
2318
+ rx: args[i],
2319
+ ry: args[i + 1],
2320
+ rotation: args[i + 2],
2321
+ large: args[i + 3],
2322
+ sweep: args[i + 4],
2323
+ x: nx,
2324
+ y: ny
2325
+ });
2326
+ cx = nx;
2327
+ cy = ny;
2328
+ }
2329
+ break;
2330
+ }
2331
+ case "S":
2332
+ case "s": break;
2333
+ case "T":
2334
+ case "t": break;
2335
+ case "Z":
2336
+ case "z":
2337
+ commands.push({ type: "Z" });
2338
+ cx = sx;
2339
+ cy = sy;
2340
+ }
2341
+ }
2342
+ return commands;
2343
+ }
2344
+ function dist(a, b) {
2345
+ return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
2346
+ }
2347
+ function lerp(a, b, t) {
2348
+ return {
2349
+ x: a.x + (b.x - a.x) * t,
2350
+ y: a.y + (b.y - a.y) * t
2351
+ };
2352
+ }
2353
+ function angleBetween(v1, v2) {
2354
+ const dot = v1.x * v2.x + v1.y * v2.y;
2355
+ const mag1 = Math.sqrt(v1.x ** 2 + v1.y ** 2);
2356
+ const mag2 = Math.sqrt(v2.x ** 2 + v2.y ** 2);
2357
+ if (mag1 === 0 || mag2 === 0) return Math.PI;
2358
+ return Math.acos(Math.max(-1, Math.min(1, dot / (mag1 * mag2))));
2359
+ }
2360
+ function getEndpoint(cmd) {
2361
+ if (cmd.type === "Z") return null;
2362
+ return {
2363
+ x: cmd.x,
2364
+ y: cmd.y
2365
+ };
2366
+ }
2367
+ function n(v) {
2368
+ return Number(v.toFixed(3)).toString();
2369
+ }
2370
+ function serializePath(cmds) {
2371
+ return cmds.map((cmd) => {
2372
+ switch (cmd.type) {
2373
+ case "M": return `M${n(cmd.x)} ${n(cmd.y)}`;
2374
+ case "L": return `L${n(cmd.x)} ${n(cmd.y)}`;
2375
+ case "C": return `C${n(cmd.x1)} ${n(cmd.y1)} ${n(cmd.x2)} ${n(cmd.y2)} ${n(cmd.x)} ${n(cmd.y)}`;
2376
+ case "Q": return `Q${n(cmd.cx)} ${n(cmd.cy)} ${n(cmd.x)} ${n(cmd.y)}`;
2377
+ case "A": return `A${n(cmd.rx)} ${n(cmd.ry)} ${n(cmd.rotation)} ${cmd.large} ${cmd.sweep} ${n(cmd.x)} ${n(cmd.y)}`;
2378
+ case "Z": return "Z";
2379
+ default: return "";
2380
+ }
2381
+ }).join(" ");
2382
+ }
2383
+ //#endregion
2384
+ //#region src/SVGIcon/pathTransforms/transforms.ts
2385
+ /**
2386
+ * Round corners at L→L junctions by inserting quadratic bezier curves.
2387
+ *
2388
+ * @param d - SVG path d-string
2389
+ * @param cornerRounding - 0-1 percentile. 0 = sharp (no change). 1 = max rounding.
2390
+ * @returns Modified d-string with rounded corners
2391
+ */
2392
+ function roundCorners(d, cornerRounding) {
2393
+ if (cornerRounding <= 0) return d;
2394
+ cornerRounding = Math.min(1, cornerRounding);
2395
+ const cmds = parsePath(d);
2396
+ const result = [];
2397
+ for (let i = 0; i < cmds.length; i++) {
2398
+ const prev = i > 0 ? cmds[i - 1] : null;
2399
+ const curr = cmds[i];
2400
+ const next = i < cmds.length - 1 ? cmds[i + 1] : null;
2401
+ if (curr.type === "Z" && prev && (prev.type === "L" || prev.type === "Q")) {
2402
+ let mIdx = i - 1;
2403
+ while (mIdx >= 0 && cmds[mIdx].type !== "M") mIdx--;
2404
+ if (mIdx >= 0) {
2405
+ const mCmd = cmds[mIdx];
2406
+ const mPt = getEndpoint(mCmd);
2407
+ const prevPt = getEndpoint(prev);
2408
+ if (!mPt || !prevPt) {
2409
+ result.push(curr);
2410
+ continue;
2411
+ }
2412
+ let firstL = mIdx + 1;
2413
+ while (firstL < cmds.length && cmds[firstL].type !== "L") firstL++;
2414
+ if (firstL < cmds.length) {
2415
+ const firstLPt = getEndpoint(cmds[firstL]);
2416
+ if (!firstLPt) {
2417
+ result.push(curr);
2418
+ continue;
2419
+ }
2420
+ const d1 = dist(prevPt, mPt);
2421
+ const d2 = dist(mPt, firstLPt);
2422
+ const maxR = Math.min(d1, d2) / 2;
2423
+ const sharp = 1 - angleBetween({
2424
+ x: prevPt.x - mPt.x,
2425
+ y: prevPt.y - mPt.y
2426
+ }, {
2427
+ x: firstLPt.x - mPt.x,
2428
+ y: firstLPt.y - mPt.y
2429
+ }) / Math.PI;
2430
+ const r = maxR * cornerRounding * sharp * .85;
2431
+ if (r >= .1) {
2432
+ const t1 = r / d1;
2433
+ const t2 = r / d2;
2434
+ const pb1 = lerp(mPt, prevPt, t1);
2435
+ const pb2 = lerp(mPt, firstLPt, t2);
2436
+ result.push({
2437
+ type: "L",
2438
+ x: pb1.x,
2439
+ y: pb1.y
2440
+ });
2441
+ result.push({
2442
+ type: "Q",
2443
+ cx: mPt.x,
2444
+ cy: mPt.y,
2445
+ x: pb2.x,
2446
+ y: pb2.y
2447
+ });
2448
+ result.push({ type: "Z" });
2449
+ continue;
2450
+ }
2451
+ }
2452
+ }
2453
+ result.push(curr);
2454
+ continue;
2455
+ }
2456
+ if (curr.type === "L" && prev && (prev.type === "L" || prev.type === "M" || prev.type === "Q") && next && (next.type === "L" || next.type === "Z")) {
2457
+ const prevPt = getEndpoint(prev);
2458
+ if (!prevPt) {
2459
+ result.push(curr);
2460
+ continue;
2461
+ }
2462
+ const cornerPt = {
2463
+ x: curr.x,
2464
+ y: curr.y
2465
+ };
2466
+ let nextPt;
2467
+ if (next.type === "Z") {
2468
+ const mCmd = cmds.find((c) => c.type === "M");
2469
+ nextPt = mCmd ? getEndpoint(mCmd) : null;
2470
+ } else nextPt = getEndpoint(next);
2471
+ if (!nextPt) {
2472
+ result.push(curr);
2473
+ continue;
2474
+ }
2475
+ const d1 = dist(prevPt, cornerPt);
2476
+ const d2 = dist(cornerPt, nextPt);
2477
+ const maxRadius = Math.min(d1, d2) / 2;
2478
+ const sharpness = 1 - angleBetween({
2479
+ x: prevPt.x - cornerPt.x,
2480
+ y: prevPt.y - cornerPt.y
2481
+ }, {
2482
+ x: nextPt.x - cornerPt.x,
2483
+ y: nextPt.y - cornerPt.y
2484
+ }) / Math.PI;
2485
+ const radius = maxRadius * cornerRounding * sharpness * .85;
2486
+ if (radius < .1) {
2487
+ result.push(curr);
2488
+ continue;
2489
+ }
2490
+ const t1 = radius / d1;
2491
+ const t2 = radius / d2;
2492
+ const pullBack1 = lerp(cornerPt, prevPt, t1);
2493
+ const pullBack2 = lerp(cornerPt, nextPt, t2);
2494
+ result.push({
2495
+ type: "L",
2496
+ x: pullBack1.x,
2497
+ y: pullBack1.y
2498
+ });
2499
+ result.push({
2500
+ type: "Q",
2501
+ cx: cornerPt.x,
2502
+ cy: cornerPt.y,
2503
+ x: pullBack2.x,
2504
+ y: pullBack2.y
2505
+ });
2506
+ } else result.push(curr);
2507
+ }
2508
+ return serializePath(result);
2509
+ }
2510
+ /**
2511
+ * Bow straight line segments by replacing L with Q.
2512
+ *
2513
+ * @param d - SVG path d-string
2514
+ * @param curvature - 0-1 percentile. 0 = straight. 1 = max bow.
2515
+ * @returns Modified d-string with curved segments
2516
+ */
2517
+ function addCurvature(d, curvature) {
2518
+ if (curvature <= 0) return d;
2519
+ curvature = Math.min(1, curvature);
2520
+ const cmds = parsePath(d);
2521
+ const result = [];
2522
+ for (let i = 0; i < cmds.length; i++) {
2523
+ const prev = i > 0 ? cmds[i - 1] : null;
2524
+ const curr = cmds[i];
2525
+ if (curr.type === "L" && prev) {
2526
+ const prevPt = getEndpoint(prev);
2527
+ if (!prevPt) {
2528
+ result.push(curr);
2529
+ continue;
2530
+ }
2531
+ const endPt = {
2532
+ x: curr.x,
2533
+ y: curr.y
2534
+ };
2535
+ const segLen = dist(prevPt, endPt);
2536
+ if (segLen < 2) {
2537
+ result.push(curr);
2538
+ continue;
2539
+ }
2540
+ const mid = lerp(prevPt, endPt, .5);
2541
+ const dx = endPt.x - prevPt.x;
2542
+ const perpX = -(endPt.y - prevPt.y) / segLen;
2543
+ const perpY = dx / segLen;
2544
+ const offset = segLen * curvature * .25;
2545
+ const cp = {
2546
+ x: mid.x + perpX * offset,
2547
+ y: mid.y + perpY * offset
2548
+ };
2549
+ result.push({
2550
+ type: "Q",
2551
+ cx: cp.x,
2552
+ cy: cp.y,
2553
+ x: endPt.x,
2554
+ y: endPt.y
2555
+ });
2556
+ } else result.push(curr);
2557
+ }
2558
+ return serializePath(result);
2559
+ }
2560
+ /**
2561
+ * Adjust tension on existing bezier curves.
2562
+ *
2563
+ * @param d - SVG path d-string
2564
+ * @param tension - 0-1. 0 = loose (curves balloon). 0.5 = unchanged. 1 = tight.
2565
+ * @returns Modified d-string
2566
+ */
2567
+ function adjustTension(d, tension) {
2568
+ if (tension === .5) return d;
2569
+ const cmds = parsePath(d);
2570
+ const result = [];
2571
+ const scale = 1.5 - tension;
2572
+ for (let i = 0; i < cmds.length; i++) {
2573
+ const prev = i > 0 ? cmds[i - 1] : null;
2574
+ const curr = cmds[i];
2575
+ if (curr.type === "C" && prev) {
2576
+ const start = getEndpoint(prev) || {
2577
+ x: 0,
2578
+ y: 0
2579
+ };
2580
+ const end = {
2581
+ x: curr.x,
2582
+ y: curr.y
2583
+ };
2584
+ const cp1 = {
2585
+ x: start.x + (curr.x1 - start.x) * scale,
2586
+ y: start.y + (curr.y1 - start.y) * scale
2587
+ };
2588
+ const cp2 = {
2589
+ x: end.x + (curr.x2 - end.x) * scale,
2590
+ y: end.y + (curr.y2 - end.y) * scale
2591
+ };
2592
+ result.push({
2593
+ type: "C",
2594
+ x1: cp1.x,
2595
+ y1: cp1.y,
2596
+ x2: cp2.x,
2597
+ y2: cp2.y,
2598
+ x: end.x,
2599
+ y: end.y
2600
+ });
2601
+ } else if (curr.type === "Q" && prev) {
2602
+ const start = getEndpoint(prev) || {
2603
+ x: 0,
2604
+ y: 0
2605
+ };
2606
+ const end = {
2607
+ x: curr.x,
2608
+ y: curr.y
2609
+ };
2610
+ const mid = lerp(start, end, .5);
2611
+ const cp = {
2612
+ x: mid.x + (curr.cx - mid.x) * scale,
2613
+ y: mid.y + (curr.cy - mid.y) * scale
2614
+ };
2615
+ result.push({
2616
+ type: "Q",
2617
+ cx: cp.x,
2618
+ cy: cp.y,
2619
+ x: end.x,
2620
+ y: end.y
2621
+ });
2622
+ } else result.push(curr);
2623
+ }
2624
+ return serializePath(result);
2625
+ }
2626
+ /**
2627
+ * Apply all personality transforms to a path d-string.
2628
+ * Order: corner rounding → segment curvature → tension.
2629
+ */
2630
+ function applyPersonality(d, personality) {
2631
+ let result = d;
2632
+ if (personality.cornerRounding != null && personality.cornerRounding > 0) result = roundCorners(result, personality.cornerRounding);
2633
+ if (personality.segmentCurvature != null && personality.segmentCurvature > 0) result = addCurvature(result, personality.segmentCurvature);
2634
+ if (personality.tension != null && personality.tension !== .5) result = adjustTension(result, personality.tension);
2635
+ return result;
2636
+ }
2637
+ //#endregion
2638
+ //#region src/SVGIcon/SVGIcon.vue
2639
+ const _hoisted_1$3 = [
2640
+ "viewBox",
2641
+ "data-variation",
2642
+ "data-size",
2643
+ "data-color"
2644
+ ];
2645
+ const _hoisted_2$3 = { key: 0 };
2646
+ const _hoisted_3$3 = ["mask"];
2647
+ const _hoisted_4$2 = {
2648
+ key: 2,
2649
+ class: "astryx-svg-icon__layer astryx-svg-icon__layer--primary-stroke"
2650
+ };
2651
+ const _hoisted_5$1 = {
2652
+ key: 0,
2653
+ class: "astryx-svg-icon__layer astryx-svg-icon__layer--secondary-fill"
2654
+ };
2655
+ const _hoisted_6$1 = {
2656
+ key: 1,
2657
+ class: "astryx-svg-icon__layer astryx-svg-icon__layer--secondary-stroke"
2658
+ };
2659
+ const _sfc_main$5 = /*@__PURE__*/ defineComponent({
2660
+ name: "AstryxSvgIcon",
2661
+ inheritAttrs: false,
2662
+ __name: "SVGIcon",
2663
+ props: {
2664
+ icon: {},
2665
+ variation: { default: "linear" },
2666
+ size: { default: "md" },
2667
+ color: { default: "primary" },
2668
+ strokeWidth: { default: void 0 }
2669
+ },
2670
+ setup(__props) {
2671
+ const props = __props;
2672
+ const attrs = useAttrs();
2673
+ const viewBox = computed(() => props.icon.viewBox ?? "0 0 24 24");
2674
+ const maskId = `astryx-svg-icon-${useId().replace(/\W/g, "")}-mask`;
2675
+ /** 按角色把一层图形拆成「填充组」和「描边组」。 */
2676
+ function splitByRole(shapes) {
2677
+ const fill = [];
2678
+ const stroke = [];
2679
+ for (const shape of shapes) if (shape.role === "stroke") stroke.push(shape);
2680
+ else fill.push(shape);
2681
+ return {
2682
+ fill,
2683
+ stroke
2684
+ };
2685
+ }
2686
+ const primary = computed(() => splitByRole(props.icon.primary));
2687
+ const secondary = computed(() => {
2688
+ const shapes = props.icon.secondary ?? [];
2689
+ return shapes.length > 0 ? splitByRole(shapes) : {
2690
+ fill: [],
2691
+ stroke: []
2692
+ };
2693
+ });
2694
+ const hasSecondary = computed(() => (props.icon.secondary?.length ?? 0) > 0);
2695
+ const useMask = computed(() => props.variation === "bold" && primary.value.fill.length > 0 && secondary.value.fill.length + secondary.value.stroke.length > 0);
2696
+ const knockoutShapes = computed(() => [...secondary.value.fill, ...secondary.value.stroke]);
2697
+ const a11yAttrs = computed(() => {
2698
+ if (attrs["aria-hidden"] != null || attrs.role != null) return {};
2699
+ return attrs["aria-label"] != null || attrs["aria-labelledby"] != null ? { role: "img" } : { "aria-hidden": "true" };
2700
+ });
2701
+ const mergedAttrs = computed(() => ({
2702
+ ...a11yAttrs.value,
2703
+ ...attrs
2704
+ }));
2705
+ const svgStyle = computed(() => {
2706
+ const style = attrs.style ?? {};
2707
+ if (props.strokeWidth == null) return style;
2708
+ return {
2709
+ ...style,
2710
+ "--astryx-icon-stroke-width": String(props.strokeWidth)
2711
+ };
2712
+ });
2713
+ return (_ctx, _cache) => {
2714
+ return openBlock(), createElementBlock("svg", mergeProps({
2715
+ xmlns: "http://www.w3.org/2000/svg",
2716
+ viewBox: viewBox.value,
2717
+ class: ["astryx-svg-icon", [unref(attrs).class, `astryx-svg-icon--${__props.variation}`]],
2718
+ "data-variation": __props.variation,
2719
+ "data-size": __props.size,
2720
+ "data-color": __props.color
2721
+ }, mergedAttrs.value, { style: svgStyle.value }), [
2722
+ createCommentVNode(" 遮罩:把次层的填充形状从主层里挖出带间隙的洞。 "),
2723
+ useMask.value ? (openBlock(), createElementBlock("defs", _hoisted_2$3, [createElementVNode("mask", { id: maskId }, [_cache[0] || (_cache[0] = createElementVNode("rect", {
2724
+ width: "24",
2725
+ height: "24",
2726
+ fill: "white"
2727
+ }, null, -1)), (openBlock(true), createElementBlock(Fragment, null, renderList(knockoutShapes.value, (shape, index) => {
2728
+ return openBlock(), createBlock(resolveDynamicComponent(shape.type), mergeProps({ key: `knockout-${index}` }, { ref_for: true }, shape.attrs, {
2729
+ fill: (shape.role ?? "fill") === "fill" ? "black" : "none",
2730
+ stroke: "black",
2731
+ "stroke-width": (shape.role ?? "fill") === "stroke" ? "calc(var(--astryx-icon-stroke-width, 1.5) + var(--astryx-icon-gap, 0.75) * 2)" : "var(--astryx-icon-gap, 0.75)",
2732
+ "stroke-linecap": "round",
2733
+ "stroke-linejoin": "var(--astryx-icon-gap-linejoin, round)"
2734
+ }), null, 16, ["fill", "stroke-width"]);
2735
+ }), 128))])])) : createCommentVNode("v-if", true),
2736
+ createCommentVNode(" 主层:填充组(bold 下带遮罩)、描边组。 "),
2737
+ primary.value.fill.length > 0 ? (openBlock(), createElementBlock("g", {
2738
+ key: 1,
2739
+ class: "astryx-svg-icon__layer astryx-svg-icon__layer--primary-fill",
2740
+ mask: useMask.value ? `url(#${maskId})` : void 0
2741
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(primary.value.fill, (shape, index) => {
2742
+ return openBlock(), createBlock(resolveDynamicComponent(shape.type), mergeProps({ key: `primary-fill-${index}` }, { ref_for: true }, shape.attrs), null, 16);
2743
+ }), 128))], 8, _hoisted_3$3)) : createCommentVNode("v-if", true),
2744
+ primary.value.stroke.length > 0 ? (openBlock(), createElementBlock("g", _hoisted_4$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(primary.value.stroke, (shape, index) => {
2745
+ return openBlock(), createBlock(resolveDynamicComponent(shape.type), mergeProps({ key: `primary-stroke-${index}` }, { ref_for: true }, shape.attrs), null, 16);
2746
+ }), 128))])) : createCommentVNode("v-if", true),
2747
+ createCommentVNode(" 次层:bold + 遮罩时整层不画,它只作为挖洞存在。 "),
2748
+ hasSecondary.value && !useMask.value ? (openBlock(), createElementBlock(Fragment, { key: 3 }, [secondary.value.fill.length > 0 ? (openBlock(), createElementBlock("g", _hoisted_5$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(secondary.value.fill, (shape, index) => {
2749
+ return openBlock(), createBlock(resolveDynamicComponent(shape.type), mergeProps({ key: `secondary-fill-${index}` }, { ref_for: true }, shape.attrs), null, 16);
2750
+ }), 128))])) : createCommentVNode("v-if", true), secondary.value.stroke.length > 0 ? (openBlock(), createElementBlock("g", _hoisted_6$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(secondary.value.stroke, (shape, index) => {
2751
+ return openBlock(), createBlock(resolveDynamicComponent(shape.type), mergeProps({ key: `secondary-stroke-${index}` }, { ref_for: true }, shape.attrs), null, 16);
2752
+ }), 128))])) : createCommentVNode("v-if", true)], 64)) : createCommentVNode("v-if", true)
2753
+ ], 16, _hoisted_1$3);
2754
+ };
2755
+ }
2756
+ });
2757
+ //#endregion
2758
+ //#region src/SVGIcon/xif-examples.ts
2759
+ const xifCheck = {
2760
+ name: "check",
2761
+ paths: [{
2762
+ type: "path",
2763
+ attrs: { d: "M5 13l4 4L19 7" },
2764
+ role: "stroke"
2765
+ }],
2766
+ tags: ["action", "confirm"]
2767
+ };
2768
+ const xifHome = {
2769
+ name: "home",
2770
+ paths: [{
2771
+ type: "path",
2772
+ attrs: { d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" },
2773
+ role: "fill",
2774
+ layer: "primary"
2775
+ }, {
2776
+ type: "path",
2777
+ attrs: { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" },
2778
+ role: "fill",
2779
+ layer: "secondary"
2780
+ }],
2781
+ tags: ["navigation"]
2782
+ };
2783
+ const xifFile = {
2784
+ name: "file",
2785
+ paths: [{
2786
+ type: "path",
2787
+ attrs: { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" },
2788
+ role: "fill",
2789
+ layer: "primary"
2790
+ }, {
2791
+ type: "path",
2792
+ attrs: { d: "M14 2v4a2 2 0 0 0 2 2h4" },
2793
+ role: "stroke",
2794
+ layer: "secondary"
2795
+ }],
2796
+ slots: [{
2797
+ name: "badge",
2798
+ position: {
2799
+ x: .5,
2800
+ y: .65
2801
+ },
2802
+ size: .35
2803
+ }],
2804
+ tags: ["file", "document"]
2805
+ };
2806
+ const xifShield = {
2807
+ name: "shield",
2808
+ paths: [{
2809
+ type: "path",
2810
+ attrs: { d: "M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" },
2811
+ role: "fill",
2812
+ layer: "primary"
2813
+ }],
2814
+ slots: [{
2815
+ name: "badge",
2816
+ position: "center",
2817
+ size: .42
2818
+ }],
2819
+ tags: ["security", "protection"]
2820
+ };
2821
+ const xifBell = {
2822
+ name: "bell",
2823
+ paths: [{
2824
+ type: "path",
2825
+ attrs: { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" },
2826
+ role: "fill",
2827
+ layer: "primary",
2828
+ animate: {
2829
+ type: "scale",
2830
+ sequence: 1
2831
+ }
2832
+ }, {
2833
+ type: "path",
2834
+ attrs: { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" },
2835
+ role: "stroke",
2836
+ layer: "secondary",
2837
+ animate: {
2838
+ type: "draw",
2839
+ sequence: 2
2840
+ }
2841
+ }],
2842
+ tags: ["notification", "alert"]
2843
+ };
2844
+ const xifStar = {
2845
+ name: "star",
2846
+ paths: [{
2847
+ type: "path",
2848
+ attrs: { d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" },
2849
+ role: "fill",
2850
+ layer: "primary",
2851
+ personality: { cornerRounding: 0 }
2852
+ }],
2853
+ tags: ["rating", "favorite"]
2854
+ };
2855
+ const xifBellOverride = {
2856
+ name: "bell-override",
2857
+ paths: [{
2858
+ type: "path",
2859
+ attrs: { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" },
2860
+ role: "fill",
2861
+ layer: "primary"
2862
+ }, {
2863
+ type: "path",
2864
+ attrs: { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" },
2865
+ role: "stroke",
2866
+ layer: "secondary"
2867
+ }],
2868
+ overrides: { bold: [{
2869
+ type: "path",
2870
+ attrs: { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" },
2871
+ role: "fill",
2872
+ layer: "primary"
2873
+ }, {
2874
+ type: "path",
2875
+ attrs: { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" },
2876
+ role: "fill",
2877
+ layer: "secondary"
2878
+ }] },
2879
+ tags: ["notification"]
2880
+ };
2881
+ const xifExamples = [
2882
+ xifCheck,
2883
+ xifHome,
2884
+ xifFile,
2885
+ xifShield,
2886
+ xifBell,
2887
+ xifStar,
2888
+ xifBellOverride
2889
+ ];
2890
+ //#endregion
2891
+ //#region src/TransferList/transferItems.ts
2892
+ /** 把 `fromIndex` 那一项挪到 `toIndex`,返回新数组。 */
2893
+ function moveItem(value, fromIndex, toIndex) {
2894
+ const nextValue = [...value];
2895
+ const [item] = nextValue.splice(fromIndex, 1);
2896
+ nextValue.splice(toIndex, 0, item);
2897
+ return nextValue;
2898
+ }
2899
+ /**
2900
+ * 一行是否匹配搜索词。大小写折叠不跟语言走,和 Selector、MultiSelector、
2901
+ * CommandPalette 保持一致。`description` 只有是纯文字时才参与匹配。
2902
+ */
2903
+ function matchesQuery(option, query) {
2904
+ if (query === "") return true;
2905
+ const description = typeof option.description === "string" ? option.description : "";
2906
+ return [
2907
+ option.label,
2908
+ description,
2909
+ option.group ?? ""
2910
+ ].some((part) => part.toLowerCase().includes(query));
2911
+ }
2912
+ /** 按 `group` 把可选条目分段,保持出现顺序;没分组的归到空字符串那一段。 */
2913
+ function groupOptions(options) {
2914
+ const groups = /* @__PURE__ */ new Map();
2915
+ for (const option of options) {
2916
+ const group = option.group ?? "";
2917
+ const items = groups.get(group);
2918
+ if (items) items.push(option);
2919
+ else groups.set(group, [option]);
2920
+ }
2921
+ return [...groups.entries()];
2922
+ }
2923
+ /**
2924
+ * 这一项能挪到的范围。遇到 `isReorderDisabled` 的邻居就停,
2925
+ * 所以被锁住的条目位置不动,别人也跨不过去。
2926
+ */
2927
+ function movableRange(optionByValue, optionValue, orderedValue) {
2928
+ const index = orderedValue.indexOf(optionValue);
2929
+ let start = 0;
2930
+ let end = orderedValue.length - 1;
2931
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
2932
+ const option = optionByValue.get(orderedValue[cursor]);
2933
+ if (option == null || option.isReorderDisabled) {
2934
+ start = cursor + 1;
2935
+ break;
2936
+ }
2937
+ }
2938
+ for (let cursor = index + 1; cursor < orderedValue.length; cursor += 1) {
2939
+ const option = optionByValue.get(orderedValue[cursor]);
2940
+ if (option == null || option.isReorderDisabled) {
2941
+ end = cursor - 1;
2942
+ break;
2943
+ }
2944
+ }
2945
+ return {
2946
+ index,
2947
+ start,
2948
+ end
2949
+ };
2950
+ }
2951
+ //#endregion
2952
+ //#region src/TransferList/TransferListPanel.vue
2953
+ const _hoisted_1$2 = ["data-side", "aria-labelledby"];
2954
+ const _hoisted_2$2 = { class: "astryx-transfer-list-panel__header" };
2955
+ const _hoisted_3$2 = ["disabled"];
2956
+ const _hoisted_4$1 = ["data-transfer-list-panel-body"];
2957
+ const _hoisted_5 = {
2958
+ key: 0,
2959
+ role: "presentation",
2960
+ class: "astryx-transfer-list-panel__group-heading"
2961
+ };
2962
+ const _hoisted_6 = ["data-transfer-list-empty"];
2963
+ const _sfc_main$9 = /*@__PURE__*/ defineComponent({
2964
+ name: "AstryxTransferListPanel",
2965
+ __name: "TransferListPanel",
2966
+ props: {
2967
+ side: {},
2968
+ headingId: {},
2969
+ label: {},
2970
+ showAction: { type: Boolean },
2971
+ actionLabel: {},
2972
+ isActionDisabled: { type: Boolean },
2973
+ groups: {},
2974
+ showGroupHeadings: { type: Boolean },
2975
+ ungroupedLabel: {},
2976
+ emptyText: {},
2977
+ isReorderable: { type: Boolean },
2978
+ draggingValue: {},
2979
+ grabbedValue: {},
2980
+ dropTarget: {},
2981
+ reorderInstructionsId: {},
2982
+ labels: {},
2983
+ handlers: {}
2984
+ },
2985
+ emits: [
2986
+ "action",
2987
+ "add",
2988
+ "remove"
2989
+ ],
2990
+ setup(__props, { emit: __emit }) {
2991
+ const props = __props;
2992
+ const emit = __emit;
2993
+ /** 分组壳子在、但一行都没有时也要走空状态。 */
2994
+ const hasRows = computed(() => props.groups.some((group) => group.options.length > 0));
2995
+ function dropPositionOf(option) {
2996
+ return props.dropTarget?.value === option.value ? props.dropTarget.position : void 0;
2997
+ }
2998
+ function rowState(option) {
2999
+ if (option.isTransferDisabled || option.isReorderDisabled) return "disabled";
3000
+ if (props.draggingValue === option.value || props.grabbedValue === option.value) return "reordering";
3001
+ return "enabled";
3002
+ }
3003
+ return (_ctx, _cache) => {
3004
+ return openBlock(), createElementBlock("div", {
3005
+ class: "astryx-transfer-list-panel",
3006
+ "data-side": __props.side,
3007
+ role: "group",
3008
+ "aria-labelledby": __props.headingId,
3009
+ tabindex: "-1"
3010
+ }, [createElementVNode("div", _hoisted_2$2, [createVNode(unref(Text), {
3011
+ id: __props.headingId,
3012
+ type: "label",
3013
+ color: "secondary"
3014
+ }, {
3015
+ default: withCtx(() => [createTextVNode(toDisplayString(__props.label), 1)]),
3016
+ _: 1
3017
+ }, 8, ["id"]), __props.showAction ? (openBlock(), createElementBlock("button", {
3018
+ key: 0,
3019
+ type: "button",
3020
+ class: "astryx-transfer-list-header-action",
3021
+ "data-transfer-list-header-action": "true",
3022
+ disabled: __props.isActionDisabled,
3023
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("action"))
3024
+ }, toDisplayString(__props.actionLabel), 9, _hoisted_3$2)) : createCommentVNode("v-if", true)]), createElementVNode("div", {
3025
+ class: "astryx-transfer-list-panel__body",
3026
+ "data-transfer-list-panel-body": __props.side
3027
+ }, [hasRows.value ? (openBlock(), createBlock(unref(List), {
3028
+ key: 0,
3029
+ density: "compact"
3030
+ }, {
3031
+ header: withCtx(() => [createVNode(unref(VisuallyHidden), null, {
3032
+ default: withCtx(() => [createTextVNode(toDisplayString(__props.label), 1)]),
3033
+ _: 1
3034
+ })]),
3035
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.groups, (group) => {
3036
+ return openBlock(), createElementBlock(Fragment, { key: group.group || "ungrouped" }, [__props.showGroupHeadings && (group.group !== "" || __props.groups.length > 1) ? (openBlock(), createElementBlock("li", _hoisted_5, [createVNode(unref(Text), {
3037
+ type: "body",
3038
+ weight: "bold",
3039
+ color: "primary"
3040
+ }, {
3041
+ default: withCtx(() => [createTextVNode(toDisplayString(group.group || __props.ungroupedLabel), 1)]),
3042
+ _: 2
3043
+ }, 1024)])) : createCommentVNode("v-if", true), (openBlock(true), createElementBlock(Fragment, null, renderList(group.options, (row) => {
3044
+ return openBlock(), createBlock(unref(Item), {
3045
+ key: row.option.value,
3046
+ as: "li",
3047
+ density: "compact",
3048
+ class: "astryx-transfer-list-item",
3049
+ "data-transfer-list-row": __props.side === "selected" ? row.option.value : void 0,
3050
+ "data-transfer-list-reorder-source": __props.draggingValue === row.option.value || void 0,
3051
+ "data-transfer-list-drop-target": dropPositionOf(row.option),
3052
+ "data-transfer-list-index": __props.side === "selected" ? String(row.index) : void 0,
3053
+ "data-side": __props.side,
3054
+ "data-state": rowState(row.option)
3055
+ }, createSlots({
3056
+ label: withCtx(() => [renderSlot(_ctx.$slots, "option", { option: row.option }, () => [createTextVNode(toDisplayString(row.option.label), 1)])]),
3057
+ endContent: withCtx(() => [__props.side === "available" ? (openBlock(), createBlock(unref(IconButton), {
3058
+ key: 0,
3059
+ class: "astryx-transfer-list-row-action",
3060
+ label: __props.labels.addOption(row.option.label),
3061
+ size: "sm",
3062
+ variant: "ghost",
3063
+ "is-disabled": row.option.isTransferDisabled === true,
3064
+ tooltip: row.option.isTransferDisabled === true ? row.option.disabledMessage ?? __props.labels.transferDisabled(row.option.label) : void 0,
3065
+ "data-transfer-list-action": "available",
3066
+ onClick: ($event) => emit("add", row.option, row.index)
3067
+ }, {
3068
+ default: withCtx(() => [createVNode(unref(Icon), {
3069
+ icon: unref(PlusIcon),
3070
+ size: "sm"
3071
+ }, null, 8, ["icon"])]),
3072
+ _: 1
3073
+ }, 8, [
3074
+ "label",
3075
+ "is-disabled",
3076
+ "tooltip",
3077
+ "onClick"
3078
+ ])) : (openBlock(), createBlock(unref(IconButton), {
3079
+ key: 1,
3080
+ class: "astryx-transfer-list-row-action",
3081
+ label: __props.labels.removeOption(row.option.label),
3082
+ size: "sm",
3083
+ variant: "ghost",
3084
+ "is-disabled": row.option.isTransferDisabled === true,
3085
+ tooltip: row.option.isTransferDisabled === true ? row.option.disabledMessage ?? __props.labels.transferDisabled(row.option.label) : void 0,
3086
+ "data-transfer-list-action": "selected",
3087
+ onClick: ($event) => emit("remove", row.option, row.index)
3088
+ }, {
3089
+ default: withCtx(() => [createVNode(unref(Icon), {
3090
+ icon: "close",
3091
+ size: "sm"
3092
+ })]),
3093
+ _: 1
3094
+ }, 8, [
3095
+ "label",
3096
+ "is-disabled",
3097
+ "tooltip",
3098
+ "onClick"
3099
+ ]))]),
3100
+ _: 2
3101
+ }, [__props.side === "selected" ? {
3102
+ name: "startContent",
3103
+ fn: withCtx(() => [__props.isReorderable ? (openBlock(), createBlock(unref(IconButton), {
3104
+ key: 0,
3105
+ class: "astryx-transfer-list-reorder-handle",
3106
+ label: __props.labels.reorderOption(row.option.label),
3107
+ "aria-describedby": __props.reorderInstructionsId,
3108
+ "aria-pressed": __props.grabbedValue === row.option.value || void 0,
3109
+ "is-disabled": row.option.isReorderDisabled === true,
3110
+ tooltip: row.option.isReorderDisabled === true ? row.option.disabledMessage ?? __props.labels.reorderDisabled(row.option.label) : void 0,
3111
+ size: "sm",
3112
+ variant: "ghost",
3113
+ onClick: ($event) => __props.handlers.click(row.option),
3114
+ onKeydown: ($event) => __props.handlers.keydown($event, row.option),
3115
+ onPointerdown: ($event) => __props.handlers.pointerdown($event, row.option),
3116
+ onPointermove: ($event) => __props.handlers.pointermove($event, row.option),
3117
+ onPointerup: ($event) => __props.handlers.pointerup($event, row.option),
3118
+ onPointercancel: ($event) => __props.handlers.pointercancel($event, row.option),
3119
+ onLostpointercapture: _cache[1] || (_cache[1] = ($event) => __props.handlers.lostpointercapture($event))
3120
+ }, {
3121
+ default: withCtx(() => [createVNode(unref(Icon), {
3122
+ icon: unref(GripVerticalIcon),
3123
+ size: "sm"
3124
+ }, null, 8, ["icon"])]),
3125
+ _: 1
3126
+ }, 8, [
3127
+ "label",
3128
+ "aria-describedby",
3129
+ "aria-pressed",
3130
+ "is-disabled",
3131
+ "tooltip",
3132
+ "onClick",
3133
+ "onKeydown",
3134
+ "onPointerdown",
3135
+ "onPointermove",
3136
+ "onPointerup",
3137
+ "onPointercancel"
3138
+ ])) : createCommentVNode("v-if", true)]),
3139
+ key: "0"
3140
+ } : void 0]), 1032, [
3141
+ "data-transfer-list-row",
3142
+ "data-transfer-list-reorder-source",
3143
+ "data-transfer-list-drop-target",
3144
+ "data-transfer-list-index",
3145
+ "data-side",
3146
+ "data-state"
3147
+ ]);
3148
+ }), 128))], 64);
3149
+ }), 128))]),
3150
+ _: 3
3151
+ })) : (openBlock(), createElementBlock("div", {
3152
+ key: 1,
3153
+ class: "astryx-transfer-list-panel__empty",
3154
+ "data-transfer-list-empty": __props.side
3155
+ }, [createVNode(unref(Text), {
3156
+ type: "supporting",
3157
+ color: "secondary"
3158
+ }, {
3159
+ default: withCtx(() => [createTextVNode(toDisplayString(__props.emptyText), 1)]),
3160
+ _: 1
3161
+ })], 8, _hoisted_6))], 8, _hoisted_4$1)], 8, _hoisted_1$2);
3162
+ };
3163
+ }
3164
+ });
3165
+ //#endregion
3166
+ //#region src/TransferList/useTransferReorder.ts
3167
+ /** 松手前先要跨过这么远的竖直距离,才算真的在拖。 */
3168
+ const POINTER_DRAG_THRESHOLD_PX = 5;
3169
+ /**
3170
+ * 已选栏的重排状态机:键盘拿起/放下、指针拖动、读屏播报,都在这里。
3171
+ *
3172
+ * 键盘走「每一步立刻提交」,指针走「松手才提交」,和上游一致:
3173
+ * 键盘用户按一次方向键就该听到新位置,鼠标用户拖动过程中不该改动数据。
3174
+ */
3175
+ function useTransferReorder(options) {
3176
+ const session = ref(null);
3177
+ const liveValue = ref([...options.currentValue()]);
3178
+ let suppressClick = false;
3179
+ watch(options.currentValue, (next) => {
3180
+ liveValue.value = [...next];
3181
+ });
3182
+ function commit(next) {
3183
+ liveValue.value = [...next];
3184
+ options.commit(next);
3185
+ }
3186
+ function setSession(next) {
3187
+ session.value = next;
3188
+ }
3189
+ function begin(option, mode, geometry) {
3190
+ if (option.isReorderDisabled || !options.isReorderable()) return;
3191
+ const index = liveValue.value.indexOf(option.value);
3192
+ if (index < 0 || mode === "pointer" && geometry == null) return;
3193
+ setSession({
3194
+ value: option.value,
3195
+ label: option.label,
3196
+ mode,
3197
+ originalValue: [...liveValue.value],
3198
+ fromIndex: index,
3199
+ toIndex: index,
3200
+ ...geometry,
3201
+ hasPointerMoved: false
3202
+ });
3203
+ if (mode === "keyboard") options.announce(options.messages.grabbed(option.label, index + 1, liveValue.value.length));
3204
+ }
3205
+ function finish(cancelled) {
3206
+ const current = session.value;
3207
+ if (current == null) return;
3208
+ if (current.mode === "pointer") {
3209
+ if (cancelled || !current.hasPointerMoved) {
3210
+ setSession(null);
3211
+ if (cancelled) options.announce(options.messages.moveCancelled(current.label));
3212
+ return;
3213
+ }
3214
+ const hasChanged = current.fromIndex !== current.toIndex;
3215
+ const nextValue = moveItem(current.originalValue, current.fromIndex, current.toIndex);
3216
+ setSession(null);
3217
+ if (hasChanged) {
3218
+ commit(nextValue);
3219
+ options.announce(options.messages.dropped(current.label, current.toIndex + 1, current.originalValue.length));
3220
+ } else options.announce(options.messages.returned(current.label, current.fromIndex + 1));
3221
+ return;
3222
+ }
3223
+ if (cancelled) {
3224
+ commit(current.originalValue);
3225
+ options.announce(options.messages.moveCancelled(current.label));
3226
+ } else {
3227
+ const index = liveValue.value.indexOf(current.value);
3228
+ options.announce(options.messages.dropped(current.label, index + 1, liveValue.value.length));
3229
+ }
3230
+ setSession(null);
3231
+ }
3232
+ function moveTo(option, requestedIndex) {
3233
+ const nextValue = [...liveValue.value];
3234
+ const { index, start, end } = movableRange(options.optionByValue(), option.value, nextValue);
3235
+ if (index < 0) return false;
3236
+ const target = Math.max(start, Math.min(end, requestedIndex));
3237
+ if (target === index) return false;
3238
+ nextValue.splice(index, 1);
3239
+ nextValue.splice(target, 0, option.value);
3240
+ commit(nextValue);
3241
+ return true;
3242
+ }
3243
+ function onHandleClick(option) {
3244
+ if (suppressClick) {
3245
+ suppressClick = false;
3246
+ return;
3247
+ }
3248
+ if (session.value?.value === option.value) finish(false);
3249
+ else begin(option, "keyboard");
3250
+ }
3251
+ function onHandleKeydown(event, option) {
3252
+ const current = session.value;
3253
+ if (current?.value !== option.value || current.mode !== "keyboard") return;
3254
+ if (event.key === "Escape") {
3255
+ event.preventDefault();
3256
+ finish(true);
3257
+ return;
3258
+ }
3259
+ if (event.key === " " || event.key === "Enter") {
3260
+ event.preventDefault();
3261
+ finish(false);
3262
+ return;
3263
+ }
3264
+ const { index, start, end } = movableRange(options.optionByValue(), option.value, liveValue.value);
3265
+ const target = {
3266
+ ArrowUp: index - 1,
3267
+ ArrowDown: index + 1,
3268
+ Home: start,
3269
+ End: end
3270
+ }[event.key];
3271
+ if (target == null) return;
3272
+ event.preventDefault();
3273
+ if (moveTo(option, target)) {
3274
+ const nextIndex = liveValue.value.indexOf(option.value);
3275
+ options.announce(options.messages.movedToPosition(option.label, nextIndex + 1, liveValue.value.length));
3276
+ }
3277
+ }
3278
+ function onHandlePointerDown(event, option) {
3279
+ if (event.button !== 0 || option.isReorderDisabled) return;
3280
+ const sourceRow = options.rowElement(option.value);
3281
+ if (sourceRow == null) return;
3282
+ const bounds = sourceRow.getBoundingClientRect();
3283
+ suppressClick = true;
3284
+ event.currentTarget?.setPointerCapture?.(event.pointerId);
3285
+ begin(option, "pointer", {
3286
+ pointerId: event.pointerId,
3287
+ pointerStartY: event.clientY,
3288
+ pointerOffsetY: event.clientY - bounds.top,
3289
+ pointerClientY: event.clientY,
3290
+ previewX: bounds.left,
3291
+ previewWidth: bounds.width
3292
+ });
3293
+ }
3294
+ function onHandlePointerMove(event, option) {
3295
+ const current = session.value;
3296
+ if (current?.value !== option.value || current.mode !== "pointer" || current.pointerId !== event.pointerId) return;
3297
+ const verticalDistance = event.clientY - (current.pointerStartY ?? event.clientY);
3298
+ const hasCrossedThreshold = current.hasPointerMoved === true || Math.abs(verticalDistance) >= POINTER_DRAG_THRESHOLD_PX;
3299
+ let targetIndex = current.fromIndex;
3300
+ if (hasCrossedThreshold) {
3301
+ event.preventDefault();
3302
+ const { start, end } = movableRange(options.optionByValue(), current.value, current.originalValue);
3303
+ const remainingValues = current.originalValue.filter((optionValue) => optionValue !== current.value);
3304
+ const candidates = current.originalValue.map((optionValue, originalIndex) => ({
3305
+ optionValue,
3306
+ originalIndex
3307
+ })).filter((candidate) => candidate.optionValue !== current.value && candidate.originalIndex >= start && candidate.originalIndex <= end).map((candidate) => ({
3308
+ row: options.rowElement(candidate.optionValue),
3309
+ targetIndex: remainingValues.indexOf(candidate.optionValue)
3310
+ })).filter((candidate) => candidate.row != null);
3311
+ if (candidates.length > 0) {
3312
+ targetIndex = candidates[candidates.length - 1].targetIndex + 1;
3313
+ for (const candidate of candidates) {
3314
+ const candidateBounds = candidate.row.getBoundingClientRect();
3315
+ if (event.clientY < candidateBounds.top + candidateBounds.height / 2) {
3316
+ targetIndex = candidate.targetIndex;
3317
+ break;
3318
+ }
3319
+ }
3320
+ targetIndex = Math.max(start, Math.min(end, targetIndex));
3321
+ }
3322
+ }
3323
+ setSession({
3324
+ ...current,
3325
+ pointerClientY: event.clientY,
3326
+ hasPointerMoved: hasCrossedThreshold,
3327
+ toIndex: targetIndex
3328
+ });
3329
+ if (hasCrossedThreshold && current.toIndex !== targetIndex) options.announce(options.messages.movedToPosition(option.label, targetIndex + 1, current.originalValue.length));
3330
+ }
3331
+ function onHandlePointerUp(event, option) {
3332
+ const current = session.value;
3333
+ if (current?.mode !== "pointer" || current.pointerId !== event.pointerId) return;
3334
+ if (current.value !== option.value) return;
3335
+ suppressClick = false;
3336
+ finish(false);
3337
+ const target = event.currentTarget;
3338
+ if (target?.hasPointerCapture?.(event.pointerId)) target.releasePointerCapture?.(event.pointerId);
3339
+ }
3340
+ function onHandlePointerCancel(event, option) {
3341
+ const current = session.value;
3342
+ if (current?.mode !== "pointer" || current.pointerId !== event.pointerId) return;
3343
+ if (current.value !== option.value) return;
3344
+ suppressClick = false;
3345
+ finish(true);
3346
+ const target = event.currentTarget;
3347
+ if (target?.hasPointerCapture?.(event.pointerId)) target.releasePointerCapture?.(event.pointerId);
3348
+ }
3349
+ function onHandleLostPointerCapture(event) {
3350
+ const current = session.value;
3351
+ if (current?.mode === "pointer" && current.pointerId === event.pointerId) {
3352
+ suppressClick = false;
3353
+ finish(true);
3354
+ }
3355
+ }
3356
+ const draggingValue = computed(() => session.value?.mode === "pointer" ? session.value.value : null);
3357
+ const grabbedValue = computed(() => session.value?.mode === "keyboard" ? session.value.value : null);
3358
+ const pointerPlacement = computed(() => {
3359
+ const current = session.value;
3360
+ if (current?.mode !== "pointer" || !current.hasPointerMoved || current.toIndex === current.fromIndex) return null;
3361
+ const remainingValues = current.originalValue.filter((optionValue) => optionValue !== current.value);
3362
+ const beforeValue = remainingValues[current.toIndex];
3363
+ if (beforeValue != null) return {
3364
+ value: beforeValue,
3365
+ position: "before"
3366
+ };
3367
+ const afterValue = remainingValues.at(-1);
3368
+ return afterValue == null ? null : {
3369
+ value: afterValue,
3370
+ position: "after"
3371
+ };
3372
+ });
3373
+ const previewStyle = computed(() => {
3374
+ const current = session.value;
3375
+ if (current?.mode !== "pointer" || current.pointerClientY == null || current.pointerOffsetY == null || current.previewX == null || current.previewWidth == null) return null;
3376
+ return {
3377
+ top: `${current.pointerClientY - current.pointerOffsetY}px`,
3378
+ left: `${current.previewX}px`,
3379
+ width: `${current.previewWidth}px`
3380
+ };
3381
+ });
3382
+ watch(() => session.value?.mode === "pointer" ? session.value.value : null, async (cloneValue) => {
3383
+ if (cloneValue == null) return;
3384
+ await nextTick();
3385
+ const host = options.previewHost.value;
3386
+ if (host == null) return;
3387
+ const sourceRow = options.rowElement(cloneValue);
3388
+ if (sourceRow == null) return;
3389
+ const clone = sourceRow.cloneNode(true);
3390
+ host.style.direction = getComputedStyle(sourceRow).direction;
3391
+ clone.style.opacity = "1";
3392
+ clone.setAttribute("aria-hidden", "true");
3393
+ for (const element of [clone, ...clone.querySelectorAll("*")]) {
3394
+ element.removeAttribute("id");
3395
+ element.removeAttribute("aria-describedby");
3396
+ element.removeAttribute("aria-labelledby");
3397
+ element.removeAttribute("name");
3398
+ for (const attribute of [...element.attributes]) if (attribute.name.startsWith("data-transfer-list-")) element.removeAttribute(attribute.name);
3399
+ element.setAttribute("tabindex", "-1");
3400
+ }
3401
+ host.replaceChildren(clone);
3402
+ });
3403
+ watch(() => session.value?.mode === "pointer" ? session.value.value : null, (cloneValue) => {
3404
+ if (cloneValue != null) return;
3405
+ options.previewHost.value?.replaceChildren();
3406
+ });
3407
+ return {
3408
+ session,
3409
+ draggingValue,
3410
+ grabbedValue,
3411
+ pointerPlacement,
3412
+ previewStyle,
3413
+ onHandleClick,
3414
+ onHandleKeydown,
3415
+ onHandlePointerDown,
3416
+ onHandlePointerMove,
3417
+ onHandlePointerUp,
3418
+ onHandlePointerCancel,
3419
+ onHandleLostPointerCapture
3420
+ };
3421
+ }
3422
+ //#endregion
3423
+ //#region src/TransferList/TransferList.vue
3424
+ const _hoisted_1$1 = [
3425
+ "role",
3426
+ "aria-labelledby",
3427
+ "aria-describedby"
3428
+ ];
3429
+ const _hoisted_2$1 = { class: "astryx-transfer-list__heading" };
3430
+ const _hoisted_3$1 = {
3431
+ key: 0,
3432
+ class: "astryx-transfer-list__controls"
3433
+ };
3434
+ const _hoisted_4 = { class: "astryx-transfer-list-collection" };
3435
+ const _sfc_main$7 = /*@__PURE__*/ defineComponent({
3436
+ name: "AstryxTransferList",
3437
+ inheritAttrs: false,
3438
+ __name: "TransferList",
3439
+ props: {
3440
+ label: {},
3441
+ isLabelHidden: {
3442
+ type: Boolean,
3443
+ default: false
3444
+ },
3445
+ description: {},
3446
+ options: {},
3447
+ modelValue: { default: () => [] },
3448
+ selectedLabel: {},
3449
+ availableLabel: {},
3450
+ hasSearch: {
3451
+ type: Boolean,
3452
+ default: false
3453
+ },
3454
+ searchLabel: {},
3455
+ searchPlaceholder: {},
3456
+ isReorderable: {
3457
+ type: Boolean,
3458
+ default: true
3459
+ },
3460
+ hasSelectAll: {
3461
+ type: Boolean,
3462
+ default: false
3463
+ },
3464
+ hasClear: {
3465
+ type: Boolean,
3466
+ default: false
3467
+ },
3468
+ selectedEmptyText: {},
3469
+ availableEmptyText: {},
3470
+ noResultsText: {}
3471
+ },
3472
+ emits: ["update:modelValue", "change"],
3473
+ setup(__props, { emit: __emit }) {
3474
+ const props = __props;
3475
+ const emit = __emit;
3476
+ const attrs = useAttrs();
3477
+ const { t } = useTranslator();
3478
+ const announce = useAnnounce();
3479
+ const labelId = `astryx-transfer-list-label-${useId()}`;
3480
+ const descriptionId = `astryx-transfer-list-description-${useId()}`;
3481
+ const selectedHeadingId = `astryx-transfer-list-selected-${useId()}`;
3482
+ const availableHeadingId = `astryx-transfer-list-available-${useId()}`;
3483
+ const reorderInstructionsId = `astryx-transfer-list-reorder-${useId()}`;
3484
+ const rootRef = useTemplateRef("rootRef");
3485
+ const searchRef = useTemplateRef("searchRef");
3486
+ const previewHost = ref(null);
3487
+ const query = ref("");
3488
+ /** 提交之后 props 才会回来,中间的连点读不到最新值,这里留一份同步的顺序。 */
3489
+ const liveValue = ref([...props.modelValue]);
3490
+ watch(() => props.modelValue, (next) => {
3491
+ liveValue.value = [...next];
3492
+ });
3493
+ function commit(next) {
3494
+ liveValue.value = [...next];
3495
+ emit("update:modelValue", next);
3496
+ emit("change", next);
3497
+ }
3498
+ const selectedLabel = computed(() => props.selectedLabel ?? t("@astryx.transferList.selectedLabel"));
3499
+ const availableLabel = computed(() => props.availableLabel ?? t("@astryx.transferList.availableLabel"));
3500
+ const searchLabel = computed(() => props.searchLabel ?? t("@astryx.transferList.searchLabel", { label: props.label }));
3501
+ const searchPlaceholder = computed(() => props.searchPlaceholder ?? t("@astryx.transferList.searchPlaceholder"));
3502
+ const selectedEmptyText = computed(() => props.selectedEmptyText ?? t("@astryx.transferList.selectedEmpty"));
3503
+ const availableEmptyText = computed(() => props.availableEmptyText ?? t("@astryx.transferList.availableEmpty"));
3504
+ const noResultsText = computed(() => props.noResultsText ?? t("@astryx.transferList.noResults"));
3505
+ const optionByValue = computed(() => {
3506
+ const map = /* @__PURE__ */ new Map();
3507
+ for (const option of props.options) map.set(option.value, option);
3508
+ return map;
3509
+ });
3510
+ const normalizedQuery = computed(() => query.value.trim().toLowerCase());
3511
+ const selectedOptions = computed(() => props.modelValue.map((optionValue) => optionByValue.value.get(optionValue)).filter((option) => option != null && matchesQuery(option, normalizedQuery.value)));
3512
+ const availableOptions = computed(() => {
3513
+ const selectedValues = new Set(props.modelValue);
3514
+ return props.options.filter((option) => !selectedValues.has(option.value) && matchesQuery(option, normalizedQuery.value));
3515
+ });
3516
+ const selectedGroups = computed(() => [{
3517
+ group: "",
3518
+ options: selectedOptions.value.map((option, index) => ({
3519
+ option,
3520
+ index
3521
+ }))
3522
+ }]);
3523
+ const availableGroups = computed(() => groupOptions(availableOptions.value).map(([group, options]) => ({
3524
+ group,
3525
+ options: options.map((option) => ({
3526
+ option,
3527
+ index: availableOptions.value.indexOf(option)
3528
+ }))
3529
+ })));
3530
+ function rowElement(value) {
3531
+ const rows = rootRef.value?.querySelectorAll("[data-transfer-list-row]");
3532
+ if (rows == null) return null;
3533
+ for (const row of rows) if (row.dataset.transferListRow === value) return row;
3534
+ return null;
3535
+ }
3536
+ const reorder = useTransferReorder({
3537
+ isReorderable: () => props.isReorderable,
3538
+ currentValue: () => liveValue.value,
3539
+ optionByValue: () => optionByValue.value,
3540
+ rowElement,
3541
+ previewHost,
3542
+ commit,
3543
+ announce,
3544
+ messages: {
3545
+ grabbed: (label, position, total) => t("@astryx.transferList.announceGrabbed", {
3546
+ label,
3547
+ position,
3548
+ total
3549
+ }),
3550
+ moveCancelled: (label) => t("@astryx.transferList.announceMoveCancelled", { label }),
3551
+ dropped: (label, position, total) => t("@astryx.transferList.announceDropped", {
3552
+ label,
3553
+ position,
3554
+ total
3555
+ }),
3556
+ returned: (label, position) => t("@astryx.transferList.announceReturned", {
3557
+ label,
3558
+ position
3559
+ }),
3560
+ movedToPosition: (label, position, total) => t("@astryx.transferList.announceMovedToPosition", {
3561
+ label,
3562
+ position,
3563
+ total
3564
+ })
3565
+ }
3566
+ });
3567
+ /** 转移之后把焦点交回同侧的下一行,没有可聚焦的就退回搜索框或面板本身。 */
3568
+ function focusAfterTransfer(side, index) {
3569
+ const moveFocus = () => {
3570
+ const panel = rootRef.value?.querySelector(`.astryx-transfer-list-panel[data-side="${side}"]`);
3571
+ const actions = panel?.querySelectorAll(`[data-transfer-list-action="${side}"]:not([disabled]):not([aria-disabled="true"])`);
3572
+ if (actions != null && actions.length > 0) actions[Math.min(index, actions.length - 1)]?.focus();
3573
+ else if (props.hasSearch) searchRef.value?.focus();
3574
+ else panel?.focus();
3575
+ };
3576
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(moveFocus);
3577
+ else setTimeout(moveFocus, 0);
3578
+ }
3579
+ function addOption(option, index) {
3580
+ if (option.isTransferDisabled || liveValue.value.includes(option.value)) return;
3581
+ const nextValue = [...liveValue.value, option.value];
3582
+ commit(nextValue);
3583
+ announce(t("@astryx.transferList.announceAdded", {
3584
+ label: option.label,
3585
+ count: nextValue.length
3586
+ }));
3587
+ focusAfterTransfer("available", index);
3588
+ }
3589
+ function removeOption(option, index) {
3590
+ if (option.isTransferDisabled) return;
3591
+ const nextValue = liveValue.value.filter((item) => item !== option.value);
3592
+ commit(nextValue);
3593
+ announce(t("@astryx.transferList.announceRemoved", {
3594
+ label: option.label,
3595
+ count: nextValue.length
3596
+ }));
3597
+ focusAfterTransfer("selected", index);
3598
+ }
3599
+ function addAll() {
3600
+ const additions = props.options.filter((option) => !option.isTransferDisabled && !liveValue.value.includes(option.value)).map((option) => option.value);
3601
+ if (additions.length === 0) return;
3602
+ commit([...liveValue.value, ...additions]);
3603
+ announce(t("@astryx.transferList.announceBulkAdded", { count: additions.length }));
3604
+ }
3605
+ function clearSelected() {
3606
+ const nextValue = liveValue.value.filter((optionValue) => {
3607
+ const option = optionByValue.value.get(optionValue);
3608
+ return option == null || option.isTransferDisabled;
3609
+ });
3610
+ const removed = liveValue.value.length - nextValue.length;
3611
+ if (removed === 0) return;
3612
+ commit(nextValue);
3613
+ announce(t("@astryx.transferList.announceBulkRemoved", { count: removed }));
3614
+ }
3615
+ function handleSearch(nextQuery) {
3616
+ query.value = nextQuery;
3617
+ const normalized = nextQuery.trim().toLowerCase();
3618
+ if (normalized === "") {
3619
+ announce("");
3620
+ return;
3621
+ }
3622
+ const count = props.options.filter((option) => matchesQuery(option, normalized)).length;
3623
+ announce(t("@astryx.transferList.announceSearchResults", { count }));
3624
+ }
3625
+ const isClearDisabled = computed(() => !props.modelValue.some((optionValue) => !optionByValue.value.get(optionValue)?.isTransferDisabled));
3626
+ const isAddAllDisabled = computed(() => !props.options.some((option) => !option.isTransferDisabled && !props.modelValue.includes(option.value)));
3627
+ const describedBy = computed(() => {
3628
+ const ids = [attrs["aria-describedby"], props.description != null ? descriptionId : void 0].filter(Boolean);
3629
+ return ids.length > 0 ? ids.join(" ") : void 0;
3630
+ });
3631
+ const rootAttrs = computed(() => Object.fromEntries(Object.entries(attrs).filter(([key]) => ![
3632
+ "class",
3633
+ "style",
3634
+ "role",
3635
+ "aria-labelledby",
3636
+ "aria-describedby"
3637
+ ].includes(key))));
3638
+ const labels = {
3639
+ addOption: (label) => t("@astryx.transferList.addOption", { label }),
3640
+ removeOption: (label) => t("@astryx.transferList.removeOption", { label }),
3641
+ reorderOption: (label) => t("@astryx.transferList.reorderOption", { label }),
3642
+ transferDisabled: (label) => t("@astryx.transferList.transferDisabled", { label }),
3643
+ reorderDisabled: (label) => t("@astryx.transferList.reorderDisabled", { label })
3644
+ };
3645
+ const handlers = {
3646
+ click: reorder.onHandleClick,
3647
+ keydown: reorder.onHandleKeydown,
3648
+ pointerdown: reorder.onHandlePointerDown,
3649
+ pointermove: reorder.onHandlePointerMove,
3650
+ pointerup: reorder.onHandlePointerUp,
3651
+ pointercancel: reorder.onHandlePointerCancel,
3652
+ lostpointercapture: reorder.onHandleLostPointerCapture
3653
+ };
3654
+ return (_ctx, _cache) => {
3655
+ return openBlock(), createElementBlock("div", mergeProps({
3656
+ ref_key: "rootRef",
3657
+ ref: rootRef
3658
+ }, rootAttrs.value, {
3659
+ class: ["astryx-transfer-list", unref(attrs).class],
3660
+ style: unref(attrs).style,
3661
+ role: unref(attrs).role ?? "group",
3662
+ "aria-labelledby": unref(attrs)["aria-labelledby"] ?? labelId,
3663
+ "aria-describedby": describedBy.value
3664
+ }), [
3665
+ createElementVNode("div", _hoisted_2$1, [__props.isLabelHidden ? (openBlock(), createBlock(unref(VisuallyHidden), {
3666
+ key: 0,
3667
+ id: labelId
3668
+ }, {
3669
+ default: withCtx(() => [createTextVNode(toDisplayString(__props.label), 1)]),
3670
+ _: 1
3671
+ })) : (openBlock(), createBlock(unref(Text), {
3672
+ key: 1,
3673
+ id: labelId,
3674
+ type: "label",
3675
+ display: "block"
3676
+ }, {
3677
+ default: withCtx(() => [createTextVNode(toDisplayString(__props.label), 1)]),
3678
+ _: 1
3679
+ })), __props.description != null ? (openBlock(), createBlock(unref(Text), {
3680
+ key: 2,
3681
+ id: descriptionId,
3682
+ type: "supporting",
3683
+ display: "block"
3684
+ }, {
3685
+ default: withCtx(() => [createTextVNode(toDisplayString(__props.description), 1)]),
3686
+ _: 1
3687
+ })) : createCommentVNode("v-if", true)]),
3688
+ __props.hasSearch ? (openBlock(), createElementBlock("div", _hoisted_3$1, [createVNode(unref(TextInput), {
3689
+ "model-value": query.value,
3690
+ role: "searchbox",
3691
+ label: searchLabel.value,
3692
+ "is-label-hidden": "",
3693
+ placeholder: searchPlaceholder.value,
3694
+ "start-icon": unref(SearchIcon),
3695
+ "has-clear": "",
3696
+ width: "100%",
3697
+ "control-class": "astryx-transfer-list__search",
3698
+ "onUpdate:modelValue": handleSearch
3699
+ }, null, 8, [
3700
+ "model-value",
3701
+ "label",
3702
+ "placeholder",
3703
+ "start-icon"
3704
+ ])])) : createCommentVNode("v-if", true),
3705
+ createVNode(unref(VisuallyHidden), { id: reorderInstructionsId }, {
3706
+ default: withCtx(() => [createTextVNode(toDisplayString(unref(t)("@astryx.transferList.reorderInstructions")), 1)]),
3707
+ _: 1
3708
+ }),
3709
+ createElementVNode("div", _hoisted_4, [createVNode(_sfc_main$9, {
3710
+ side: "selected",
3711
+ "heading-id": selectedHeadingId,
3712
+ label: selectedLabel.value,
3713
+ "show-action": __props.hasClear,
3714
+ "action-label": unref(t)("@astryx.transferList.clear"),
3715
+ "is-action-disabled": isClearDisabled.value,
3716
+ groups: selectedGroups.value,
3717
+ "show-group-headings": false,
3718
+ "ungrouped-label": unref(t)("@astryx.transferList.ungroupedLabel"),
3719
+ "empty-text": normalizedQuery.value === "" ? selectedEmptyText.value : noResultsText.value,
3720
+ "is-reorderable": __props.isReorderable,
3721
+ "dragging-value": unref(reorder).draggingValue.value,
3722
+ "grabbed-value": unref(reorder).grabbedValue.value,
3723
+ "drop-target": unref(reorder).pointerPlacement.value,
3724
+ "reorder-instructions-id": reorderInstructionsId,
3725
+ labels,
3726
+ handlers,
3727
+ onAction: clearSelected,
3728
+ onRemove: removeOption
3729
+ }, createSlots({ _: 2 }, [_ctx.$slots.option ? {
3730
+ name: "option",
3731
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "option", { option: scope.option })]),
3732
+ key: "0"
3733
+ } : void 0]), 1032, [
3734
+ "label",
3735
+ "show-action",
3736
+ "action-label",
3737
+ "is-action-disabled",
3738
+ "groups",
3739
+ "ungrouped-label",
3740
+ "empty-text",
3741
+ "is-reorderable",
3742
+ "dragging-value",
3743
+ "grabbed-value",
3744
+ "drop-target"
3745
+ ]), createVNode(_sfc_main$9, {
3746
+ side: "available",
3747
+ "heading-id": availableHeadingId,
3748
+ label: availableLabel.value,
3749
+ "show-action": __props.hasSelectAll,
3750
+ "action-label": unref(t)("@astryx.transferList.addAll"),
3751
+ "is-action-disabled": isAddAllDisabled.value,
3752
+ groups: availableGroups.value,
3753
+ "show-group-headings": true,
3754
+ "ungrouped-label": unref(t)("@astryx.transferList.ungroupedLabel"),
3755
+ "empty-text": normalizedQuery.value === "" ? availableEmptyText.value : noResultsText.value,
3756
+ "is-reorderable": false,
3757
+ "dragging-value": null,
3758
+ "grabbed-value": null,
3759
+ "drop-target": null,
3760
+ "reorder-instructions-id": reorderInstructionsId,
3761
+ labels,
3762
+ handlers,
3763
+ onAction: addAll,
3764
+ onAdd: addOption
3765
+ }, createSlots({ _: 2 }, [_ctx.$slots.option ? {
3766
+ name: "option",
3767
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "option", { option: scope.option })]),
3768
+ key: "0"
3769
+ } : void 0]), 1032, [
3770
+ "label",
3771
+ "show-action",
3772
+ "action-label",
3773
+ "is-action-disabled",
3774
+ "groups",
3775
+ "ungrouped-label",
3776
+ "empty-text"
3777
+ ])]),
3778
+ (openBlock(), createBlock(Teleport, { to: "body" }, [unref(reorder).previewStyle.value != null ? (openBlock(), createElementBlock("div", {
3779
+ key: 0,
3780
+ ref_key: "previewHost",
3781
+ ref: previewHost,
3782
+ class: "astryx-transfer-list-drag-preview",
3783
+ "data-transfer-list-drag-preview": "",
3784
+ "aria-hidden": "true",
3785
+ style: normalizeStyle(unref(reorder).previewStyle.value)
3786
+ }, null, 4)) : createCommentVNode("v-if", true)]))
3787
+ ], 16, _hoisted_1$1);
3788
+ };
3789
+ }
3790
+ });
3791
+ //#endregion
3792
+ //#region src/TransferList/TransferListSelector.vue
3793
+ const _hoisted_1 = ["aria-busy", "data-busy"];
3794
+ const _hoisted_2 = ["disabled"];
3795
+ const _hoisted_3 = {
3796
+ key: 0,
3797
+ class: "astryx-transfer-list-selector__footer"
3798
+ };
3799
+ const DEFAULT_SELECTOR_WIDTH = "min(41rem, calc(100vw - 32px))";
3800
+ const _sfc_main$8 = /*@__PURE__*/ defineComponent({
3801
+ name: "AstryxTransferListSelector",
3802
+ __name: "TransferListSelector",
3803
+ props: {
3804
+ label: {},
3805
+ modelValue: { default: () => [] },
3806
+ options: {},
3807
+ description: {},
3808
+ isLabelHidden: { type: Boolean },
3809
+ isOptional: { type: Boolean },
3810
+ isRequired: { type: Boolean },
3811
+ isDisabled: { type: Boolean },
3812
+ isLoading: { type: Boolean },
3813
+ labelTooltip: {},
3814
+ status: {},
3815
+ statusVariant: {},
3816
+ size: {},
3817
+ width: {},
3818
+ placement: {},
3819
+ triggerLabel: {},
3820
+ selectedLabel: {},
3821
+ availableLabel: {},
3822
+ hasSearch: { type: Boolean },
3823
+ searchLabel: {},
3824
+ searchPlaceholder: {},
3825
+ isReorderable: {
3826
+ type: Boolean,
3827
+ default: void 0
3828
+ },
3829
+ hasSelectAll: { type: Boolean },
3830
+ hasClear: { type: Boolean },
3831
+ selectedEmptyText: {},
3832
+ availableEmptyText: {},
3833
+ noResultsText: {},
3834
+ commitBehavior: { default: "immediate" },
3835
+ applyLabel: {},
3836
+ cancelLabel: {}
3837
+ },
3838
+ emits: [
3839
+ "update:modelValue",
3840
+ "change",
3841
+ "changeAction",
3842
+ "openChange"
3843
+ ],
3844
+ setup(__props, { emit: __emit }) {
3845
+ const props = __props;
3846
+ const emit = __emit;
3847
+ /** 弹层和字段的默认宽度:留出 32px 的视口空隙,再宽不超过 41rem。 */
3848
+ const { t } = useTranslator();
3849
+ const applyLabel = computed(() => props.applyLabel ?? t("@astryx.transferListSelector.apply"));
3850
+ const cancelLabel = computed(() => props.cancelLabel ?? t("@astryx.transferListSelector.cancel"));
3851
+ const triggerLabel = computed(() => props.triggerLabel ?? t("@astryx.transferListSelector.triggerLabel", { count: props.modelValue.length }));
3852
+ const resolvedWidth = computed(() => typeof props.width === "number" ? `${props.width}px` : props.width ?? DEFAULT_SELECTOR_WIDTH);
3853
+ /** 弹层内容和字段同宽、去掉内边距,两栏贴到弹层边上;高度留出视口空隙,自己不滚。 */
3854
+ const contentStyle = computed(() => ({
3855
+ width: resolvedWidth.value,
3856
+ maxWidth: resolvedWidth.value,
3857
+ maxHeight: "calc(100vh - 32px)",
3858
+ padding: "0",
3859
+ overflow: "hidden",
3860
+ borderRadius: "inherit"
3861
+ }));
3862
+ /** 分段提交时先放在这里,按 Apply 才真正提交。 */
3863
+ const draftValue = ref([...props.modelValue]);
3864
+ const draftSource = {
3865
+ value: [...props.modelValue],
3866
+ commitBehavior: props.commitBehavior
3867
+ };
3868
+ const isOpen = ref(false);
3869
+ function areOrderedValuesEqual(first, second) {
3870
+ return first.length === second.length && first.every((item, index) => item === second[index]);
3871
+ }
3872
+ function resetDraft() {
3873
+ draftValue.value = [...props.modelValue];
3874
+ }
3875
+ watch(() => [props.modelValue, props.commitBehavior], () => {
3876
+ if (!areOrderedValuesEqual(draftSource.value, props.modelValue) || draftSource.commitBehavior !== props.commitBehavior) {
3877
+ draftSource.value = [...props.modelValue];
3878
+ draftSource.commitBehavior = props.commitBehavior;
3879
+ resetDraft();
3880
+ }
3881
+ }, {
3882
+ flush: "sync",
3883
+ deep: false
3884
+ });
3885
+ function onOpenChange(next) {
3886
+ if (isOpen.value && !next) resetDraft();
3887
+ isOpen.value = next;
3888
+ emit("openChange", next);
3889
+ }
3890
+ function handleTransferChange(nextValue, commit, isBusy) {
3891
+ if (isBusy) return;
3892
+ if (props.commitBehavior === "staged") {
3893
+ draftValue.value = [...nextValue];
3894
+ return;
3895
+ }
3896
+ commit([...nextValue]);
3897
+ }
3898
+ function applyStaged(commit, close) {
3899
+ if (!areOrderedValuesEqual(draftValue.value, props.modelValue)) commit([...draftValue.value]);
3900
+ close();
3901
+ }
3902
+ function cancelStaged(close) {
3903
+ resetDraft();
3904
+ close();
3905
+ }
3906
+ return (_ctx, _cache) => {
3907
+ return openBlock(), createBlock(unref(ComplexSelector), {
3908
+ label: __props.label,
3909
+ "model-value": __props.modelValue,
3910
+ "trigger-label": triggerLabel.value,
3911
+ description: __props.description,
3912
+ "is-label-hidden": __props.isLabelHidden,
3913
+ "is-optional": __props.isOptional,
3914
+ "is-required": __props.isRequired,
3915
+ "is-disabled": __props.isDisabled,
3916
+ "is-loading": __props.isLoading,
3917
+ "label-tooltip": __props.labelTooltip,
3918
+ status: __props.status,
3919
+ "status-variant": __props.statusVariant,
3920
+ size: __props.size,
3921
+ width: resolvedWidth.value,
3922
+ placement: __props.placement,
3923
+ "popup-width": resolvedWidth.value,
3924
+ "content-style": contentStyle.value,
3925
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => emit("update:modelValue", $event)),
3926
+ onChange: _cache[1] || (_cache[1] = ($event) => emit("change", $event)),
3927
+ onChangeAction: _cache[2] || (_cache[2] = ($event) => emit("changeAction", $event)),
3928
+ onOpenChange
3929
+ }, {
3930
+ default: withCtx(({ value: appliedValue, commit, close, state }) => [createElementVNode("div", {
3931
+ "aria-busy": state.isBusy || void 0,
3932
+ "data-busy": state.isBusy || void 0,
3933
+ class: "astryx-transfer-list-selector__surface"
3934
+ }, [createElementVNode("fieldset", {
3935
+ class: "astryx-transfer-list-selector__fieldset",
3936
+ disabled: __props.isDisabled
3937
+ }, [createVNode(_sfc_main$7, {
3938
+ label: __props.label,
3939
+ "is-label-hidden": "",
3940
+ options: __props.options,
3941
+ "model-value": __props.commitBehavior === "staged" ? draftValue.value : appliedValue,
3942
+ "selected-label": __props.selectedLabel,
3943
+ "available-label": __props.availableLabel,
3944
+ "has-search": __props.hasSearch,
3945
+ "search-label": __props.searchLabel,
3946
+ "search-placeholder": __props.searchPlaceholder,
3947
+ "is-reorderable": __props.isReorderable,
3948
+ "has-select-all": __props.hasSelectAll,
3949
+ "has-clear": __props.hasClear,
3950
+ "selected-empty-text": __props.selectedEmptyText,
3951
+ "available-empty-text": __props.availableEmptyText,
3952
+ "no-results-text": __props.noResultsText,
3953
+ onChange: ($event) => handleTransferChange($event, commit, state.isBusy)
3954
+ }, createSlots({ _: 2 }, [_ctx.$slots.option ? {
3955
+ name: "option",
3956
+ fn: withCtx((scope) => [renderSlot(_ctx.$slots, "option", { option: scope.option })]),
3957
+ key: "0"
3958
+ } : void 0]), 1032, [
3959
+ "label",
3960
+ "options",
3961
+ "model-value",
3962
+ "selected-label",
3963
+ "available-label",
3964
+ "has-search",
3965
+ "search-label",
3966
+ "search-placeholder",
3967
+ "is-reorderable",
3968
+ "has-select-all",
3969
+ "has-clear",
3970
+ "selected-empty-text",
3971
+ "available-empty-text",
3972
+ "no-results-text",
3973
+ "onChange"
3974
+ ])], 8, _hoisted_2), __props.commitBehavior === "staged" ? (openBlock(), createElementBlock("div", _hoisted_3, [createVNode(unref(Button), {
3975
+ label: cancelLabel.value,
3976
+ variant: "ghost",
3977
+ size: "sm",
3978
+ onClick: ($event) => cancelStaged(close)
3979
+ }, null, 8, ["label", "onClick"]), createVNode(unref(Button), {
3980
+ label: applyLabel.value,
3981
+ variant: "primary",
3982
+ size: "sm",
3983
+ "is-disabled": __props.isDisabled || state.isBusy,
3984
+ "is-loading": state.isBusy,
3985
+ onClick: ($event) => applyStaged(commit, close)
3986
+ }, null, 8, [
3987
+ "label",
3988
+ "is-disabled",
3989
+ "is-loading",
3990
+ "onClick"
3991
+ ])])) : createCommentVNode("v-if", true)], 8, _hoisted_1)]),
3992
+ _: 3
3993
+ }, 8, [
3994
+ "label",
3995
+ "model-value",
3996
+ "trigger-label",
3997
+ "description",
3998
+ "is-label-hidden",
3999
+ "is-optional",
4000
+ "is-required",
4001
+ "is-disabled",
4002
+ "is-loading",
4003
+ "label-tooltip",
4004
+ "status",
4005
+ "status-variant",
4006
+ "size",
4007
+ "width",
4008
+ "placement",
4009
+ "popup-width",
4010
+ "content-style"
4011
+ ]);
4012
+ };
4013
+ }
4014
+ });
4015
+ //#endregion
4016
+ export { _sfc_main as CircularProgress, _sfc_main$1 as InfoTip, _sfc_main$2 as ListInput, _sfc_main$3 as LogStream, _sfc_main$4 as MobileTokenizer, _sfc_main$5 as SVGIcon, _sfc_main$6 as Stat, _sfc_main$7 as TransferList, _sfc_main$8 as TransferListSelector, addCurvature, adjustTension, applyPersonality, bellIcon, calendarIcon, checkIcon, eyeIcon, folderIcon, heartIcon, homeIcon, lockIcon, mailIcon, menuIcon, roundCorners, searchIcon, settingsIcon, shieldIcon, starIcon, starterIcons, xIcon, xifBell, xifBellOverride, xifCheck, xifExamples, xifFile, xifHome, xifShield, xifStar };