@eifi1/ui-kit 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -9
- package/dist/chart.d.ts +10 -1
- package/dist/components/account-chips.d.ts +153 -0
- package/dist/components/account-chips.js +135 -0
- package/dist/components/account-chips.js.map +1 -0
- package/dist/components/account-settings-labels.d.ts +10 -2
- package/dist/components/account-settings-labels.js +3 -0
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/amount-input.d.ts +18 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -1
- package/dist/components/calculator.d.ts +10 -1
- package/dist/components/chip.d.ts +25 -1
- package/dist/components/chip.js +143 -6
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +42 -1
- package/dist/components/confirm-dialog.js +36 -5
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +10 -1
- package/dist/components/danger-confirm.d.ts +55 -4
- package/dist/components/danger-confirm.js +67 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +93 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -1
- package/dist/components/file-button.d.ts +10 -1
- package/dist/components/file-dropzone.d.ts +10 -1
- package/dist/components/form-actions.d.ts +10 -1
- package/dist/components/form-actions.js +35 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +30 -6
- package/dist/components/line-items.js +77 -33
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +13 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +10 -1
- package/dist/components/number-field.d.ts +11 -2
- package/dist/components/number-field.js +23 -22
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +10 -1
- package/dist/components/numpad-sheet.d.ts +10 -1
- package/dist/components/passkeys-setting.d.ts +27 -1
- package/dist/components/passkeys-setting.js +31 -5
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +25 -1
- package/dist/components/progress-bar.js +21 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +90 -0
- package/dist/components/reauth-dialog.js +158 -0
- package/dist/components/reauth-dialog.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -1
- package/dist/components/server-wake.d.ts +58 -0
- package/dist/components/server-wake.js +58 -0
- package/dist/components/server-wake.js.map +1 -0
- package/dist/components/settings-fields.d.ts +10 -1
- package/dist/components/share-card.d.ts +61 -0
- package/dist/components/share-card.js +350 -0
- package/dist/components/share-card.js.map +1 -0
- package/dist/components/signed-amount.d.ts +12 -3
- package/dist/components/signed-amount.js +6 -4
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/status-dot.d.ts +2 -0
- package/dist/components/text-link.d.ts +10 -1
- package/dist/components/time-input.d.ts +10 -1
- package/dist/components/toast.d.ts +10 -1
- package/dist/components/toast.js +42 -2
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +5 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +10 -1
- package/dist/components/ui.js +67 -30
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +2 -0
- package/dist/components/write-lock.d.ts +68 -0
- package/dist/components/write-lock.js +33 -0
- package/dist/components/write-lock.js.map +1 -0
- package/dist/{data-table-CfVXtCeG.d.ts → data-table-3mi2R3so.d.ts} +59 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +10 -1
- package/dist/hooks/use-overlay-history.js +16 -0
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +10 -1
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +78 -0
- package/dist/i18n/{locales/de.js → german.js} +59 -9
- package/dist/i18n/german.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +10 -1
- package/dist/i18n/kit-labels.js +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +23 -8
- package/dist/i18n/locales/de-CH.js +7 -3
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/{de.d.ts → en.d.ts} +24 -10
- package/dist/i18n/locales/en.js +95 -0
- package/dist/i18n/locales/en.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +10 -1
- package/dist/i18n/locales/es.js +51 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -1
- package/dist/i18n/locales/fr.js +51 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -1
- package/dist/i18n/locales/hu.js +54 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -1
- package/dist/i18n/locales/it.js +51 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -1
- package/dist/i18n/locales/zh.js +50 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +3 -3
- package/dist/i18n/swiss.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-DE--LmCk.d.ts → kit-labels-CPbsjlXr.d.ts} +282 -10
- package/dist/lib/server-wake.d.ts +130 -0
- package/dist/lib/server-wake.js +121 -0
- package/dist/lib/server-wake.js.map +1 -0
- package/dist/rhf/fields.d.ts +10 -1
- package/dist/rhf/form.d.ts +10 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +10 -1
- package/dist/search/global-search.d.ts +24 -4
- package/dist/search/global-search.js +31 -3
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/shell/app-shell.d.ts +10 -1
- package/dist/shell/top-bar-brand.d.ts +10 -1
- package/dist/shell/topbar-action-menu.d.ts +2 -0
- package/dist/shell.d.ts +10 -1
- package/dist/wizard/stepper-nav.d.ts +10 -1
- package/dist/wizard.d.ts +10 -1
- package/package.json +1 -1
- package/src/components/account-chips.tsx +302 -0
- package/src/components/account-settings-labels.ts +16 -2
- package/src/components/amount-input.tsx +8 -0
- package/src/components/chip.tsx +239 -6
- package/src/components/confirm-dialog.tsx +82 -7
- package/src/components/danger-confirm.tsx +118 -33
- package/src/components/data-table.tsx +185 -3
- package/src/components/form-actions.tsx +93 -4
- package/src/components/line-items.tsx +116 -40
- package/src/components/list.tsx +13 -1
- package/src/components/number-field.tsx +32 -26
- package/src/components/passkeys-setting.tsx +68 -7
- package/src/components/progress-bar.tsx +40 -4
- package/src/components/reauth-dialog.tsx +273 -0
- package/src/components/server-wake.tsx +124 -0
- package/src/components/share-card.tsx +615 -0
- package/src/components/signed-amount.tsx +16 -5
- package/src/components/toast.tsx +72 -2
- package/src/components/toggle-group.tsx +5 -2
- package/src/components/ui.tsx +145 -34
- package/src/components/write-lock.tsx +96 -0
- package/src/hooks/use-overlay-history.ts +51 -0
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/{locales/de.ts → german.ts} +68 -13
- package/src/i18n/kit-labels.tsx +22 -2
- package/src/i18n/locales/de-CH.ts +17 -8
- package/src/i18n/locales/en.ts +116 -0
- package/src/i18n/locales/es.ts +51 -0
- package/src/i18n/locales/fr.ts +52 -0
- package/src/i18n/locales/hu.ts +55 -0
- package/src/i18n/locales/it.ts +51 -0
- package/src/i18n/locales/zh.ts +50 -0
- package/src/i18n/swiss.ts +3 -3
- package/src/index.ts +23 -1
- package/src/lib/server-wake.ts +270 -0
- package/src/rhf/line-items.tsx +2 -1
- package/src/search/global-search.tsx +59 -6
- package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
- package/dist/i18n/locales/de-CH-informal.js +0 -8
- package/dist/i18n/locales/de-CH-informal.js.map +0 -1
- package/dist/i18n/locales/de-informal.d.ts +0 -82
- package/dist/i18n/locales/de-informal.js +0 -37
- package/dist/i18n/locales/de-informal.js.map +0 -1
- package/dist/i18n/locales/de.js.map +0 -1
- package/src/i18n/locales/de-CH-informal.ts +0 -10
- package/src/i18n/locales/de-informal.ts +0 -61
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Is the server asleep, or is the app broken?" — the cold-start watchdog, lifted
|
|
3
|
+
* from keksdose (feedback #199) so kastlan and Kurvenschmiede get it too.
|
|
4
|
+
*
|
|
5
|
+
* All three backends run on Cloud Run with `--min-instances=0`: after an idle
|
|
6
|
+
* period the container is gone and the first request has to start it, which takes
|
|
7
|
+
* several seconds and occasionally more. Nothing in the UI said so, and none of the
|
|
8
|
+
* axios instances has a timeout, so the honest experience was a spinner that looked
|
|
9
|
+
* stuck. This counts the in-flight requests a cold start would actually delay and
|
|
10
|
+
* escalates in two steps, so a merely slow response says "still loading" and only a
|
|
11
|
+
* genuinely long wait blames the sleeping server.
|
|
12
|
+
*
|
|
13
|
+
* Framework-free on purpose (no zustand, no axios, no React): a counter, two timers
|
|
14
|
+
* and a listener set, read by `useServerWakeStage` through `useSyncExternalStore`.
|
|
15
|
+
* The adapters below take the app's axios instance / fetch as an ARGUMENT, so the
|
|
16
|
+
* kit never imports either.
|
|
17
|
+
*
|
|
18
|
+
* WHAT ARMS IT, and why the default is narrow:
|
|
19
|
+
* - reads (GET) — what every page does on open — always;
|
|
20
|
+
* - writes only when the app says so (`watchReadsAnd(/\/auth\/login\b/)`): the
|
|
21
|
+
* auth POSTs that gate the app are the only writes a user waits on before the
|
|
22
|
+
* app exists at all, so a cold start there looks exactly like a broken login.
|
|
23
|
+
* An UPLOAD must never be watched — a statement PDF or a photo takes seconds on a
|
|
24
|
+
* perfectly warm server, and "the server is waking up" during a 4 s upload would
|
|
25
|
+
* simply be false. With the default that holds by itself (an upload is a POST);
|
|
26
|
+
* an app widening `shouldWatch` keeps its upload routes out of it.
|
|
27
|
+
*
|
|
28
|
+
* It never arms while the browser is offline, whatever `shouldWatch` says: that state
|
|
29
|
+
* has its own, truthful indicator, and a request that never left the device is not
|
|
30
|
+
* evidence of anything about the server.
|
|
31
|
+
*/
|
|
32
|
+
/** `slow` = longer than a warm server ever takes; `waking` = long enough that a cold
|
|
33
|
+
* start is the only sensible explanation. */
|
|
34
|
+
type ServerWakeStage = "idle" | "slow" | "waking";
|
|
35
|
+
/** Decides which requests count. `method` arrives lower-cased (`"get"` when the
|
|
36
|
+
* request named none — axios's and fetch's default); `url` as the client had it. */
|
|
37
|
+
type ServerWakeFilter = (method: string, url: string) => boolean;
|
|
38
|
+
interface ServerWakeOptions {
|
|
39
|
+
/** Past this, a warm round trip is out of the question (a cold database query on a
|
|
40
|
+
* free tier can still take a second, so it is not tighter). Default 2000 ms. */
|
|
41
|
+
slowMs?: number;
|
|
42
|
+
/** Past this, the container was almost certainly scaled to zero. Default 7000 ms. */
|
|
43
|
+
wakingMs?: number;
|
|
44
|
+
/** Which requests count. Default {@link watchReadsAnd}`()` — GETs only. The offline
|
|
45
|
+
* guard runs before it and cannot be overridden. */
|
|
46
|
+
shouldWatch?: ServerWakeFilter;
|
|
47
|
+
}
|
|
48
|
+
/** What a request is, for {@link ServerWakeWatcher.track}. */
|
|
49
|
+
interface ServerWakeRequest {
|
|
50
|
+
method?: string;
|
|
51
|
+
url?: string;
|
|
52
|
+
}
|
|
53
|
+
interface ServerWakeWatcher {
|
|
54
|
+
/**
|
|
55
|
+
* Count a request that is going out, and arm the escalation if it is the first.
|
|
56
|
+
* Returns whether it was counted — the caller MUST call {@link end} exactly once
|
|
57
|
+
* for every `true`, or the counter never returns to zero and the notice sticks.
|
|
58
|
+
* (The axios adapter stashes it on the request config for that reason.)
|
|
59
|
+
*/
|
|
60
|
+
start(method?: string, url?: string): boolean;
|
|
61
|
+
/** A counted request settled — either way: an error still proves the server
|
|
62
|
+
* answered or gave up. When the last one does, the notice goes away. Never takes
|
|
63
|
+
* the counter below zero. */
|
|
64
|
+
end(): void;
|
|
65
|
+
/** `start` + `end` around a promise (or a function returning one), for a request
|
|
66
|
+
* that does not go through an adapter. Resolves and rejects as the work does. */
|
|
67
|
+
track<T>(work: PromiseLike<T> | (() => PromiseLike<T>), request?: ServerWakeRequest): Promise<T>;
|
|
68
|
+
/** Back to a pristine watchdog — for tests, which share an instance across cases. */
|
|
69
|
+
reset(): void;
|
|
70
|
+
/** `useSyncExternalStore`'s pair. Listeners hear stage changes only. */
|
|
71
|
+
subscribe(listener: () => void): () => void;
|
|
72
|
+
getStage(): ServerWakeStage;
|
|
73
|
+
/** Counted requests currently outstanding. */
|
|
74
|
+
getInFlight(): number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The usual filter: every GET, plus any other request whose URL matches `writes` —
|
|
78
|
+
* keksdose's `/\/auth\/(login|register|demo-session)\b/`. Keep upload routes out of
|
|
79
|
+
* the pattern (see the module comment).
|
|
80
|
+
*/
|
|
81
|
+
declare function watchReadsAnd(writes?: RegExp): ServerWakeFilter;
|
|
82
|
+
declare function createServerWake({ slowMs, wakingMs, shouldWatch, }?: ServerWakeOptions): ServerWakeWatcher;
|
|
83
|
+
/**
|
|
84
|
+
* The shared instance: what `<ServerWakeNotice />`, `useServerWakeStage()`,
|
|
85
|
+
* {@link attachServerWake} and {@link wrapFetch} use when no watcher is passed. GETs
|
|
86
|
+
* only, default timings — an app that also watches its login POST creates its own
|
|
87
|
+
* with {@link createServerWake} and passes it to both ends.
|
|
88
|
+
*/
|
|
89
|
+
declare const serverWake: ServerWakeWatcher;
|
|
90
|
+
/** The two config fields the watchdog reads — what axios's request config has. */
|
|
91
|
+
interface ServerWakeAxiosConfig {
|
|
92
|
+
method?: string;
|
|
93
|
+
url?: string;
|
|
94
|
+
}
|
|
95
|
+
/** The slice of an interceptor manager the adapter uses. A method (not a function
|
|
96
|
+
* property) so axios's wider `use(onFulfilled?, onRejected?, options?)` fits it. */
|
|
97
|
+
interface InterceptorManagerLike<V> {
|
|
98
|
+
use(onFulfilled: (value: V) => V | Promise<V>, onRejected: (error: unknown) => unknown): number;
|
|
99
|
+
eject(id: number): void;
|
|
100
|
+
}
|
|
101
|
+
/** An axios instance, structurally — so the kit types the adapter without importing
|
|
102
|
+
* axios. `axios.create()`'s result fits it as is. */
|
|
103
|
+
interface AxiosLikeInstance<C extends ServerWakeAxiosConfig, R extends {
|
|
104
|
+
config: C;
|
|
105
|
+
}> {
|
|
106
|
+
interceptors: {
|
|
107
|
+
request: InterceptorManagerLike<C>;
|
|
108
|
+
response: InterceptorManagerLike<R>;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Drive a watcher from an axios instance's interceptors: `attachServerWake(api)`.
|
|
113
|
+
* Returns a function that ejects both interceptors again.
|
|
114
|
+
*
|
|
115
|
+
* Attach it BEFORE the app's own response interceptors: axios runs those in the order
|
|
116
|
+
* they were added, so the wake one ends the wait before a 401 handler awaits a token
|
|
117
|
+
* refresh — otherwise the notice would keep counting a request that has already been
|
|
118
|
+
* answered. (Request-side order does not matter.)
|
|
119
|
+
*/
|
|
120
|
+
declare function attachServerWake<C extends ServerWakeAxiosConfig, R extends {
|
|
121
|
+
config: C;
|
|
122
|
+
}>(instance: AxiosLikeInstance<C, R>, watcher?: ServerWakeWatcher): () => void;
|
|
123
|
+
/**
|
|
124
|
+
* A `fetch` that drives a watcher: `const apiFetch = wrapFetch(fetch)`. The wait ends
|
|
125
|
+
* when the response's HEADERS arrive (or the call fails) — the server is awake by then,
|
|
126
|
+
* however long the body takes.
|
|
127
|
+
*/
|
|
128
|
+
declare function wrapFetch(fetchImpl?: typeof fetch, watcher?: ServerWakeWatcher): typeof fetch;
|
|
129
|
+
|
|
130
|
+
export { type AxiosLikeInstance, type ServerWakeAxiosConfig, type ServerWakeFilter, type ServerWakeOptions, type ServerWakeRequest, type ServerWakeStage, type ServerWakeWatcher, attachServerWake, createServerWake, serverWake, watchReadsAnd, wrapFetch };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
function watchReadsAnd(writes) {
|
|
3
|
+
return (method, url) => method === "get" || writes !== void 0 && writes.test(url);
|
|
4
|
+
}
|
|
5
|
+
function isOffline() {
|
|
6
|
+
return typeof navigator !== "undefined" && navigator.onLine === false;
|
|
7
|
+
}
|
|
8
|
+
function createServerWake({
|
|
9
|
+
slowMs = 2e3,
|
|
10
|
+
wakingMs = 7e3,
|
|
11
|
+
shouldWatch = watchReadsAnd()
|
|
12
|
+
} = {}) {
|
|
13
|
+
let inFlight = 0;
|
|
14
|
+
let stage = "idle";
|
|
15
|
+
let slowTimer = null;
|
|
16
|
+
let wakingTimer = null;
|
|
17
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
18
|
+
const setStage = (next) => {
|
|
19
|
+
if (next === stage) return;
|
|
20
|
+
stage = next;
|
|
21
|
+
for (const listener of listeners) listener();
|
|
22
|
+
};
|
|
23
|
+
const clearTimers = () => {
|
|
24
|
+
if (slowTimer !== null) clearTimeout(slowTimer);
|
|
25
|
+
if (wakingTimer !== null) clearTimeout(wakingTimer);
|
|
26
|
+
slowTimer = null;
|
|
27
|
+
wakingTimer = null;
|
|
28
|
+
};
|
|
29
|
+
const start = (method, url) => {
|
|
30
|
+
if (isOffline()) return false;
|
|
31
|
+
if (!shouldWatch((method ?? "get").toLowerCase(), url ?? "")) return false;
|
|
32
|
+
inFlight += 1;
|
|
33
|
+
if (inFlight === 1) {
|
|
34
|
+
clearTimers();
|
|
35
|
+
slowTimer = setTimeout(() => setStage("slow"), slowMs);
|
|
36
|
+
wakingTimer = setTimeout(() => setStage("waking"), wakingMs);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
const end = () => {
|
|
41
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
42
|
+
if (inFlight > 0) return;
|
|
43
|
+
clearTimers();
|
|
44
|
+
setStage("idle");
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
start,
|
|
48
|
+
end,
|
|
49
|
+
track(work, request = {}) {
|
|
50
|
+
const counted = start(request.method, request.url);
|
|
51
|
+
let promise;
|
|
52
|
+
try {
|
|
53
|
+
promise = Promise.resolve(typeof work === "function" ? work() : work);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
promise = Promise.reject(error);
|
|
56
|
+
}
|
|
57
|
+
return counted ? promise.finally(end) : promise;
|
|
58
|
+
},
|
|
59
|
+
reset() {
|
|
60
|
+
inFlight = 0;
|
|
61
|
+
clearTimers();
|
|
62
|
+
setStage("idle");
|
|
63
|
+
},
|
|
64
|
+
subscribe(listener) {
|
|
65
|
+
listeners.add(listener);
|
|
66
|
+
return () => {
|
|
67
|
+
listeners.delete(listener);
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
getStage: () => stage,
|
|
71
|
+
getInFlight: () => inFlight
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const serverWake = createServerWake();
|
|
75
|
+
const COUNTED = "_serverWake";
|
|
76
|
+
function attachServerWake(instance, watcher = serverWake) {
|
|
77
|
+
const settle = (config) => {
|
|
78
|
+
const marked = config;
|
|
79
|
+
if (marked?.[COUNTED]) {
|
|
80
|
+
marked[COUNTED] = false;
|
|
81
|
+
watcher.end();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const request = instance.interceptors.request.use(
|
|
85
|
+
(config) => {
|
|
86
|
+
config[COUNTED] = watcher.start(config.method, config.url);
|
|
87
|
+
return config;
|
|
88
|
+
},
|
|
89
|
+
(error) => Promise.reject(error)
|
|
90
|
+
);
|
|
91
|
+
const response = instance.interceptors.response.use(
|
|
92
|
+
(res) => {
|
|
93
|
+
settle(res.config);
|
|
94
|
+
return res;
|
|
95
|
+
},
|
|
96
|
+
(error) => {
|
|
97
|
+
settle(error?.config);
|
|
98
|
+
return Promise.reject(error);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
return () => {
|
|
102
|
+
instance.interceptors.request.eject(request);
|
|
103
|
+
instance.interceptors.response.eject(response);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function wrapFetch(fetchImpl = globalThis.fetch, watcher = serverWake) {
|
|
107
|
+
return (input, init) => {
|
|
108
|
+
const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
|
|
109
|
+
const method = init?.method ?? request?.method;
|
|
110
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : request?.url ?? "";
|
|
111
|
+
return watcher.track(() => fetchImpl(input, init), { method, url });
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
attachServerWake,
|
|
116
|
+
createServerWake,
|
|
117
|
+
serverWake,
|
|
118
|
+
watchReadsAnd,
|
|
119
|
+
wrapFetch
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=server-wake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/server-wake.ts"],"sourcesContent":["/**\n * \"Is the server asleep, or is the app broken?\" — the cold-start watchdog, lifted\n * from keksdose (feedback #199) so kastlan and Kurvenschmiede get it too.\n *\n * All three backends run on Cloud Run with `--min-instances=0`: after an idle\n * period the container is gone and the first request has to start it, which takes\n * several seconds and occasionally more. Nothing in the UI said so, and none of the\n * axios instances has a timeout, so the honest experience was a spinner that looked\n * stuck. This counts the in-flight requests a cold start would actually delay and\n * escalates in two steps, so a merely slow response says \"still loading\" and only a\n * genuinely long wait blames the sleeping server.\n *\n * Framework-free on purpose (no zustand, no axios, no React): a counter, two timers\n * and a listener set, read by `useServerWakeStage` through `useSyncExternalStore`.\n * The adapters below take the app's axios instance / fetch as an ARGUMENT, so the\n * kit never imports either.\n *\n * WHAT ARMS IT, and why the default is narrow:\n * - reads (GET) — what every page does on open — always;\n * - writes only when the app says so (`watchReadsAnd(/\\/auth\\/login\\b/)`): the\n * auth POSTs that gate the app are the only writes a user waits on before the\n * app exists at all, so a cold start there looks exactly like a broken login.\n * An UPLOAD must never be watched — a statement PDF or a photo takes seconds on a\n * perfectly warm server, and \"the server is waking up\" during a 4 s upload would\n * simply be false. With the default that holds by itself (an upload is a POST);\n * an app widening `shouldWatch` keeps its upload routes out of it.\n *\n * It never arms while the browser is offline, whatever `shouldWatch` says: that state\n * has its own, truthful indicator, and a request that never left the device is not\n * evidence of anything about the server.\n */\n\n/** `slow` = longer than a warm server ever takes; `waking` = long enough that a cold\n * start is the only sensible explanation. */\nexport type ServerWakeStage = \"idle\" | \"slow\" | \"waking\";\n\n/** Decides which requests count. `method` arrives lower-cased (`\"get\"` when the\n * request named none — axios's and fetch's default); `url` as the client had it. */\nexport type ServerWakeFilter = (method: string, url: string) => boolean;\n\nexport interface ServerWakeOptions {\n /** Past this, a warm round trip is out of the question (a cold database query on a\n * free tier can still take a second, so it is not tighter). Default 2000 ms. */\n slowMs?: number;\n /** Past this, the container was almost certainly scaled to zero. Default 7000 ms. */\n wakingMs?: number;\n /** Which requests count. Default {@link watchReadsAnd}`()` — GETs only. The offline\n * guard runs before it and cannot be overridden. */\n shouldWatch?: ServerWakeFilter;\n}\n\n/** What a request is, for {@link ServerWakeWatcher.track}. */\nexport interface ServerWakeRequest {\n method?: string;\n url?: string;\n}\n\nexport interface ServerWakeWatcher {\n /**\n * Count a request that is going out, and arm the escalation if it is the first.\n * Returns whether it was counted — the caller MUST call {@link end} exactly once\n * for every `true`, or the counter never returns to zero and the notice sticks.\n * (The axios adapter stashes it on the request config for that reason.)\n */\n start(method?: string, url?: string): boolean;\n /** A counted request settled — either way: an error still proves the server\n * answered or gave up. When the last one does, the notice goes away. Never takes\n * the counter below zero. */\n end(): void;\n /** `start` + `end` around a promise (or a function returning one), for a request\n * that does not go through an adapter. Resolves and rejects as the work does. */\n track<T>(work: PromiseLike<T> | (() => PromiseLike<T>), request?: ServerWakeRequest): Promise<T>;\n /** Back to a pristine watchdog — for tests, which share an instance across cases. */\n reset(): void;\n /** `useSyncExternalStore`'s pair. Listeners hear stage changes only. */\n subscribe(listener: () => void): () => void;\n getStage(): ServerWakeStage;\n /** Counted requests currently outstanding. */\n getInFlight(): number;\n}\n\n/**\n * The usual filter: every GET, plus any other request whose URL matches `writes` —\n * keksdose's `/\\/auth\\/(login|register|demo-session)\\b/`. Keep upload routes out of\n * the pattern (see the module comment).\n */\nexport function watchReadsAnd(writes?: RegExp): ServerWakeFilter {\n return (method, url) => method === \"get\" || (writes !== undefined && writes.test(url));\n}\n\nfunction isOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\nexport function createServerWake({\n slowMs = 2000,\n wakingMs = 7000,\n shouldWatch = watchReadsAnd(),\n}: ServerWakeOptions = {}): ServerWakeWatcher {\n let inFlight = 0;\n let stage: ServerWakeStage = \"idle\";\n let slowTimer: ReturnType<typeof setTimeout> | null = null;\n let wakingTimer: ReturnType<typeof setTimeout> | null = null;\n const listeners = new Set<() => void>();\n\n const setStage = (next: ServerWakeStage) => {\n if (next === stage) return;\n stage = next;\n for (const listener of listeners) listener();\n };\n\n const clearTimers = () => {\n if (slowTimer !== null) clearTimeout(slowTimer);\n if (wakingTimer !== null) clearTimeout(wakingTimer);\n slowTimer = null;\n wakingTimer = null;\n };\n\n const start = (method?: string, url?: string): boolean => {\n if (isOffline()) return false;\n if (!shouldWatch((method ?? \"get\").toLowerCase(), url ?? \"\")) return false;\n inFlight += 1;\n // The clock runs from the FIRST outstanding request: a page firing a second read\n // at 1.9 s is still waiting on the same sleeping server, not starting afresh.\n if (inFlight === 1) {\n clearTimers();\n slowTimer = setTimeout(() => setStage(\"slow\"), slowMs);\n wakingTimer = setTimeout(() => setStage(\"waking\"), wakingMs);\n }\n return true;\n };\n\n const end = () => {\n inFlight = Math.max(0, inFlight - 1);\n if (inFlight > 0) return;\n clearTimers();\n setStage(\"idle\");\n };\n\n return {\n start,\n end,\n track<T>(work: PromiseLike<T> | (() => PromiseLike<T>), request: ServerWakeRequest = {}) {\n const counted = start(request.method, request.url);\n let promise: Promise<T>;\n try {\n promise = Promise.resolve(typeof work === \"function\" ? work() : work);\n } catch (error) {\n // A thunk that throws before it returns a promise settled too.\n promise = Promise.reject(error);\n }\n return counted ? promise.finally(end) : promise;\n },\n reset() {\n inFlight = 0;\n clearTimers();\n setStage(\"idle\");\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getStage: () => stage,\n getInFlight: () => inFlight,\n };\n}\n\n/**\n * The shared instance: what `<ServerWakeNotice />`, `useServerWakeStage()`,\n * {@link attachServerWake} and {@link wrapFetch} use when no watcher is passed. GETs\n * only, default timings — an app that also watches its login POST creates its own\n * with {@link createServerWake} and passes it to both ends.\n */\nexport const serverWake: ServerWakeWatcher = createServerWake();\n\n// ── axios ─────────────────────────────────────────────────────────────────────\n\n/** The two config fields the watchdog reads — what axios's request config has. */\nexport interface ServerWakeAxiosConfig {\n method?: string;\n url?: string;\n}\n\n/** The slice of an interceptor manager the adapter uses. A method (not a function\n * property) so axios's wider `use(onFulfilled?, onRejected?, options?)` fits it. */\ninterface InterceptorManagerLike<V> {\n use(onFulfilled: (value: V) => V | Promise<V>, onRejected: (error: unknown) => unknown): number;\n eject(id: number): void;\n}\n\n/** An axios instance, structurally — so the kit types the adapter without importing\n * axios. `axios.create()`'s result fits it as is. */\nexport interface AxiosLikeInstance<C extends ServerWakeAxiosConfig, R extends { config: C }> {\n interceptors: {\n request: InterceptorManagerLike<C>;\n response: InterceptorManagerLike<R>;\n };\n}\n\n/** Whether the watchdog counted this request — the response side owes a matching\n * `end()` only then. On the config, as keksdose's `_wake` was: it travels with the\n * request onto the response and onto an error. Cleared once settled, so a 401 replay\n * of the same config re-enters the request interceptor and is counted afresh — the\n * pairing stays balanced. */\nconst COUNTED = \"_serverWake\";\ntype Marked = { [COUNTED]?: boolean };\n\n/**\n * Drive a watcher from an axios instance's interceptors: `attachServerWake(api)`.\n * Returns a function that ejects both interceptors again.\n *\n * Attach it BEFORE the app's own response interceptors: axios runs those in the order\n * they were added, so the wake one ends the wait before a 401 handler awaits a token\n * refresh — otherwise the notice would keep counting a request that has already been\n * answered. (Request-side order does not matter.)\n */\nexport function attachServerWake<C extends ServerWakeAxiosConfig, R extends { config: C }>(\n instance: AxiosLikeInstance<C, R>,\n watcher: ServerWakeWatcher = serverWake,\n): () => void {\n const settle = (config: unknown) => {\n const marked = config as Marked | undefined;\n if (marked?.[COUNTED]) {\n marked[COUNTED] = false;\n watcher.end();\n }\n };\n const request = instance.interceptors.request.use(\n (config) => {\n (config as C & Marked)[COUNTED] = watcher.start(config.method, config.url);\n return config;\n },\n (error) => Promise.reject(error),\n );\n const response = instance.interceptors.response.use(\n (res) => {\n settle(res.config);\n return res;\n },\n (error) => {\n settle((error as { config?: unknown } | null)?.config);\n return Promise.reject(error);\n },\n );\n return () => {\n instance.interceptors.request.eject(request);\n instance.interceptors.response.eject(response);\n };\n}\n\n// ── fetch ─────────────────────────────────────────────────────────────────────\n\n/**\n * A `fetch` that drives a watcher: `const apiFetch = wrapFetch(fetch)`. The wait ends\n * when the response's HEADERS arrive (or the call fails) — the server is awake by then,\n * however long the body takes.\n */\nexport function wrapFetch(\n fetchImpl: typeof fetch = globalThis.fetch,\n watcher: ServerWakeWatcher = serverWake,\n): typeof fetch {\n return (input, init) => {\n const request = typeof Request !== \"undefined\" && input instanceof Request ? input : undefined;\n const method = init?.method ?? request?.method;\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : (request?.url ?? \"\");\n return watcher.track(() => fetchImpl(input, init), { method, url });\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/rhf/fields.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ReactElement } from 'react';
|
|
3
3
|
import { FieldValues, FieldPath, ControllerProps, ControllerRenderProps, ControllerFieldState } from 'react-hook-form';
|
|
4
|
-
import {
|
|
4
|
+
import { aQ as SelectProps, aE as InputProps, b9 as TextareaProps } from '../kit-labels-CPbsjlXr.js';
|
|
5
5
|
import { NumberFieldProps } from '../components/number-field.js';
|
|
6
6
|
import { DatePickerProps } from '../components/date-picker.js';
|
|
7
7
|
import { CheckboxProps } from '../components/checkbox.js';
|
|
@@ -58,6 +58,15 @@ import '../components/authed-image.js';
|
|
|
58
58
|
import '../hooks/use-authed-src.js';
|
|
59
59
|
import '../components/image-grid.js';
|
|
60
60
|
import '../components/lightbox.js';
|
|
61
|
+
import '../components/write-lock.js';
|
|
62
|
+
import '../components/account-chips.js';
|
|
63
|
+
import '../lib/format.js';
|
|
64
|
+
import '../data-table-3mi2R3so.js';
|
|
65
|
+
import '../components/data-table-sort.js';
|
|
66
|
+
import '../components/swipeable-row.js';
|
|
67
|
+
import '../components/reauth-dialog.js';
|
|
68
|
+
import '../components/server-wake.js';
|
|
69
|
+
import '../lib/server-wake.js';
|
|
61
70
|
import '../components/number-input.js';
|
|
62
71
|
import '../components/calculator.js';
|
|
63
72
|
import '../components/numpad-sheet.js';
|
package/dist/rhf/form.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactElement, ComponentProps } from 'react';
|
|
3
3
|
import * as react_hook_form from 'react-hook-form';
|
|
4
4
|
import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
|
|
5
|
-
import {
|
|
5
|
+
import { aJ as LabelProps } from '../kit-labels-CPbsjlXr.js';
|
|
6
6
|
import '../components/data-table-labels.js';
|
|
7
7
|
import '../components/mini-calendar.js';
|
|
8
8
|
import '../components/calendar-heatmap.js';
|
|
@@ -53,6 +53,15 @@ import '../components/authed-image.js';
|
|
|
53
53
|
import '../hooks/use-authed-src.js';
|
|
54
54
|
import '../components/image-grid.js';
|
|
55
55
|
import '../components/lightbox.js';
|
|
56
|
+
import '../components/write-lock.js';
|
|
57
|
+
import '../components/account-chips.js';
|
|
58
|
+
import '../lib/format.js';
|
|
59
|
+
import '../data-table-3mi2R3so.js';
|
|
60
|
+
import '../components/data-table-sort.js';
|
|
61
|
+
import '../components/swipeable-row.js';
|
|
62
|
+
import '../components/reauth-dialog.js';
|
|
63
|
+
import '../components/server-wake.js';
|
|
64
|
+
import '../lib/server-wake.js';
|
|
56
65
|
|
|
57
66
|
/** The form's `useForm()` return, spread in: `<Form {...form}>`. react-hook-form's own
|
|
58
67
|
* `FormProvider`, under the name the shadcn file gave it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rhf/line-items.tsx"],"sourcesContent":["/**\n * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <RhfLineItems\n * name=\"components\"\n * newItem={() => ({ type: \"net_rent\", amount: null, validFrom: \"\" })}\n * minItems={1}\n * columns={[\n * { key: \"type\", header: \"Type\", render: ({ name, label }) => (\n * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />\n * ) },\n * { key: \"amount\", header: \"Amount\", width: \"8rem\", align: \"end\", render: ({ name, label }) => (\n * <RhfMoneyField name={`${name}.amount`} ariaLabel={label} />\n * ) },\n * ]}\n * totals={{ amount: formatMoney(total) }}\n * />\n * ```\n *\n * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),\n * Add appends `newItem()`, remove is `remove(index)`, and an error on the array\n * itself — the `rules` here, or a resolver's `lines: \"At least two lines\"` — is the\n * list's `error`. Each cell's `render` gets `name`, the row's path\n * (`\"components.2\"`), to bind its field to.\n *\n * `summary`, `fieldLabels` and `
|
|
1
|
+
{"version":3,"sources":["../../src/rhf/line-items.tsx"],"sourcesContent":["/**\n * {@link LineItems} over react-hook-form's `useFieldArray`. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <RhfLineItems\n * name=\"components\"\n * newItem={() => ({ type: \"net_rent\", amount: null, validFrom: \"\" })}\n * minItems={1}\n * columns={[\n * { key: \"type\", header: \"Type\", render: ({ name, label }) => (\n * <RhfSelect name={`${name}.type`} options={TYPES} aria-label={label} />\n * ) },\n * { key: \"amount\", header: \"Amount\", width: \"8rem\", align: \"end\", render: ({ name, label }) => (\n * <RhfMoneyField name={`${name}.amount`} ariaLabel={label} />\n * ) },\n * ]}\n * totals={{ amount: formatMoney(total) }}\n * />\n * ```\n *\n * The rows are the array's `fields` (keyed by their `id`, as react-hook-form asks),\n * Add appends `newItem()`, remove is `remove(index)`, and an error on the array\n * itself — the `rules` here, or a resolver's `lines: \"At least two lines\"` — is the\n * list's `error`. Each cell's `render` gets `name`, the row's path\n * (`\"components.2\"`), to bind its field to.\n *\n * `summary`, `fieldLabels`, `narrowColumns`, `removePlacement` and `removeAlign` pass\n * through to {@link LineItems}. With\n * `fieldLabels=\"floating\"` a cell's `fieldLabel` is the column's name; the bound\n * fields' own `label` is a `FormLabel` ABOVE the control, so for the floating\n * one render the kit field through `RhfField`:\n * `render={({ field, invalid }) => <Input label={fieldLabel} aria-label={label} {...field} invalid={invalid} />}`.\n */\nimport type { ReactNode } from \"react\";\nimport {\n useFieldArray,\n useFormState,\n type FieldArray,\n type FieldArrayPath,\n type FieldArrayWithId,\n type FieldValues,\n type UseFieldArrayProps,\n} from \"react-hook-form\";\nimport {\n LineItems,\n type LineItemCellContext,\n type LineItemsColumn,\n type LineItemsProps,\n} from \"../components/line-items\";\n\n/** A cell's context, with the row's path in the form. */\nexport interface RhfLineItemCellContext<TItem, TArrayName extends string>\n extends LineItemCellContext<TItem> {\n /** The row's path: `${name}.${index}` — bind the cell's field to\n * `` `${name}.amount` ``. */\n name: `${TArrayName}.${number}`;\n}\n\nexport interface RhfLineItemsColumn<TItem, TArrayName extends string>\n extends Omit<LineItemsColumn<TItem>, \"render\"> {\n render: (ctx: RhfLineItemCellContext<TItem, TArrayName>) => ReactNode;\n}\n\nexport interface RhfLineItemsProps<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends Omit<\n LineItemsProps<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>,\n \"items\" | \"columns\" | \"onAdd\" | \"onRemove\" | \"getKey\"\n > {\n name: TArrayName;\n /** `form.control`. Optional under `<Form {...form}>`. */\n control?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"control\"];\n /** `useFieldArray`'s `rules` (`minLength`, `validate` …); its message is the list's\n * error. */\n rules?: UseFieldArrayProps<TFieldValues, TArrayName, \"id\", TTransformed>[\"rules\"];\n columns: readonly RhfLineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">, TArrayName>[];\n /** A new row's values. Omitted: no add button. */\n newItem?: () => FieldArray<TFieldValues, TArrayName>;\n /** `false`: no remove buttons. Default `true`. */\n removable?: boolean;\n}\n\n/** The message of an error at `path` in react-hook-form's errors tree: the array's\n * own (`root`, from `rules`) or one a resolver put on the array. */\nfunction arrayError(errors: unknown, path: string): string | undefined {\n let node: unknown = errors;\n for (const key of path.split(\".\")) {\n if (node === null || typeof node !== \"object\") return undefined;\n node = (node as Record<string, unknown>)[key];\n }\n if (node === null || typeof node !== \"object\") return undefined;\n const { root, message } = node as { root?: { message?: unknown }; message?: unknown };\n const text = root?.message ?? message;\n return typeof text === \"string\" && text !== \"\" ? text : undefined;\n}\n\n/** {@link LineItems} bound to a field array. See the module note. */\nexport function RhfLineItems<\n TFieldValues extends FieldValues = FieldValues,\n TArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n columns,\n newItem,\n removable = true,\n error,\n ...rest\n}: RhfLineItemsProps<TFieldValues, TArrayName, TTransformed>) {\n const { fields, append, remove } = useFieldArray<TFieldValues, TArrayName, \"id\", TTransformed>({\n control,\n name,\n rules,\n });\n const { errors } = useFormState({ control, name: name as never });\n const bound: LineItemsColumn<FieldArrayWithId<TFieldValues, TArrayName, \"id\">>[] = columns.map(\n ({ render, ...column }) => ({\n ...column,\n render: (ctx) => render({ ...ctx, name: `${name}.${ctx.index}` as `${TArrayName}.${number}` }),\n }),\n );\n return (\n <LineItems\n {...rest}\n items={fields}\n columns={bound}\n getKey={(field) => field.id}\n onAdd={newItem ? () => append(newItem()) : undefined}\n onRemove={removable ? (index) => remove(index) : undefined}\n error={error ?? arrayError(errors, name)}\n />\n );\n}\n"],"mappings":";AA8HI;AA5FJ;AAAA,EACE;AAAA,EACA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,OAIK;AAsCP,SAAS,WAAW,QAAiB,MAAkC;AACrE,MAAI,OAAgB;AACpB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,WAAQ,KAAiC,GAAG;AAAA,EAC9C;AACA,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,QAAM,EAAE,MAAM,QAAQ,IAAI;AAC1B,QAAM,OAAO,MAAM,WAAW;AAC9B,SAAO,OAAO,SAAS,YAAY,SAAS,KAAK,OAAO;AAC1D;AAGO,SAAS,aAId;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAA8D;AAC5D,QAAM,EAAE,QAAQ,QAAQ,OAAO,IAAI,cAA4D;AAAA,IAC7F;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,EAAE,OAAO,IAAI,aAAa,EAAE,SAAS,KAAoB,CAAC;AAChE,QAAM,QAA6E,QAAQ;AAAA,IACzF,CAAC,EAAE,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC1B,GAAG;AAAA,MACH,QAAQ,CAAC,QAAQ,OAAO,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,GAAgC,CAAC;AAAA,IAC/F;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ,CAAC,UAAU,MAAM;AAAA,MACzB,OAAO,UAAU,MAAM,OAAO,QAAQ,CAAC,IAAI;AAAA,MAC3C,UAAU,YAAY,CAAC,UAAU,OAAO,KAAK,IAAI;AAAA,MACjD,OAAO,SAAS,WAAW,QAAQ,IAAI;AAAA;AAAA,EACzC;AAEJ;","names":[]}
|
package/dist/rhf.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { RhfCheckbox, RhfCheckboxProps, RhfCombobox, RhfComboboxProps, RhfDateFi
|
|
|
4
4
|
export { RhfLineItemCellContext, RhfLineItems, RhfLineItemsColumn, RhfLineItemsProps } from './rhf/line-items.js';
|
|
5
5
|
import 'react';
|
|
6
6
|
import 'react-hook-form';
|
|
7
|
-
import './kit-labels-
|
|
7
|
+
import './kit-labels-CPbsjlXr.js';
|
|
8
8
|
import './components/data-table-labels.js';
|
|
9
9
|
import './components/mini-calendar.js';
|
|
10
10
|
import './components/calendar-heatmap.js';
|
|
@@ -55,6 +55,15 @@ import './components/authed-image.js';
|
|
|
55
55
|
import './hooks/use-authed-src.js';
|
|
56
56
|
import './components/image-grid.js';
|
|
57
57
|
import './components/lightbox.js';
|
|
58
|
+
import './components/write-lock.js';
|
|
59
|
+
import './components/account-chips.js';
|
|
60
|
+
import './lib/format.js';
|
|
61
|
+
import './data-table-3mi2R3so.js';
|
|
62
|
+
import './components/data-table-sort.js';
|
|
63
|
+
import './components/swipeable-row.js';
|
|
64
|
+
import './components/reauth-dialog.js';
|
|
65
|
+
import './components/server-wake.js';
|
|
66
|
+
import './lib/server-wake.js';
|
|
58
67
|
import './components/number-field.js';
|
|
59
68
|
import './components/number-input.js';
|
|
60
69
|
import './components/calculator.js';
|
|
@@ -26,8 +26,22 @@ declare const DEFAULT_GLOBAL_SEARCH_LABELS: GlobalSearchLabels;
|
|
|
26
26
|
interface GlobalSearchSource {
|
|
27
27
|
/** Stable key — the effect that runs the source is keyed by it, not by identity. */
|
|
28
28
|
id: string;
|
|
29
|
-
/** The group its results are shown under (an entry's own `group` is ignored)
|
|
29
|
+
/** The group its results are shown under (an entry's own `group` is ignored) — unless
|
|
30
|
+
* `groups` is set, where it is only the home of entries that name no declared group.
|
|
31
|
+
* If every entry names a declared group, it is never shown: a catch-all heading such
|
|
32
|
+
* as "Other" is the honest value, so a stray type would still surface, labelled. */
|
|
30
33
|
group: string;
|
|
34
|
+
/**
|
|
35
|
+
* Split ONE request into several groups — kastlan's record search answers with
|
|
36
|
+
* properties, units, contacts, leases and invoices at once, and one source per group
|
|
37
|
+
* meant a dedupe layer so the network still saw one request. Each entry is then shown
|
|
38
|
+
* under the group its own `group` names: a declared `key` (shown by its `label`) or a
|
|
39
|
+
* plain string (key and heading at once). The groups appear in THIS order whatever the
|
|
40
|
+
* answer's order, each kept to `limit` rows; an entry naming no declared group falls
|
|
41
|
+
* under `group`. While the request runs, or when it fails, the source shows one status
|
|
42
|
+
* line under its first group, not one per group. Unset: one group, as before.
|
|
43
|
+
*/
|
|
44
|
+
groups?: readonly (string | GlobalSearchSourceGroup)[];
|
|
31
45
|
/** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked
|
|
32
46
|
* them — and never re-filtered by the index's matcher. `signal` aborts when the query
|
|
33
47
|
* moves on or the palette closes; a stale answer is dropped either way. */
|
|
@@ -36,7 +50,7 @@ interface GlobalSearchSource {
|
|
|
36
50
|
minChars?: number;
|
|
37
51
|
/** Quiet period after the last keystroke before it is asked. Default 200ms. */
|
|
38
52
|
debounceMs?: number;
|
|
39
|
-
/** Rows kept. Default: `indexOptions.groupLimit` (8). */
|
|
53
|
+
/** Rows kept — per group, when `groups` is set. Default: `indexOptions.groupLimit` (8). */
|
|
40
54
|
limit?: number;
|
|
41
55
|
/** Mask every row of this source for session replay, hints (amounts) included. An
|
|
42
56
|
* entry's own `redact` still wins. */
|
|
@@ -45,6 +59,12 @@ interface GlobalSearchSource {
|
|
|
45
59
|
* shows the palette's `error` line instead of rows. */
|
|
46
60
|
onError?: (error: unknown) => void;
|
|
47
61
|
}
|
|
62
|
+
/** One of a multi-group source's groups: the `key` its entries name in `group`, and the
|
|
63
|
+
* heading shown — so a server's type ("unit") need not be translated per entry. */
|
|
64
|
+
interface GlobalSearchSourceGroup {
|
|
65
|
+
key: string;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
48
68
|
/** What a custom trigger gets: everything the default button uses. */
|
|
49
69
|
interface GlobalSearchTriggerProps {
|
|
50
70
|
open: () => void;
|
|
@@ -73,7 +93,7 @@ interface GlobalSearchProps {
|
|
|
73
93
|
* neither re-indexes nor loops, and the icons and handlers used are always the latest.
|
|
74
94
|
*/
|
|
75
95
|
entries?: readonly SearchEntry[];
|
|
76
|
-
/** The async sources, each streamed into its own group. */
|
|
96
|
+
/** The async sources, each streamed into its own group — or several (`groups`). */
|
|
77
97
|
sources?: readonly GlobalSearchSource[];
|
|
78
98
|
/** What the empty query shows, under the `suggestions` heading. */
|
|
79
99
|
suggestions?: readonly GlobalSearchSuggestion[];
|
|
@@ -163,4 +183,4 @@ interface GlobalSearchProps {
|
|
|
163
183
|
*/
|
|
164
184
|
declare function GlobalSearch(props: GlobalSearchProps): react.JSX.Element;
|
|
165
185
|
|
|
166
|
-
export { DEFAULT_GLOBAL_SEARCH_LABELS, GlobalSearch, type GlobalSearchLabels, type GlobalSearchProps, type GlobalSearchSource, type GlobalSearchSuggestion, type GlobalSearchTriggerProps };
|
|
186
|
+
export { DEFAULT_GLOBAL_SEARCH_LABELS, GlobalSearch, type GlobalSearchLabels, type GlobalSearchProps, type GlobalSearchSource, type GlobalSearchSourceGroup, type GlobalSearchSuggestion, type GlobalSearchTriggerProps };
|
|
@@ -111,7 +111,17 @@ function GlobalSearchImpl({
|
|
|
111
111
|
const groupLimit = indexOptions?.groupLimit ?? 8;
|
|
112
112
|
const index = useMemo(() => createSearchIndex(list, indexOptions), [sig, optionsKey]);
|
|
113
113
|
const byId = useMemo(() => new Map(list.map((e) => [e.id, e])), [sig]);
|
|
114
|
-
const sourceKey = (sources ?? []).map(
|
|
114
|
+
const sourceKey = (sources ?? []).map(
|
|
115
|
+
(s) => [
|
|
116
|
+
s.id,
|
|
117
|
+
s.group,
|
|
118
|
+
(s.groups ?? []).map((g) => typeof g === "string" ? g : `${g.key}${g.label}`).join(""),
|
|
119
|
+
s.minChars ?? 2,
|
|
120
|
+
s.debounceMs ?? 200,
|
|
121
|
+
s.limit,
|
|
122
|
+
s.redact
|
|
123
|
+
].join("")
|
|
124
|
+
).join("");
|
|
115
125
|
const sourceList = useMemo(() => sources ?? [], [sourceKey]);
|
|
116
126
|
const suggestionsKey = (suggestions ?? []).map((s) => typeof s === "string" ? `i:${s}` : "query" in s ? `q:${s.query}` : `e:${signature([s])}`).join("");
|
|
117
127
|
const suggestionList = useMemo(() => suggestions ?? [], [suggestionsKey]);
|
|
@@ -204,10 +214,12 @@ function GlobalSearchImpl({
|
|
|
204
214
|
for (const source of sourceList) {
|
|
205
215
|
if (q.length < (source.minChars ?? 2)) continue;
|
|
206
216
|
const result = sourceResults[source.id];
|
|
217
|
+
const declared = (source.groups ?? []).map((g) => typeof g === "string" ? { key: g, label: g } : g);
|
|
207
218
|
const status = (label, icon) => ({
|
|
208
219
|
id: `src:${source.id}:status`,
|
|
209
220
|
label,
|
|
210
|
-
group
|
|
221
|
+
// One line for the one request, under the group that will head its answer.
|
|
222
|
+
group: declared[0]?.label ?? source.group,
|
|
211
223
|
icon,
|
|
212
224
|
kind: "status",
|
|
213
225
|
onSelect: noop
|
|
@@ -216,10 +228,26 @@ function GlobalSearchImpl({
|
|
|
216
228
|
out.push(status(pl.loading, /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin" })));
|
|
217
229
|
} else if (result.status === "error") {
|
|
218
230
|
out.push(status(pl.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error, /* @__PURE__ */ jsx(AlertCircle, { className: "size-3.5" })));
|
|
219
|
-
} else {
|
|
231
|
+
} else if (!declared.length) {
|
|
220
232
|
for (const entry of result.entries.slice(0, source.limit ?? groupLimit)) {
|
|
221
233
|
out.push(toItem(`src:${source.id}:`, entry, source.group, source.redact ?? redactLabels));
|
|
222
234
|
}
|
|
235
|
+
} else {
|
|
236
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const g of declared) buckets.set(g.key, { label: g.label, entries: [] });
|
|
238
|
+
const rest = [];
|
|
239
|
+
for (const entry of result.entries) {
|
|
240
|
+
const bucket = entry.group === void 0 ? void 0 : buckets.get(entry.group);
|
|
241
|
+
(bucket ? bucket.entries : rest).push(entry);
|
|
242
|
+
}
|
|
243
|
+
const limit = source.limit ?? groupLimit;
|
|
244
|
+
const emit = (key, label, list2) => {
|
|
245
|
+
for (const entry of list2.slice(0, limit)) {
|
|
246
|
+
out.push(toItem(`src:${source.id}:${key}:`, entry, label, source.redact ?? redactLabels));
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
for (const [key, { label, entries: list2 }] of buckets) emit(key, label, list2);
|
|
250
|
+
emit("~", source.group, rest);
|
|
223
251
|
}
|
|
224
252
|
}
|
|
225
253
|
return ordered(out);
|