@leavepulse/ui 0.28.5 → 0.29.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/dist/assets/LpTable-DMOyAUOh.css +44 -0
- package/dist/chunks/{LpInfraNode-qIL3l7IO.js → LpInfraNode-Ds_8SZgN.js} +22 -8
- package/dist/chunks/{LpThemeSwitcher-7mAK6bI7.js → LpThemeSwitcher-Dj4VcTu7.js} +10 -3
- package/dist/chunks/{LpTopologyCanvas-ebP67CLx.js → LpTopologyCanvas-Cm1L39uL.js} +6 -5
- package/dist/components/LpInfraNode.vue.d.ts +30 -0
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpTable.vue.js +278 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.d.ts +11 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/composables/useFormDraft.d.ts +67 -0
- package/dist/composables/useFormDraft.js +124 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -22
- package/package.json +1 -1
- package/src/components/LpInfraNode.vue +75 -9
- package/src/components/LpTable.vue +96 -13
- package/src/components/LpThemeSwitcher.vue +25 -3
- package/src/components/LpTopologyCanvas.vue +4 -0
- package/src/composables/useFormDraft.ts +259 -0
- package/src/index.ts +3 -0
- package/dist/chunks/LpTable-DTj52qlg.js +0 -266
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
onScopeDispose,
|
|
3
|
+
ref,
|
|
4
|
+
toValue,
|
|
5
|
+
watch,
|
|
6
|
+
type MaybeRefOrGetter,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from "vue"
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* "The form I had filled in is still here."
|
|
12
|
+
*
|
|
13
|
+
* Long forms get abandoned mid-way by things that have nothing to do with the
|
|
14
|
+
* person filling them in: a session that ages out, a reload, a tab closed by
|
|
15
|
+
* accident, a laptop that sleeps. What was typed is held in component state,
|
|
16
|
+
* so all of those end the same way — an empty form and a person who has to
|
|
17
|
+
* remember what they had already read off the thing in their hand.
|
|
18
|
+
*
|
|
19
|
+
* The panel's own inventory forms are the case that prompted this: an operator
|
|
20
|
+
* standing at a shelf, copying figures off a label, is exactly the person who
|
|
21
|
+
* cannot retype them from memory.
|
|
22
|
+
*
|
|
23
|
+
* Consumers were writing this by hand, field by field: one object literal to
|
|
24
|
+
* save thirty values and thirty typed branches to read them back, which has to
|
|
25
|
+
* be kept in step with the form forever and silently stops covering whatever
|
|
26
|
+
* field was added last. This owns the mechanism instead — what to keep is the
|
|
27
|
+
* caller's business, keeping it is not.
|
|
28
|
+
*
|
|
29
|
+
* What this deliberately does NOT do: decide that a draft is worth restoring.
|
|
30
|
+
* It reports one and hands back the value; showing it, and asking, is the
|
|
31
|
+
* caller's, because only the caller knows whether the record moved underneath
|
|
32
|
+
* it in the meantime.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Bumped when the envelope changes shape. A draft written by an older kit is
|
|
36
|
+
* dropped rather than half-read: a partially-applied draft is worse than none,
|
|
37
|
+
* because it looks like something a person typed. */
|
|
38
|
+
const ENVELOPE_VERSION = 1
|
|
39
|
+
|
|
40
|
+
interface DraftEnvelope<T> {
|
|
41
|
+
version: number
|
|
42
|
+
savedAt: string
|
|
43
|
+
value: T
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UseFormDraftOptions<T> {
|
|
47
|
+
/**
|
|
48
|
+
* Where this draft lives. Include whatever identifies the subject — a draft
|
|
49
|
+
* for device A must not surface while editing device B.
|
|
50
|
+
*/
|
|
51
|
+
key: MaybeRefOrGetter<string>
|
|
52
|
+
/** The form's current value. Watched deeply; every change rewrites the draft. */
|
|
53
|
+
value: MaybeRefOrGetter<T>
|
|
54
|
+
/**
|
|
55
|
+
* Whether there is anything worth keeping. Return false while the form still
|
|
56
|
+
* matches what was loaded, so an untouched form leaves nothing behind and no
|
|
57
|
+
* later visit is offered its own starting state back as a "draft".
|
|
58
|
+
*/
|
|
59
|
+
dirty?: MaybeRefOrGetter<boolean>
|
|
60
|
+
/**
|
|
61
|
+
* How long a draft stays offerable, in milliseconds. Older ones are dropped
|
|
62
|
+
* on read: a fortnight-old draft of a record that has been edited twice since
|
|
63
|
+
* is not a rescue, it is a way to overwrite current facts with stale ones.
|
|
64
|
+
* Defaults to a week. `0` keeps them indefinitely.
|
|
65
|
+
*/
|
|
66
|
+
maxAgeMs?: number
|
|
67
|
+
/** Skip the write without tearing the composable out of the component. */
|
|
68
|
+
disabled?: MaybeRefOrGetter<boolean>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface UseFormDraft<T> {
|
|
72
|
+
/** A draft was found for this key, and has not been restored or discarded. */
|
|
73
|
+
hasDraft: Ref<boolean>
|
|
74
|
+
/** When that draft was written, ISO-8601, or `null` when there is none. */
|
|
75
|
+
savedAt: Ref<string | null>
|
|
76
|
+
/**
|
|
77
|
+
* The stored value, or `null`. Reading does not apply it — the caller decides
|
|
78
|
+
* what to do with each field, because only it knows the form's shape.
|
|
79
|
+
*/
|
|
80
|
+
read: () => T | null
|
|
81
|
+
/** Forget the draft. Call after applying it, and after a successful save. */
|
|
82
|
+
discard: () => void
|
|
83
|
+
/** Write now, ignoring `dirty`. For "save and come back to this later". */
|
|
84
|
+
save: () => void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function storage(): Storage | null {
|
|
88
|
+
// Private windows, storage disabled by policy, SSR. A form that cannot keep
|
|
89
|
+
// a draft still has to work, so every access is allowed to fail.
|
|
90
|
+
try {
|
|
91
|
+
if (typeof window === "undefined") return null
|
|
92
|
+
return window.localStorage
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Keeps an unsaved form in browser storage, so losing the page does not lose
|
|
102
|
+
* what was typed into it.
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* const draft = useFormDraft({
|
|
106
|
+
* key: () => `lp.draft.device-power:${deviceId.value}`,
|
|
107
|
+
* value: () => interfaces.value,
|
|
108
|
+
* dirty: () => hasUnsavedChanges.value,
|
|
109
|
+
* })
|
|
110
|
+
* ```
|
|
111
|
+
* ```vue
|
|
112
|
+
* <LpAlert v-if="draft.hasDraft.value" variant="info">
|
|
113
|
+
* {{ t("draft.found", { when: draft.savedAt.value }) }}
|
|
114
|
+
* <LpButton @click="applyDraft">Restore</LpButton>
|
|
115
|
+
* <LpButton variant="ghost" @click="draft.discard()">Discard</LpButton>
|
|
116
|
+
* </LpAlert>
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* `hasDraft` answers for the key as it is now, so a form that switches subject
|
|
120
|
+
* re-reads rather than carrying the previous subject's answer.
|
|
121
|
+
*
|
|
122
|
+
* Call `discard()` once the record is saved. A draft that outlives the save it
|
|
123
|
+
* describes is offered back on the next visit as though it were newer than the
|
|
124
|
+
* record, which is how a form helpfully undoes somebody's work.
|
|
125
|
+
*/
|
|
126
|
+
export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T> {
|
|
127
|
+
const hasDraft = ref(false)
|
|
128
|
+
const savedAt = ref<string | null>(null)
|
|
129
|
+
const maxAge = options.maxAgeMs ?? WEEK_MS
|
|
130
|
+
|
|
131
|
+
function currentKey(): string {
|
|
132
|
+
return String(toValue(options.key) || "").trim()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function read(): T | null {
|
|
136
|
+
const store = storage()
|
|
137
|
+
const key = currentKey()
|
|
138
|
+
if (!store || !key) return null
|
|
139
|
+
let raw: string | null = null
|
|
140
|
+
try {
|
|
141
|
+
raw = store.getItem(key)
|
|
142
|
+
} catch {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
if (!raw) return null
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(raw) as DraftEnvelope<T>
|
|
148
|
+
if (!parsed || parsed.version !== ENVELOPE_VERSION) {
|
|
149
|
+
discard()
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
if (maxAge > 0) {
|
|
153
|
+
const age = Date.now() - Date.parse(parsed.savedAt)
|
|
154
|
+
// NaN when the stamp is unreadable, and NaN > maxAge is false — an
|
|
155
|
+
// unreadable stamp is treated as "no idea how old", which errs towards
|
|
156
|
+
// offering the draft rather than throwing away somebody's typing.
|
|
157
|
+
if (age > maxAge) {
|
|
158
|
+
discard()
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return parsed.value
|
|
163
|
+
} catch {
|
|
164
|
+
// Corrupt or hand-edited. Nothing to rescue, and leaving it means asking
|
|
165
|
+
// about it on every visit forever.
|
|
166
|
+
discard()
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function write(value: T) {
|
|
172
|
+
const store = storage()
|
|
173
|
+
const key = currentKey()
|
|
174
|
+
if (!store || !key) return
|
|
175
|
+
const stamp = new Date().toISOString()
|
|
176
|
+
try {
|
|
177
|
+
store.setItem(
|
|
178
|
+
key,
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
version: ENVELOPE_VERSION,
|
|
181
|
+
savedAt: stamp,
|
|
182
|
+
value,
|
|
183
|
+
} satisfies DraftEnvelope<T>),
|
|
184
|
+
)
|
|
185
|
+
} catch {
|
|
186
|
+
// Quota, or storage turned off between the read and now. The form is
|
|
187
|
+
// unharmed; it simply has no safety net.
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
hasDraft.value = true
|
|
191
|
+
savedAt.value = stamp
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function discard() {
|
|
195
|
+
const store = storage()
|
|
196
|
+
const key = currentKey()
|
|
197
|
+
hasDraft.value = false
|
|
198
|
+
savedAt.value = null
|
|
199
|
+
if (!store || !key) return
|
|
200
|
+
try {
|
|
201
|
+
store.removeItem(key)
|
|
202
|
+
} catch {
|
|
203
|
+
// Nothing to do about it, and nothing depends on it.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function save() {
|
|
208
|
+
write(toValue(options.value))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Look for an existing draft, and look again when the subject changes.
|
|
212
|
+
watch(
|
|
213
|
+
() => currentKey(),
|
|
214
|
+
() => {
|
|
215
|
+
hasDraft.value = false
|
|
216
|
+
savedAt.value = null
|
|
217
|
+
const store = storage()
|
|
218
|
+
const key = currentKey()
|
|
219
|
+
if (!store || !key) return
|
|
220
|
+
let raw: string | null = null
|
|
221
|
+
try {
|
|
222
|
+
raw = store.getItem(key)
|
|
223
|
+
} catch {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (!raw) return
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(raw) as DraftEnvelope<T>
|
|
229
|
+
if (parsed?.version !== ENVELOPE_VERSION) return
|
|
230
|
+
hasDraft.value = true
|
|
231
|
+
savedAt.value = parsed.savedAt
|
|
232
|
+
} catch {
|
|
233
|
+
// Left for `read` to clear: reporting it here would mean writing to
|
|
234
|
+
// storage from what is meant to be a look.
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{ immediate: true },
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
watch(
|
|
241
|
+
() => [toValue(options.value), toValue(options.dirty), toValue(options.disabled)],
|
|
242
|
+
([value, dirty, disabled]) => {
|
|
243
|
+
if (disabled) return
|
|
244
|
+
// `dirty` unset means "every change is worth keeping"; the option exists
|
|
245
|
+
// so a form that knows it is untouched leaves no trace.
|
|
246
|
+
if (options.dirty !== undefined && !dirty) return
|
|
247
|
+
write(value as T)
|
|
248
|
+
},
|
|
249
|
+
{ deep: true },
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
onScopeDispose(() => {
|
|
253
|
+
// The draft outlives the component on purpose — that is the whole point —
|
|
254
|
+
// so there is nothing to clean up but the local flags.
|
|
255
|
+
hasDraft.value = false
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return { hasDraft, savedAt, read, discard, save }
|
|
259
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -213,6 +213,9 @@ export { useReveal } from "./composables/useReveal"
|
|
|
213
213
|
export type { UseReveal, UseRevealOptions } from "./composables/useReveal"
|
|
214
214
|
export { useRovingFocus } from "./composables/useRovingFocus"
|
|
215
215
|
export type { RovingItem, UseRovingFocusOptions } from "./composables/useRovingFocus"
|
|
216
|
+
export { useFormDraft } from "./composables/useFormDraft"
|
|
217
|
+
export type { UseFormDraft, UseFormDraftOptions } from "./composables/useFormDraft"
|
|
218
|
+
|
|
216
219
|
export { useSettled } from "./composables/useSettled"
|
|
217
220
|
export type { UseSettledOptions } from "./composables/useSettled"
|
|
218
221
|
export { useShift } from "./composables/useShift"
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { defineComponent, computed, ref, watch, onBeforeUnmount, openBlock, createBlock, withCtx, createElementVNode, normalizeClass, createElementBlock, createVNode, createCommentVNode, Fragment, renderList, normalizeStyle, createTextVNode, toDisplayString, unref, mergeProps, withModifiers, renderSlot } from "vue";
|
|
2
|
-
import { _ as _sfc_main$2 } from "./LpCheckbox-BO0zuuPh.js";
|
|
3
|
-
import { _ as _sfc_main$4 } from "./LpContextMenu-D0mzXqA7.js";
|
|
4
|
-
import { _ as _sfc_main$3 } from "./LpIcon-CCnX5_2j.js";
|
|
5
|
-
import { _ as _sfc_main$1 } from "./LpScrollArea-D8xyKXcF.js";
|
|
6
|
-
import { useRovingFocus } from "../composables/useRovingFocus.js";
|
|
7
|
-
const _hoisted_1 = { class: "w-full border-collapse text-sm" };
|
|
8
|
-
const _hoisted_2 = { class: "border-b border-line bg-surface-soft" };
|
|
9
|
-
const _hoisted_3 = {
|
|
10
|
-
key: 0,
|
|
11
|
-
class: "w-px px-4 py-3"
|
|
12
|
-
};
|
|
13
|
-
const _hoisted_4 = ["aria-sort"];
|
|
14
|
-
const _hoisted_5 = ["onClick"];
|
|
15
|
-
const _hoisted_6 = { key: 0 };
|
|
16
|
-
const _hoisted_7 = ["colspan"];
|
|
17
|
-
const _hoisted_8 = ["onClick"];
|
|
18
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
19
|
-
__name: "LpTable",
|
|
20
|
-
props: {
|
|
21
|
-
columns: {},
|
|
22
|
-
rows: {},
|
|
23
|
-
rowKey: {},
|
|
24
|
-
emptyLabel: { default: "Nothing here yet" },
|
|
25
|
-
emptyIcon: { default: "lucide:inbox" },
|
|
26
|
-
selectable: { type: Boolean, default: false },
|
|
27
|
-
selected: { default: () => [] },
|
|
28
|
-
sort: { default: null },
|
|
29
|
-
stickyHeader: { type: Boolean },
|
|
30
|
-
rowMenu: {}
|
|
31
|
-
},
|
|
32
|
-
emits: ["update:selected", "update:sort", "rowClick"],
|
|
33
|
-
setup(__props, { emit: __emit }) {
|
|
34
|
-
const props = __props;
|
|
35
|
-
const emit = __emit;
|
|
36
|
-
function keyFor(row, index) {
|
|
37
|
-
if (typeof props.rowKey === "function") return props.rowKey(row);
|
|
38
|
-
if (props.rowKey) return row[props.rowKey];
|
|
39
|
-
return index;
|
|
40
|
-
}
|
|
41
|
-
function alignClass(align) {
|
|
42
|
-
if (align === "right") return "text-right";
|
|
43
|
-
if (align === "center") return "text-center";
|
|
44
|
-
return "text-left";
|
|
45
|
-
}
|
|
46
|
-
function menuFor(row) {
|
|
47
|
-
return props.rowMenu?.(row) ?? [];
|
|
48
|
-
}
|
|
49
|
-
const displayRows = computed(() => {
|
|
50
|
-
const s = props.sort;
|
|
51
|
-
if (!s) return props.rows;
|
|
52
|
-
const col = props.columns.find((c) => c.key === s.key);
|
|
53
|
-
if (!col?.sortable) return props.rows;
|
|
54
|
-
const factor = s.dir === "asc" ? 1 : -1;
|
|
55
|
-
return [...props.rows].sort((a, b) => compare(a[s.key], b[s.key]) * factor);
|
|
56
|
-
});
|
|
57
|
-
function compare(a, b) {
|
|
58
|
-
if (a == null && b == null) return 0;
|
|
59
|
-
if (a == null) return -1;
|
|
60
|
-
if (b == null) return 1;
|
|
61
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
62
|
-
return String(a).localeCompare(String(b), void 0, { numeric: true });
|
|
63
|
-
}
|
|
64
|
-
function toggleSort(col) {
|
|
65
|
-
if (!col.sortable) return;
|
|
66
|
-
const s = props.sort;
|
|
67
|
-
let next;
|
|
68
|
-
if (!s || s.key !== col.key) next = { key: col.key, dir: "asc" };
|
|
69
|
-
else if (s.dir === "asc") next = { key: col.key, dir: "desc" };
|
|
70
|
-
else next = null;
|
|
71
|
-
emit("update:sort", next);
|
|
72
|
-
}
|
|
73
|
-
function sortIcon(col) {
|
|
74
|
-
if (props.sort?.key !== col.key) return "lucide:chevrons-up-down";
|
|
75
|
-
return props.sort.dir === "asc" ? "lucide:arrow-up" : "lucide:arrow-down";
|
|
76
|
-
}
|
|
77
|
-
const selectedSet = computed(() => new Set(props.selected));
|
|
78
|
-
const allKeys = computed(() => props.rows.map((r, i) => keyFor(r, i)));
|
|
79
|
-
const allChecked = computed(
|
|
80
|
-
() => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k))
|
|
81
|
-
);
|
|
82
|
-
const someChecked = computed(
|
|
83
|
-
() => !allChecked.value && allKeys.value.some((k) => selectedSet.value.has(k))
|
|
84
|
-
);
|
|
85
|
-
function toggleAll(checked) {
|
|
86
|
-
emit("update:selected", checked ? [...allKeys.value] : []);
|
|
87
|
-
}
|
|
88
|
-
function toggleRow(key, checked) {
|
|
89
|
-
const next = new Set(selectedSet.value);
|
|
90
|
-
if (checked) next.add(key);
|
|
91
|
-
else next.delete(key);
|
|
92
|
-
emit("update:selected", [...next]);
|
|
93
|
-
}
|
|
94
|
-
const colSpan = computed(() => props.columns.length + (props.selectable ? 1 : 0));
|
|
95
|
-
const rovingItems = computed(
|
|
96
|
-
() => displayRows.value.map((row, index) => ({
|
|
97
|
-
id: String(keyFor(row, index)),
|
|
98
|
-
label: String(row[props.columns[0]?.key ?? ""] ?? "")
|
|
99
|
-
}))
|
|
100
|
-
);
|
|
101
|
-
const rowByKey = computed(() => {
|
|
102
|
-
const map = /* @__PURE__ */ new Map();
|
|
103
|
-
displayRows.value.forEach((row, index) => map.set(String(keyFor(row, index)), row));
|
|
104
|
-
return map;
|
|
105
|
-
});
|
|
106
|
-
const roving = useRovingFocus(rovingItems, {
|
|
107
|
-
onActivate: (id) => {
|
|
108
|
-
const row = rowByKey.value.get(id);
|
|
109
|
-
if (!row) return;
|
|
110
|
-
emit("rowClick", row);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
function onRowKeydown(event) {
|
|
114
|
-
if (event.key === " " && props.selectable && roving.activeId.value) {
|
|
115
|
-
event.preventDefault();
|
|
116
|
-
const key = roving.activeId.value;
|
|
117
|
-
const row = rowByKey.value.get(key);
|
|
118
|
-
if (row) {
|
|
119
|
-
const rowKey = keyFor(row, displayRows.value.indexOf(row));
|
|
120
|
-
toggleRow(rowKey, !selectedSet.value.has(rowKey));
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
roving.onKeydown(event);
|
|
125
|
-
}
|
|
126
|
-
const headEl = ref(null);
|
|
127
|
-
const headHeight = ref(0);
|
|
128
|
-
let ro;
|
|
129
|
-
watch(
|
|
130
|
-
[headEl, () => props.stickyHeader],
|
|
131
|
-
([el, sticky]) => {
|
|
132
|
-
ro?.disconnect();
|
|
133
|
-
if (!el || !sticky || typeof ResizeObserver === "undefined") {
|
|
134
|
-
headHeight.value = 0;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
ro = new ResizeObserver(() => {
|
|
138
|
-
headHeight.value = el.offsetHeight;
|
|
139
|
-
});
|
|
140
|
-
ro.observe(el);
|
|
141
|
-
headHeight.value = el.offsetHeight;
|
|
142
|
-
},
|
|
143
|
-
{ immediate: true, flush: "post" }
|
|
144
|
-
);
|
|
145
|
-
onBeforeUnmount(() => ro?.disconnect());
|
|
146
|
-
const barInsetTop = computed(
|
|
147
|
-
() => props.stickyHeader && headHeight.value ? `${headHeight.value}px` : void 0
|
|
148
|
-
);
|
|
149
|
-
return (_ctx, _cache) => {
|
|
150
|
-
return openBlock(), createBlock(_sfc_main$1, {
|
|
151
|
-
fade: false,
|
|
152
|
-
class: "rounded-card border border-line",
|
|
153
|
-
"bar-inset-top": barInsetTop.value
|
|
154
|
-
}, {
|
|
155
|
-
default: withCtx(() => [
|
|
156
|
-
createElementVNode("table", _hoisted_1, [
|
|
157
|
-
createElementVNode("thead", {
|
|
158
|
-
ref_key: "headEl",
|
|
159
|
-
ref: headEl,
|
|
160
|
-
class: normalizeClass(__props.stickyHeader ? "sticky top-0 z-10" : "")
|
|
161
|
-
}, [
|
|
162
|
-
createElementVNode("tr", _hoisted_2, [
|
|
163
|
-
__props.selectable ? (openBlock(), createElementBlock("th", _hoisted_3, [
|
|
164
|
-
createVNode(_sfc_main$2, {
|
|
165
|
-
"model-value": allChecked.value,
|
|
166
|
-
indeterminate: someChecked.value,
|
|
167
|
-
"onUpdate:modelValue": toggleAll
|
|
168
|
-
}, null, 8, ["model-value", "indeterminate"])
|
|
169
|
-
])) : createCommentVNode("", true),
|
|
170
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (col) => {
|
|
171
|
-
return openBlock(), createElementBlock("th", {
|
|
172
|
-
key: col.key,
|
|
173
|
-
style: normalizeStyle(col.width ? { width: col.width } : void 0),
|
|
174
|
-
class: normalizeClass(["px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted", alignClass(col.align)]),
|
|
175
|
-
"aria-sort": __props.sort?.key === col.key ? __props.sort.dir === "asc" ? "ascending" : "descending" : void 0
|
|
176
|
-
}, [
|
|
177
|
-
col.sortable ? (openBlock(), createElementBlock("button", {
|
|
178
|
-
key: 0,
|
|
179
|
-
type: "button",
|
|
180
|
-
class: normalizeClass(["group/sort -mx-1 inline-flex items-center gap-1 rounded px-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring", [
|
|
181
|
-
col.align === "right" ? "flex-row-reverse" : "",
|
|
182
|
-
__props.sort?.key === col.key ? "text-ink" : ""
|
|
183
|
-
]]),
|
|
184
|
-
onClick: ($event) => toggleSort(col)
|
|
185
|
-
}, [
|
|
186
|
-
createTextVNode(toDisplayString(col.label) + " ", 1),
|
|
187
|
-
createVNode(_sfc_main$3, {
|
|
188
|
-
name: sortIcon(col),
|
|
189
|
-
size: 13,
|
|
190
|
-
class: normalizeClass(["shrink-0 transition-[transform,opacity] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)]", __props.sort?.key === col.key ? "opacity-100" : "opacity-40 group-hover/sort:opacity-70"])
|
|
191
|
-
}, null, 8, ["name", "class"])
|
|
192
|
-
], 10, _hoisted_5)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
193
|
-
createTextVNode(toDisplayString(col.label), 1)
|
|
194
|
-
], 64))
|
|
195
|
-
], 14, _hoisted_4);
|
|
196
|
-
}), 128))
|
|
197
|
-
])
|
|
198
|
-
], 2),
|
|
199
|
-
createElementVNode("tbody", {
|
|
200
|
-
ref: (el) => unref(roving).setContainer(el)
|
|
201
|
-
}, [
|
|
202
|
-
displayRows.value.length === 0 ? (openBlock(), createElementBlock("tr", _hoisted_6, [
|
|
203
|
-
createElementVNode("td", {
|
|
204
|
-
colspan: colSpan.value,
|
|
205
|
-
class: "px-4 py-10 text-center text-muted"
|
|
206
|
-
}, [
|
|
207
|
-
createVNode(_sfc_main$3, {
|
|
208
|
-
name: __props.emptyIcon,
|
|
209
|
-
size: 22,
|
|
210
|
-
class: "mx-auto mb-2 opacity-60"
|
|
211
|
-
}, null, 8, ["name"]),
|
|
212
|
-
createElementVNode("div", null, toDisplayString(__props.emptyLabel), 1)
|
|
213
|
-
], 8, _hoisted_7)
|
|
214
|
-
])) : createCommentVNode("", true),
|
|
215
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(displayRows.value, (row, index) => {
|
|
216
|
-
return openBlock(), createBlock(_sfc_main$4, {
|
|
217
|
-
key: keyFor(row, index),
|
|
218
|
-
items: menuFor(row)
|
|
219
|
-
}, {
|
|
220
|
-
default: withCtx(() => [
|
|
221
|
-
createElementVNode("tr", mergeProps({
|
|
222
|
-
class: ["border-b border-line/60 outline-none transition-colors last:border-0 hover:bg-surface-soft/60 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[active]:bg-surface-soft/40", selectedSet.value.has(keyFor(row, index)) ? "bg-brand-soft/40" : ""]
|
|
223
|
-
}, { ref_for: true }, unref(roving).itemProps(String(keyFor(row, index))), {
|
|
224
|
-
onClick: ($event) => emit("rowClick", row),
|
|
225
|
-
onKeydown: onRowKeydown
|
|
226
|
-
}), [
|
|
227
|
-
__props.selectable ? (openBlock(), createElementBlock("td", {
|
|
228
|
-
key: 0,
|
|
229
|
-
class: "w-px px-4 py-3",
|
|
230
|
-
onClick: _cache[0] || (_cache[0] = withModifiers(() => {
|
|
231
|
-
}, ["stop"]))
|
|
232
|
-
}, [
|
|
233
|
-
createVNode(_sfc_main$2, {
|
|
234
|
-
"model-value": selectedSet.value.has(keyFor(row, index)),
|
|
235
|
-
"onUpdate:modelValue": (v) => toggleRow(keyFor(row, index), v)
|
|
236
|
-
}, null, 8, ["model-value", "onUpdate:modelValue"])
|
|
237
|
-
])) : createCommentVNode("", true),
|
|
238
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.columns, (col) => {
|
|
239
|
-
return openBlock(), createElementBlock("td", {
|
|
240
|
-
key: col.key,
|
|
241
|
-
class: normalizeClass(["px-4 py-3 text-ink", alignClass(col.align)])
|
|
242
|
-
}, [
|
|
243
|
-
renderSlot(_ctx.$slots, `cell-${col.key}`, {
|
|
244
|
-
row,
|
|
245
|
-
value: row[col.key]
|
|
246
|
-
}, () => [
|
|
247
|
-
createTextVNode(toDisplayString(row[col.key]), 1)
|
|
248
|
-
])
|
|
249
|
-
], 2);
|
|
250
|
-
}), 128))
|
|
251
|
-
], 16, _hoisted_8)
|
|
252
|
-
]),
|
|
253
|
-
_: 2
|
|
254
|
-
}, 1032, ["items"]);
|
|
255
|
-
}), 128))
|
|
256
|
-
], 512)
|
|
257
|
-
])
|
|
258
|
-
]),
|
|
259
|
-
_: 3
|
|
260
|
-
}, 8, ["bar-inset-top"]);
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
export {
|
|
265
|
-
_sfc_main as _
|
|
266
|
-
};
|