@shwfed/config 3.0.19 → 3.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/actions/buttons/2026-04-21/com.shwfed.actions.button.navigation/config.d.vue.ts +2 -2
- package/dist/runtime/components/actions/buttons/2026-04-21/com.shwfed.actions.button.navigation/config.vue.d.ts +2 -2
- package/dist/runtime/components/form/FormUnitRenderer.vue +8 -1
- package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.combobox.single.remote/runtime.vue +29 -14
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.combobox.single/runtime.vue +29 -14
- package/dist/runtime/components/form/fields/2026-06-14/com.shwfed.form.field.combobox.single/runtime.vue +29 -14
- package/dist/runtime/components/form/index.vue +8 -8
- package/dist/runtime/components/form/utils/state.d.ts +18 -1
- package/dist/runtime/components/form/utils/state.js +5 -1
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/config.d.vue.ts +2 -2
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/config.vue.d.ts +2 -2
- package/dist/runtime/components/ui/icon-picker/IconPicker.d.vue.ts +0 -1
- package/dist/runtime/components/ui/icon-picker/IconPicker.vue +312 -28
- package/dist/runtime/components/ui/icon-picker/IconPicker.vue.d.ts +0 -1
- package/dist/runtime/components/ui/tabs/TabsList.vue +11 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -5,14 +5,14 @@ type __VLS_ModelProps = {
|
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
6
|
"update:modelValue": (value: {
|
|
7
7
|
readonly type: "com.shwfed.actions.button.navigation";
|
|
8
|
-
readonly mode: "
|
|
8
|
+
readonly mode: "_self" | "_blank";
|
|
9
9
|
readonly url: string;
|
|
10
10
|
readonly compatibilityDate: "2026-04-21";
|
|
11
11
|
}) => any;
|
|
12
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
13
13
|
"onUpdate:modelValue"?: ((value: {
|
|
14
14
|
readonly type: "com.shwfed.actions.button.navigation";
|
|
15
|
-
readonly mode: "
|
|
15
|
+
readonly mode: "_self" | "_blank";
|
|
16
16
|
readonly url: string;
|
|
17
17
|
readonly compatibilityDate: "2026-04-21";
|
|
18
18
|
}) => any) | undefined;
|
|
@@ -5,14 +5,14 @@ type __VLS_ModelProps = {
|
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
6
|
"update:modelValue": (value: {
|
|
7
7
|
readonly type: "com.shwfed.actions.button.navigation";
|
|
8
|
-
readonly mode: "
|
|
8
|
+
readonly mode: "_self" | "_blank";
|
|
9
9
|
readonly url: string;
|
|
10
10
|
readonly compatibilityDate: "2026-04-21";
|
|
11
11
|
}) => any;
|
|
12
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
13
13
|
"onUpdate:modelValue"?: ((value: {
|
|
14
14
|
readonly type: "com.shwfed.actions.button.navigation";
|
|
15
|
-
readonly mode: "
|
|
15
|
+
readonly mode: "_self" | "_blank";
|
|
16
16
|
readonly url: string;
|
|
17
17
|
readonly compatibilityDate: "2026-04-21";
|
|
18
18
|
}) => any) | undefined;
|
|
@@ -137,7 +137,14 @@ const gridStyle = computed(() => {
|
|
|
137
137
|
const rendered = computed(() => {
|
|
138
138
|
const comp = compaction.value;
|
|
139
139
|
if (!comp) return [];
|
|
140
|
-
|
|
140
|
+
const ordered = placedFields.value.slice().sort((a, b) => {
|
|
141
|
+
const [[ax1, ay1]] = a.placement.area;
|
|
142
|
+
const [[bx1, by1]] = b.placement.area;
|
|
143
|
+
if (ay1 !== by1) return ay1 - by1;
|
|
144
|
+
if (ax1 !== bx1) return ax1 - bx1;
|
|
145
|
+
return 0;
|
|
146
|
+
});
|
|
147
|
+
return ordered.map((p) => ({
|
|
141
148
|
key: p.field.id,
|
|
142
149
|
style: cellStyle(p.placement, comp),
|
|
143
150
|
component: p.entry.runtime,
|
|
@@ -3,7 +3,7 @@ import { Icon } from "@iconify/vue";
|
|
|
3
3
|
import { watchDebounced } from "@vueuse/core";
|
|
4
4
|
import { Effect, Option } from "effect";
|
|
5
5
|
import { Fetch } from "fx-fetch";
|
|
6
|
-
import { computed, nextTick, ref } from "vue";
|
|
6
|
+
import { computed, nextTick, onScopeDispose, ref } from "vue";
|
|
7
7
|
import { useI18n } from "vue-i18n";
|
|
8
8
|
import { asRequest, fetchJsonOption } from "../../../../../share/request";
|
|
9
9
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
@@ -23,6 +23,7 @@ import { InputGroup, InputGroupAddon, InputGroupButton } from "../../../../ui/in
|
|
|
23
23
|
import { Markdown } from "../../../../ui/markdown";
|
|
24
24
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../../../../ui/popover";
|
|
25
25
|
import { DEFAULT_FIELD_ORIENTATION } from "../../../utils/common";
|
|
26
|
+
import { useDerivedQuiescence } from "../../../utils/derived";
|
|
26
27
|
import { useFieldValidation } from "../../../utils/validation";
|
|
27
28
|
import { useFormReadonly } from "../../../utils/readonly";
|
|
28
29
|
import { useFormScope, useFormState } from "../../../utils/state";
|
|
@@ -51,7 +52,7 @@ const { locale, t } = useI18n({
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
|
-
const { getAt, setAt, setAtSilent } = useFormState();
|
|
55
|
+
const { getAt, setAt, setAtSilent, seeded } = useFormState();
|
|
55
56
|
const formScope = useFormScope();
|
|
56
57
|
const inherited = injectCELContext();
|
|
57
58
|
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
|
|
@@ -163,20 +164,34 @@ const model = computed({
|
|
|
163
164
|
setAt(path, next);
|
|
164
165
|
}
|
|
165
166
|
});
|
|
167
|
+
let seedSettled = false;
|
|
166
168
|
let lastRequestSignature;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
()
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
let lastFetchedSignature;
|
|
170
|
+
function reactToRequest() {
|
|
171
|
+
if (!seedSettled) return;
|
|
172
|
+
if (lastRequestSignature != null && requestSignature.value !== lastRequestSignature) {
|
|
173
|
+
const path = props.config.binding;
|
|
174
|
+
if (path == null) uncontrolled.value = void 0;
|
|
175
|
+
else setAtSilent(path, void 0);
|
|
176
|
+
}
|
|
177
|
+
lastRequestSignature = requestSignature.value;
|
|
178
|
+
if (requestSignature.value !== lastFetchedSignature) {
|
|
179
|
+
lastFetchedSignature = requestSignature.value;
|
|
176
180
|
void fetchOptions();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
watchDebounced(requestSignature, reactToRequest, { debounce: 200, immediate: true });
|
|
184
|
+
const quiescence = useDerivedQuiescence();
|
|
185
|
+
let disposed = false;
|
|
186
|
+
onScopeDispose(() => {
|
|
187
|
+
disposed = true;
|
|
188
|
+
});
|
|
189
|
+
function establishBaseline() {
|
|
190
|
+
if (disposed) return;
|
|
191
|
+
seedSettled = true;
|
|
192
|
+
reactToRequest();
|
|
193
|
+
}
|
|
194
|
+
seeded.then(() => quiescence.settled(), () => quiescence.settled()).then(establishBaseline);
|
|
180
195
|
function isEqual(a, b) {
|
|
181
196
|
if (a === b) return true;
|
|
182
197
|
if (a == null || b == null) return false;
|
|
@@ -3,7 +3,7 @@ import { Icon } from "@iconify/vue";
|
|
|
3
3
|
import { watchDebounced } from "@vueuse/core";
|
|
4
4
|
import { Effect, Option } from "effect";
|
|
5
5
|
import { Fetch } from "fx-fetch";
|
|
6
|
-
import { computed, nextTick, ref, watch } from "vue";
|
|
6
|
+
import { computed, nextTick, onScopeDispose, ref, watch } from "vue";
|
|
7
7
|
import { useI18n } from "vue-i18n";
|
|
8
8
|
import { asRequest, fetchJsonOption } from "../../../../../share/request";
|
|
9
9
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
@@ -24,6 +24,7 @@ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "
|
|
|
24
24
|
import { Markdown } from "../../../../ui/markdown";
|
|
25
25
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../../../../ui/popover";
|
|
26
26
|
import { DEFAULT_FIELD_ORIENTATION } from "../../../utils/common";
|
|
27
|
+
import { useDerivedQuiescence } from "../../../utils/derived";
|
|
27
28
|
import { useFormReadonly } from "../../../utils/readonly";
|
|
28
29
|
import { useFormScope, useFormState } from "../../../utils/state";
|
|
29
30
|
import { useFieldValidation } from "../../../utils/validation";
|
|
@@ -55,7 +56,7 @@ const { locale, t } = useI18n({
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
|
-
const { getAt, setAt, setAtSilent } = useFormState();
|
|
59
|
+
const { getAt, setAt, setAtSilent, seeded } = useFormState();
|
|
59
60
|
const formScope = useFormScope();
|
|
60
61
|
const inherited = injectCELContext();
|
|
61
62
|
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
|
|
@@ -220,20 +221,34 @@ const model = computed({
|
|
|
220
221
|
setAt(path, next);
|
|
221
222
|
}
|
|
222
223
|
});
|
|
224
|
+
let seedSettled = false;
|
|
223
225
|
let lastRequestSignature;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
let lastFetchedSignature;
|
|
227
|
+
function reactToRequest() {
|
|
228
|
+
if (!seedSettled) return;
|
|
229
|
+
if (lastRequestSignature != null && requestSignature.value !== lastRequestSignature) {
|
|
230
|
+
const path = props.config.binding;
|
|
231
|
+
if (path == null) uncontrolled.value = void 0;
|
|
232
|
+
else setAtSilent(path, void 0);
|
|
233
|
+
}
|
|
234
|
+
lastRequestSignature = requestSignature.value;
|
|
235
|
+
if (requestSignature.value !== lastFetchedSignature) {
|
|
236
|
+
lastFetchedSignature = requestSignature.value;
|
|
233
237
|
void fetchOptions();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
watchDebounced(requestSignature, reactToRequest, { debounce: 200, immediate: true });
|
|
241
|
+
const quiescence = useDerivedQuiescence();
|
|
242
|
+
let disposed = false;
|
|
243
|
+
onScopeDispose(() => {
|
|
244
|
+
disposed = true;
|
|
245
|
+
});
|
|
246
|
+
function establishBaseline() {
|
|
247
|
+
if (disposed) return;
|
|
248
|
+
seedSettled = true;
|
|
249
|
+
reactToRequest();
|
|
250
|
+
}
|
|
251
|
+
seeded.then(() => quiescence.settled(), () => quiescence.settled()).then(establishBaseline);
|
|
237
252
|
function isEqual(a, b) {
|
|
238
253
|
if (a === b) return true;
|
|
239
254
|
if (a == null || b == null) return false;
|
|
@@ -3,7 +3,7 @@ import { Icon } from "@iconify/vue";
|
|
|
3
3
|
import { watchDebounced } from "@vueuse/core";
|
|
4
4
|
import { Effect, Option } from "effect";
|
|
5
5
|
import { Fetch } from "fx-fetch";
|
|
6
|
-
import { computed, nextTick, ref, watch } from "vue";
|
|
6
|
+
import { computed, nextTick, onScopeDispose, ref, watch } from "vue";
|
|
7
7
|
import { useI18n } from "vue-i18n";
|
|
8
8
|
import { asRequest, fetchJsonOption } from "../../../../../share/request";
|
|
9
9
|
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
@@ -24,6 +24,7 @@ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "
|
|
|
24
24
|
import { Markdown } from "../../../../ui/markdown";
|
|
25
25
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../../../../ui/popover";
|
|
26
26
|
import { DEFAULT_FIELD_ORIENTATION } from "../../../utils/common";
|
|
27
|
+
import { useDerivedQuiescence } from "../../../utils/derived";
|
|
27
28
|
import { useFormReadonly } from "../../../utils/readonly";
|
|
28
29
|
import { useFormScope, useFormState } from "../../../utils/state";
|
|
29
30
|
import { useFieldValidation } from "../../../utils/validation";
|
|
@@ -55,7 +56,7 @@ const { locale, t } = useI18n({
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
|
-
const { getAt, setAt, setAtSilent } = useFormState();
|
|
59
|
+
const { getAt, setAt, setAtSilent, seeded } = useFormState();
|
|
59
60
|
const formScope = useFormScope();
|
|
60
61
|
const inherited = injectCELContext();
|
|
61
62
|
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
|
|
@@ -223,20 +224,34 @@ const model = computed({
|
|
|
223
224
|
setAt(path, next);
|
|
224
225
|
}
|
|
225
226
|
});
|
|
227
|
+
let seedSettled = false;
|
|
226
228
|
let lastRequestSignature;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
let lastFetchedSignature;
|
|
230
|
+
function reactToRequest() {
|
|
231
|
+
if (!seedSettled) return;
|
|
232
|
+
if (lastRequestSignature != null && requestSignature.value !== lastRequestSignature) {
|
|
233
|
+
const path = props.config.binding;
|
|
234
|
+
if (path == null) uncontrolled.value = void 0;
|
|
235
|
+
else setAtSilent(path, void 0);
|
|
236
|
+
}
|
|
237
|
+
lastRequestSignature = requestSignature.value;
|
|
238
|
+
if (requestSignature.value !== lastFetchedSignature) {
|
|
239
|
+
lastFetchedSignature = requestSignature.value;
|
|
236
240
|
void fetchOptions();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
watchDebounced(requestSignature, reactToRequest, { debounce: 200, immediate: true });
|
|
244
|
+
const quiescence = useDerivedQuiescence();
|
|
245
|
+
let disposed = false;
|
|
246
|
+
onScopeDispose(() => {
|
|
247
|
+
disposed = true;
|
|
248
|
+
});
|
|
249
|
+
function establishBaseline() {
|
|
250
|
+
if (disposed) return;
|
|
251
|
+
seedSettled = true;
|
|
252
|
+
reactToRequest();
|
|
253
|
+
}
|
|
254
|
+
seeded.then(() => quiescence.settled(), () => quiescence.settled()).then(establishBaseline);
|
|
240
255
|
function isEqual(a, b) {
|
|
241
256
|
if (a === b) return true;
|
|
242
257
|
if (a == null || b == null) return false;
|
|
@@ -53,7 +53,14 @@ if (config.value.id) {
|
|
|
53
53
|
}
|
|
54
54
|
const inherited = injectCELContext();
|
|
55
55
|
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
|
|
56
|
-
const
|
|
56
|
+
const seeded = (() => {
|
|
57
|
+
const current = state.value ?? {};
|
|
58
|
+
if (!config.value.initial || Object.keys(current).length > 0) return Promise.resolve();
|
|
59
|
+
return computeSeed(current).then((seed) => {
|
|
60
|
+
if (seed) state.value = { ...current, ...seed };
|
|
61
|
+
});
|
|
62
|
+
})();
|
|
63
|
+
const formState = provideFormState(state, "form", seeded);
|
|
57
64
|
const commitBus = provideCommitBus();
|
|
58
65
|
const quiescence = provideDerivedQuiescence();
|
|
59
66
|
const formReadonly = computed(() => {
|
|
@@ -79,13 +86,6 @@ async function computeSeed(base) {
|
|
|
79
86
|
return null;
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
|
-
const seeded = (() => {
|
|
83
|
-
const current = state.value ?? {};
|
|
84
|
-
if (!config.value.initial || Object.keys(current).length > 0) return Promise.resolve();
|
|
85
|
-
return computeSeed(current).then((seed) => {
|
|
86
|
-
if (seed) state.value = { ...current, ...seed };
|
|
87
|
-
});
|
|
88
|
-
})();
|
|
89
89
|
async function reseed() {
|
|
90
90
|
const seed = await computeSeed({});
|
|
91
91
|
state.value = seed ? { ...seed } : {};
|
|
@@ -33,6 +33,23 @@ export type FormStateBag = {
|
|
|
33
33
|
* through the bags themselves.
|
|
34
34
|
*/
|
|
35
35
|
parent: FormStateBag | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves once the form's `initial` seed has landed in `state` (or
|
|
38
|
+
* immediately when there is no seed). The top-level form host passes its
|
|
39
|
+
* own seed promise; nested bags (list/table rows) inherit the enclosing
|
|
40
|
+
* form's, and a host-less fallback bag gets an already-resolved one.
|
|
41
|
+
*
|
|
42
|
+
* A field runtime awaits this before it establishes any request/watch
|
|
43
|
+
* baseline: while the seed is in flight `state` is empty, so a request
|
|
44
|
+
* expression that reads `form.?dep` builds a param-less-but-non-`null`
|
|
45
|
+
* request. Establishing a baseline against that pre-seed request would make
|
|
46
|
+
* the post-seed signature look like a "change away" and wrongly clear the
|
|
47
|
+
* seeded selection. Keying the first baseline off THIS boundary (not off a
|
|
48
|
+
* `null` signature, which `.query`'s drop-missing-param semantics defeats)
|
|
49
|
+
* is the correct edge. May reject when the seed itself failed — consumers
|
|
50
|
+
* must still start on rejection (mirror `useDerived`'s `ready` handling).
|
|
51
|
+
*/
|
|
52
|
+
seeded: Promise<void>;
|
|
36
53
|
state: Ref<unknown>;
|
|
37
54
|
getAt: (path: string) => unknown;
|
|
38
55
|
/**
|
|
@@ -83,7 +100,7 @@ export type FormStateBag = {
|
|
|
83
100
|
coerce: (path: string, value: unknown) => unknown;
|
|
84
101
|
};
|
|
85
102
|
export declare const FORM_STATE_KEY: InjectionKey<FormStateBag>;
|
|
86
|
-
export declare function provideFormState(state: Ref<unknown>, kind?: FormStateBagKind): FormStateBag;
|
|
103
|
+
export declare function provideFormState(state: Ref<unknown>, kind?: FormStateBagKind, seeded?: Promise<void>): FormStateBag;
|
|
87
104
|
export declare function useFormState(): FormStateBag;
|
|
88
105
|
/**
|
|
89
106
|
* The nearest `'form'`-kind bag enclosing the current field — what the CEL
|
|
@@ -2,7 +2,7 @@ import { getProperty, setProperty } from "dot-prop";
|
|
|
2
2
|
import { inject, provide, ref } from "vue";
|
|
3
3
|
export const SELF_BINDING = ".";
|
|
4
4
|
export const FORM_STATE_KEY = Symbol("shwfed-form-state");
|
|
5
|
-
export function provideFormState(state, kind = "form") {
|
|
5
|
+
export function provideFormState(state, kind = "form", seeded) {
|
|
6
6
|
const parent = inject(FORM_STATE_KEY, void 0);
|
|
7
7
|
const dirty = ref(/* @__PURE__ */ new Set());
|
|
8
8
|
const coercions = /* @__PURE__ */ new Map();
|
|
@@ -27,6 +27,10 @@ export function provideFormState(state, kind = "form") {
|
|
|
27
27
|
const bag = {
|
|
28
28
|
kind,
|
|
29
29
|
parent,
|
|
30
|
+
// Explicit seed promise when given (the top-level form host), else inherit
|
|
31
|
+
// the enclosing form's (list/table rows), else an already-resolved one (a
|
|
32
|
+
// host-less fallback bag — the standalone table, a test harness).
|
|
33
|
+
seeded: seeded ?? parent?.seeded ?? Promise.resolve(),
|
|
30
34
|
state,
|
|
31
35
|
getAt: (path) => path === SELF_BINDING ? state.value : getProperty(state.value, path),
|
|
32
36
|
setAt: (path, value) => {
|
|
@@ -4,12 +4,12 @@ type __VLS_ModelProps = {
|
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
6
|
"update:modelValue": (value: {
|
|
7
|
-
readonly mode: "
|
|
7
|
+
readonly mode: "_self" | "_blank";
|
|
8
8
|
readonly url: string;
|
|
9
9
|
}) => any;
|
|
10
10
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
11
11
|
"onUpdate:modelValue"?: ((value: {
|
|
12
|
-
readonly mode: "
|
|
12
|
+
readonly mode: "_self" | "_blank";
|
|
13
13
|
readonly url: string;
|
|
14
14
|
}) => any) | undefined;
|
|
15
15
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -4,12 +4,12 @@ type __VLS_ModelProps = {
|
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
6
|
"update:modelValue": (value: {
|
|
7
|
-
readonly mode: "
|
|
7
|
+
readonly mode: "_self" | "_blank";
|
|
8
8
|
readonly url: string;
|
|
9
9
|
}) => any;
|
|
10
10
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
11
11
|
"onUpdate:modelValue"?: ((value: {
|
|
12
|
-
readonly mode: "
|
|
12
|
+
readonly mode: "_self" | "_blank";
|
|
13
13
|
readonly url: string;
|
|
14
14
|
}) => any) | undefined;
|
|
15
15
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue?: string;
|
|
3
3
|
placeholder?: string;
|
|
4
|
-
browseUrl?: string;
|
|
5
4
|
};
|
|
6
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
6
|
"update:modelValue": (value: string) => any;
|
|
@@ -1,48 +1,332 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { Icon } from "@iconify/vue";
|
|
3
|
+
import { useVirtualizer } from "@tanstack/vue-virtual";
|
|
4
|
+
import { useElementSize } from "@vueuse/core";
|
|
5
|
+
import { ScrollArea } from "reka-ui/namespaced";
|
|
6
|
+
import { computed, ref, shallowRef, useTemplateRef, watch } from "vue";
|
|
7
|
+
import { useI18n } from "vue-i18n";
|
|
8
|
+
import { Button } from "../button";
|
|
3
9
|
import {
|
|
4
10
|
InputGroup,
|
|
5
11
|
InputGroupAddon,
|
|
6
12
|
InputGroupButton,
|
|
7
13
|
InputGroupInput
|
|
8
14
|
} from "../input-group";
|
|
15
|
+
import { Popover, PopoverAnchor, PopoverContent } from "../popover";
|
|
16
|
+
import { ScrollBar } from "../scroll-area";
|
|
17
|
+
import { Tabs, TabsList, TabsTrigger } from "../tabs";
|
|
9
18
|
const props = defineProps({
|
|
10
19
|
modelValue: { type: String, required: false },
|
|
11
|
-
placeholder: { type: String, required: false }
|
|
12
|
-
browseUrl: { type: String, required: false }
|
|
20
|
+
placeholder: { type: String, required: false }
|
|
13
21
|
});
|
|
14
22
|
const emit = defineEmits(["update:modelValue"]);
|
|
15
|
-
const
|
|
23
|
+
const { t } = useI18n({
|
|
24
|
+
inheritLocale: true,
|
|
25
|
+
messages: {
|
|
26
|
+
zh: {
|
|
27
|
+
"icon-picker-placeholder": "Iconify \u56FE\u6807\u6807\u8BC6\u7B26\uFF0C\u5982 fluent:add-20-regular",
|
|
28
|
+
"icon-picker-browse": "\u6D4F\u89C8\u56FE\u6807",
|
|
29
|
+
"icon-picker-clear": "\u6E05\u7A7A",
|
|
30
|
+
"icon-picker-regular": "\u5E38\u89C4",
|
|
31
|
+
"icon-picker-filled": "\u586B\u5145",
|
|
32
|
+
"icon-picker-loading": "\u6B63\u5728\u52A0\u8F7D\u56FE\u6807\u2026",
|
|
33
|
+
"icon-picker-error": "\u56FE\u6807\u52A0\u8F7D\u5931\u8D25",
|
|
34
|
+
"icon-picker-retry": "\u91CD\u8BD5",
|
|
35
|
+
"icon-picker-empty": "\u65E0\u5339\u914D\u56FE\u6807"
|
|
36
|
+
},
|
|
37
|
+
ja: {
|
|
38
|
+
"icon-picker-placeholder": "Iconify \u30A2\u30A4\u30B3\u30F3 ID\uFF08\u4F8B\uFF1Afluent:add-20-regular\uFF09",
|
|
39
|
+
"icon-picker-browse": "\u30A2\u30A4\u30B3\u30F3\u3092\u53C2\u7167",
|
|
40
|
+
"icon-picker-clear": "\u30AF\u30EA\u30A2",
|
|
41
|
+
"icon-picker-regular": "\u30EC\u30AE\u30E5\u30E9\u30FC",
|
|
42
|
+
"icon-picker-filled": "\u30D5\u30A3\u30EB\u30C9",
|
|
43
|
+
"icon-picker-loading": "\u30A2\u30A4\u30B3\u30F3\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
|
|
44
|
+
"icon-picker-error": "\u30A2\u30A4\u30B3\u30F3\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
45
|
+
"icon-picker-retry": "\u518D\u8A66\u884C",
|
|
46
|
+
"icon-picker-empty": "\u4E00\u81F4\u3059\u308B\u30A2\u30A4\u30B3\u30F3\u304C\u3042\u308A\u307E\u305B\u3093"
|
|
47
|
+
},
|
|
48
|
+
en: {
|
|
49
|
+
"icon-picker-placeholder": "Iconify icon id, e.g. fluent:add-20-regular",
|
|
50
|
+
"icon-picker-browse": "Browse icons",
|
|
51
|
+
"icon-picker-clear": "Clear",
|
|
52
|
+
"icon-picker-regular": "Regular",
|
|
53
|
+
"icon-picker-filled": "Filled",
|
|
54
|
+
"icon-picker-loading": "Loading icons\u2026",
|
|
55
|
+
"icon-picker-error": "Failed to load icons",
|
|
56
|
+
"icon-picker-retry": "Retry",
|
|
57
|
+
"icon-picker-empty": "No matching icon"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const ICONIFY_API = "https://api.iconify.design";
|
|
62
|
+
const SUFFIX = {
|
|
63
|
+
regular: "-20-regular",
|
|
64
|
+
filled: "-20-filled"
|
|
65
|
+
};
|
|
66
|
+
let stemsCache = null;
|
|
67
|
+
function loadStems() {
|
|
68
|
+
if (!stemsCache) {
|
|
69
|
+
stemsCache = fetch(`${ICONIFY_API}/collection?prefix=fluent`).then(async (response) => {
|
|
70
|
+
if (!response.ok) throw new Error(`iconify collection: HTTP ${response.status}`);
|
|
71
|
+
const body = await response.json();
|
|
72
|
+
const hidden = new Set(body.hidden ?? []);
|
|
73
|
+
const names = [
|
|
74
|
+
...body.uncategorized ?? [],
|
|
75
|
+
...Object.values(body.categories ?? {}).flat()
|
|
76
|
+
].filter((name) => !hidden.has(name));
|
|
77
|
+
const collect = (variant2) => names.filter((name) => name.endsWith(SUFFIX[variant2])).map((name) => name.slice(0, -SUFFIX[variant2].length));
|
|
78
|
+
return { regular: collect("regular"), filled: collect("filled") };
|
|
79
|
+
}).catch((error) => {
|
|
80
|
+
stemsCache = null;
|
|
81
|
+
throw error;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return stemsCache;
|
|
85
|
+
}
|
|
86
|
+
const stems = shallowRef(null);
|
|
87
|
+
const status = ref("idle");
|
|
88
|
+
async function ensureStems() {
|
|
89
|
+
if (stems.value) return;
|
|
90
|
+
status.value = "loading";
|
|
91
|
+
try {
|
|
92
|
+
stems.value = await loadStems();
|
|
93
|
+
status.value = "ready";
|
|
94
|
+
} catch {
|
|
95
|
+
status.value = "error";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const tokens = computed(() => {
|
|
99
|
+
const raw = (props.modelValue ?? "").toLowerCase();
|
|
100
|
+
const body = raw.slice(raw.indexOf(":") + 1);
|
|
101
|
+
return body.replace(/-\d+(?:-(?:regular|filled|light))?$/, "").split(/[^a-z0-9]+/).filter(Boolean);
|
|
102
|
+
});
|
|
103
|
+
function match(variant2) {
|
|
104
|
+
const all = stems.value?.[variant2] ?? [];
|
|
105
|
+
if (!tokens.value.length) return all;
|
|
106
|
+
return all.filter((stem) => tokens.value.every((token) => stem.includes(token)));
|
|
107
|
+
}
|
|
108
|
+
const matches = computed(() => ({
|
|
109
|
+
regular: match("regular"),
|
|
110
|
+
filled: match("filled")
|
|
111
|
+
}));
|
|
112
|
+
const variant = ref("regular");
|
|
113
|
+
const results = computed(() => matches.value[variant.value]);
|
|
114
|
+
const tabs = computed(() => [
|
|
115
|
+
{ value: "regular", label: t("icon-picker-regular") },
|
|
116
|
+
{ value: "filled", label: t("icon-picker-filled") }
|
|
117
|
+
]);
|
|
118
|
+
const open = ref(false);
|
|
119
|
+
const anchor = useTemplateRef("anchor");
|
|
120
|
+
const viewport = useTemplateRef("viewport");
|
|
121
|
+
const scroller = computed(() => viewport.value?.viewportElement ?? null);
|
|
122
|
+
function openPanel() {
|
|
123
|
+
if (open.value) return;
|
|
124
|
+
variant.value = (props.modelValue ?? "").endsWith(SUFFIX.filled) ? "filled" : "regular";
|
|
125
|
+
open.value = true;
|
|
126
|
+
void ensureStems();
|
|
127
|
+
}
|
|
16
128
|
function onInput(value) {
|
|
17
129
|
emit("update:modelValue", value == null ? "" : String(value));
|
|
130
|
+
openPanel();
|
|
131
|
+
}
|
|
132
|
+
function select(stem) {
|
|
133
|
+
emit("update:modelValue", `fluent:${stem}${SUFFIX[variant.value]}`);
|
|
134
|
+
open.value = false;
|
|
135
|
+
}
|
|
136
|
+
function clear() {
|
|
137
|
+
emit("update:modelValue", "");
|
|
138
|
+
openPanel();
|
|
139
|
+
}
|
|
140
|
+
function onInteractOutside(event) {
|
|
141
|
+
const target = event.detail?.originalEvent?.target;
|
|
142
|
+
if (target instanceof Node && anchor.value?.contains(target)) event.preventDefault();
|
|
18
143
|
}
|
|
144
|
+
const CELL = 36;
|
|
145
|
+
const { width: scrollerWidth } = useElementSize(scroller);
|
|
146
|
+
const columns = computed(() => Math.max(1, Math.floor(scrollerWidth.value / CELL)));
|
|
147
|
+
const rowVirtualizer = useVirtualizer(
|
|
148
|
+
computed(() => {
|
|
149
|
+
const element = scroller.value;
|
|
150
|
+
return {
|
|
151
|
+
count: Math.ceil(results.value.length / columns.value),
|
|
152
|
+
estimateSize: () => CELL,
|
|
153
|
+
getScrollElement: () => element,
|
|
154
|
+
overscan: 4
|
|
155
|
+
};
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
const rowTotalSize = computed(() => rowVirtualizer.value.getTotalSize());
|
|
159
|
+
const rowWindow = computed(() => rowVirtualizer.value.getVirtualItems());
|
|
160
|
+
function rowStems(index) {
|
|
161
|
+
return results.value.slice(index * columns.value, (index + 1) * columns.value);
|
|
162
|
+
}
|
|
163
|
+
function isSelected(stem) {
|
|
164
|
+
return props.modelValue === `fluent:${stem}${SUFFIX[variant.value]}`;
|
|
165
|
+
}
|
|
166
|
+
watch([results, variant], () => {
|
|
167
|
+
if (scroller.value) scroller.value.scrollTop = 0;
|
|
168
|
+
});
|
|
19
169
|
</script>
|
|
20
170
|
|
|
21
171
|
<template>
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
172
|
+
<Popover v-model:open="open">
|
|
173
|
+
<div ref="anchor">
|
|
174
|
+
<PopoverAnchor as-child>
|
|
175
|
+
<InputGroup>
|
|
176
|
+
<InputGroupAddon align="inline-start">
|
|
177
|
+
<Icon
|
|
178
|
+
:icon="props.modelValue || 'fluent:image-20-regular'"
|
|
179
|
+
:class="props.modelValue ? 'text-zinc-600' : 'text-zinc-300'"
|
|
180
|
+
/>
|
|
181
|
+
</InputGroupAddon>
|
|
182
|
+
<InputGroupInput
|
|
183
|
+
:model-value="props.modelValue ?? ''"
|
|
184
|
+
:placeholder="props.placeholder ?? t('icon-picker-placeholder')"
|
|
185
|
+
class="font-mono text-xs"
|
|
186
|
+
@focus="openPanel"
|
|
187
|
+
@click="openPanel"
|
|
188
|
+
@update:model-value="onInput"
|
|
189
|
+
/>
|
|
190
|
+
<InputGroupAddon align="inline-end">
|
|
191
|
+
<InputGroupButton
|
|
192
|
+
v-if="props.modelValue"
|
|
193
|
+
size="icon-xs"
|
|
194
|
+
data-slot="icon-picker-clear"
|
|
195
|
+
:aria-label="t('icon-picker-clear')"
|
|
196
|
+
tabindex="-1"
|
|
197
|
+
class="text-zinc-500 transition-opacity hover:text-zinc-700 [@media(hover:hover)]:opacity-0 group-focus-within/input-group:opacity-100 group-hover/input-group:opacity-100"
|
|
198
|
+
@mousedown.prevent
|
|
199
|
+
@click.stop="clear"
|
|
200
|
+
>
|
|
201
|
+
<Icon icon="fluent:dismiss-20-regular" />
|
|
202
|
+
</InputGroupButton>
|
|
203
|
+
<InputGroupButton
|
|
204
|
+
size="icon-xs"
|
|
205
|
+
data-slot="icon-picker-toggle"
|
|
206
|
+
:aria-label="t('icon-picker-browse')"
|
|
207
|
+
:aria-expanded="open"
|
|
208
|
+
@click="open ? open = false : openPanel()"
|
|
209
|
+
>
|
|
210
|
+
<Icon
|
|
211
|
+
icon="fluent:chevron-down-20-regular"
|
|
212
|
+
class="transition-transform duration-150"
|
|
213
|
+
:class="open && 'rotate-180'"
|
|
214
|
+
/>
|
|
215
|
+
</InputGroupButton>
|
|
216
|
+
</InputGroupAddon>
|
|
217
|
+
</InputGroup>
|
|
218
|
+
</PopoverAnchor>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<PopoverContent
|
|
222
|
+
align="start"
|
|
223
|
+
class="w-(--reka-popover-trigger-width) min-w-72 p-0"
|
|
224
|
+
@open-auto-focus="(event) => event.preventDefault()"
|
|
225
|
+
@interact-outside="onInteractOutside"
|
|
226
|
+
>
|
|
227
|
+
<Tabs
|
|
228
|
+
v-model="variant"
|
|
229
|
+
class="gap-0"
|
|
43
230
|
>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
231
|
+
<TabsList class="px-3">
|
|
232
|
+
<TabsTrigger
|
|
233
|
+
v-for="tab in tabs"
|
|
234
|
+
:key="tab.value"
|
|
235
|
+
:value="tab.value"
|
|
236
|
+
class="h-9 px-4 text-xs"
|
|
237
|
+
>
|
|
238
|
+
{{ tab.label }}
|
|
239
|
+
<span class="text-[0.625rem] text-zinc-400 tabular-nums">
|
|
240
|
+
{{ matches[tab.value].length }}
|
|
241
|
+
</span>
|
|
242
|
+
</TabsTrigger>
|
|
243
|
+
</TabsList>
|
|
244
|
+
|
|
245
|
+
<!-- Only the grid scrolls: the placeholder states have nothing to
|
|
246
|
+
scroll, and `h-full` wouldn't resolve for them inside the viewport's
|
|
247
|
+
`display: table` content wrapper anyway. -->
|
|
248
|
+
<section
|
|
249
|
+
v-if="status === 'loading' || status === 'idle'"
|
|
250
|
+
class="h-64 flex items-center justify-center gap-2 text-sm text-zinc-500 select-none"
|
|
251
|
+
>
|
|
252
|
+
<Icon
|
|
253
|
+
icon="fluent:spinner-ios-20-regular"
|
|
254
|
+
class="animate-spin"
|
|
255
|
+
/>
|
|
256
|
+
{{ t("icon-picker-loading") }}
|
|
257
|
+
</section>
|
|
258
|
+
|
|
259
|
+
<section
|
|
260
|
+
v-else-if="status === 'error'"
|
|
261
|
+
class="h-64 flex flex-col items-center justify-center gap-2 text-sm text-zinc-500 select-none"
|
|
262
|
+
>
|
|
263
|
+
<Icon
|
|
264
|
+
icon="fluent:plug-disconnected-20-regular"
|
|
265
|
+
class="text-2xl! text-zinc-400"
|
|
266
|
+
/>
|
|
267
|
+
{{ t("icon-picker-error") }}
|
|
268
|
+
<Button
|
|
269
|
+
size="sm"
|
|
270
|
+
@click="ensureStems"
|
|
271
|
+
>
|
|
272
|
+
{{ t("icon-picker-retry") }}
|
|
273
|
+
</Button>
|
|
274
|
+
</section>
|
|
275
|
+
|
|
276
|
+
<section
|
|
277
|
+
v-else-if="!results.length"
|
|
278
|
+
class="h-64 flex flex-col items-center justify-center gap-2 text-sm text-zinc-500 select-none"
|
|
279
|
+
>
|
|
280
|
+
<Icon
|
|
281
|
+
icon="fluent:search-20-regular"
|
|
282
|
+
class="text-2xl! text-zinc-400"
|
|
283
|
+
/>
|
|
284
|
+
{{ t("icon-picker-empty") }}
|
|
285
|
+
</section>
|
|
286
|
+
|
|
287
|
+
<ScrollArea.Root
|
|
288
|
+
v-else
|
|
289
|
+
class="relative h-64"
|
|
290
|
+
>
|
|
291
|
+
<ScrollArea.Viewport
|
|
292
|
+
ref="viewport"
|
|
293
|
+
class="size-full overscroll-contain p-2 outline-none"
|
|
294
|
+
>
|
|
295
|
+
<div
|
|
296
|
+
class="relative w-full"
|
|
297
|
+
:style="{ height: `${rowTotalSize}px` }"
|
|
298
|
+
>
|
|
299
|
+
<div
|
|
300
|
+
v-for="row in rowWindow"
|
|
301
|
+
:key="String(row.key)"
|
|
302
|
+
class="absolute top-0 left-0 grid w-full"
|
|
303
|
+
:style="{
|
|
304
|
+
height: `${row.size}px`,
|
|
305
|
+
transform: `translateY(${row.start}px)`,
|
|
306
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`
|
|
307
|
+
}"
|
|
308
|
+
>
|
|
309
|
+
<button
|
|
310
|
+
v-for="stem in rowStems(row.index)"
|
|
311
|
+
:key="stem"
|
|
312
|
+
type="button"
|
|
313
|
+
:title="`fluent:${stem}${SUFFIX[variant]}`"
|
|
314
|
+
class="flex items-center justify-center rounded text-zinc-600 transition-colors duration-150 hover:bg-zinc-100 hover:text-zinc-900"
|
|
315
|
+
:class="isSelected(stem) && 'bg-zinc-100 text-(--primary)'"
|
|
316
|
+
@click="select(stem)"
|
|
317
|
+
>
|
|
318
|
+
<Icon
|
|
319
|
+
:icon="`fluent:${stem}${SUFFIX[variant]}`"
|
|
320
|
+
class="text-xl!"
|
|
321
|
+
/>
|
|
322
|
+
</button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</ScrollArea.Viewport>
|
|
326
|
+
<ScrollBar orientation="vertical" />
|
|
327
|
+
<ScrollArea.Corner />
|
|
328
|
+
</ScrollArea.Root>
|
|
329
|
+
</Tabs>
|
|
330
|
+
</PopoverContent>
|
|
331
|
+
</Popover>
|
|
48
332
|
</template>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue?: string;
|
|
3
3
|
placeholder?: string;
|
|
4
|
-
browseUrl?: string;
|
|
5
4
|
};
|
|
6
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
6
|
"update:modelValue": (value: string) => any;
|
|
@@ -13,6 +13,7 @@ const delegatedProps = reactiveOmit(props, "class");
|
|
|
13
13
|
const barEl = ref(null);
|
|
14
14
|
const barStyle = shallowRef({});
|
|
15
15
|
const ready = ref(false);
|
|
16
|
+
const animated = ref(false);
|
|
16
17
|
let mo = null;
|
|
17
18
|
let ro = null;
|
|
18
19
|
function measure() {
|
|
@@ -29,7 +30,15 @@ function measure() {
|
|
|
29
30
|
width: `${rect.width}px`,
|
|
30
31
|
transform: `translateX(${rect.left - rootRect.left}px)`
|
|
31
32
|
};
|
|
33
|
+
if (ready.value) return;
|
|
32
34
|
ready.value = true;
|
|
35
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
36
|
+
animated.value = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
40
|
+
animated.value = true;
|
|
41
|
+
}));
|
|
33
42
|
}
|
|
34
43
|
function schedule() {
|
|
35
44
|
nextTick(measure);
|
|
@@ -79,7 +88,8 @@ onBeforeUnmount(() => {
|
|
|
79
88
|
ref="barEl"
|
|
80
89
|
data-slot="tabs-active-bar"
|
|
81
90
|
aria-hidden="true"
|
|
82
|
-
class="pointer-events-none absolute -bottom-px left-0 z-1 h-0.5 bg-(--primary)
|
|
91
|
+
class="pointer-events-none absolute -bottom-px left-0 z-1 h-0.5 bg-(--primary)"
|
|
92
|
+
:class="animated && 'transition-[width,transform] duration-300 ease-[cubic-bezier(0.645,0.045,0.355,1)]'"
|
|
83
93
|
:style="{ ...barStyle, visibility: ready ? 'visible' : 'hidden' }"
|
|
84
94
|
/>
|
|
85
95
|
</TabsList>
|