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
package/dist/app.js
CHANGED
|
@@ -1,11 +1,163 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as toRedirectPath, d as isThemePreference, g as safeRedirect, h as tapFeedback, i as BaseInput_default, l as useOnline, m as useTheme, n as BaseCheckbox_default, o as BaseAlert_default, r as BaseSheet_default, s as useToast, t as GoogleButton_default } from "./GoogleButton-BxsrxZdX.js";
|
|
2
2
|
import { n as _plugin_vue_export_helper_default, r as BaseButton_default, t as SettingsRow_default } from "./SettingsRow-MnVleeTR.js";
|
|
3
|
-
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, onUnmounted, openBlock, readonly, ref, renderList, renderSlot, toDisplayString, unref, useModel, watch, withCtx } from "vue";
|
|
3
|
+
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, onUnmounted, openBlock, reactive, readonly, ref, renderList, renderSlot, toDisplayString, unref, useModel, watch, withCtx, withModifiers } from "vue";
|
|
4
4
|
import { Check, Languages } from "lucide-vue-next";
|
|
5
|
+
//#region src/app/AuthForm.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
+
var _hoisted_1$6 = { class: "flex flex-col gap-5" };
|
|
7
|
+
var _hoisted_2$5 = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "flex items-center gap-3"
|
|
10
|
+
};
|
|
11
|
+
var _hoisted_3$5 = { class: "text-ink-soft text-xs" };
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/app/AuthForm.vue
|
|
14
|
+
var AuthForm_default = /* @__PURE__ */ defineComponent({
|
|
15
|
+
__name: "AuthForm",
|
|
16
|
+
props: /*@__PURE__*/ mergeModels({
|
|
17
|
+
mode: {},
|
|
18
|
+
labels: {},
|
|
19
|
+
busy: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: false
|
|
22
|
+
},
|
|
23
|
+
error: { default: "" },
|
|
24
|
+
validate: { type: Function }
|
|
25
|
+
}, {
|
|
26
|
+
"remember": {
|
|
27
|
+
type: Boolean,
|
|
28
|
+
default: true
|
|
29
|
+
},
|
|
30
|
+
"rememberModifiers": {}
|
|
31
|
+
}),
|
|
32
|
+
emits: /*@__PURE__*/ mergeModels(["submit", "google"], ["update:remember"]),
|
|
33
|
+
setup(__props, { emit: __emit }) {
|
|
34
|
+
const emit = __emit;
|
|
35
|
+
const remember = useModel(__props, "remember");
|
|
36
|
+
/**
|
|
37
|
+
* Remember-me belongs on sign-in, and only where the app has words for it.
|
|
38
|
+
*
|
|
39
|
+
* A brand-new account has nothing to remember, so offering the choice on
|
|
40
|
+
* sign-up is a question with one answer. Keyed off the label rather than a
|
|
41
|
+
* `showRemember` flag because Vue casts an absent Boolean prop to `false`,
|
|
42
|
+
* which makes "not passed" and "passed false" the same value — a flag that
|
|
43
|
+
* cannot express its own default is worse than no flag.
|
|
44
|
+
*/
|
|
45
|
+
const wantsRemember = computed(() => __props.mode === "signIn" && Boolean(__props.labels.rememberMe));
|
|
46
|
+
const values = reactive({
|
|
47
|
+
email: "",
|
|
48
|
+
password: "",
|
|
49
|
+
confirmPassword: ""
|
|
50
|
+
});
|
|
51
|
+
const errors = ref({});
|
|
52
|
+
watch(() => __props.mode, () => {
|
|
53
|
+
errors.value = {};
|
|
54
|
+
});
|
|
55
|
+
function submit() {
|
|
56
|
+
const payload = {
|
|
57
|
+
email: values.email,
|
|
58
|
+
password: values.password,
|
|
59
|
+
confirmPassword: __props.mode === "signUp" ? values.confirmPassword : ""
|
|
60
|
+
};
|
|
61
|
+
const found = __props.validate?.(payload) ?? null;
|
|
62
|
+
errors.value = found ?? {};
|
|
63
|
+
if (found) return;
|
|
64
|
+
emit("submit", payload);
|
|
65
|
+
}
|
|
66
|
+
return (_ctx, _cache) => {
|
|
67
|
+
return openBlock(), createElementBlock("div", _hoisted_1$6, [
|
|
68
|
+
renderSlot(_ctx.$slots, "header"),
|
|
69
|
+
renderSlot(_ctx.$slots, "oauth", {}, () => [__props.labels.google ? (openBlock(), createBlock(GoogleButton_default, {
|
|
70
|
+
key: 0,
|
|
71
|
+
label: __props.labels.google,
|
|
72
|
+
disabled: __props.busy,
|
|
73
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("google"))
|
|
74
|
+
}, null, 8, ["label", "disabled"])) : createCommentVNode("", true)]),
|
|
75
|
+
__props.labels.or ? (openBlock(), createElementBlock("div", _hoisted_2$5, [
|
|
76
|
+
_cache[5] || (_cache[5] = createElementVNode("span", { class: "bg-hair h-px flex-1" }, null, -1)),
|
|
77
|
+
createElementVNode("span", _hoisted_3$5, toDisplayString(__props.labels.or), 1),
|
|
78
|
+
_cache[6] || (_cache[6] = createElementVNode("span", { class: "bg-hair h-px flex-1" }, null, -1))
|
|
79
|
+
])) : createCommentVNode("", true),
|
|
80
|
+
createElementVNode("form", {
|
|
81
|
+
novalidate: "",
|
|
82
|
+
class: "flex flex-col gap-4",
|
|
83
|
+
onSubmit: withModifiers(submit, ["prevent"])
|
|
84
|
+
}, [
|
|
85
|
+
createVNode(BaseInput_default, {
|
|
86
|
+
modelValue: values.email,
|
|
87
|
+
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => values.email = $event),
|
|
88
|
+
type: "email",
|
|
89
|
+
label: __props.labels.email,
|
|
90
|
+
error: errors.value["email"],
|
|
91
|
+
placeholder: __props.labels.emailPlaceholder,
|
|
92
|
+
autocomplete: "email"
|
|
93
|
+
}, null, 8, [
|
|
94
|
+
"modelValue",
|
|
95
|
+
"label",
|
|
96
|
+
"error",
|
|
97
|
+
"placeholder"
|
|
98
|
+
]),
|
|
99
|
+
createVNode(BaseInput_default, {
|
|
100
|
+
modelValue: values.password,
|
|
101
|
+
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => values.password = $event),
|
|
102
|
+
type: "password",
|
|
103
|
+
label: __props.labels.password,
|
|
104
|
+
error: errors.value["password"],
|
|
105
|
+
hint: __props.mode === "signUp" ? __props.labels.passwordHint : void 0,
|
|
106
|
+
autocomplete: __props.mode === "signUp" ? "new-password" : "current-password"
|
|
107
|
+
}, null, 8, [
|
|
108
|
+
"modelValue",
|
|
109
|
+
"label",
|
|
110
|
+
"error",
|
|
111
|
+
"hint",
|
|
112
|
+
"autocomplete"
|
|
113
|
+
]),
|
|
114
|
+
__props.mode === "signUp" ? (openBlock(), createBlock(BaseInput_default, {
|
|
115
|
+
key: 0,
|
|
116
|
+
modelValue: values.confirmPassword,
|
|
117
|
+
"onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => values.confirmPassword = $event),
|
|
118
|
+
type: "password",
|
|
119
|
+
label: __props.labels.confirmPassword,
|
|
120
|
+
error: errors.value["confirmPassword"],
|
|
121
|
+
autocomplete: "new-password"
|
|
122
|
+
}, null, 8, [
|
|
123
|
+
"modelValue",
|
|
124
|
+
"label",
|
|
125
|
+
"error"
|
|
126
|
+
])) : createCommentVNode("", true),
|
|
127
|
+
wantsRemember.value ? (openBlock(), createBlock(BaseCheckbox_default, {
|
|
128
|
+
key: 1,
|
|
129
|
+
modelValue: remember.value,
|
|
130
|
+
"onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => remember.value = $event),
|
|
131
|
+
size: "sm",
|
|
132
|
+
label: __props.labels.rememberMe ?? ""
|
|
133
|
+
}, null, 8, ["modelValue", "label"])) : createCommentVNode("", true),
|
|
134
|
+
__props.error ? (openBlock(), createBlock(BaseAlert_default, {
|
|
135
|
+
key: 2,
|
|
136
|
+
tone: "danger",
|
|
137
|
+
assertive: ""
|
|
138
|
+
}, {
|
|
139
|
+
default: withCtx(() => [createTextVNode(toDisplayString(__props.error), 1)]),
|
|
140
|
+
_: 1
|
|
141
|
+
})) : createCommentVNode("", true),
|
|
142
|
+
createVNode(BaseButton_default, {
|
|
143
|
+
type: "submit",
|
|
144
|
+
class: "w-full",
|
|
145
|
+
loading: __props.busy
|
|
146
|
+
}, {
|
|
147
|
+
default: withCtx(() => [createTextVNode(toDisplayString(__props.busy && __props.labels.submitBusy || __props.labels.submit), 1)]),
|
|
148
|
+
_: 1
|
|
149
|
+
}, 8, ["loading"])
|
|
150
|
+
], 32),
|
|
151
|
+
renderSlot(_ctx.$slots, "foot")
|
|
152
|
+
]);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
//#endregion
|
|
5
157
|
//#region src/app/AuthShell.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
-
var _hoisted_1$
|
|
7
|
-
var _hoisted_2$
|
|
8
|
-
var _hoisted_3$
|
|
158
|
+
var _hoisted_1$5 = { class: "flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10" };
|
|
159
|
+
var _hoisted_2$4 = { class: "w-full max-w-[22rem]" };
|
|
160
|
+
var _hoisted_3$4 = {
|
|
9
161
|
key: 0,
|
|
10
162
|
class: "mt-auto"
|
|
11
163
|
};
|
|
@@ -27,19 +179,48 @@ var AuthShell_default = /* @__PURE__ */ defineComponent({
|
|
|
27
179
|
* somebody who does not read the browser's language cannot get to one.
|
|
28
180
|
*/
|
|
29
181
|
return (_ctx, _cache) => {
|
|
30
|
-
return openBlock(), createElementBlock("div", _hoisted_1$
|
|
182
|
+
return openBlock(), createElementBlock("div", _hoisted_1$5, [
|
|
31
183
|
renderSlot(_ctx.$slots, "brand"),
|
|
32
|
-
createElementVNode("main", _hoisted_2$
|
|
33
|
-
_ctx.$slots.foot ? (openBlock(), createElementBlock("div", _hoisted_3$
|
|
184
|
+
createElementVNode("main", _hoisted_2$4, [renderSlot(_ctx.$slots, "default")]),
|
|
185
|
+
_ctx.$slots.foot ? (openBlock(), createElementBlock("div", _hoisted_3$4, [renderSlot(_ctx.$slots, "foot")])) : createCommentVNode("", true)
|
|
34
186
|
]);
|
|
35
187
|
};
|
|
36
188
|
}
|
|
37
189
|
});
|
|
38
190
|
//#endregion
|
|
191
|
+
//#region src/app/FabButton.vue?vue&type=script&setup=true&lang.ts
|
|
192
|
+
var _hoisted_1$4 = { class: "rk-fab-slot" };
|
|
193
|
+
var _hoisted_2$3 = {
|
|
194
|
+
class: "rk-fab-icon",
|
|
195
|
+
"aria-hidden": "true"
|
|
196
|
+
};
|
|
197
|
+
var _hoisted_3$3 = { class: "rk-fab-label" };
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/app/FabButton.vue
|
|
200
|
+
var FabButton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
201
|
+
__name: "FabButton",
|
|
202
|
+
props: { label: {} },
|
|
203
|
+
emits: ["click"],
|
|
204
|
+
setup(__props, { emit: __emit }) {
|
|
205
|
+
const emit = __emit;
|
|
206
|
+
function press() {
|
|
207
|
+
tapFeedback();
|
|
208
|
+
emit("click");
|
|
209
|
+
}
|
|
210
|
+
return (_ctx, _cache) => {
|
|
211
|
+
return openBlock(), createElementBlock("div", _hoisted_1$4, [createElementVNode("button", {
|
|
212
|
+
type: "button",
|
|
213
|
+
class: "rk-fab",
|
|
214
|
+
onClick: press
|
|
215
|
+
}, [createElementVNode("span", _hoisted_2$3, [renderSlot(_ctx.$slots, "default", {}, void 0, true)]), createElementVNode("span", _hoisted_3$3, toDisplayString(__props.label), 1)])]);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}), [["__scopeId", "data-v-8f94cb0b"]]);
|
|
219
|
+
//#endregion
|
|
39
220
|
//#region src/app/LocaleSheet.vue?vue&type=script&setup=true&lang.ts
|
|
40
|
-
var _hoisted_1$
|
|
41
|
-
var _hoisted_2$
|
|
42
|
-
var _hoisted_3$
|
|
221
|
+
var _hoisted_1$3 = { class: "text-ink-soft text-sm" };
|
|
222
|
+
var _hoisted_2$2 = { class: "flex flex-col" };
|
|
223
|
+
var _hoisted_3$2 = { class: "flex-1 text-base" };
|
|
43
224
|
//#endregion
|
|
44
225
|
//#region src/app/LocaleSheet.vue
|
|
45
226
|
var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
@@ -75,7 +256,7 @@ var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
|
75
256
|
interactive: "",
|
|
76
257
|
onClick: _cache[0] || (_cache[0] = ($event) => open.value = true)
|
|
77
258
|
}, {
|
|
78
|
-
default: withCtx(() => [createElementVNode("span", _hoisted_1$
|
|
259
|
+
default: withCtx(() => [createElementVNode("span", _hoisted_1$3, toDisplayString(current.value), 1)]),
|
|
79
260
|
_: 1
|
|
80
261
|
}, 8, [
|
|
81
262
|
"label",
|
|
@@ -88,14 +269,14 @@ var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
|
88
269
|
subtitle: __props.hint,
|
|
89
270
|
"close-label": __props.closeLabel
|
|
90
271
|
}, {
|
|
91
|
-
default: withCtx(() => [createElementVNode("ul", _hoisted_2$
|
|
272
|
+
default: withCtx(() => [createElementVNode("ul", _hoisted_2$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (row) => {
|
|
92
273
|
return openBlock(), createElementBlock("li", { key: row.value }, [createVNode(BaseButton_default, {
|
|
93
274
|
variant: "row",
|
|
94
275
|
class: "rounded-xl",
|
|
95
276
|
pressed: model.value === row.value,
|
|
96
277
|
onClick: ($event) => select(row.value)
|
|
97
278
|
}, {
|
|
98
|
-
default: withCtx(() => [createElementVNode("span", _hoisted_3$
|
|
279
|
+
default: withCtx(() => [createElementVNode("span", _hoisted_3$2, toDisplayString(row.label), 1), model.value === row.value ? (openBlock(), createBlock(unref(Check), {
|
|
99
280
|
key: 0,
|
|
100
281
|
class: "text-primary size-5 shrink-0",
|
|
101
282
|
"aria-hidden": "true"
|
|
@@ -114,6 +295,81 @@ var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
|
114
295
|
}
|
|
115
296
|
});
|
|
116
297
|
//#endregion
|
|
298
|
+
//#region src/app/OfflineBanner.vue?vue&type=script&setup=true&lang.ts
|
|
299
|
+
var _hoisted_1$2 = {
|
|
300
|
+
key: 0,
|
|
301
|
+
role: "status",
|
|
302
|
+
class: "rk-offline-banner"
|
|
303
|
+
};
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/app/OfflineBanner.vue
|
|
306
|
+
var OfflineBanner_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
307
|
+
__name: "OfflineBanner",
|
|
308
|
+
props: { label: {} },
|
|
309
|
+
setup(__props) {
|
|
310
|
+
const isOnline = useOnline();
|
|
311
|
+
return (_ctx, _cache) => {
|
|
312
|
+
return openBlock(), createBlock(Transition, { name: "rk-offline" }, {
|
|
313
|
+
default: withCtx(() => [!unref(isOnline) ? (openBlock(), createElementBlock("p", _hoisted_1$2, toDisplayString(__props.label), 1)) : createCommentVNode("", true)]),
|
|
314
|
+
_: 1
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}), [["__scopeId", "data-v-0781b5b6"]]);
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/app/TabShell.vue?vue&type=script&setup=true&lang.ts
|
|
321
|
+
var _hoisted_1$1 = { class: "rk-screen" };
|
|
322
|
+
var _hoisted_2$1 = {
|
|
323
|
+
key: 0,
|
|
324
|
+
class: "rk-screen-aside"
|
|
325
|
+
};
|
|
326
|
+
var _hoisted_3$1 = { class: "shell-frame rk-shell" };
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/app/TabShell.vue
|
|
329
|
+
var TabShell_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
330
|
+
__name: "TabShell",
|
|
331
|
+
setup(__props) {
|
|
332
|
+
/**
|
|
333
|
+
* The phone frame the whole app sits inside.
|
|
334
|
+
*
|
|
335
|
+
* On a phone this is invisible — the shell fills the screen and there is
|
|
336
|
+
* nothing around it. On a desktop it is the bordered, rounded card in the
|
|
337
|
+
* middle of a textured field, which is what makes a phone-shaped app look
|
|
338
|
+
* deliberate on a wide screen rather than stretched or abandoned.
|
|
339
|
+
*
|
|
340
|
+
* Both phone apps had this at 120 lines, differing in the product name, two
|
|
341
|
+
* colour variables and one hover colour. None of those is a reason to own a
|
|
342
|
+
* frame, so the colours are custom properties and the name is a slot.
|
|
343
|
+
*
|
|
344
|
+
* ## What stays in the app
|
|
345
|
+
*
|
|
346
|
+
* Which layout a route uses, and the `RouterView` inside it. That is the one
|
|
347
|
+
* part that genuinely differs — an app with no auth screens has no layout
|
|
348
|
+
* switch — and it is also the part that must stay in the app so a page that
|
|
349
|
+
* throws does not take the tab bar with it.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```vue
|
|
353
|
+
* <TabShell>
|
|
354
|
+
* <template #aside><AppCredits /></template>
|
|
355
|
+
* <template #chrome><UpdatePrompt /></template>
|
|
356
|
+
*
|
|
357
|
+
* <component :is="layoutComponent">
|
|
358
|
+
* <RouterView v-slot="{ Component, route }">
|
|
359
|
+
* <Transition :name="tabTransition.name.value">
|
|
360
|
+
* <component :is="Component" :key="route.path" :class="pageClass" />
|
|
361
|
+
* </Transition>
|
|
362
|
+
* </RouterView>
|
|
363
|
+
* </component>
|
|
364
|
+
* </TabShell>
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
return (_ctx, _cache) => {
|
|
368
|
+
return openBlock(), createElementBlock("div", _hoisted_1$1, [_ctx.$slots.aside ? (openBlock(), createElementBlock("aside", _hoisted_2$1, [renderSlot(_ctx.$slots, "aside", {}, void 0, true)])) : createCommentVNode("", true), createElementVNode("div", _hoisted_3$1, [renderSlot(_ctx.$slots, "chrome", {}, void 0, true), renderSlot(_ctx.$slots, "default", {}, void 0, true)])]);
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}), [["__scopeId", "data-v-f19fd72a"]]);
|
|
372
|
+
//#endregion
|
|
117
373
|
//#region src/app/TourShell.vue?vue&type=script&setup=true&lang.ts
|
|
118
374
|
var _hoisted_1 = ["aria-label"];
|
|
119
375
|
var _hoisted_2 = { class: "shell-frame md:rounded-shell bg-canvas relative flex flex-col overflow-hidden" };
|
|
@@ -251,6 +507,171 @@ var TourShell_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PU
|
|
|
251
507
|
}
|
|
252
508
|
}), [["__scopeId", "data-v-1b9e7f4d"]]);
|
|
253
509
|
//#endregion
|
|
510
|
+
//#region src/app/guards.ts
|
|
511
|
+
/**
|
|
512
|
+
* The route guard three apps had written separately.
|
|
513
|
+
*
|
|
514
|
+
* It answers two questions and nothing else: may this visitor see this route,
|
|
515
|
+
* and where do they go if not. Everything that differs between the apps — the
|
|
516
|
+
* name of the login route, whether the return path travels as `redirect` or
|
|
517
|
+
* `next`, whether a session has to be restored first — is an option, because
|
|
518
|
+
* each of those is a decision about the app rather than about guarding.
|
|
519
|
+
*
|
|
520
|
+
* It does nothing while prerendering. Every prerendered route is public, the
|
|
521
|
+
* server has no session to read, and a guard that redirected there would write
|
|
522
|
+
* a login page into a file meant to be content.
|
|
523
|
+
*
|
|
524
|
+
* The return path goes through `toRedirectPath`, so an OAuth fragment is
|
|
525
|
+
* dropped rather than copied into a query string the server will see.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```ts
|
|
529
|
+
* router.beforeEach(
|
|
530
|
+
* createAuthGuard({
|
|
531
|
+
* isAuthenticated: () => useAuthStore().isAuthenticated,
|
|
532
|
+
* signIn: { name: 'LoginView' },
|
|
533
|
+
* }),
|
|
534
|
+
* )
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
function createAuthGuard(options) {
|
|
538
|
+
const { isAuthenticated, signIn, home, redirectQuery = "redirect", ready } = options;
|
|
539
|
+
return async (to) => {
|
|
540
|
+
if ({
|
|
541
|
+
"BASE_URL": "/",
|
|
542
|
+
"DEV": false,
|
|
543
|
+
"MODE": "production",
|
|
544
|
+
"PROD": true,
|
|
545
|
+
"SSR": false
|
|
546
|
+
}["SSR"]) return true;
|
|
547
|
+
await ready?.();
|
|
548
|
+
if (to.meta["requiresAuth"] && !isAuthenticated()) return {
|
|
549
|
+
...typeof signIn === "string" ? { path: signIn } : signIn,
|
|
550
|
+
query: { [redirectQuery]: toRedirectPath(to.fullPath) }
|
|
551
|
+
};
|
|
552
|
+
if (to.meta["guestOnly"] && isAuthenticated()) return home ?? safeRedirect(to.query[redirectQuery]);
|
|
553
|
+
return true;
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* The document title, which a single-page app has to set for itself.
|
|
558
|
+
*
|
|
559
|
+
* An `afterEach` rather than a `beforeEach`: the title describes where the
|
|
560
|
+
* visitor arrived, and a guard that can still cancel has not arrived anywhere.
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```ts
|
|
564
|
+
* router.afterEach(createTitleGuard('Kakei')) // "Ledger · Kakei"
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
567
|
+
function createTitleGuard(suffix, separator = "·") {
|
|
568
|
+
return (to) => {
|
|
569
|
+
const title = to.meta["title"];
|
|
570
|
+
document.title = title ? `${String(title)} ${separator} ${suffix}` : suffix;
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/app/query-defaults.ts
|
|
575
|
+
/**
|
|
576
|
+
* The cache settings two apps had picked independently and identically.
|
|
577
|
+
*
|
|
578
|
+
* Returned as a plain object rather than a built `QueryClient`, so the kit does
|
|
579
|
+
* not depend on TanStack Query. An app that uses a different cache — or none —
|
|
580
|
+
* pays nothing for this file, and the app keeps the client as its own
|
|
581
|
+
* module-level singleton, which is what lets non-Vue code reach it (the auth
|
|
582
|
+
* store calls `clear()` on sign-out).
|
|
583
|
+
*
|
|
584
|
+
* The numbers are not arbitrary and they are not TanStack's defaults:
|
|
585
|
+
*
|
|
586
|
+
* - `staleTime: 60_000` — the default is 0, which refetches on every mount. A
|
|
587
|
+
* phone app remounts a screen every time a tab is touched, so the default
|
|
588
|
+
* turns a tab bar into a network request per tap.
|
|
589
|
+
* - `refetchOnWindowFocus` — left on. Coming back to a backgrounded phone app
|
|
590
|
+
* is exactly when the data is most likely to be stale.
|
|
591
|
+
* - `retry: 2` on queries, `0` on mutations. Retrying a read is free; retrying
|
|
592
|
+
* a write that may already have landed is how a double charge happens.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```ts
|
|
596
|
+
* export const queryClient = new QueryClient({ defaultOptions: createQueryDefaults() })
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
function createQueryDefaults(overrides = {}) {
|
|
600
|
+
const { staleTime = 6e4, gcTime = 3e5, retry = 2 } = overrides;
|
|
601
|
+
return {
|
|
602
|
+
queries: {
|
|
603
|
+
staleTime,
|
|
604
|
+
gcTime,
|
|
605
|
+
refetchOnWindowFocus: true,
|
|
606
|
+
retry
|
|
607
|
+
},
|
|
608
|
+
mutations: { retry: 0 }
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/app/write-report.ts
|
|
613
|
+
/**
|
|
614
|
+
* Saying that a write happened.
|
|
615
|
+
*
|
|
616
|
+
* In one place so every mutation reports the same way. Both phone apps had
|
|
617
|
+
* written this, and both had written it because before it existed none of their
|
|
618
|
+
* mutations reported at all: a row was added, a row was deleted, and the only
|
|
619
|
+
* evidence was that nothing had visibly broken.
|
|
620
|
+
*
|
|
621
|
+
* Deliberately generic. A message naming the thing that was saved reads better
|
|
622
|
+
* once and worse every time after, and these are screens somebody uses dozens
|
|
623
|
+
* of times in a sitting.
|
|
624
|
+
*
|
|
625
|
+
* Form validation does **not** come through here: a rejected field says so
|
|
626
|
+
* beside itself, where the eye already is and where it stays until fixed. These
|
|
627
|
+
* are for what has already happened.
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```ts
|
|
631
|
+
* export const report = createWriteReport({
|
|
632
|
+
* saved: () => t('common.saved'),
|
|
633
|
+
* deleted: () => t('common.deleted'),
|
|
634
|
+
* failed: () => t('common.failed'),
|
|
635
|
+
* })
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
function createWriteReport(messages) {
|
|
639
|
+
return {
|
|
640
|
+
saved: () => useToast().success(messages.saved()),
|
|
641
|
+
deleted: () => useToast().success(messages.deleted()),
|
|
642
|
+
failed: () => useToast().danger(messages.failed())
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/app/field-errors.ts
|
|
647
|
+
/**
|
|
648
|
+
* Field errors keyed by field name, or null when the values are valid.
|
|
649
|
+
*
|
|
650
|
+
* Zod's own error shape is a tree; a form needs one message per input, and
|
|
651
|
+
* flattening it here keeps that reshaping out of every component that has a
|
|
652
|
+
* form in it. The first issue per field wins — a field shows one message, and
|
|
653
|
+
* the first is the one the reader can act on.
|
|
654
|
+
*
|
|
655
|
+
* Null rather than an empty object, so `if (errors)` is the guard and a caller
|
|
656
|
+
* cannot accidentally treat "no errors" as a failure.
|
|
657
|
+
*
|
|
658
|
+
* @example
|
|
659
|
+
* ```ts
|
|
660
|
+
* const found = fieldErrors(signupSchema(), values)
|
|
661
|
+
* if (found) return (errors.value = found)
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
function fieldErrors(schema, values) {
|
|
665
|
+
const result = schema.safeParse(values);
|
|
666
|
+
if (result.success) return null;
|
|
667
|
+
const errors = {};
|
|
668
|
+
for (const issue of result.error.issues) {
|
|
669
|
+
const field = String(issue.path[0] ?? "");
|
|
670
|
+
if (field && !errors[field]) errors[field] = issue.message;
|
|
671
|
+
}
|
|
672
|
+
return errors;
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
254
675
|
//#region src/app/use-tab-transition.ts
|
|
255
676
|
/**
|
|
256
677
|
* Which way a tabbed app is moving.
|
|
@@ -289,6 +710,18 @@ function createTabTransition(order) {
|
|
|
289
710
|
/** Direction of the current tab change. Read by the route transition. */
|
|
290
711
|
direction: readonly(direction),
|
|
291
712
|
/**
|
|
713
|
+
* The `<Transition>` name for the current direction, ready to bind.
|
|
714
|
+
*
|
|
715
|
+
* Empty when there is nothing to slide, which is how a `<Transition>` is
|
|
716
|
+
* told to do nothing — an unnamed transition still runs a default `v-*`
|
|
717
|
+
* animation, so the empty string matters.
|
|
718
|
+
*
|
|
719
|
+
* Both phone apps derived this from `direction` in their `App.vue`, in the
|
|
720
|
+
* same three lines, and the names match the classes shipped in
|
|
721
|
+
* `rei-kit/shell/mobile.css`.
|
|
722
|
+
*/
|
|
723
|
+
name: computed(() => direction.value === "none" ? "" : `slide-${direction.value}`),
|
|
724
|
+
/**
|
|
292
725
|
* Resolves the direction for a navigation. Call once per route change.
|
|
293
726
|
*
|
|
294
727
|
* @param to - Tab being entered, if the route has one.
|
|
@@ -352,6 +785,6 @@ function useThemeSync(stored) {
|
|
|
352
785
|
}, { immediate: true });
|
|
353
786
|
}
|
|
354
787
|
//#endregion
|
|
355
|
-
export { AuthShell_default as AuthShell, LocaleSheet_default as LocaleSheet, TourShell_default as TourShell, createTabTransition, useThemeSync };
|
|
788
|
+
export { AuthForm_default as AuthForm, AuthShell_default as AuthShell, FabButton_default as FabButton, LocaleSheet_default as LocaleSheet, OfflineBanner_default as OfflineBanner, TabShell_default as TabShell, TourShell_default as TourShell, createAuthGuard, createQueryDefaults, createTabTransition, createTitleGuard, createWriteReport, fieldErrors, useThemeSync };
|
|
356
789
|
|
|
357
790
|
//# sourceMappingURL=app.js.map
|