@sparklingopinions/sparkling-ui 0.7.0 → 0.9.1
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/README.md +2 -2
- package/dist/aliases.d.ts +23 -2
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/Button.vue.d.ts +4 -0
- package/dist/components/Button/Button.vue_vue_type_script_setup_true_lang.js +21 -11
- package/dist/components/Button/Button.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.vue_vue_type_script_setup_true_lang.js +29 -20
- package/dist/components/ContextMenu/ContextMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DataList/DataList.js.map +1 -1
- package/dist/components/DataList/DataList.vue.d.ts +30 -3
- package/dist/components/DataList/DataList.vue_vue_type_script_setup_true_lang.js +188 -100
- package/dist/components/DataList/DataList.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/DataList/types.d.ts +2 -0
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.vue_vue_type_script_setup_true_lang.js +32 -23
- package/dist/components/DropdownMenu/DropdownMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/Icon/iconSet.js +1 -0
- package/dist/components/Icon/iconSet.js.map +1 -1
- package/dist/components/Icon/resolver.js +3 -2
- package/dist/components/Icon/resolver.js.map +1 -1
- package/dist/components/Popover/PopoverDialog.js +7 -0
- package/dist/components/Popover/PopoverDialog.js.map +1 -0
- package/dist/components/Popover/PopoverDialog.vue.d.ts +56 -0
- package/dist/components/Popover/PopoverDialog.vue_vue_type_script_setup_true_lang.js +158 -0
- package/dist/components/Popover/PopoverDialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/Popover/index.d.ts +2 -0
- package/dist/components/PropertyList/Editable.js.map +1 -1
- package/dist/components/PropertyList/Editable.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/components/PropertyList/Editable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/Select.vue_vue_type_script_setup_true_lang.js +189 -139
- package/dist/components/Select/Select.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/Select/types.d.ts +5 -0
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Table/Table.vue.d.ts +2 -0
- package/dist/components/Table/Table.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/components/Table/Table.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Tag/Tag.vue.d.ts +13 -3
- package/dist/components/Tag/Tag.vue_vue_type_script_setup_true_lang.js +41 -27
- package/dist/components/Tag/Tag.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/Toast/Toaster.js.map +1 -1
- package/dist/components/Toast/Toaster.vue.d.ts +4 -0
- package/dist/components/Toast/Toaster.vue_vue_type_script_setup_true_lang.js +49 -32
- package/dist/components/Toast/Toaster.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/Toolbar/Toolbar.js +7 -0
- package/dist/components/Toolbar/Toolbar.js.map +1 -0
- package/dist/components/Toolbar/Toolbar.vue.d.ts +23 -0
- package/dist/components/Toolbar/Toolbar.vue_vue_type_script_setup_true_lang.js +52 -0
- package/dist/components/Toolbar/Toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/Toolbar/index.d.ts +2 -0
- package/dist/components/internal/MenuItemContent.js.map +1 -1
- package/dist/components/internal/MenuItemContent.vue.d.ts +1 -0
- package/dist/components/internal/MenuItemContent.vue_vue_type_script_setup_true_lang.js +30 -14
- package/dist/components/internal/MenuItemContent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/internal/menu.d.ts +5 -1
- package/dist/components/internal/menu.js +17 -14
- package/dist/components/internal/menu.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -16
- package/package.json +1 -1
- package/src/styles/tailwind.css +22 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ComponentPublicInstance, type MaybeRef } from 'vue';
|
|
2
|
+
import { type FilterTriggerProps } from '../Filters/trigger';
|
|
3
|
+
export type PopoverDialogTriggerProps = FilterTriggerProps;
|
|
4
|
+
export interface PopoverDialogProps {
|
|
5
|
+
title: string;
|
|
6
|
+
/** What it opens under on the desktop: an element, a component, or a ref to one. Without it, the `trigger` slot. */
|
|
7
|
+
anchor?: MaybeRef<HTMLElement | ComponentPublicInstance | null | undefined>;
|
|
8
|
+
/** The popover's width in px, or a CSS width. */
|
|
9
|
+
width?: number | string;
|
|
10
|
+
/** Shows Cancel and this confirm button in the footer; it emits `confirm`, as ⌘ or Ctrl Enter does. */
|
|
11
|
+
confirmLabel?: string;
|
|
12
|
+
confirmDisabled?: boolean;
|
|
13
|
+
cancelLabel?: string;
|
|
14
|
+
closeLabel?: string;
|
|
15
|
+
/** The viewport width in px below which it's a bottom sheet. */
|
|
16
|
+
sheetBelow?: number;
|
|
17
|
+
}
|
|
18
|
+
type __VLS_Props = PopoverDialogProps;
|
|
19
|
+
type __VLS_Slots = {
|
|
20
|
+
default?: () => unknown;
|
|
21
|
+
/** Replaces Cancel and the confirm button. */
|
|
22
|
+
footer?: () => unknown;
|
|
23
|
+
/** The opener, when there's no `anchor`: spread `props` on your button. */
|
|
24
|
+
trigger?: (props: {
|
|
25
|
+
props: PopoverDialogTriggerProps;
|
|
26
|
+
open: boolean;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
};
|
|
29
|
+
type __VLS_ModelProps = {
|
|
30
|
+
'open'?: boolean;
|
|
31
|
+
};
|
|
32
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
33
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
34
|
+
cancel: () => any;
|
|
35
|
+
confirm: () => any;
|
|
36
|
+
"update:open": (value: boolean) => any;
|
|
37
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
38
|
+
onCancel?: (() => any) | undefined;
|
|
39
|
+
onConfirm?: (() => any) | undefined;
|
|
40
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
41
|
+
}>, {
|
|
42
|
+
anchor: HTMLElement | ComponentPublicInstance | import("vue").Ref<HTMLElement | ComponentPublicInstance | null | undefined, HTMLElement | ComponentPublicInstance | null | undefined> | import("vue").ShallowRef<HTMLElement | ComponentPublicInstance | null | undefined, HTMLElement | ComponentPublicInstance | null | undefined> | import("vue").WritableComputedRef<HTMLElement | ComponentPublicInstance | null | undefined, HTMLElement | ComponentPublicInstance | null | undefined> | null;
|
|
43
|
+
width: string | number;
|
|
44
|
+
confirmLabel: string;
|
|
45
|
+
cancelLabel: string;
|
|
46
|
+
closeLabel: string;
|
|
47
|
+
sheetBelow: number;
|
|
48
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
49
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
50
|
+
declare const _default: typeof __VLS_export;
|
|
51
|
+
export default _default;
|
|
52
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
53
|
+
new (): {
|
|
54
|
+
$slots: S;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import e from "../Button/Button.js";
|
|
2
|
+
import t from "../Dialog/Dialog.js";
|
|
3
|
+
import { useCustomTrigger as n } from "../Filters/trigger.js";
|
|
4
|
+
import { Fragment as r, computed as i, createBlock as a, createCommentVNode as o, createElementBlock as s, createElementVNode as c, createSlots as l, createTextVNode as u, createVNode as d, defineComponent as f, mergeModels as p, normalizeStyle as m, onBeforeUnmount as h, openBlock as g, ref as _, renderSlot as v, shallowRef as y, toDisplayString as b, toValue as x, unref as S, useModel as C, useSlots as w, watch as T, withCtx as E, withKeys as D, withModifiers as O } from "vue";
|
|
5
|
+
import { PopoverAnchor as k, PopoverContent as A, PopoverPortal as j, PopoverRoot as M } from "reka-ui";
|
|
6
|
+
//#region src/components/Popover/PopoverDialog.vue?vue&type=script&setup=true&lang.ts
|
|
7
|
+
var N = { class: "m-0 text-sm font-semibold text-sui-ink" }, P = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "flex items-center justify-end gap-2 pt-0.5"
|
|
10
|
+
}, F = /*@__PURE__*/ f({
|
|
11
|
+
__name: "PopoverDialog",
|
|
12
|
+
props: /*@__PURE__*/ p({
|
|
13
|
+
title: {},
|
|
14
|
+
anchor: { default: void 0 },
|
|
15
|
+
width: { default: 320 },
|
|
16
|
+
confirmLabel: { default: void 0 },
|
|
17
|
+
confirmDisabled: { type: Boolean },
|
|
18
|
+
cancelLabel: { default: "Cancel" },
|
|
19
|
+
closeLabel: { default: "Close" },
|
|
20
|
+
sheetBelow: { default: 640 }
|
|
21
|
+
}, {
|
|
22
|
+
open: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: !1
|
|
25
|
+
},
|
|
26
|
+
openModifiers: {}
|
|
27
|
+
}),
|
|
28
|
+
emits: /*@__PURE__*/ p(["confirm", "cancel"], ["update:open"]),
|
|
29
|
+
setup(f, { emit: p }) {
|
|
30
|
+
let F = f, I = p, L = C(f, "open"), R = w(), z = n(L), B = y(null), V = i(() => ({
|
|
31
|
+
...z.props.value,
|
|
32
|
+
ref: (e) => {
|
|
33
|
+
z.props.value.ref(e), B.value = e instanceof Element ? e : e?.$el ?? null;
|
|
34
|
+
}
|
|
35
|
+
})), H = i(() => {
|
|
36
|
+
let e = x(F.anchor);
|
|
37
|
+
return e instanceof Element ? e : e?.$el ?? B.value ?? void 0;
|
|
38
|
+
}), U = _(!1), W, G = () => U.value = !!W?.matches;
|
|
39
|
+
T(() => F.sheetBelow, (e) => {
|
|
40
|
+
W?.removeEventListener("change", G), W = typeof window < "u" && window.matchMedia ? window.matchMedia(`(max-width: ${e - .02}px)`) : void 0, W?.addEventListener("change", G), G();
|
|
41
|
+
}, { immediate: !0 }), h(() => W?.removeEventListener("change", G));
|
|
42
|
+
let K = i(() => U.value ? "sheet" : H.value ? "popover" : "dialog"), q = null, J = !1;
|
|
43
|
+
T(L, (e) => {
|
|
44
|
+
if (!e) return;
|
|
45
|
+
let t = document.activeElement;
|
|
46
|
+
q = t && t !== document.body ? t : null, J = !1;
|
|
47
|
+
});
|
|
48
|
+
function Y() {
|
|
49
|
+
I("cancel"), L.value = !1;
|
|
50
|
+
}
|
|
51
|
+
function X(e) {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
let t = e.target.querySelector("[role=\"dialog\"]") ?? e.target;
|
|
54
|
+
(t.querySelector("[autofocus]") ?? t).focus();
|
|
55
|
+
}
|
|
56
|
+
function Z(e) {
|
|
57
|
+
e.preventDefault(), !J && (q?.isConnected ? q : H.value)?.focus();
|
|
58
|
+
}
|
|
59
|
+
function Q(e) {
|
|
60
|
+
let t = e.detail.originalEvent.target;
|
|
61
|
+
t instanceof Node && B.value?.contains(t) ? e.preventDefault() : (J = !0, I("cancel"));
|
|
62
|
+
}
|
|
63
|
+
function $(e) {
|
|
64
|
+
(e.metaKey || e.ctrlKey) && (e.preventDefault(), F.confirmLabel && !F.confirmDisabled && I("confirm"));
|
|
65
|
+
}
|
|
66
|
+
let ee = i(() => typeof F.width == "number" ? `${F.width}px` : F.width);
|
|
67
|
+
return (n, i) => (g(), s(r, null, [S(R).trigger ? v(n.$slots, "trigger", {
|
|
68
|
+
open: L.value,
|
|
69
|
+
props: V.value
|
|
70
|
+
}, void 0, void 0, 0) : o("", !0), K.value === "popover" ? (g(), a(S(M), {
|
|
71
|
+
key: 1,
|
|
72
|
+
open: L.value,
|
|
73
|
+
"onUpdate:open": i[3] ||= (e) => L.value = e
|
|
74
|
+
}, {
|
|
75
|
+
default: E(() => [d(S(k), { reference: H.value }, null, 8, ["reference"]), d(S(j), null, {
|
|
76
|
+
default: E(() => [d(S(A), {
|
|
77
|
+
align: "start",
|
|
78
|
+
"aria-label": f.title,
|
|
79
|
+
class: "z-[1100] flex max-h-(--reka-popover-content-available-height) flex-col gap-2.5 overflow-y-auto rounded-lg border border-sui-line-subtle bg-sui-surface px-4 py-3.5 text-sm text-sui-ink shadow-xl outline-hidden data-[state=closed]:animate-sui-overlay-out data-[state=open]:animate-sui-overlay-in",
|
|
80
|
+
"collision-padding": 8,
|
|
81
|
+
"data-sui-panel": S(z).panelKey,
|
|
82
|
+
role: "dialog",
|
|
83
|
+
side: "bottom",
|
|
84
|
+
"side-offset": 4,
|
|
85
|
+
style: m({ width: ee.value }),
|
|
86
|
+
onCloseAutoFocus: Z,
|
|
87
|
+
onEscapeKeyDown: i[1] ||= (e) => I("cancel"),
|
|
88
|
+
onFocusOutside: i[2] ||= O(() => {}, ["prevent"]),
|
|
89
|
+
onKeydown: D($, ["enter"]),
|
|
90
|
+
onOpenAutoFocus: X,
|
|
91
|
+
onPointerDownOutside: Q
|
|
92
|
+
}, {
|
|
93
|
+
default: E(() => [
|
|
94
|
+
c("h2", N, b(f.title), 1),
|
|
95
|
+
v(n.$slots, "default"),
|
|
96
|
+
n.$slots.footer || f.confirmLabel ? (g(), s("div", P, [v(n.$slots, "footer", {}, () => [d(e, {
|
|
97
|
+
size: "sm",
|
|
98
|
+
type: "button",
|
|
99
|
+
variant: "ghost",
|
|
100
|
+
onClick: Y
|
|
101
|
+
}, {
|
|
102
|
+
default: E(() => [u(b(f.cancelLabel), 1)]),
|
|
103
|
+
_: 1
|
|
104
|
+
}), d(e, {
|
|
105
|
+
disabled: f.confirmDisabled,
|
|
106
|
+
size: "sm",
|
|
107
|
+
type: "button",
|
|
108
|
+
variant: "primary",
|
|
109
|
+
onClick: i[0] ||= (e) => I("confirm")
|
|
110
|
+
}, {
|
|
111
|
+
default: E(() => [u(b(f.confirmLabel), 1)]),
|
|
112
|
+
_: 1
|
|
113
|
+
}, 8, ["disabled"])])])) : o("", !0)
|
|
114
|
+
]),
|
|
115
|
+
_: 3
|
|
116
|
+
}, 8, [
|
|
117
|
+
"aria-label",
|
|
118
|
+
"data-sui-panel",
|
|
119
|
+
"style"
|
|
120
|
+
])]),
|
|
121
|
+
_: 3
|
|
122
|
+
})]),
|
|
123
|
+
_: 3
|
|
124
|
+
}, 8, ["open"])) : (g(), a(t, {
|
|
125
|
+
key: 2,
|
|
126
|
+
open: L.value,
|
|
127
|
+
"onUpdate:open": i[4] ||= (e) => L.value = e,
|
|
128
|
+
"cancel-label": f.cancelLabel,
|
|
129
|
+
"close-label": f.closeLabel,
|
|
130
|
+
"confirm-disabled": f.confirmDisabled,
|
|
131
|
+
"confirm-label": f.confirmLabel,
|
|
132
|
+
presentation: K.value === "sheet" ? "sheet" : "dialog",
|
|
133
|
+
size: "sm",
|
|
134
|
+
title: f.title,
|
|
135
|
+
onCancel: i[5] ||= (e) => I("cancel"),
|
|
136
|
+
onConfirm: i[6] ||= (e) => I("confirm")
|
|
137
|
+
}, l({
|
|
138
|
+
default: E(() => [v(n.$slots, "default")]),
|
|
139
|
+
_: 2
|
|
140
|
+
}, [n.$slots.footer ? {
|
|
141
|
+
name: "footer",
|
|
142
|
+
fn: E(() => [v(n.$slots, "footer")]),
|
|
143
|
+
key: "0"
|
|
144
|
+
} : void 0]), 1032, [
|
|
145
|
+
"open",
|
|
146
|
+
"cancel-label",
|
|
147
|
+
"close-label",
|
|
148
|
+
"confirm-disabled",
|
|
149
|
+
"confirm-label",
|
|
150
|
+
"presentation",
|
|
151
|
+
"title"
|
|
152
|
+
]))], 64));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
//#endregion
|
|
156
|
+
export { F as default };
|
|
157
|
+
|
|
158
|
+
//# sourceMappingURL=PopoverDialog.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PopoverDialog.vue_vue_type_script_setup_true_lang.js","names":["$slots","_useModel","_openBlock","_createElementBlock","_Fragment","_unref","_renderSlot","_createCommentVNode","_createBlock","_withCtx","_createVNode","_normalizeStyle","_withModifiers","_withKeys","_createElementVNode","_toDisplayString","Button","_createTextVNode","Dialog","_createSlots","Button_default","Dialog_default","_hoisted_1","_hoisted_2","PopoverDialog_vue_vue_type_script_setup_true_lang_default","\"open\"","\"openModifiers\"","__emit","props","__props","emit","open","useModel","slots","useSlots","trigger","useCustomTrigger","triggerElement","shallowRef","triggerProps","computed","node","anchorElement","value","toValue","narrow","ref","query","onQuery","watch","below","onBeforeUnmount","presentation","returnTo","pressedOutside","isOpen","active","cancel","onOpenAutoFocus","panel","event","onCloseAutoFocus","onPointerDownOutside","target","confirmFromKeyboard","panelWidth","openBlock","createElementBlock","Fragment","unref","renderSlot","_ctx","createCommentVNode","createBlock","PopoverRoot","_cache","$event","withCtx","createVNode","PopoverAnchor","PopoverPortal","PopoverContent","normalizeStyle","withModifiers","withKeys","createElementVNode","toDisplayString","createTextVNode","createSlots"],"sources":["../../../src/components/Popover/PopoverDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n type ComponentPublicInstance,\n type MaybeRef,\n computed,\n onBeforeUnmount,\n ref,\n shallowRef,\n toValue,\n useSlots,\n watch\n} from 'vue'\n\nimport { PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot } from 'reka-ui'\n\nimport Button from '../Button/Button.vue'\nimport Dialog from '../Dialog/Dialog.vue'\nimport { type FilterTriggerProps, useCustomTrigger } from '../Filters/trigger'\n\nexport type PopoverDialogTriggerProps = FilterTriggerProps\n\nexport interface PopoverDialogProps {\n title: string\n /** What it opens under on the desktop: an element, a component, or a ref to one. Without it, the `trigger` slot. */\n anchor?: MaybeRef<HTMLElement | ComponentPublicInstance | null | undefined>\n /** The popover's width in px, or a CSS width. */\n width?: number | string\n /** Shows Cancel and this confirm button in the footer; it emits `confirm`, as ⌘ or Ctrl Enter does. */\n confirmLabel?: string\n confirmDisabled?: boolean\n cancelLabel?: string\n closeLabel?: string\n /** The viewport width in px below which it's a bottom sheet. */\n sheetBelow?: number\n}\n\nconst props = withDefaults(defineProps<PopoverDialogProps>(), {\n anchor: undefined,\n width: 320,\n confirmLabel: undefined,\n cancelLabel: 'Cancel',\n closeLabel: 'Close',\n sheetBelow: 640\n})\n\nconst emit = defineEmits<{ confirm: []; cancel: [] }>()\n\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n default?: () => unknown\n /** Replaces Cancel and the confirm button. */\n footer?: () => unknown\n /** The opener, when there's no `anchor`: spread `props` on your button. */\n trigger?: (props: { props: PopoverDialogTriggerProps; open: boolean }) => unknown\n}>()\n\nconst slots = useSlots()\nconst trigger = useCustomTrigger(open)\nconst triggerElement = shallowRef<HTMLElement | null>(null)\nconst triggerProps = computed<PopoverDialogTriggerProps>(() => ({\n ...trigger.props.value,\n ref: (node) => {\n trigger.props.value.ref(node)\n triggerElement.value = node instanceof Element ? (node as HTMLElement) : ((node?.$el as HTMLElement) ?? null)\n }\n}))\n\nconst anchorElement = computed(() => {\n const value = toValue(props.anchor)\n if (value instanceof Element) return value as HTMLElement\n return (value?.$el as HTMLElement | undefined) ?? triggerElement.value ?? undefined\n})\n\nconst narrow = ref(false)\nlet query: MediaQueryList | undefined\nconst onQuery = () => (narrow.value = !!query?.matches)\nwatch(\n () => props.sheetBelow,\n (below) => {\n query?.removeEventListener('change', onQuery)\n query =\n typeof window !== 'undefined' && window.matchMedia\n ? window.matchMedia(`(max-width: ${below - 0.02}px)`)\n : undefined\n query?.addEventListener('change', onQuery)\n onQuery()\n },\n { immediate: true }\n)\nonBeforeUnmount(() => query?.removeEventListener('change', onQuery))\n\n// A popover needs something to sit under; without one it's a small dialog, and on a phone always a sheet.\nconst presentation = computed(() => (narrow.value ? 'sheet' : anchorElement.value ? 'popover' : 'dialog'))\n\n// Focus goes back to what had it when it opened (a row opened by a shortcut, a button), else the anchor; a press\n// outside leaves it where it landed.\nlet returnTo: HTMLElement | null = null\nlet pressedOutside = false\nwatch(open, (isOpen) => {\n if (!isOpen) return\n const active = document.activeElement as HTMLElement | null\n returnTo = active && active !== document.body ? active : null\n pressedOutside = false\n})\n\nfunction cancel() {\n emit('cancel')\n open.value = false\n}\n\nfunction onOpenAutoFocus(event: Event) {\n event.preventDefault()\n const panel =\n (event.target as HTMLElement).querySelector<HTMLElement>('[role=\"dialog\"]') ?? (event.target as HTMLElement)\n ;(panel.querySelector<HTMLElement>('[autofocus]') ?? panel).focus()\n}\n\nfunction onCloseAutoFocus(event: Event) {\n event.preventDefault()\n if (pressedOutside) return\n const target = returnTo?.isConnected ? returnTo : anchorElement.value\n target?.focus()\n}\n\nfunction onPointerDownOutside(event: CustomEvent<{ originalEvent: PointerEvent }>) {\n const target = event.detail.originalEvent.target\n if (target instanceof Node && triggerElement.value?.contains(target)) {\n event.preventDefault()\n return\n }\n pressedOutside = true\n emit('cancel')\n}\n\nfunction confirmFromKeyboard(event: KeyboardEvent) {\n if (!event.metaKey && !event.ctrlKey) return\n event.preventDefault()\n if (props.confirmLabel && !props.confirmDisabled) emit('confirm')\n}\n\nconst panelWidth = computed(() => (typeof props.width === 'number' ? `${props.width}px` : props.width))\n</script>\n\n<!-- A small decision: a popover under its anchor on the desktop, a bottom sheet on the phone. -->\n<template>\n <slot v-if=\"slots.trigger\" name=\"trigger\" :open=\"open\" :props=\"triggerProps\" />\n <PopoverRoot v-if=\"presentation === 'popover'\" v-model:open=\"open\">\n <PopoverAnchor :reference=\"anchorElement\" />\n <PopoverPortal>\n <PopoverContent\n align=\"start\"\n :aria-label=\"title\"\n class=\"z-[1100] flex max-h-(--reka-popover-content-available-height) flex-col gap-2.5 overflow-y-auto rounded-lg border border-sui-line-subtle bg-sui-surface px-4 py-3.5 text-sm text-sui-ink shadow-xl outline-hidden data-[state=closed]:animate-sui-overlay-out data-[state=open]:animate-sui-overlay-in\"\n :collision-padding=\"8\"\n :data-sui-panel=\"trigger.panelKey\"\n role=\"dialog\"\n side=\"bottom\"\n :side-offset=\"4\"\n :style=\"{ width: panelWidth }\"\n @close-auto-focus=\"onCloseAutoFocus\"\n @escape-key-down=\"emit('cancel')\"\n @focus-outside.prevent\n @keydown.enter=\"confirmFromKeyboard\"\n @open-auto-focus=\"onOpenAutoFocus\"\n @pointer-down-outside=\"onPointerDownOutside\"\n >\n <h2 class=\"m-0 text-sm font-semibold text-sui-ink\">{{ title }}</h2>\n <slot />\n <div v-if=\"$slots.footer || confirmLabel\" class=\"flex items-center justify-end gap-2 pt-0.5\">\n <slot name=\"footer\">\n <Button size=\"sm\" type=\"button\" variant=\"ghost\" @click=\"cancel\">{{ cancelLabel }}</Button>\n <Button :disabled=\"confirmDisabled\" size=\"sm\" type=\"button\" variant=\"primary\" @click=\"emit('confirm')\">\n {{ confirmLabel }}\n </Button>\n </slot>\n </div>\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n <Dialog\n v-else\n v-model:open=\"open\"\n :cancel-label=\"cancelLabel\"\n :close-label=\"closeLabel\"\n :confirm-disabled=\"confirmDisabled\"\n :confirm-label=\"confirmLabel\"\n :presentation=\"presentation === 'sheet' ? 'sheet' : 'dialog'\"\n size=\"sm\"\n :title=\"title\"\n @cancel=\"emit('cancel')\"\n @confirm=\"emit('confirm')\"\n >\n <slot />\n <template v-if=\"$slots.footer\" #footer><slot name=\"footer\" /></template>\n </Dialog>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Editable.js","names":["_sfc_main","Editable_vue_vue_type_script_setup_true_lang_default","Editable_default"],"sources":["../../../src/components/PropertyList/Editable.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, toRaw, useAttrs, useTemplateRef, watch } from 'vue'\n\nimport Select from '../Select/Select.vue'\nimport type { SelectOption, SelectValue } from '../Select/types'\nimport Tag from '../Tag/Tag.vue'\nimport Textarea from '../Textarea/Textarea.vue'\n\nexport interface EditableProps {\n /** `text` (one line), `multiline` (Shift+Enter for a new line), `select` (an outline Tag, picked from `options`) or `title`\n * (a record's 18px name). */\n variant?: 'text' | 'multiline' | 'select' | 'title'\n placeholder?: string\n /** For `select`. */\n options?: SelectOption[]\n disabled?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<EditableProps>(), {\n variant: 'text',\n placeholder: undefined,\n options: () => []\n})\n\nconst emit = defineEmits<{ blur: [] }>()\n\nconst model = defineModel<string | SelectValue | null>({ default: null })\n\n// Only the id, class and data- and aria- attributes go on the root. The name is said before the value, not as an\n// aria-label, which would hide the value from it.\nconst attrs = useAttrs()\nconst rootAttrs = computed(() =>\n Object.fromEntries(\n Object.entries(attrs).filter(\n ([key]) => key !== 'aria-label' && (key === 'id' || key === 'class' || /^(data|aria)-/.test(key))\n )\n )\n)\nconst name = computed(() => (attrs['aria-label'] as string | undefined) ?? props.placeholder)\n\nconst title = computed(() => props.variant === 'title')\nconst multiline = computed(() => props.variant === 'multiline')\n\nconst local = ref(toRaw(model.value) as string | null)\nconst original = ref(local.value)\nconst editing = ref(false)\nconst wrapper = useTemplateRef<HTMLElement>('wrapper')\n\n// Follows the value from outside (a failed save rolled back, a refetch) unless it's being edited.\nwatch(model, (value) => {\n if (editing.value) return\n local.value = toRaw(value) as string | null\n original.value = local.value\n})\n\nconst rows = computed(() => (title.value ? 1 : Math.max(1, Math.ceil((local.value?.length ?? 0) / 34))))\n\nasync function edit() {\n if (props.disabled) return\n editing.value = true\n await nextTick()\n wrapper.value?.querySelector('textarea')?.focus()\n}\n\nfunction save() {\n editing.value = false\n const value = toRaw(local.value) || null\n // Leaving it untouched isn't a change: an empty value stays as it came, '' or null.\n if (value !== (toRaw(model.value) || null)) model.value = value\n original.value = local.value\n emit('blur')\n}\n\nfunction revert(event: KeyboardEvent) {\n event.preventDefault()\n event.stopPropagation()\n local.value = original.value\n editing.value = false\n}\n\nfunction onEnter(event: KeyboardEvent) {\n if (multiline.value && event.shiftKey) return\n event.preventDefault()\n ;(event.target as HTMLElement).blur()\n}\n\nconst field =\n 'block w-full rounded-lg border border-transparent px-[0.6875rem] py-1.25 text-left text-sui-ink transition-[background-color,border-color,box-shadow] outline-hidden focus-visible:border-sui-accent focus-visible:sui-ring-field'\n</script>\n\n<!-- One value shown as plain text and edited where it is: click or Enter to edit, Enter or leaving to save, Escape to\n undo. A `select` opens its list under the value instead. -->\n<template>\n <Select\n v-if=\"variant === 'select'\"\n v-model=\"model as SelectValue | null\"\n :aria-label=\"name\"\n :disabled=\"disabled\"\n :options=\"options\"\n @update:model-value=\"emit('blur')\"\n >\n <template #trigger=\"{ open, toggle, selected }\">\n <button\n v-bind=\"rootAttrs\"\n :aria-expanded=\"open\"\n aria-haspopup=\"listbox\"\n :class=\"[\n field,\n 'max-w-[16rem] text-sm leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken'\n ]\"\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle\"\n >\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <Tag
|
|
1
|
+
{"version":3,"file":"Editable.js","names":["_sfc_main","Editable_vue_vue_type_script_setup_true_lang_default","Editable_default"],"sources":["../../../src/components/PropertyList/Editable.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, toRaw, useAttrs, useTemplateRef, watch } from 'vue'\n\nimport Select from '../Select/Select.vue'\nimport type { SelectOption, SelectValue } from '../Select/types'\nimport Tag from '../Tag/Tag.vue'\nimport Textarea from '../Textarea/Textarea.vue'\n\nexport interface EditableProps {\n /** `text` (one line), `multiline` (Shift+Enter for a new line), `select` (an outline Tag, picked from `options`) or `title`\n * (a record's 18px name). */\n variant?: 'text' | 'multiline' | 'select' | 'title'\n placeholder?: string\n /** For `select`. */\n options?: SelectOption[]\n disabled?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<EditableProps>(), {\n variant: 'text',\n placeholder: undefined,\n options: () => []\n})\n\nconst emit = defineEmits<{ blur: [] }>()\n\nconst model = defineModel<string | SelectValue | null>({ default: null })\n\n// Only the id, class and data- and aria- attributes go on the root. The name is said before the value, not as an\n// aria-label, which would hide the value from it.\nconst attrs = useAttrs()\nconst rootAttrs = computed(() =>\n Object.fromEntries(\n Object.entries(attrs).filter(\n ([key]) => key !== 'aria-label' && (key === 'id' || key === 'class' || /^(data|aria)-/.test(key))\n )\n )\n)\nconst name = computed(() => (attrs['aria-label'] as string | undefined) ?? props.placeholder)\n\nconst title = computed(() => props.variant === 'title')\nconst multiline = computed(() => props.variant === 'multiline')\n\nconst local = ref(toRaw(model.value) as string | null)\nconst original = ref(local.value)\nconst editing = ref(false)\nconst wrapper = useTemplateRef<HTMLElement>('wrapper')\n\n// Follows the value from outside (a failed save rolled back, a refetch) unless it's being edited.\nwatch(model, (value) => {\n if (editing.value) return\n local.value = toRaw(value) as string | null\n original.value = local.value\n})\n\nconst rows = computed(() => (title.value ? 1 : Math.max(1, Math.ceil((local.value?.length ?? 0) / 34))))\n\nasync function edit() {\n if (props.disabled) return\n editing.value = true\n await nextTick()\n wrapper.value?.querySelector('textarea')?.focus()\n}\n\nfunction save() {\n editing.value = false\n const value = toRaw(local.value) || null\n // Leaving it untouched isn't a change: an empty value stays as it came, '' or null.\n if (value !== (toRaw(model.value) || null)) model.value = value\n original.value = local.value\n emit('blur')\n}\n\nfunction revert(event: KeyboardEvent) {\n event.preventDefault()\n event.stopPropagation()\n local.value = original.value\n editing.value = false\n}\n\nfunction onEnter(event: KeyboardEvent) {\n if (multiline.value && event.shiftKey) return\n event.preventDefault()\n ;(event.target as HTMLElement).blur()\n}\n\nconst field =\n 'block w-full rounded-lg border border-transparent px-[0.6875rem] py-1.25 text-left text-sui-ink transition-[background-color,border-color,box-shadow] outline-hidden focus-visible:border-sui-accent focus-visible:sui-ring-field'\n</script>\n\n<!-- One value shown as plain text and edited where it is: click or Enter to edit, Enter or leaving to save, Escape to\n undo. A `select` opens its list under the value instead. -->\n<template>\n <Select\n v-if=\"variant === 'select'\"\n v-model=\"model as SelectValue | null\"\n :aria-label=\"name\"\n :disabled=\"disabled\"\n :options=\"options\"\n @update:model-value=\"emit('blur')\"\n >\n <template #trigger=\"{ open, toggle, selected }\">\n <button\n v-bind=\"rootAttrs\"\n :aria-expanded=\"open\"\n aria-haspopup=\"listbox\"\n :class=\"[\n field,\n 'max-w-[16rem] text-sm leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken'\n ]\"\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle\"\n >\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <Tag\n v-if=\"selected[0]\"\n :icon=\"selected[0].icon\"\n :icon-color=\"selected[0].iconColor\"\n :name=\"selected[0].label\"\n variant=\"outline\"\n />\n <span v-else class=\"text-sui-ink-muted\">{{ placeholder }}</span>\n </button>\n </template>\n </Select>\n <div\n v-else\n v-bind=\"rootAttrs\"\n ref=\"wrapper\"\n class=\"relative\"\n :class=\"[\n field,\n title\n ? 'min-h-10 min-w-4 text-lg leading-7 font-semibold [&_textarea]:min-h-10 [&_textarea]:text-lg [&_textarea]:leading-7 [&_textarea]:font-semibold [&_textarea]:whitespace-nowrap [&_textarea]:placeholder:text-lg [&_textarea]:placeholder:font-semibold'\n : 'max-w-[16rem] text-sm leading-5 [&_textarea]:placeholder:text-sm [&_textarea]:placeholder:leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken',\n '[&_textarea]:bg-sui-surface [&_textarea]:placeholder:text-sui-ink-muted'\n ]\"\n role=\"button\"\n :tabindex=\"disabled ? -1 : 0\"\n @click=\"edit\"\n @keydown.enter=\"!editing && ($event.preventDefault(), edit())\"\n >\n <div>\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <span v-if=\"!local\" class=\"text-sui-ink-muted\">{{ placeholder }}</span>\n <span v-else :class=\"{ 'whitespace-pre-wrap': multiline }\">{{ local }}</span>\n </div>\n <div v-if=\"editing\" class=\"absolute inset-0 z-10\" :class=\"{ '-mt-px': title }\">\n <Textarea\n v-model=\"local\"\n :aria-label=\"name\"\n :auto-resize=\"!title\"\n class=\"resize-none\"\n :class=\"title && 'h-full px-3 py-1.5'\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @blur=\"save\"\n @keydown.enter.stop=\"onEnter\"\n @keydown.esc.stop=\"revert\"\n />\n </div>\n </div>\n</template>\n"],"mappings":""}
|
|
@@ -73,9 +73,15 @@ var T = [
|
|
|
73
73
|
onClick: n
|
|
74
74
|
}), [L.value ? (m(), o("span", E, g(L.value) + ": ", 1)) : a("", !0), r[0] ? (m(), i(e, {
|
|
75
75
|
key: 1,
|
|
76
|
+
icon: r[0].icon,
|
|
77
|
+
"icon-color": r[0].iconColor,
|
|
76
78
|
name: r[0].label,
|
|
77
79
|
variant: "outline"
|
|
78
|
-
}, null, 8, [
|
|
80
|
+
}, null, 8, [
|
|
81
|
+
"icon",
|
|
82
|
+
"icon-color",
|
|
83
|
+
"name"
|
|
84
|
+
])) : (m(), o("span", D, g(l.placeholder), 1))], 16, T)]),
|
|
79
85
|
_: 1
|
|
80
86
|
}, 8, [
|
|
81
87
|
"modelValue",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Editable.vue_vue_type_script_setup_true_lang.js","names":["_useModel","_openBlock","_createBlock","Select","_withCtx","_createElementVNode","_mergeProps","_createElementBlock","_toDisplayString","_createCommentVNode","Tag","_withKeys","_normalizeClass","_createVNode","Textarea","_withModifiers","Tag_default","Select_default","Textarea_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","field","Editable_vue_vue_type_script_setup_true_lang_default","\"modelValue\"","\"modelModifiers\"","__emit","props","__props","emit","model","useModel","attrs","useAttrs","rootAttrs","computed","key","name","title","multiline","local","ref","toRaw","original","editing","wrapper","useTemplateRef","value","rows","edit","nextTick","save","revert","event","onEnter","openBlock","createBlock","_cache","$event","withCtx","createElementVNode","mergeProps","open","toggle","createElementBlock","toDisplayString","createCommentVNode","selected","withKeys","normalizeClass","createVNode","withModifiers"],"sources":["../../../src/components/PropertyList/Editable.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, toRaw, useAttrs, useTemplateRef, watch } from 'vue'\n\nimport Select from '../Select/Select.vue'\nimport type { SelectOption, SelectValue } from '../Select/types'\nimport Tag from '../Tag/Tag.vue'\nimport Textarea from '../Textarea/Textarea.vue'\n\nexport interface EditableProps {\n /** `text` (one line), `multiline` (Shift+Enter for a new line), `select` (an outline Tag, picked from `options`) or `title`\n * (a record's 18px name). */\n variant?: 'text' | 'multiline' | 'select' | 'title'\n placeholder?: string\n /** For `select`. */\n options?: SelectOption[]\n disabled?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<EditableProps>(), {\n variant: 'text',\n placeholder: undefined,\n options: () => []\n})\n\nconst emit = defineEmits<{ blur: [] }>()\n\nconst model = defineModel<string | SelectValue | null>({ default: null })\n\n// Only the id, class and data- and aria- attributes go on the root. The name is said before the value, not as an\n// aria-label, which would hide the value from it.\nconst attrs = useAttrs()\nconst rootAttrs = computed(() =>\n Object.fromEntries(\n Object.entries(attrs).filter(\n ([key]) => key !== 'aria-label' && (key === 'id' || key === 'class' || /^(data|aria)-/.test(key))\n )\n )\n)\nconst name = computed(() => (attrs['aria-label'] as string | undefined) ?? props.placeholder)\n\nconst title = computed(() => props.variant === 'title')\nconst multiline = computed(() => props.variant === 'multiline')\n\nconst local = ref(toRaw(model.value) as string | null)\nconst original = ref(local.value)\nconst editing = ref(false)\nconst wrapper = useTemplateRef<HTMLElement>('wrapper')\n\n// Follows the value from outside (a failed save rolled back, a refetch) unless it's being edited.\nwatch(model, (value) => {\n if (editing.value) return\n local.value = toRaw(value) as string | null\n original.value = local.value\n})\n\nconst rows = computed(() => (title.value ? 1 : Math.max(1, Math.ceil((local.value?.length ?? 0) / 34))))\n\nasync function edit() {\n if (props.disabled) return\n editing.value = true\n await nextTick()\n wrapper.value?.querySelector('textarea')?.focus()\n}\n\nfunction save() {\n editing.value = false\n const value = toRaw(local.value) || null\n // Leaving it untouched isn't a change: an empty value stays as it came, '' or null.\n if (value !== (toRaw(model.value) || null)) model.value = value\n original.value = local.value\n emit('blur')\n}\n\nfunction revert(event: KeyboardEvent) {\n event.preventDefault()\n event.stopPropagation()\n local.value = original.value\n editing.value = false\n}\n\nfunction onEnter(event: KeyboardEvent) {\n if (multiline.value && event.shiftKey) return\n event.preventDefault()\n ;(event.target as HTMLElement).blur()\n}\n\nconst field =\n 'block w-full rounded-lg border border-transparent px-[0.6875rem] py-1.25 text-left text-sui-ink transition-[background-color,border-color,box-shadow] outline-hidden focus-visible:border-sui-accent focus-visible:sui-ring-field'\n</script>\n\n<!-- One value shown as plain text and edited where it is: click or Enter to edit, Enter or leaving to save, Escape to\n undo. A `select` opens its list under the value instead. -->\n<template>\n <Select\n v-if=\"variant === 'select'\"\n v-model=\"model as SelectValue | null\"\n :aria-label=\"name\"\n :disabled=\"disabled\"\n :options=\"options\"\n @update:model-value=\"emit('blur')\"\n >\n <template #trigger=\"{ open, toggle, selected }\">\n <button\n v-bind=\"rootAttrs\"\n :aria-expanded=\"open\"\n aria-haspopup=\"listbox\"\n :class=\"[\n field,\n 'max-w-[16rem] text-sm leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken'\n ]\"\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle\"\n >\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <Tag v-if=\"selected[0]\" :name=\"selected[0].label\" variant=\"outline\" />\n <span v-else class=\"text-sui-ink-muted\">{{ placeholder }}</span>\n </button>\n </template>\n </Select>\n <div\n v-else\n v-bind=\"rootAttrs\"\n ref=\"wrapper\"\n class=\"relative\"\n :class=\"[\n field,\n title\n ? 'min-h-10 min-w-4 text-lg leading-7 font-semibold [&_textarea]:min-h-10 [&_textarea]:text-lg [&_textarea]:leading-7 [&_textarea]:font-semibold [&_textarea]:whitespace-nowrap [&_textarea]:placeholder:text-lg [&_textarea]:placeholder:font-semibold'\n : 'max-w-[16rem] text-sm leading-5 [&_textarea]:placeholder:text-sm [&_textarea]:placeholder:leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken',\n '[&_textarea]:bg-sui-surface [&_textarea]:placeholder:text-sui-ink-muted'\n ]\"\n role=\"button\"\n :tabindex=\"disabled ? -1 : 0\"\n @click=\"edit\"\n @keydown.enter=\"!editing && ($event.preventDefault(), edit())\"\n >\n <div>\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <span v-if=\"!local\" class=\"text-sui-ink-muted\">{{ placeholder }}</span>\n <span v-else :class=\"{ 'whitespace-pre-wrap': multiline }\">{{ local }}</span>\n </div>\n <div v-if=\"editing\" class=\"absolute inset-0 z-10\" :class=\"{ '-mt-px': title }\">\n <Textarea\n v-model=\"local\"\n :aria-label=\"name\"\n :auto-resize=\"!title\"\n class=\"resize-none\"\n :class=\"title && 'h-full px-3 py-1.5'\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @blur=\"save\"\n @keydown.enter.stop=\"onEnter\"\n @keydown.esc.stop=\"revert\"\n />\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Editable.vue_vue_type_script_setup_true_lang.js","names":["_useModel","_openBlock","_createBlock","Select","_withCtx","_createElementVNode","_mergeProps","_createElementBlock","_toDisplayString","_createCommentVNode","Tag","_withKeys","_normalizeClass","_createVNode","Textarea","_withModifiers","Tag_default","Select_default","Textarea_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","field","Editable_vue_vue_type_script_setup_true_lang_default","\"modelValue\"","\"modelModifiers\"","__emit","props","__props","emit","model","useModel","attrs","useAttrs","rootAttrs","computed","key","name","title","multiline","local","ref","toRaw","original","editing","wrapper","useTemplateRef","value","rows","edit","nextTick","save","revert","event","onEnter","openBlock","createBlock","_cache","$event","withCtx","createElementVNode","mergeProps","open","toggle","createElementBlock","toDisplayString","createCommentVNode","selected","withKeys","normalizeClass","createVNode","withModifiers"],"sources":["../../../src/components/PropertyList/Editable.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, toRaw, useAttrs, useTemplateRef, watch } from 'vue'\n\nimport Select from '../Select/Select.vue'\nimport type { SelectOption, SelectValue } from '../Select/types'\nimport Tag from '../Tag/Tag.vue'\nimport Textarea from '../Textarea/Textarea.vue'\n\nexport interface EditableProps {\n /** `text` (one line), `multiline` (Shift+Enter for a new line), `select` (an outline Tag, picked from `options`) or `title`\n * (a record's 18px name). */\n variant?: 'text' | 'multiline' | 'select' | 'title'\n placeholder?: string\n /** For `select`. */\n options?: SelectOption[]\n disabled?: boolean\n}\n\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<EditableProps>(), {\n variant: 'text',\n placeholder: undefined,\n options: () => []\n})\n\nconst emit = defineEmits<{ blur: [] }>()\n\nconst model = defineModel<string | SelectValue | null>({ default: null })\n\n// Only the id, class and data- and aria- attributes go on the root. The name is said before the value, not as an\n// aria-label, which would hide the value from it.\nconst attrs = useAttrs()\nconst rootAttrs = computed(() =>\n Object.fromEntries(\n Object.entries(attrs).filter(\n ([key]) => key !== 'aria-label' && (key === 'id' || key === 'class' || /^(data|aria)-/.test(key))\n )\n )\n)\nconst name = computed(() => (attrs['aria-label'] as string | undefined) ?? props.placeholder)\n\nconst title = computed(() => props.variant === 'title')\nconst multiline = computed(() => props.variant === 'multiline')\n\nconst local = ref(toRaw(model.value) as string | null)\nconst original = ref(local.value)\nconst editing = ref(false)\nconst wrapper = useTemplateRef<HTMLElement>('wrapper')\n\n// Follows the value from outside (a failed save rolled back, a refetch) unless it's being edited.\nwatch(model, (value) => {\n if (editing.value) return\n local.value = toRaw(value) as string | null\n original.value = local.value\n})\n\nconst rows = computed(() => (title.value ? 1 : Math.max(1, Math.ceil((local.value?.length ?? 0) / 34))))\n\nasync function edit() {\n if (props.disabled) return\n editing.value = true\n await nextTick()\n wrapper.value?.querySelector('textarea')?.focus()\n}\n\nfunction save() {\n editing.value = false\n const value = toRaw(local.value) || null\n // Leaving it untouched isn't a change: an empty value stays as it came, '' or null.\n if (value !== (toRaw(model.value) || null)) model.value = value\n original.value = local.value\n emit('blur')\n}\n\nfunction revert(event: KeyboardEvent) {\n event.preventDefault()\n event.stopPropagation()\n local.value = original.value\n editing.value = false\n}\n\nfunction onEnter(event: KeyboardEvent) {\n if (multiline.value && event.shiftKey) return\n event.preventDefault()\n ;(event.target as HTMLElement).blur()\n}\n\nconst field =\n 'block w-full rounded-lg border border-transparent px-[0.6875rem] py-1.25 text-left text-sui-ink transition-[background-color,border-color,box-shadow] outline-hidden focus-visible:border-sui-accent focus-visible:sui-ring-field'\n</script>\n\n<!-- One value shown as plain text and edited where it is: click or Enter to edit, Enter or leaving to save, Escape to\n undo. A `select` opens its list under the value instead. -->\n<template>\n <Select\n v-if=\"variant === 'select'\"\n v-model=\"model as SelectValue | null\"\n :aria-label=\"name\"\n :disabled=\"disabled\"\n :options=\"options\"\n @update:model-value=\"emit('blur')\"\n >\n <template #trigger=\"{ open, toggle, selected }\">\n <button\n v-bind=\"rootAttrs\"\n :aria-expanded=\"open\"\n aria-haspopup=\"listbox\"\n :class=\"[\n field,\n 'max-w-[16rem] text-sm leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken'\n ]\"\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle\"\n >\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <Tag\n v-if=\"selected[0]\"\n :icon=\"selected[0].icon\"\n :icon-color=\"selected[0].iconColor\"\n :name=\"selected[0].label\"\n variant=\"outline\"\n />\n <span v-else class=\"text-sui-ink-muted\">{{ placeholder }}</span>\n </button>\n </template>\n </Select>\n <div\n v-else\n v-bind=\"rootAttrs\"\n ref=\"wrapper\"\n class=\"relative\"\n :class=\"[\n field,\n title\n ? 'min-h-10 min-w-4 text-lg leading-7 font-semibold [&_textarea]:min-h-10 [&_textarea]:text-lg [&_textarea]:leading-7 [&_textarea]:font-semibold [&_textarea]:whitespace-nowrap [&_textarea]:placeholder:text-lg [&_textarea]:placeholder:font-semibold'\n : 'max-w-[16rem] text-sm leading-5 [&_textarea]:placeholder:text-sm [&_textarea]:placeholder:leading-5',\n disabled ? 'cursor-auto' : 'cursor-pointer hover:bg-sui-sunken',\n '[&_textarea]:bg-sui-surface [&_textarea]:placeholder:text-sui-ink-muted'\n ]\"\n role=\"button\"\n :tabindex=\"disabled ? -1 : 0\"\n @click=\"edit\"\n @keydown.enter=\"!editing && ($event.preventDefault(), edit())\"\n >\n <div>\n <span v-if=\"name\" class=\"sr-only\">{{ name }}: </span>\n <span v-if=\"!local\" class=\"text-sui-ink-muted\">{{ placeholder }}</span>\n <span v-else :class=\"{ 'whitespace-pre-wrap': multiline }\">{{ local }}</span>\n </div>\n <div v-if=\"editing\" class=\"absolute inset-0 z-10\" :class=\"{ '-mt-px': title }\">\n <Textarea\n v-model=\"local\"\n :aria-label=\"name\"\n :auto-resize=\"!title\"\n class=\"resize-none\"\n :class=\"title && 'h-full px-3 py-1.5'\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n @blur=\"save\"\n @keydown.enter.stop=\"onEnter\"\n @keydown.esc.stop=\"revert\"\n />\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.js","names":["_sfc_main","Select_vue_vue_type_script_setup_true_lang_default","Select_default"],"sources":["../../../src/components/Select/Select.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends SelectValue\">\nimport { computed, nextTick, ref, shallowRef, useAttrs } from 'vue'\n\nimport {\n ListboxContent,\n ListboxFilter,\n ListboxItem,\n ListboxRoot,\n ListboxVirtualizer,\n PopoverAnchor,\n PopoverContent,\n PopoverPortal,\n PopoverRoot\n} from 'reka-ui'\n\nimport type { Hue } from '../../utils/hues'\nimport { useFieldControl } from '../FormField/context'\nimport Spinner from '../Spinner/Spinner.vue'\nimport Tag from '../Tag/Tag.vue'\nimport CheckIcon from '../internal/CheckIcon.vue'\nimport type { SelectOption, SelectValue } from './types'\n\nexport interface SelectProps<T extends SelectValue> {\n options: SelectOption<T>[]\n /** The options for chosen values that aren't in `options`, such as a saved choice while `options` holds search\n * results. Options picked from the list are remembered without it. */\n selectedOptions?: SelectOption<T>[]\n multiple?: boolean\n /** Lets a single choice be removed, as a tag in the filter row or with Backspace there. */\n clearable?: boolean\n /** A filter row at the top of the list; on by default for `multiple`, `canCreate` or more than 8 options. */\n searchable?: boolean\n /** Narrows `options` to what's typed: by label, or by your function. `false` when `options` are already the results\n * for the query, such as from a server search through `query-change`. */\n filter?: boolean | ((option: SelectOption<T>, query: string) => boolean)\n /** Offers to create what was typed, through `createOption`. */\n canCreate?: boolean\n /** Makes the created option; by default its value is its label. */\n createOption?: (draft: { label: string; color?: Hue }) => SelectOption<T> | Promise<SelectOption<T>>\n /** Creates with a hue, shown as a tag. */\n createColored?: boolean\n loading?: boolean\n /** 36px for forms, 28px for table feet and toolbars. */\n size?: 'md' | 'sm'\n disabled?: boolean\n invalid?: boolean\n placeholder?: string\n searchPlaceholder?: string\n emptyLabel?: string\n createLabel?: string\n removeLabel?: string\n /** Where the list opens; it flips when there's no room. */\n side?: 'bottom' | 'top'\n align?: 'start' | 'end'\n /** The list's width, when it should differ from the trigger's, e.g. `20rem`. */\n panelWidth?: string\n}\n\n// Attributes (aria-label, class for its width) go to the trigger, the one element in the page's flow.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<SelectProps<T>>(), {\n selectedOptions: () => [],\n searchable: undefined,\n filter: true,\n createOption: undefined,\n createColored: true,\n size: 'md',\n placeholder: 'Select an option',\n searchPlaceholder: 'Type to filter',\n emptyLabel: 'No results found',\n createLabel: 'Create',\n removeLabel: 'Remove',\n side: 'bottom',\n align: 'start',\n panelWidth: undefined\n})\n\nconst emit = defineEmits<{ 'query-change': [query: string]; 'create-error': [error: unknown] }>()\n\n// An aria-label would hide the trigger's visible value from its name; it's said before the value instead.\nconst attrs = useAttrs()\nconst label = computed(() => attrs['aria-label'] as string | undefined)\nconst triggerAttrs = computed(() => Object.fromEntries(Object.entries(attrs).filter(([key]) => key !== 'aria-label')))\n\nconst model = defineModel<T | T[] | null>({ default: null })\n\nconst open = ref(false)\nconst query = ref('')\nconst keyboard = ref(false)\n\nconst values = computed<T[]>(() =>\n Array.isArray(model.value) ? model.value : model.value === null || model.value === undefined ? [] : [model.value]\n)\n// Options picked from earlier results, which `options` may no longer hold.\nconst picked = shallowRef<SelectOption<T>[]>([])\nconst selected = computed(() =>\n values.value\n .map((value) =>\n [props.options, props.selectedOptions, picked.value]\n .map((list) => list.find((option) => option.value === value))\n .find((option) => !!option)\n )\n .filter((option) => !!option)\n)\nconst search = computed(\n () =>\n props.searchable ??\n (props.multiple || props.canCreate || props.clearable || props.filter === false || props.options.length > 8)\n)\nconst removable = computed(() => props.multiple || props.clearable)\nconst matches = (option: SelectOption<T>, text: string) =>\n typeof props.filter === 'function'\n ? props.filter(option, text)\n : option.label.toLowerCase().includes(text.toLowerCase())\nconst listed = computed(() => {\n const text = query.value.trim()\n return props.options\n .filter((option) => !(props.multiple && search.value && values.value.includes(option.value)))\n .filter((option) => !text || props.filter === false || matches(option, text))\n})\nconst showCreate = computed(() => {\n const text = query.value.trim().toLowerCase()\n return props.canCreate && !!text && !props.options.some((option) => option.label.toLowerCase() === text)\n})\n// More than this and the list renders only the rows in view.\nconst virtual = computed(() => listed.value.length > 20)\n\nconst createHues: Hue[] = ['gray', 'purple', 'red', 'green', 'orange', 'pink', 'blue', 'slate']\nconst newHue = computed(() => createHues[query.value.length % createHues.length])\nconst CREATE = { create: true } as const\n\nconst { attrs: fieldAttrs, invalid: isInvalid } = useFieldControl(\n () => undefined,\n () => props.invalid\n)\n\nfunction setQuery(text: string) {\n query.value = text\n emit('query-change', text)\n}\n\nfunction show(viaKeyboard: boolean) {\n if (props.disabled) return\n keyboard.value = viaKeyboard\n open.value = true\n}\n\nfunction close() {\n open.value = false\n if (query.value) setQuery('')\n}\n\nfunction toggle(event?: Event) {\n if (open.value) close()\n // A click from the keyboard (Enter, Space) has no pointer position.\n else show(!!event && (event as MouseEvent).detail === 0)\n}\n\nfunction choose(option: SelectOption<T>) {\n picked.value = [option, ...picked.value.filter((known) => known.value !== option.value)]\n if (props.multiple) {\n model.value = values.value.includes(option.value)\n ? values.value.filter((value) => value !== option.value)\n : [...values.value, option.value]\n setQuery('')\n } else {\n model.value = option.value\n close()\n }\n}\n\nconst creating = ref(false)\n\n// A failed createOption keeps what was typed, so it can be tried again.\nasync function create() {\n if (creating.value) return\n const label = query.value.trim()\n const draft = props.createColored ? { label, color: newHue.value } : { label }\n creating.value = true\n try {\n const option = props.createOption\n ? await props.createOption(draft)\n : ({ value: label as T, label, color: draft.color } as SelectOption<T>)\n choose(option)\n } catch (error) {\n emit('create-error', error)\n } finally {\n creating.value = false\n }\n}\n\nfunction onSelect(event: CustomEvent, row: SelectOption<T> | typeof CREATE) {\n // What's chosen lives in the model; the listbox keeps no selection of its own.\n event.preventDefault()\n if ('create' in row) create()\n else if (!row.disabled) choose(row)\n}\n\nfunction remove(value: T) {\n model.value = props.multiple ? values.value.filter((chosen) => chosen !== value) : null\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) keyboard.value = true\n if (event.key === 'Backspace' && removable.value && !query.value && values.value.length)\n remove(values.value[values.value.length - 1]!)\n}\n\nfunction onTriggerKeydown(event: KeyboardEvent) {\n if (!open.value && ['ArrowDown', 'ArrowUp'].includes(event.key)) {\n event.preventDefault()\n show(true)\n }\n}\n\n// The filter takes focus when there is one, or else the list, so the arrow keys work at once.\nfunction onOpenAutoFocus(event: Event) {\n event.preventDefault()\n nextTick(() =>\n (event.target as HTMLElement | null)?.querySelector<HTMLElement>('[data-select-filter], [role=\"listbox\"]')?.focus()\n )\n}\n\n// Back to the trigger, which Reka can't find: it's our anchor, not its trigger.\nconst anchor = ref<{ $el: HTMLElement }>()\n\n// Nor does Reka know a press on it isn't outside: closing there would let the click reopen the list.\nfunction onPointerDownOutside(event: CustomEvent<{ originalEvent: PointerEvent }>) {\n const target = event.detail.originalEvent.target\n if (target instanceof Node && anchor.value?.$el?.contains(target)) event.preventDefault()\n}\nfunction returnFocus(event: Event) {\n event.preventDefault()\n anchor.value?.$el?.focus()\n}\n\n// Reka highlights the first option as the list mounts; only the chosen one, or the keyboard, should be.\nconst listbox = ref()\nfunction onHighlight(payload: { value: unknown } | undefined) {\n if (keyboard.value || !payload) return\n if (values.value.includes(payload.value as T)) return\n nextTick(() => listbox.value && !keyboard.value && (listbox.value.highlightedElement = null))\n}\n\nconst triggerSizes = { md: 'min-h-9 pr-2.5 pl-3 text-sm', sm: 'h-7 pr-1.5 pl-2 text-13' }\nconst optionClass =\n 'flex min-h-8 cursor-pointer items-center justify-between gap-3 rounded-lg py-1.5 pr-2 pl-2.5 text-13 whitespace-nowrap text-sui-ink outline-hidden select-none data-disabled:cursor-default data-disabled:opacity-50 data-highlighted:bg-sui-sunken data-[state=checked]:font-medium data-[state=checked]:text-sui-accent group-data-keyboard/list:data-highlighted:sui-ring-inset'\n</script>\n\n<!-- One select for every choice: a short fixed list, a searchable one, several values as tags (`multiple`), or creating\n what was typed (`canCreate`). For a server search, set `:filter=\"false\"` and replace `options` on `query-change`. The `trigger` slot draws your own trigger and gets `open`, `toggle` and `selected`. -->\n<template>\n <PopoverRoot :open=\"open\" @update:open=\"(value) => (value ? show(false) : close())\">\n <PopoverAnchor ref=\"anchor\" as-child>\n <slot name=\"trigger\" :open=\"open\" :selected=\"selected\" :toggle=\"toggle\">\n <button\n v-bind=\"{ ...fieldAttrs, ...triggerAttrs }\"\n :aria-expanded=\"open\"\n aria-haspopup=\"listbox\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n class=\"group inline-flex items-center justify-between gap-1.5 rounded-(--sui-radius-button) border bg-sui-surface text-left text-sui-ink transition-[border-color,box-shadow] duration-150 focus-visible:border-sui-accent focus-visible:sui-ring-field disabled:pointer-events-none disabled:opacity-60\"\n :class=\"[\n triggerSizes[size],\n isInvalid ? 'border-sui-danger-mid' : 'border-sui-line-strong hover:border-sui-ink-muted'\n ]\"\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle\"\n @keydown=\"onTriggerKeydown\"\n >\n <span v-if=\"label\" class=\"sr-only\">{{ label }}: </span>\n <span v-if=\"!selected.length\" class=\"text-sui-ink-faint\">{{ placeholder }}</span>\n <span v-else-if=\"multiple\" class=\"flex flex-wrap gap-1 py-1.25\">\n <Tag\n v-for=\"option in selected\"\n :key=\"option.value\"\n :color=\"option.color ?? undefined\"\n :name=\"option.label\"\n />\n </span>\n <Tag v-else-if=\"selected[0]!.color\" :color=\"selected[0]!.color\" :name=\"selected[0]!.label\" />\n <span v-else class=\"min-w-0 truncate whitespace-nowrap\">{{ selected[0]!.label }}</span>\n <svg\n aria-hidden=\"true\"\n class=\"size-4 shrink-0 fill-none stroke-sui-ink-muted transition-transform duration-150 group-aria-expanded:rotate-180\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.5\"\n viewBox=\"0 0 16 16\"\n >\n <path d=\"M4.5 6.5 8 10l3.5-3.5\" />\n </svg>\n </button>\n </slot>\n </PopoverAnchor>\n <PopoverPortal>\n <PopoverContent\n :align=\"align\"\n :aria-label=\"label ?? placeholder\"\n class=\"z-[1100] overflow-hidden rounded-xl border border-sui-line-subtle bg-sui-surface text-sui-ink shadow-lg data-[state=open]:animate-sui-menu-in\"\n :class=\"\n search ? 'min-w-[max(var(--reka-popover-trigger-width),15rem)]' : 'min-w-(--reka-popover-trigger-width)'\n \"\n :side=\"side\"\n :side-offset=\"4\"\n :style=\"panelWidth ? { width: panelWidth } : undefined\"\n @close-auto-focus=\"returnFocus\"\n @open-auto-focus=\"onOpenAutoFocus\"\n @pointer-down-outside=\"onPointerDownOutside\"\n >\n <ListboxRoot\n ref=\"listbox\"\n :highlight-on-hover=\"true\"\n :model-value=\"multiple ? values : (values[0] ?? undefined)\"\n :multiple=\"multiple\"\n @highlight=\"onHighlight\"\n @keydown=\"onKeydown\"\n @pointermove=\"keyboard = false\"\n >\n <div\n v-if=\"search\"\n class=\"flex min-h-11 flex-wrap items-center gap-1 border-b border-sui-line-subtle px-3 py-2.5\"\n >\n <template v-if=\"removable\">\n <Tag\n v-for=\"option in selected\"\n :key=\"option.value\"\n :color=\"option.color ?? undefined\"\n :name=\"option.label\"\n removable\n :remove-label=\"removeLabel\"\n @remove=\"remove(option.value)\"\n />\n </template>\n <!-- focus: classes too: an app's forms plugin rings focused text fields, and outranks plain utilities. -->\n <ListboxFilter\n :aria-label=\"label ?? placeholder\"\n class=\"h-5 min-w-16 flex-1 rounded-none border-none bg-transparent p-0 text-13 text-sui-ink shadow-none outline-hidden placeholder:text-sui-ink-faint focus:border-none focus:shadow-none focus:ring-0 focus:outline-hidden\"\n data-select-filter\n :model-value=\"query\"\n :placeholder=\"removable && selected.length ? '' : searchPlaceholder\"\n @update:model-value=\"setQuery\"\n />\n </div>\n <div v-if=\"loading\" class=\"flex justify-center py-6\"><Spinner size=\"sm\" /></div>\n <ListboxContent\n v-else-if=\"listed.length || showCreate\"\n :aria-label=\"label ?? placeholder\"\n class=\"group/list max-h-60 overflow-auto p-1 outline-hidden\"\n :data-keyboard=\"keyboard || undefined\"\n >\n <ListboxVirtualizer\n v-if=\"virtual\"\n v-slot=\"{ option }\"\n :estimate-size=\"32\"\n :options=\"listed\"\n :text-content=\"(option) => option.label\"\n >\n <ListboxItem\n :class=\"optionClass\"\n :disabled=\"option.disabled\"\n :value=\"option.value\"\n @select=\"onSelect($event, option as SelectOption<T>)\"\n >\n <span class=\"inline-flex min-w-0 items-center gap-1.5 truncate\">\n <slot name=\"option\" :option=\"option as SelectOption<T>\">\n <Tag v-if=\"option.color\" :color=\"option.color\" :name=\"option.label\" />\n <template v-else>{{ option.label }}</template>\n </slot>\n </span>\n <CheckIcon v-if=\"values.includes(option.value)\" />\n </ListboxItem>\n </ListboxVirtualizer>\n <template v-else>\n <ListboxItem\n v-for=\"option in listed\"\n :key=\"option.value\"\n :class=\"optionClass\"\n :disabled=\"option.disabled\"\n :value=\"option.value\"\n @select=\"onSelect($event, option)\"\n >\n <span class=\"inline-flex min-w-0 items-center gap-1.5 truncate\">\n <slot name=\"option\" :option=\"option\">\n <Tag v-if=\"option.color\" :color=\"option.color\" :name=\"option.label\" />\n <template v-else>{{ option.label }}</template>\n </slot>\n </span>\n <CheckIcon v-if=\"values.includes(option.value)\" />\n </ListboxItem>\n </template>\n <ListboxItem v-if=\"showCreate\" :class=\"optionClass\" :value=\"CREATE\" @select=\"onSelect($event, CREATE)\">\n <span class=\"inline-flex min-w-0 items-center gap-1.5\">\n {{ createLabel }}\n <Tag v-if=\"createColored\" :color=\"newHue\" :name=\"query.trim()\" />\n <strong v-else class=\"font-medium\">“{{ query.trim() }}”</strong>\n </span>\n </ListboxItem>\n </ListboxContent>\n <div v-else class=\"px-3.5 py-3 text-13 text-sui-ink-muted\">{{ emptyLabel }}</div>\n </ListboxRoot>\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"Select.js","names":["_sfc_main","Select_vue_vue_type_script_setup_true_lang_default","Select_default"],"sources":["../../../src/components/Select/Select.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T extends SelectValue\">\nimport { computed, nextTick, ref, shallowRef, useAttrs } from 'vue'\n\nimport {\n ListboxContent,\n ListboxFilter,\n ListboxItem,\n ListboxRoot,\n ListboxVirtualizer,\n PopoverAnchor,\n PopoverContent,\n PopoverPortal,\n PopoverRoot\n} from 'reka-ui'\n\nimport type { Hue } from '../../utils/hues'\nimport { useFieldControl } from '../FormField/context'\nimport Icon from '../Icon/Icon.vue'\nimport Spinner from '../Spinner/Spinner.vue'\nimport Tag from '../Tag/Tag.vue'\nimport CheckIcon from '../internal/CheckIcon.vue'\nimport type { SelectOption, SelectValue } from './types'\n\nexport interface SelectProps<T extends SelectValue> {\n options: SelectOption<T>[]\n /** The options for chosen values that aren't in `options`, such as a saved choice while `options` holds search\n * results. Options picked from the list are remembered without it. */\n selectedOptions?: SelectOption<T>[]\n multiple?: boolean\n /** Lets a single choice be removed, as a tag in the filter row or with Backspace there. */\n clearable?: boolean\n /** A filter row at the top of the list; on by default for `multiple`, `canCreate` or more than 8 options. */\n searchable?: boolean\n /** Narrows `options` to what's typed: by label, or by your function. `false` when `options` are already the results\n * for the query, such as from a server search through `query-change`. */\n filter?: boolean | ((option: SelectOption<T>, query: string) => boolean)\n /** Offers to create what was typed, through `createOption`. */\n canCreate?: boolean\n /** Makes the created option; by default its value is its label. */\n createOption?: (draft: { label: string; color?: Hue }) => SelectOption<T> | Promise<SelectOption<T>>\n /** Creates with a hue, shown as a tag. */\n createColored?: boolean\n loading?: boolean\n /** 36px for forms, 28px for table feet and toolbars. */\n size?: 'md' | 'sm'\n disabled?: boolean\n invalid?: boolean\n placeholder?: string\n searchPlaceholder?: string\n emptyLabel?: string\n createLabel?: string\n removeLabel?: string\n /** Where the list opens; it flips when there's no room. */\n side?: 'bottom' | 'top'\n align?: 'start' | 'end'\n /** The list's width, when it should differ from the trigger's, e.g. `20rem`. */\n panelWidth?: string\n}\n\n// Attributes (aria-label, class for its width) go to the trigger, the one element in the page's flow.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<SelectProps<T>>(), {\n selectedOptions: () => [],\n searchable: undefined,\n filter: true,\n createOption: undefined,\n createColored: true,\n size: 'md',\n placeholder: 'Select an option',\n searchPlaceholder: 'Type to filter',\n emptyLabel: 'No results found',\n createLabel: 'Create',\n removeLabel: 'Remove',\n side: 'bottom',\n align: 'start',\n panelWidth: undefined\n})\n\nconst emit = defineEmits<{ 'query-change': [query: string]; 'create-error': [error: unknown] }>()\n\n// An aria-label would hide the trigger's visible value from its name; it's said before the value instead.\nconst attrs = useAttrs()\nconst label = computed(() => attrs['aria-label'] as string | undefined)\nconst triggerAttrs = computed(() => Object.fromEntries(Object.entries(attrs).filter(([key]) => key !== 'aria-label')))\n\nconst model = defineModel<T | T[] | null>({ default: null })\n\nconst open = ref(false)\nconst query = ref('')\nconst keyboard = ref(false)\n\nconst values = computed<T[]>(() =>\n Array.isArray(model.value) ? model.value : model.value === null || model.value === undefined ? [] : [model.value]\n)\n// Options picked from earlier results, which `options` may no longer hold.\nconst picked = shallowRef<SelectOption<T>[]>([])\nconst selected = computed(() =>\n values.value\n .map((value) =>\n [props.options, props.selectedOptions, picked.value]\n .map((list) => list.find((option) => option.value === value))\n .find((option) => !!option)\n )\n .filter((option) => !!option)\n)\nconst search = computed(\n () =>\n props.searchable ??\n (props.multiple || props.canCreate || props.clearable || props.filter === false || props.options.length > 8)\n)\nconst removable = computed(() => props.multiple || props.clearable)\nconst matches = (option: SelectOption<T>, text: string) =>\n typeof props.filter === 'function'\n ? props.filter(option, text)\n : option.label.toLowerCase().includes(text.toLowerCase())\nconst listed = computed(() => {\n const text = query.value.trim()\n return props.options\n .filter((option) => !(props.multiple && search.value && values.value.includes(option.value)))\n .filter((option) => !text || props.filter === false || matches(option, text))\n})\nconst showCreate = computed(() => {\n const text = query.value.trim().toLowerCase()\n return props.canCreate && !!text && !props.options.some((option) => option.label.toLowerCase() === text)\n})\n// More than this and the list renders only the rows in view.\nconst virtual = computed(() => listed.value.length > 20)\n\nconst createHues: Hue[] = ['gray', 'purple', 'red', 'green', 'orange', 'pink', 'blue', 'slate']\nconst newHue = computed(() => createHues[query.value.length % createHues.length])\nconst CREATE = { create: true } as const\n\nconst { attrs: fieldAttrs, invalid: isInvalid } = useFieldControl(\n () => undefined,\n () => props.invalid\n)\n\nfunction setQuery(text: string) {\n query.value = text\n emit('query-change', text)\n}\n\nfunction show(viaKeyboard: boolean) {\n if (props.disabled) return\n keyboard.value = viaKeyboard\n open.value = true\n}\n\nfunction close() {\n open.value = false\n if (query.value) setQuery('')\n}\n\nfunction toggle(event?: Event) {\n if (open.value) close()\n // A click from the keyboard (Enter, Space) has no pointer position.\n else show(!!event && (event as MouseEvent).detail === 0)\n}\n\nfunction choose(option: SelectOption<T>) {\n picked.value = [option, ...picked.value.filter((known) => known.value !== option.value)]\n if (props.multiple) {\n model.value = values.value.includes(option.value)\n ? values.value.filter((value) => value !== option.value)\n : [...values.value, option.value]\n setQuery('')\n } else {\n model.value = option.value\n close()\n }\n}\n\nconst creating = ref(false)\n\n// A failed createOption keeps what was typed, so it can be tried again.\nasync function create() {\n if (creating.value) return\n const label = query.value.trim()\n const draft = props.createColored ? { label, color: newHue.value } : { label }\n creating.value = true\n try {\n const option = props.createOption\n ? await props.createOption(draft)\n : ({ value: label as T, label, color: draft.color } as SelectOption<T>)\n choose(option)\n } catch (error) {\n emit('create-error', error)\n } finally {\n creating.value = false\n }\n}\n\nfunction onSelect(event: CustomEvent, row: SelectOption<T> | typeof CREATE) {\n // What's chosen lives in the model; the listbox keeps no selection of its own.\n event.preventDefault()\n if ('create' in row) create()\n else if (!row.disabled) choose(row)\n}\n\nfunction remove(value: T) {\n model.value = props.multiple ? values.value.filter((chosen) => chosen !== value) : null\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) keyboard.value = true\n if (event.key === 'Backspace' && removable.value && !query.value && values.value.length)\n remove(values.value[values.value.length - 1]!)\n}\n\nfunction onTriggerKeydown(event: KeyboardEvent) {\n if (!open.value && ['ArrowDown', 'ArrowUp'].includes(event.key)) {\n event.preventDefault()\n show(true)\n }\n}\n\n// The filter takes focus when there is one, or else the list, so the arrow keys work at once.\nfunction onOpenAutoFocus(event: Event) {\n event.preventDefault()\n nextTick(() =>\n (event.target as HTMLElement | null)?.querySelector<HTMLElement>('[data-select-filter], [role=\"listbox\"]')?.focus()\n )\n}\n\n// Back to the trigger, which Reka can't find: it's our anchor, not its trigger.\nconst anchor = ref<{ $el: HTMLElement }>()\n\n// Nor does Reka know a press on it isn't outside: closing there would let the click reopen the list.\nfunction onPointerDownOutside(event: CustomEvent<{ originalEvent: PointerEvent }>) {\n const target = event.detail.originalEvent.target\n if (target instanceof Node && anchor.value?.$el?.contains(target)) event.preventDefault()\n}\nfunction returnFocus(event: Event) {\n event.preventDefault()\n anchor.value?.$el?.focus()\n}\n\n// Reka highlights the first option as the list mounts; only the chosen one, or the keyboard, should be.\nconst listbox = ref()\nfunction onHighlight(payload: { value: unknown } | undefined) {\n if (keyboard.value || !payload) return\n if (values.value.includes(payload.value as T)) return\n nextTick(() => listbox.value && !keyboard.value && (listbox.value.highlightedElement = null))\n}\n\nconst triggerSizes = { md: 'min-h-9 pr-2.5 pl-3 text-sm', sm: 'h-7 pr-1.5 pl-2 text-13' }\nconst optionClass =\n 'flex min-h-8 cursor-pointer items-center justify-between gap-3 rounded-lg py-1.5 pr-2 pl-2.5 text-13 whitespace-nowrap text-sui-ink outline-hidden select-none data-disabled:cursor-default data-disabled:opacity-50 data-highlighted:bg-sui-sunken data-[state=checked]:font-medium data-[state=checked]:text-sui-accent group-data-keyboard/list:data-highlighted:sui-ring-inset'\n</script>\n\n<!-- One select for every choice: a short fixed list, a searchable one, several values as tags (`multiple`), or creating\n what was typed (`canCreate`). For a server search, set `:filter=\"false\"` and replace `options` on `query-change`. The `trigger` slot draws your own trigger and gets `open`, `toggle` and `selected`. -->\n<template>\n <PopoverRoot :open=\"open\" @update:open=\"(value) => (value ? show(false) : close())\">\n <PopoverAnchor ref=\"anchor\" as-child>\n <slot name=\"trigger\" :open=\"open\" :selected=\"selected\" :toggle=\"toggle\">\n <button\n v-bind=\"{ ...fieldAttrs, ...triggerAttrs }\"\n :aria-expanded=\"open\"\n aria-haspopup=\"listbox\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n class=\"group inline-flex items-center justify-between gap-1.5 rounded-(--sui-radius-button) border bg-sui-surface text-left text-sui-ink transition-[border-color,box-shadow] duration-150 focus-visible:border-sui-accent focus-visible:sui-ring-field disabled:pointer-events-none disabled:opacity-60\"\n :class=\"[\n triggerSizes[size],\n isInvalid ? 'border-sui-danger-mid' : 'border-sui-line-strong hover:border-sui-ink-muted'\n ]\"\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle\"\n @keydown=\"onTriggerKeydown\"\n >\n <span v-if=\"label\" class=\"sr-only\">{{ label }}: </span>\n <span v-if=\"!selected.length\" class=\"text-sui-ink-faint\">{{ placeholder }}</span>\n <span v-else-if=\"multiple\" class=\"flex flex-wrap gap-1 py-1.25\">\n <Tag\n v-for=\"option in selected\"\n :key=\"option.value\"\n :color=\"option.color ?? undefined\"\n :icon=\"option.icon\"\n :icon-color=\"option.iconColor\"\n :name=\"option.label\"\n />\n </span>\n <Tag\n v-else-if=\"selected[0]!.color\"\n :color=\"selected[0]!.color\"\n :icon=\"selected[0]!.icon\"\n :icon-color=\"selected[0]!.iconColor\"\n :name=\"selected[0]!.label\"\n />\n <span v-else-if=\"selected[0]!.icon\" class=\"inline-flex min-w-0 items-center gap-1.5\">\n <Icon class=\"shrink-0\" :color=\"selected[0]!.iconColor ?? 'ink-subtle'\" :name=\"selected[0]!.icon\" />\n <span class=\"truncate whitespace-nowrap\">{{ selected[0]!.label }}</span>\n </span>\n <span v-else class=\"min-w-0 truncate whitespace-nowrap\">{{ selected[0]!.label }}</span>\n <svg\n aria-hidden=\"true\"\n class=\"size-4 shrink-0 fill-none stroke-sui-ink-muted transition-transform duration-150 group-aria-expanded:rotate-180\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.5\"\n viewBox=\"0 0 16 16\"\n >\n <path d=\"M4.5 6.5 8 10l3.5-3.5\" />\n </svg>\n </button>\n </slot>\n </PopoverAnchor>\n <PopoverPortal>\n <PopoverContent\n :align=\"align\"\n :aria-label=\"label ?? placeholder\"\n class=\"z-[1100] overflow-hidden rounded-xl border border-sui-line-subtle bg-sui-surface text-sui-ink shadow-lg data-[state=open]:animate-sui-menu-in\"\n :class=\"\n search ? 'min-w-[max(var(--reka-popover-trigger-width),15rem)]' : 'min-w-(--reka-popover-trigger-width)'\n \"\n :side=\"side\"\n :side-offset=\"4\"\n :style=\"panelWidth ? { width: panelWidth } : undefined\"\n @close-auto-focus=\"returnFocus\"\n @open-auto-focus=\"onOpenAutoFocus\"\n @pointer-down-outside=\"onPointerDownOutside\"\n >\n <ListboxRoot\n ref=\"listbox\"\n :highlight-on-hover=\"true\"\n :model-value=\"multiple ? values : (values[0] ?? undefined)\"\n :multiple=\"multiple\"\n @highlight=\"onHighlight\"\n @keydown=\"onKeydown\"\n @pointermove=\"keyboard = false\"\n >\n <div\n v-if=\"search\"\n class=\"flex min-h-11 flex-wrap items-center gap-1 border-b border-sui-line-subtle px-3 py-2.5\"\n >\n <template v-if=\"removable\">\n <Tag\n v-for=\"option in selected\"\n :key=\"option.value\"\n :color=\"option.color ?? undefined\"\n :icon=\"option.icon\"\n :icon-color=\"option.iconColor\"\n :name=\"option.label\"\n removable\n :remove-label=\"removeLabel\"\n @remove=\"remove(option.value)\"\n />\n </template>\n <!-- focus: classes too: an app's forms plugin rings focused text fields, and outranks plain utilities. -->\n <ListboxFilter\n :aria-label=\"label ?? placeholder\"\n class=\"h-5 min-w-16 flex-1 rounded-none border-none bg-transparent p-0 text-13 text-sui-ink shadow-none outline-hidden placeholder:text-sui-ink-faint focus:border-none focus:shadow-none focus:ring-0 focus:outline-hidden\"\n data-select-filter\n :model-value=\"query\"\n :placeholder=\"removable && selected.length ? '' : searchPlaceholder\"\n @update:model-value=\"setQuery\"\n />\n </div>\n <div v-if=\"loading\" class=\"flex justify-center py-6\"><Spinner size=\"sm\" /></div>\n <ListboxContent\n v-else-if=\"listed.length || showCreate\"\n :aria-label=\"label ?? placeholder\"\n class=\"group/list max-h-60 overflow-auto p-1 outline-hidden\"\n :data-keyboard=\"keyboard || undefined\"\n >\n <ListboxVirtualizer\n v-if=\"virtual\"\n v-slot=\"{ option }\"\n :estimate-size=\"32\"\n :options=\"listed\"\n :text-content=\"(option) => option.label\"\n >\n <ListboxItem\n :class=\"optionClass\"\n :disabled=\"option.disabled\"\n :value=\"option.value\"\n @select=\"onSelect($event, option as SelectOption<T>)\"\n >\n <span class=\"inline-flex min-w-0 items-center gap-1.5 truncate\">\n <slot name=\"option\" :option=\"option as SelectOption<T>\">\n <Tag\n v-if=\"option.color\"\n :color=\"option.color\"\n :icon=\"option.icon\"\n :icon-color=\"option.iconColor\"\n :name=\"option.label\"\n />\n <template v-else>\n <Icon\n v-if=\"option.icon\"\n class=\"shrink-0\"\n :color=\"option.iconColor ?? 'ink-subtle'\"\n :name=\"option.icon\"\n />{{ option.label }}\n </template>\n </slot>\n </span>\n <CheckIcon v-if=\"values.includes(option.value)\" />\n </ListboxItem>\n </ListboxVirtualizer>\n <template v-else>\n <ListboxItem\n v-for=\"option in listed\"\n :key=\"option.value\"\n :class=\"optionClass\"\n :disabled=\"option.disabled\"\n :value=\"option.value\"\n @select=\"onSelect($event, option)\"\n >\n <span class=\"inline-flex min-w-0 items-center gap-1.5 truncate\">\n <slot name=\"option\" :option=\"option\">\n <Tag\n v-if=\"option.color\"\n :color=\"option.color\"\n :icon=\"option.icon\"\n :icon-color=\"option.iconColor\"\n :name=\"option.label\"\n />\n <template v-else>\n <Icon\n v-if=\"option.icon\"\n class=\"shrink-0\"\n :color=\"option.iconColor ?? 'ink-subtle'\"\n :name=\"option.icon\"\n />{{ option.label }}\n </template>\n </slot>\n </span>\n <CheckIcon v-if=\"values.includes(option.value)\" />\n </ListboxItem>\n </template>\n <ListboxItem v-if=\"showCreate\" :class=\"optionClass\" :value=\"CREATE\" @select=\"onSelect($event, CREATE)\">\n <span class=\"inline-flex min-w-0 items-center gap-1.5\">\n {{ createLabel }}\n <Tag v-if=\"createColored\" :color=\"newHue\" :name=\"query.trim()\" />\n <strong v-else class=\"font-medium\">“{{ query.trim() }}”</strong>\n </span>\n </ListboxItem>\n </ListboxContent>\n <div v-else class=\"px-3.5 py-3 text-13 text-sui-ink-muted\">{{ emptyLabel }}</div>\n </ListboxRoot>\n </PopoverContent>\n </PopoverPortal>\n </PopoverRoot>\n</template>\n"],"mappings":""}
|