rei-kit 0.14.1 → 0.16.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 +8 -1
- package/dist/GoogleButton-BxsrxZdX.js +730 -0
- package/dist/GoogleButton-BxsrxZdX.js.map +1 -0
- package/dist/app/AuthForm.vue.d.ts +78 -0
- package/dist/app/FabButton.vue.d.ts +34 -0
- package/dist/app/OfflineBanner.vue.d.ts +21 -0
- package/dist/app/TabShell.vue.d.ts +63 -0
- package/dist/app/auth-form.d.ts +36 -0
- package/dist/app/field-errors.d.ts +39 -0
- package/dist/app/guards.d.ts +68 -0
- package/dist/app/index.d.ts +13 -0
- package/dist/app/query-defaults.d.ts +43 -0
- package/dist/app/use-tab-transition.d.ts +12 -0
- package/dist/app/write-report.d.ts +43 -0
- package/dist/app.js +448 -15
- package/dist/app.js.map +1 -1
- package/dist/components/GoogleButton.vue.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -444
- package/dist/index.js.map +1 -1
- package/dist/styles.css +166 -0
- package/dist/supabase/auth-errors.d.ts +40 -0
- package/dist/supabase/index.d.ts +2 -0
- package/dist/supabase.js +46 -2
- package/dist/supabase.js.map +1 -1
- package/dist/utils/redirect.d.ts +24 -0
- package/package.json +1 -1
- package/dist/BaseSheet-CLIhXDwe.js +0 -253
- package/dist/BaseSheet-CLIhXDwe.js.map +0 -1
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { n as _plugin_vue_export_helper_default, r as BaseButton_default } from "./SettingsRow-MnVleeTR.js";
|
|
2
|
+
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createStaticVNode, createTextVNode, createVNode, defineComponent, mergeModels, mergeProps, nextTick, normalizeClass, normalizeStyle, onMounted, onScopeDispose, onUnmounted, openBlock, readonly, ref, renderSlot, toDisplayString, unref, useId, useModel, vModelCheckbox, vModelDynamic, watch, withCtx, withDirectives } from "vue";
|
|
3
|
+
import { X } from "lucide-vue-next";
|
|
4
|
+
//#region src/utils/redirect.ts
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a `?redirect=` query value into a safe in-app path.
|
|
7
|
+
*
|
|
8
|
+
* Only same-origin paths are accepted. Anything else falls back to `/`,
|
|
9
|
+
* so a crafted link cannot bounce a user from the real login page to a
|
|
10
|
+
* phishing clone.
|
|
11
|
+
*
|
|
12
|
+
* Pure: takes the query value instead of reading the router, so it also
|
|
13
|
+
* works inside navigation guards and can be unit tested.
|
|
14
|
+
*
|
|
15
|
+
* @param target - Raw `route.query.redirect` value. May be a string, an
|
|
16
|
+
* array (repeated query key), `null`, or `undefined`.
|
|
17
|
+
* @returns A path starting with a single `/`. Defaults to `/`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* // in a view
|
|
22
|
+
* await router.push(safeRedirect(route.query.redirect))
|
|
23
|
+
*
|
|
24
|
+
* // in a guard
|
|
25
|
+
* return safeRedirect(to.query.redirect)
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* safeRedirect('/week') // '/week'
|
|
31
|
+
* safeRedirect('https://evil.com') // '/'
|
|
32
|
+
* safeRedirect('//evil.com') // '/' (protocol-relative URL)
|
|
33
|
+
* safeRedirect(['/a', '/b']) // '/'
|
|
34
|
+
* safeRedirect(undefined) // '/'
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function safeRedirect(target) {
|
|
38
|
+
if (typeof target === "string" && target.startsWith("/") && !target.startsWith("//")) return target;
|
|
39
|
+
return "/";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Where to send somebody back to after signing in, without the fragment.
|
|
43
|
+
*
|
|
44
|
+
* Supabase's implicit OAuth flow hands the browser back with the access and
|
|
45
|
+
* refresh tokens in the URL fragment. A fragment is client-side only — it is
|
|
46
|
+
* never sent to a server. Copied into a query parameter it stops being one:
|
|
47
|
+
* `/login?redirect=/%23access_token=…` is sent on the very next request and
|
|
48
|
+
* lands in the host's access logs, in `Referer` headers and in browser history.
|
|
49
|
+
*
|
|
50
|
+
* So the redirect keeps the path and the query and drops everything from the
|
|
51
|
+
* `#`. There is nothing after it worth returning to anyway.
|
|
52
|
+
*
|
|
53
|
+
* One of the two phone apps had this and the other was passing `fullPath`
|
|
54
|
+
* straight through, which is the leak above with nothing in the way of it. That
|
|
55
|
+
* is the shape of bug a shared kit exists to end: it was fixed once, in the app
|
|
56
|
+
* whose author happened to think of it.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* toRedirectPath('/ledger?direction=out') // '/ledger?direction=out'
|
|
61
|
+
* toRedirectPath('/#access_token=abc') // '/'
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function toRedirectPath(fullPath) {
|
|
65
|
+
const [path] = fullPath.split("#");
|
|
66
|
+
return path === void 0 || path === "" ? "/" : path;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/utils/haptics.ts
|
|
70
|
+
/**
|
|
71
|
+
* A short vibration for a confirmed tap.
|
|
72
|
+
*
|
|
73
|
+
* Optional chaining is not decoration: iOS Safari has no `vibrate` at all, and
|
|
74
|
+
* calling it unguarded would throw on every marked day.
|
|
75
|
+
*
|
|
76
|
+
* @param duration - Milliseconds. Keep it under ~15ms; longer reads as an alert.
|
|
77
|
+
*/
|
|
78
|
+
function tapFeedback(duration = 10) {
|
|
79
|
+
navigator.vibrate?.(duration);
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/composables/use-theme.ts
|
|
83
|
+
/**
|
|
84
|
+
* Namespaced by the app, not by this package.
|
|
85
|
+
*
|
|
86
|
+
* Two rei-kit apps served from the same origin would otherwise share one theme
|
|
87
|
+
* setting — and during development on localhost, they will be.
|
|
88
|
+
*/
|
|
89
|
+
var storageKey = "rei-theme";
|
|
90
|
+
function isThemePreference(value) {
|
|
91
|
+
return value === "system" || value === "light" || value === "dark";
|
|
92
|
+
}
|
|
93
|
+
/** Reads the stored preference, falling back to `system`. */
|
|
94
|
+
function readStoredTheme() {
|
|
95
|
+
try {
|
|
96
|
+
const stored = localStorage.getItem(storageKey);
|
|
97
|
+
return isThemePreference(stored) ? stored : "system";
|
|
98
|
+
} catch {
|
|
99
|
+
return "system";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function storeTheme(preference) {
|
|
103
|
+
try {
|
|
104
|
+
localStorage.setItem(storageKey, preference);
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Does the environment prefer a dark scheme?
|
|
109
|
+
*
|
|
110
|
+
* `matchMedia` is checked for on its own rather than inferred from `document`.
|
|
111
|
+
* Having one does not imply having the other: jsdom supplies a document and no
|
|
112
|
+
* `matchMedia`, so a consumer's component test that so much as mounts something
|
|
113
|
+
* calling `useTheme` threw — and some embedded webviews are the same. Where
|
|
114
|
+
* there is nothing to ask, the answer is no rather than an exception.
|
|
115
|
+
*/
|
|
116
|
+
function prefersDarkScheme() {
|
|
117
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: dark)").matches : false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Adds or removes `.dark` on `<html>`, resolving `system` against the OS.
|
|
121
|
+
*
|
|
122
|
+
* A no-op without a document. There is no OS preference to read on a server and
|
|
123
|
+
* no `<html>` to write to, so a prerender leaves the class off and the app
|
|
124
|
+
* decides the theme before hydration — see the note in the README.
|
|
125
|
+
*/
|
|
126
|
+
function applyTheme(preference) {
|
|
127
|
+
if (typeof document === "undefined") return;
|
|
128
|
+
const isDark = preference === "dark" || preference === "system" && prefersDarkScheme();
|
|
129
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The shared preference, created on first use rather than at import.
|
|
133
|
+
*
|
|
134
|
+
* Lazy on purpose: reading storage at import time would lock in the default key
|
|
135
|
+
* before an app had a chance to set its own, leaving the controller reading one
|
|
136
|
+
* key and writing another.
|
|
137
|
+
*/
|
|
138
|
+
var preference = null;
|
|
139
|
+
function controller() {
|
|
140
|
+
if (preference) return preference;
|
|
141
|
+
preference = ref(readStoredTheme());
|
|
142
|
+
watch(preference, (next) => {
|
|
143
|
+
storeTheme(next);
|
|
144
|
+
applyTheme(next);
|
|
145
|
+
}, { immediate: true });
|
|
146
|
+
if (typeof window !== "undefined" && typeof window.matchMedia === "function") window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
147
|
+
if (preference?.value === "system") applyTheme("system");
|
|
148
|
+
});
|
|
149
|
+
return preference;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Sets where the preference is stored.
|
|
153
|
+
*
|
|
154
|
+
* Safe in either order: called before the first `useTheme()` it simply changes
|
|
155
|
+
* the key, and called after it re-reads under the new one, so the controller
|
|
156
|
+
* never reads from one key while writing to another.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* setThemeStorageKey('hibi-theme') // once, at startup
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
function setThemeStorageKey(key) {
|
|
164
|
+
storageKey = key;
|
|
165
|
+
if (preference) preference.value = readStoredTheme();
|
|
166
|
+
}
|
|
167
|
+
/** @returns The shared preference ref; assigning to it stores and applies it. */
|
|
168
|
+
function useTheme() {
|
|
169
|
+
return controller();
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/composables/use-online.ts
|
|
173
|
+
/**
|
|
174
|
+
* Tracks whether the browser thinks it has a network connection.
|
|
175
|
+
*
|
|
176
|
+
* Note the limit: `navigator.onLine` only reports whether a network interface
|
|
177
|
+
* is up, not whether requests actually succeed. Treat it as a hint for the UI,
|
|
178
|
+
* never as a reason to skip error handling.
|
|
179
|
+
*
|
|
180
|
+
* Listeners are removed on unmount, so the composable is safe to call per view.
|
|
181
|
+
*
|
|
182
|
+
* @returns A readonly ref that flips with the browser's online/offline events.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const isOnline = useOnline()
|
|
187
|
+
* // <p v-if="!isOnline">You're offline.</p>
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
function useOnline() {
|
|
191
|
+
const isOnline = ref(true);
|
|
192
|
+
function update() {
|
|
193
|
+
isOnline.value = navigator.onLine;
|
|
194
|
+
}
|
|
195
|
+
onMounted(() => {
|
|
196
|
+
update();
|
|
197
|
+
window.addEventListener("online", update);
|
|
198
|
+
window.addEventListener("offline", update);
|
|
199
|
+
});
|
|
200
|
+
onUnmounted(() => {
|
|
201
|
+
window.removeEventListener("online", update);
|
|
202
|
+
window.removeEventListener("offline", update);
|
|
203
|
+
});
|
|
204
|
+
return readonly(isOnline);
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/composables/use-visual-viewport.ts
|
|
208
|
+
/**
|
|
209
|
+
* Tracks the visual viewport.
|
|
210
|
+
*
|
|
211
|
+
* Chrome and Android browsers honour `interactive-widget=resizes-content`, so
|
|
212
|
+
* the layout viewport already shrinks for the keyboard there. Safari on iOS
|
|
213
|
+
* does not implement it: it shrinks only the *visual* viewport, leaving a sheet
|
|
214
|
+
* sized in `dvh` sitting partly underneath the keyboard.
|
|
215
|
+
*
|
|
216
|
+
* `null` means the API is unavailable, which callers should read as "trust the
|
|
217
|
+
* layout viewport" rather than as zero. A server has no viewport at all, so it
|
|
218
|
+
* gets that same `null` — this runs during `setup`, and a component using it
|
|
219
|
+
* has to survive being rendered there.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const viewport = useVisualViewport()
|
|
224
|
+
* // :style="viewport ? { height: `${viewport.height}px` } : undefined"
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
function useVisualViewport() {
|
|
228
|
+
const rect = ref(null);
|
|
229
|
+
const viewport = typeof window === "undefined" ? void 0 : window.visualViewport;
|
|
230
|
+
if (!viewport) return readonly(rect);
|
|
231
|
+
function read() {
|
|
232
|
+
if (!viewport) return;
|
|
233
|
+
rect.value = {
|
|
234
|
+
height: viewport.height,
|
|
235
|
+
offsetTop: viewport.offsetTop
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
read();
|
|
239
|
+
viewport.addEventListener("resize", read);
|
|
240
|
+
viewport.addEventListener("scroll", read);
|
|
241
|
+
onScopeDispose(() => {
|
|
242
|
+
viewport.removeEventListener("resize", read);
|
|
243
|
+
viewport.removeEventListener("scroll", read);
|
|
244
|
+
});
|
|
245
|
+
return readonly(rect);
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/composables/use-toast.ts
|
|
249
|
+
/**
|
|
250
|
+
* Four seconds: long enough to read a short sentence twice, short enough that
|
|
251
|
+
* a second action does not queue behind it.
|
|
252
|
+
*/
|
|
253
|
+
var DEFAULT_DURATION = 4e3;
|
|
254
|
+
/**
|
|
255
|
+
* A failure is read more slowly than a confirmation, and more often twice.
|
|
256
|
+
*/
|
|
257
|
+
var DANGER_DURATION = 7e3;
|
|
258
|
+
/**
|
|
259
|
+
* Three at once. A fourth pushes the oldest out rather than growing the stack
|
|
260
|
+
* off the top of the screen — an action that produces ten toasts is a loop,
|
|
261
|
+
* and a loop should not be able to cover the app it is running in.
|
|
262
|
+
*/
|
|
263
|
+
var MAX_VISIBLE = 3;
|
|
264
|
+
var items = ref([]);
|
|
265
|
+
var nextId = 0;
|
|
266
|
+
var countdowns = /* @__PURE__ */ new Map();
|
|
267
|
+
function clearCountdown(id) {
|
|
268
|
+
const countdown = countdowns.get(id);
|
|
269
|
+
if (countdown === void 0) return;
|
|
270
|
+
clearTimeout(countdown.handle);
|
|
271
|
+
countdowns.delete(id);
|
|
272
|
+
}
|
|
273
|
+
/** Removes a toast, whether it timed out or was dismissed. */
|
|
274
|
+
function dismiss(id) {
|
|
275
|
+
clearCountdown(id);
|
|
276
|
+
items.value = items.value.filter((item) => item.id !== id);
|
|
277
|
+
}
|
|
278
|
+
/** Removes everything on screen. For a route change, or a sign-out. */
|
|
279
|
+
function dismissAll() {
|
|
280
|
+
for (const id of countdowns.keys()) clearCountdown(id);
|
|
281
|
+
items.value = [];
|
|
282
|
+
}
|
|
283
|
+
function arm(id, remaining) {
|
|
284
|
+
if (typeof window === "undefined" || remaining <= 0) return;
|
|
285
|
+
countdowns.set(id, {
|
|
286
|
+
handle: setTimeout(() => dismiss(id), remaining),
|
|
287
|
+
remaining,
|
|
288
|
+
startedAt: Date.now()
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Stops the clock on a toast the reader is pointing at.
|
|
293
|
+
*
|
|
294
|
+
* Somebody who has moved the pointer onto it is reading it, and taking it away
|
|
295
|
+
* mid-sentence is the one thing a notification must not do.
|
|
296
|
+
*/
|
|
297
|
+
function pause(id) {
|
|
298
|
+
const countdown = countdowns.get(id);
|
|
299
|
+
if (countdown === void 0) return;
|
|
300
|
+
clearTimeout(countdown.handle);
|
|
301
|
+
countdowns.set(id, {
|
|
302
|
+
...countdown,
|
|
303
|
+
remaining: Math.max(0, countdown.remaining - (Date.now() - countdown.startedAt))
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/** Starts it again, from where it stopped rather than from the beginning. */
|
|
307
|
+
function resume(id) {
|
|
308
|
+
const countdown = countdowns.get(id);
|
|
309
|
+
if (countdown === void 0) return;
|
|
310
|
+
arm(id, countdown.remaining);
|
|
311
|
+
}
|
|
312
|
+
function push(tone, message, options = {}) {
|
|
313
|
+
const id = ++nextId;
|
|
314
|
+
const duration = options.duration ?? (tone === "danger" ? DANGER_DURATION : DEFAULT_DURATION);
|
|
315
|
+
const next = [...items.value, {
|
|
316
|
+
id,
|
|
317
|
+
message,
|
|
318
|
+
tone,
|
|
319
|
+
duration
|
|
320
|
+
}];
|
|
321
|
+
while (next.length > MAX_VISIBLE) {
|
|
322
|
+
const oldest = next.shift();
|
|
323
|
+
if (oldest !== void 0) clearCountdown(oldest.id);
|
|
324
|
+
}
|
|
325
|
+
items.value = next;
|
|
326
|
+
arm(id, duration);
|
|
327
|
+
return id;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The stack, and the four ways to add to it.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```ts
|
|
334
|
+
* const toast = useToast()
|
|
335
|
+
*
|
|
336
|
+
* toast.success(t('habit.saved'))
|
|
337
|
+
* toast.danger(t('common.failed'), { duration: 0 }) // stays until dismissed
|
|
338
|
+
*
|
|
339
|
+
* const id = toast.info(t('export.preparing'), { duration: 0 })
|
|
340
|
+
* toast.dismiss(id)
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
function useToast() {
|
|
344
|
+
return {
|
|
345
|
+
/** Every toast on screen, oldest first. `ToastHost` renders this. */
|
|
346
|
+
toasts: readonly(items),
|
|
347
|
+
info: (message, options) => push("info", message, options),
|
|
348
|
+
success: (message, options) => push("success", message, options),
|
|
349
|
+
warning: (message, options) => push("warning", message, options),
|
|
350
|
+
danger: (message, options) => push("danger", message, options),
|
|
351
|
+
dismiss,
|
|
352
|
+
dismissAll,
|
|
353
|
+
pause,
|
|
354
|
+
resume
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/components/BaseAlert.vue?vue&type=script&setup=true&lang.ts
|
|
359
|
+
var _hoisted_1$5 = ["role", "aria-live"];
|
|
360
|
+
var _hoisted_2$2 = { class: "min-w-0 flex-1" };
|
|
361
|
+
var _hoisted_3$2 = {
|
|
362
|
+
key: 0,
|
|
363
|
+
class: "text-ink font-semibold"
|
|
364
|
+
};
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/components/BaseAlert.vue
|
|
367
|
+
var BaseAlert_default = /* @__PURE__ */ defineComponent({
|
|
368
|
+
__name: "BaseAlert",
|
|
369
|
+
props: {
|
|
370
|
+
tone: { default: "info" },
|
|
371
|
+
assertive: {
|
|
372
|
+
type: Boolean,
|
|
373
|
+
default: false
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
setup(__props) {
|
|
377
|
+
const TONES = {
|
|
378
|
+
info: "border-hair bg-muted/40 text-ink",
|
|
379
|
+
success: "border-positive/35 bg-positive/8 text-ink",
|
|
380
|
+
warning: "border-warning/40 bg-warning/8 text-ink",
|
|
381
|
+
danger: "border-negative/35 bg-negative/8 text-ink"
|
|
382
|
+
};
|
|
383
|
+
const MARKS = {
|
|
384
|
+
info: "bg-ink-soft/15 text-ink-soft",
|
|
385
|
+
success: "bg-positive/15 text-positive",
|
|
386
|
+
warning: "bg-warning/15 text-warning",
|
|
387
|
+
danger: "bg-negative/15 text-negative"
|
|
388
|
+
};
|
|
389
|
+
const skin = computed(() => TONES[__props.tone]);
|
|
390
|
+
const mark = computed(() => MARKS[__props.tone]);
|
|
391
|
+
return (_ctx, _cache) => {
|
|
392
|
+
return openBlock(), createElementBlock("div", {
|
|
393
|
+
class: normalizeClass(["rounded-card flex items-start gap-3 border px-4 py-3.5 text-sm leading-relaxed", skin.value]),
|
|
394
|
+
role: __props.assertive ? "alert" : "status",
|
|
395
|
+
"aria-live": __props.assertive ? "assertive" : "polite"
|
|
396
|
+
}, [
|
|
397
|
+
_ctx.$slots.mark ? (openBlock(), createElementBlock("span", {
|
|
398
|
+
key: 0,
|
|
399
|
+
class: normalizeClass(["mt-px grid size-6 shrink-0 place-items-center rounded-full text-xs font-semibold", mark.value]),
|
|
400
|
+
"aria-hidden": "true"
|
|
401
|
+
}, [renderSlot(_ctx.$slots, "mark")], 2)) : createCommentVNode("", true),
|
|
402
|
+
createElementVNode("div", _hoisted_2$2, [_ctx.$slots.title ? (openBlock(), createElementBlock("p", _hoisted_3$2, [renderSlot(_ctx.$slots, "title")])) : createCommentVNode("", true), createElementVNode("div", { class: normalizeClass(_ctx.$slots.title ? "mt-1" : "") }, [renderSlot(_ctx.$slots, "default")], 2)]),
|
|
403
|
+
renderSlot(_ctx.$slots, "action")
|
|
404
|
+
], 10, _hoisted_1$5);
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/components/FormField.vue?vue&type=script&setup=true&lang.ts
|
|
410
|
+
var _hoisted_1$4 = ["for"];
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/components/FormField.vue
|
|
413
|
+
var FormField_default = /* @__PURE__ */ defineComponent({
|
|
414
|
+
__name: "FormField",
|
|
415
|
+
props: {
|
|
416
|
+
label: {},
|
|
417
|
+
error: { default: "" },
|
|
418
|
+
hint: { default: "" },
|
|
419
|
+
size: { default: "md" },
|
|
420
|
+
labelHidden: {
|
|
421
|
+
type: Boolean,
|
|
422
|
+
default: false
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
setup(__props) {
|
|
426
|
+
const id = useId();
|
|
427
|
+
const errorId = `${id}-error`;
|
|
428
|
+
const hintId = `${id}-hint`;
|
|
429
|
+
const describedBy = computed(() => {
|
|
430
|
+
if (__props.error) return errorId;
|
|
431
|
+
if (__props.hint) return hintId;
|
|
432
|
+
});
|
|
433
|
+
return (_ctx, _cache) => {
|
|
434
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass(["flex flex-col", __props.size === "sm" ? "gap-1" : "gap-1.5"]) }, [
|
|
435
|
+
createElementVNode("label", {
|
|
436
|
+
for: unref(id),
|
|
437
|
+
class: normalizeClass(["font-medium", [__props.labelHidden ? "sr-only" : "", __props.size === "sm" ? "text-ink-soft text-xs" : "text-ink text-sm"]])
|
|
438
|
+
}, toDisplayString(__props.label), 11, _hoisted_1$4),
|
|
439
|
+
renderSlot(_ctx.$slots, "default", {
|
|
440
|
+
id: unref(id),
|
|
441
|
+
describedBy: describedBy.value,
|
|
442
|
+
invalid: Boolean(__props.error),
|
|
443
|
+
size: __props.size
|
|
444
|
+
}),
|
|
445
|
+
__props.error ? (openBlock(), createElementBlock("p", {
|
|
446
|
+
key: 0,
|
|
447
|
+
id: errorId,
|
|
448
|
+
class: "text-negative text-xs"
|
|
449
|
+
}, toDisplayString(__props.error), 1)) : __props.hint ? (openBlock(), createElementBlock("p", {
|
|
450
|
+
key: 1,
|
|
451
|
+
id: hintId,
|
|
452
|
+
class: "text-ink-soft text-xs"
|
|
453
|
+
}, toDisplayString(__props.hint), 1)) : createCommentVNode("", true)
|
|
454
|
+
], 2);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/components/BaseInput.vue?vue&type=script&setup=true&lang.ts
|
|
460
|
+
var _hoisted_1$3 = [
|
|
461
|
+
"id",
|
|
462
|
+
"type",
|
|
463
|
+
"aria-invalid",
|
|
464
|
+
"aria-describedby"
|
|
465
|
+
];
|
|
466
|
+
var CONTROL_CLASS = "h-11 text-base";
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/components/BaseInput.vue
|
|
469
|
+
var BaseInput_default = /* @__PURE__ */ defineComponent({
|
|
470
|
+
inheritAttrs: false,
|
|
471
|
+
__name: "BaseInput",
|
|
472
|
+
props: /*@__PURE__*/ mergeModels({
|
|
473
|
+
label: {},
|
|
474
|
+
error: { default: "" },
|
|
475
|
+
hint: { default: "" },
|
|
476
|
+
labelHidden: {
|
|
477
|
+
type: Boolean,
|
|
478
|
+
default: false
|
|
479
|
+
},
|
|
480
|
+
type: { default: "text" },
|
|
481
|
+
size: { default: "md" },
|
|
482
|
+
variant: { default: "default" }
|
|
483
|
+
}, {
|
|
484
|
+
"modelValue": {},
|
|
485
|
+
"modelModifiers": {}
|
|
486
|
+
}),
|
|
487
|
+
emits: ["update:modelValue"],
|
|
488
|
+
setup(__props) {
|
|
489
|
+
const model = useModel(__props, "modelValue");
|
|
490
|
+
return (_ctx, _cache) => {
|
|
491
|
+
return openBlock(), createBlock(FormField_default, {
|
|
492
|
+
label: __props.label,
|
|
493
|
+
error: __props.error,
|
|
494
|
+
hint: __props.hint,
|
|
495
|
+
"label-hidden": __props.labelHidden,
|
|
496
|
+
size: __props.size
|
|
497
|
+
}, {
|
|
498
|
+
default: withCtx(({ id, describedBy, invalid }) => [withDirectives(createElementVNode("input", mergeProps({
|
|
499
|
+
id,
|
|
500
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => model.value = $event),
|
|
501
|
+
type: __props.type,
|
|
502
|
+
"aria-invalid": invalid,
|
|
503
|
+
"aria-describedby": describedBy
|
|
504
|
+
}, _ctx.$attrs, { class: [
|
|
505
|
+
__props.variant === "unstyled" ? "" : "border-hair bg-surface text-ink rounded-card focus-visible:outline-primary border px-3 focus-visible:outline-2 focus-visible:outline-offset-1",
|
|
506
|
+
__props.variant === "unstyled" ? "text-base" : CONTROL_CLASS,
|
|
507
|
+
__props.variant !== "unstyled" && invalid ? "border-negative" : ""
|
|
508
|
+
] }), null, 16, _hoisted_1$3), [[vModelDynamic, model.value]])]),
|
|
509
|
+
_: 1
|
|
510
|
+
}, 8, [
|
|
511
|
+
"label",
|
|
512
|
+
"error",
|
|
513
|
+
"hint",
|
|
514
|
+
"label-hidden",
|
|
515
|
+
"size"
|
|
516
|
+
]);
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/components/BaseSheet.vue?vue&type=script&setup=true&lang.ts
|
|
522
|
+
var _hoisted_1$2 = { class: "shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden" };
|
|
523
|
+
var _hoisted_2$1 = ["aria-label"];
|
|
524
|
+
var _hoisted_3$1 = { class: "flex shrink-0 items-start gap-3 px-6 pt-4 pb-5" };
|
|
525
|
+
var _hoisted_4 = { class: "min-w-0 flex-1" };
|
|
526
|
+
var _hoisted_5 = { class: "text-ink text-xl leading-tight font-semibold" };
|
|
527
|
+
var _hoisted_6 = {
|
|
528
|
+
key: 0,
|
|
529
|
+
class: "text-ink-soft mt-1 text-sm leading-snug"
|
|
530
|
+
};
|
|
531
|
+
var _hoisted_7 = { class: "min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]" };
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/components/BaseSheet.vue
|
|
534
|
+
var BaseSheet_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
535
|
+
__name: "BaseSheet",
|
|
536
|
+
props: /*@__PURE__*/ mergeModels({
|
|
537
|
+
title: {},
|
|
538
|
+
subtitle: { default: "" },
|
|
539
|
+
closeLabel: { default: "Close" }
|
|
540
|
+
}, {
|
|
541
|
+
"modelValue": {
|
|
542
|
+
type: Boolean,
|
|
543
|
+
required: true
|
|
544
|
+
},
|
|
545
|
+
"modelModifiers": {}
|
|
546
|
+
}),
|
|
547
|
+
emits: ["update:modelValue"],
|
|
548
|
+
setup(__props) {
|
|
549
|
+
const open = useModel(__props, "modelValue");
|
|
550
|
+
const viewport = useVisualViewport();
|
|
551
|
+
/**
|
|
552
|
+
* Pins the sheet to the area the keyboard has left visible.
|
|
553
|
+
*
|
|
554
|
+
* Only needed where the layout viewport does not shrink on its own — iOS. On
|
|
555
|
+
* Android the numbers already agree, so this is a no-op there rather than a
|
|
556
|
+
* second, competing adjustment.
|
|
557
|
+
*/
|
|
558
|
+
const viewportStyle = computed(() => viewport.value ? {
|
|
559
|
+
height: `${viewport.value.height}px`,
|
|
560
|
+
top: `${viewport.value.offsetTop}px`
|
|
561
|
+
} : void 0);
|
|
562
|
+
const panel = ref(null);
|
|
563
|
+
let lastFocused = null;
|
|
564
|
+
function close() {
|
|
565
|
+
open.value = false;
|
|
566
|
+
}
|
|
567
|
+
function onKeydown(event) {
|
|
568
|
+
if (event.key === "Escape") close();
|
|
569
|
+
}
|
|
570
|
+
watch(open, async (isOpen) => {
|
|
571
|
+
if (isOpen) {
|
|
572
|
+
setBackgroundInert(true);
|
|
573
|
+
lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
574
|
+
window.addEventListener("keydown", onKeydown);
|
|
575
|
+
await nextTick();
|
|
576
|
+
panel.value?.focus();
|
|
577
|
+
} else {
|
|
578
|
+
window.removeEventListener("keydown", onKeydown);
|
|
579
|
+
lastFocused?.focus();
|
|
580
|
+
lastFocused = null;
|
|
581
|
+
setBackgroundInert(false);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
/**
|
|
585
|
+
* `inert` takes the whole app out of tab order and pointer events while the
|
|
586
|
+
* sheet is open — a real focus trap without keydown bookkeeping.
|
|
587
|
+
*
|
|
588
|
+
* The sheet itself is teleported to `#sheet-root`, a sibling of `#app`, so it
|
|
589
|
+
* stays interactive.
|
|
590
|
+
*/
|
|
591
|
+
function setBackgroundInert(isInert) {
|
|
592
|
+
document.getElementById("app")?.toggleAttribute("inert", isInert);
|
|
593
|
+
}
|
|
594
|
+
onUnmounted(() => {
|
|
595
|
+
window.removeEventListener("keydown", onKeydown);
|
|
596
|
+
setBackgroundInert(false);
|
|
597
|
+
});
|
|
598
|
+
return (_ctx, _cache) => {
|
|
599
|
+
return openBlock(), createBlock(Teleport, { to: "#sheet-root" }, [createVNode(Transition, { name: "sheet" }, {
|
|
600
|
+
default: withCtx(() => [open.value ? (openBlock(), createElementBlock("div", {
|
|
601
|
+
key: 0,
|
|
602
|
+
class: "fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center",
|
|
603
|
+
style: normalizeStyle(viewportStyle.value)
|
|
604
|
+
}, [createElementVNode("div", _hoisted_1$2, [createElementVNode("div", {
|
|
605
|
+
class: "bg-ink/45 absolute inset-0 backdrop-blur-[2px]",
|
|
606
|
+
onClick: close
|
|
607
|
+
}), createElementVNode("section", {
|
|
608
|
+
ref_key: "panel",
|
|
609
|
+
ref: panel,
|
|
610
|
+
role: "dialog",
|
|
611
|
+
"aria-modal": "true",
|
|
612
|
+
"aria-label": __props.title,
|
|
613
|
+
tabindex: "-1",
|
|
614
|
+
class: "sheet-panel bg-surface relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[28px] shadow-2xl outline-none"
|
|
615
|
+
}, [
|
|
616
|
+
_cache[0] || (_cache[0] = createElementVNode("div", {
|
|
617
|
+
class: "flex shrink-0 justify-center pt-3",
|
|
618
|
+
"aria-hidden": "true"
|
|
619
|
+
}, [createElementVNode("span", { class: "bg-hair h-1.5 w-10 rounded-full" })], -1)),
|
|
620
|
+
createElementVNode("header", _hoisted_3$1, [createElementVNode("div", _hoisted_4, [createElementVNode("h2", _hoisted_5, toDisplayString(__props.title), 1), __props.subtitle ? (openBlock(), createElementBlock("p", _hoisted_6, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true)]), createVNode(BaseButton_default, {
|
|
621
|
+
variant: "unstyled",
|
|
622
|
+
class: "text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90",
|
|
623
|
+
"aria-label": __props.closeLabel,
|
|
624
|
+
onClick: close
|
|
625
|
+
}, {
|
|
626
|
+
default: withCtx(() => [createVNode(unref(X), { class: "size-5" })]),
|
|
627
|
+
_: 1
|
|
628
|
+
}, 8, ["aria-label"])]),
|
|
629
|
+
createElementVNode("div", _hoisted_7, [renderSlot(_ctx.$slots, "default", {}, void 0, true)])
|
|
630
|
+
], 8, _hoisted_2$1)])], 4)) : createCommentVNode("", true)]),
|
|
631
|
+
_: 3
|
|
632
|
+
})]);
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}), [["__scopeId", "data-v-51709579"]]);
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/components/BaseCheckbox.vue?vue&type=script&setup=true&lang.ts
|
|
638
|
+
var _hoisted_1$1 = { class: "flex flex-col gap-1.5" };
|
|
639
|
+
var _hoisted_2 = ["for"];
|
|
640
|
+
var _hoisted_3 = [
|
|
641
|
+
"id",
|
|
642
|
+
"disabled",
|
|
643
|
+
"aria-invalid",
|
|
644
|
+
"aria-describedby"
|
|
645
|
+
];
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region src/components/BaseCheckbox.vue
|
|
648
|
+
var BaseCheckbox_default = /* @__PURE__ */ defineComponent({
|
|
649
|
+
__name: "BaseCheckbox",
|
|
650
|
+
props: /*@__PURE__*/ mergeModels({
|
|
651
|
+
label: {},
|
|
652
|
+
error: { default: "" },
|
|
653
|
+
hint: { default: "" },
|
|
654
|
+
disabled: {
|
|
655
|
+
type: Boolean,
|
|
656
|
+
default: false
|
|
657
|
+
},
|
|
658
|
+
size: { default: "md" }
|
|
659
|
+
}, {
|
|
660
|
+
"modelValue": {
|
|
661
|
+
type: Boolean,
|
|
662
|
+
default: false
|
|
663
|
+
},
|
|
664
|
+
"modelModifiers": {}
|
|
665
|
+
}),
|
|
666
|
+
emits: ["update:modelValue"],
|
|
667
|
+
setup(__props) {
|
|
668
|
+
const model = useModel(__props, "modelValue");
|
|
669
|
+
const id = useId();
|
|
670
|
+
const errorId = `${id}-error`;
|
|
671
|
+
const hintId = `${id}-hint`;
|
|
672
|
+
const describedBy = computed(() => {
|
|
673
|
+
if (__props.error) return errorId;
|
|
674
|
+
if (__props.hint) return hintId;
|
|
675
|
+
});
|
|
676
|
+
return (_ctx, _cache) => {
|
|
677
|
+
return openBlock(), createElementBlock("div", _hoisted_1$1, [createElementVNode("label", {
|
|
678
|
+
for: unref(id),
|
|
679
|
+
class: normalizeClass(["flex items-center", [__props.size === "sm" ? "gap-2" : "gap-3", __props.disabled ? "opacity-50" : "cursor-pointer"]])
|
|
680
|
+
}, [withDirectives(createElementVNode("input", {
|
|
681
|
+
id: unref(id),
|
|
682
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => model.value = $event),
|
|
683
|
+
type: "checkbox",
|
|
684
|
+
disabled: __props.disabled,
|
|
685
|
+
"aria-invalid": Boolean(__props.error),
|
|
686
|
+
"aria-describedby": describedBy.value,
|
|
687
|
+
class: "accent-primary focus-visible:outline-primary size-4 shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
688
|
+
}, null, 8, _hoisted_3), [[vModelCheckbox, model.value]]), createElementVNode("span", { class: normalizeClass(["text-sm", __props.size === "sm" ? "text-ink-soft" : "text-ink"]) }, toDisplayString(__props.label), 3)], 10, _hoisted_2), __props.error ? (openBlock(), createElementBlock("p", {
|
|
689
|
+
key: 0,
|
|
690
|
+
id: errorId,
|
|
691
|
+
class: "text-negative text-xs"
|
|
692
|
+
}, toDisplayString(__props.error), 1)) : __props.hint ? (openBlock(), createElementBlock("p", {
|
|
693
|
+
key: 1,
|
|
694
|
+
id: hintId,
|
|
695
|
+
class: "text-ink-soft text-xs"
|
|
696
|
+
}, toDisplayString(__props.hint), 1)) : createCommentVNode("", true)]);
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/components/GoogleButton.vue?vue&type=script&setup=true&lang.ts
|
|
702
|
+
var _hoisted_1 = ["disabled"];
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/components/GoogleButton.vue
|
|
705
|
+
var GoogleButton_default = /* @__PURE__ */ defineComponent({
|
|
706
|
+
__name: "GoogleButton",
|
|
707
|
+
props: {
|
|
708
|
+
label: {},
|
|
709
|
+
disabled: {
|
|
710
|
+
type: Boolean,
|
|
711
|
+
default: false
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
emits: ["click"],
|
|
715
|
+
setup(__props, { emit: __emit }) {
|
|
716
|
+
const emit = __emit;
|
|
717
|
+
return (_ctx, _cache) => {
|
|
718
|
+
return openBlock(), createElementBlock("button", {
|
|
719
|
+
type: "button",
|
|
720
|
+
disabled: __props.disabled,
|
|
721
|
+
class: "border-hair bg-surface text-ink rounded-card hover:bg-muted flex h-11 w-full items-center justify-center gap-2 border text-sm font-medium transition-colors active:scale-95 disabled:pointer-events-none disabled:opacity-50",
|
|
722
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("click"))
|
|
723
|
+
}, [_cache[1] || (_cache[1] = createStaticVNode("<svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\"><path fill=\"#EA4335\" d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"></path><path fill=\"#4285F4\" d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"></path><path fill=\"#FBBC05\" d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"></path><path fill=\"#34A853\" d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"></path></svg>", 1)), createTextVNode(" " + toDisplayString(__props.label), 1)], 8, _hoisted_1);
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
//#endregion
|
|
728
|
+
export { toRedirectPath as _, FormField_default as a, useVisualViewport as c, isThemePreference as d, readStoredTheme as f, safeRedirect as g, tapFeedback as h, BaseInput_default as i, useOnline as l, useTheme as m, BaseCheckbox_default as n, BaseAlert_default as o, setThemeStorageKey as p, BaseSheet_default as r, useToast as s, GoogleButton_default as t, applyTheme as u };
|
|
729
|
+
|
|
730
|
+
//# sourceMappingURL=GoogleButton-BxsrxZdX.js.map
|