rei-kit 0.15.0 → 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 -8
- package/dist/{GoogleButton-BQSsm0hp.js → GoogleButton-BxsrxZdX.js} +226 -3
- package/dist/GoogleButton-BxsrxZdX.js.map +1 -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/guards.d.ts +68 -0
- package/dist/app/index.d.ts +9 -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 +272 -20
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -200
- package/dist/index.js.map +1 -1
- package/dist/styles.css +166 -0
- package/dist/utils/redirect.d.ts +24 -0
- package/package.json +1 -1
- package/dist/GoogleButton-BQSsm0hp.js.map +0 -1
package/dist/app/index.d.ts
CHANGED
|
@@ -13,8 +13,17 @@
|
|
|
13
13
|
export { default as AuthForm } from './AuthForm.vue';
|
|
14
14
|
export type { AuthFormLabels, AuthFormValues } from './auth-form';
|
|
15
15
|
export { default as AuthShell } from './AuthShell.vue';
|
|
16
|
+
export { default as FabButton } from './FabButton.vue';
|
|
16
17
|
export { default as LocaleSheet } from './LocaleSheet.vue';
|
|
18
|
+
export { default as OfflineBanner } from './OfflineBanner.vue';
|
|
19
|
+
export { default as TabShell } from './TabShell.vue';
|
|
17
20
|
export { default as TourShell } from './TourShell.vue';
|
|
21
|
+
export { createAuthGuard, createTitleGuard } from './guards';
|
|
22
|
+
export type { AuthGuardOptions } from './guards';
|
|
23
|
+
export { createQueryDefaults } from './query-defaults';
|
|
24
|
+
export type { QueryDefaultsOverrides } from './query-defaults';
|
|
25
|
+
export { createWriteReport } from './write-report';
|
|
26
|
+
export type { WriteReport, WriteReportMessages } from './write-report';
|
|
18
27
|
export { fieldErrors } from './field-errors';
|
|
19
28
|
export type { SafeParsable } from './field-errors';
|
|
20
29
|
export { createTabTransition } from './use-tab-transition';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cache settings two apps had picked independently and identically.
|
|
3
|
+
*
|
|
4
|
+
* Returned as a plain object rather than a built `QueryClient`, so the kit does
|
|
5
|
+
* not depend on TanStack Query. An app that uses a different cache — or none —
|
|
6
|
+
* pays nothing for this file, and the app keeps the client as its own
|
|
7
|
+
* module-level singleton, which is what lets non-Vue code reach it (the auth
|
|
8
|
+
* store calls `clear()` on sign-out).
|
|
9
|
+
*
|
|
10
|
+
* The numbers are not arbitrary and they are not TanStack's defaults:
|
|
11
|
+
*
|
|
12
|
+
* - `staleTime: 60_000` — the default is 0, which refetches on every mount. A
|
|
13
|
+
* phone app remounts a screen every time a tab is touched, so the default
|
|
14
|
+
* turns a tab bar into a network request per tap.
|
|
15
|
+
* - `refetchOnWindowFocus` — left on. Coming back to a backgrounded phone app
|
|
16
|
+
* is exactly when the data is most likely to be stale.
|
|
17
|
+
* - `retry: 2` on queries, `0` on mutations. Retrying a read is free; retrying
|
|
18
|
+
* a write that may already have landed is how a double charge happens.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* export const queryClient = new QueryClient({ defaultOptions: createQueryDefaults() })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createQueryDefaults(overrides?: QueryDefaultsOverrides): {
|
|
26
|
+
queries: {
|
|
27
|
+
staleTime: number;
|
|
28
|
+
gcTime: number;
|
|
29
|
+
refetchOnWindowFocus: boolean;
|
|
30
|
+
retry: number;
|
|
31
|
+
};
|
|
32
|
+
mutations: {
|
|
33
|
+
retry: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export type QueryDefaultsOverrides = {
|
|
37
|
+
/** How long a result is served without refetching. Default 60s. */
|
|
38
|
+
staleTime?: number | undefined;
|
|
39
|
+
/** How long an unused result is kept before eviction. Default 5m. */
|
|
40
|
+
gcTime?: number | undefined;
|
|
41
|
+
/** Attempts for a failed *query*. Mutations are never retried. Default 2. */
|
|
42
|
+
retry?: number | undefined;
|
|
43
|
+
};
|
|
@@ -33,6 +33,18 @@ export type SlideDirection = 'forward' | 'backward' | 'none';
|
|
|
33
33
|
export declare function createTabTransition<K extends string>(order: readonly K[]): {
|
|
34
34
|
/** Direction of the current tab change. Read by the route transition. */
|
|
35
35
|
direction: Readonly<import('vue').Ref<SlideDirection, SlideDirection>>;
|
|
36
|
+
/**
|
|
37
|
+
* The `<Transition>` name for the current direction, ready to bind.
|
|
38
|
+
*
|
|
39
|
+
* Empty when there is nothing to slide, which is how a `<Transition>` is
|
|
40
|
+
* told to do nothing — an unnamed transition still runs a default `v-*`
|
|
41
|
+
* animation, so the empty string matters.
|
|
42
|
+
*
|
|
43
|
+
* Both phone apps derived this from `direction` in their `App.vue`, in the
|
|
44
|
+
* same three lines, and the names match the classes shipped in
|
|
45
|
+
* `rei-kit/shell/mobile.css`.
|
|
46
|
+
*/
|
|
47
|
+
name: import('vue').ComputedRef<string>;
|
|
36
48
|
/**
|
|
37
49
|
* Resolves the direction for a navigation. Call once per route change.
|
|
38
50
|
*
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saying that a write happened.
|
|
3
|
+
*
|
|
4
|
+
* In one place so every mutation reports the same way. Both phone apps had
|
|
5
|
+
* written this, and both had written it because before it existed none of their
|
|
6
|
+
* mutations reported at all: a row was added, a row was deleted, and the only
|
|
7
|
+
* evidence was that nothing had visibly broken.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately generic. A message naming the thing that was saved reads better
|
|
10
|
+
* once and worse every time after, and these are screens somebody uses dozens
|
|
11
|
+
* of times in a sitting.
|
|
12
|
+
*
|
|
13
|
+
* Form validation does **not** come through here: a rejected field says so
|
|
14
|
+
* beside itself, where the eye already is and where it stays until fixed. These
|
|
15
|
+
* are for what has already happened.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* export const report = createWriteReport({
|
|
20
|
+
* saved: () => t('common.saved'),
|
|
21
|
+
* deleted: () => t('common.deleted'),
|
|
22
|
+
* failed: () => t('common.failed'),
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createWriteReport(messages: WriteReportMessages): WriteReport;
|
|
27
|
+
/**
|
|
28
|
+
* Functions, not strings.
|
|
29
|
+
*
|
|
30
|
+
* A string would be read once, when the report is built, and would then keep
|
|
31
|
+
* whichever language was active at that moment for the life of the app. Called
|
|
32
|
+
* per report, the wording follows a language switch.
|
|
33
|
+
*/
|
|
34
|
+
export type WriteReportMessages = {
|
|
35
|
+
saved: () => string;
|
|
36
|
+
deleted: () => string;
|
|
37
|
+
failed: () => string;
|
|
38
|
+
};
|
|
39
|
+
export type WriteReport = {
|
|
40
|
+
saved: () => void;
|
|
41
|
+
deleted: () => void;
|
|
42
|
+
failed: () => void;
|
|
43
|
+
};
|
package/dist/app.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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
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
5
|
//#region src/app/AuthForm.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
-
var _hoisted_1$
|
|
7
|
-
var _hoisted_2$
|
|
6
|
+
var _hoisted_1$6 = { class: "flex flex-col gap-5" };
|
|
7
|
+
var _hoisted_2$5 = {
|
|
8
8
|
key: 0,
|
|
9
9
|
class: "flex items-center gap-3"
|
|
10
10
|
};
|
|
11
|
-
var _hoisted_3$
|
|
11
|
+
var _hoisted_3$5 = { class: "text-ink-soft text-xs" };
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/app/AuthForm.vue
|
|
14
14
|
var AuthForm_default = /* @__PURE__ */ defineComponent({
|
|
@@ -64,7 +64,7 @@ var AuthForm_default = /* @__PURE__ */ defineComponent({
|
|
|
64
64
|
emit("submit", payload);
|
|
65
65
|
}
|
|
66
66
|
return (_ctx, _cache) => {
|
|
67
|
-
return openBlock(), createElementBlock("div", _hoisted_1$
|
|
67
|
+
return openBlock(), createElementBlock("div", _hoisted_1$6, [
|
|
68
68
|
renderSlot(_ctx.$slots, "header"),
|
|
69
69
|
renderSlot(_ctx.$slots, "oauth", {}, () => [__props.labels.google ? (openBlock(), createBlock(GoogleButton_default, {
|
|
70
70
|
key: 0,
|
|
@@ -72,9 +72,9 @@ var AuthForm_default = /* @__PURE__ */ defineComponent({
|
|
|
72
72
|
disabled: __props.busy,
|
|
73
73
|
onClick: _cache[0] || (_cache[0] = ($event) => emit("google"))
|
|
74
74
|
}, null, 8, ["label", "disabled"])) : createCommentVNode("", true)]),
|
|
75
|
-
__props.labels.or ? (openBlock(), createElementBlock("div", _hoisted_2$
|
|
75
|
+
__props.labels.or ? (openBlock(), createElementBlock("div", _hoisted_2$5, [
|
|
76
76
|
_cache[5] || (_cache[5] = createElementVNode("span", { class: "bg-hair h-px flex-1" }, null, -1)),
|
|
77
|
-
createElementVNode("span", _hoisted_3$
|
|
77
|
+
createElementVNode("span", _hoisted_3$5, toDisplayString(__props.labels.or), 1),
|
|
78
78
|
_cache[6] || (_cache[6] = createElementVNode("span", { class: "bg-hair h-px flex-1" }, null, -1))
|
|
79
79
|
])) : createCommentVNode("", true),
|
|
80
80
|
createElementVNode("form", {
|
|
@@ -155,9 +155,9 @@ var AuthForm_default = /* @__PURE__ */ defineComponent({
|
|
|
155
155
|
});
|
|
156
156
|
//#endregion
|
|
157
157
|
//#region src/app/AuthShell.vue?vue&type=script&setup=true&lang.ts
|
|
158
|
-
var _hoisted_1$
|
|
159
|
-
var _hoisted_2$
|
|
160
|
-
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 = {
|
|
161
161
|
key: 0,
|
|
162
162
|
class: "mt-auto"
|
|
163
163
|
};
|
|
@@ -179,19 +179,48 @@ var AuthShell_default = /* @__PURE__ */ defineComponent({
|
|
|
179
179
|
* somebody who does not read the browser's language cannot get to one.
|
|
180
180
|
*/
|
|
181
181
|
return (_ctx, _cache) => {
|
|
182
|
-
return openBlock(), createElementBlock("div", _hoisted_1$
|
|
182
|
+
return openBlock(), createElementBlock("div", _hoisted_1$5, [
|
|
183
183
|
renderSlot(_ctx.$slots, "brand"),
|
|
184
|
-
createElementVNode("main", _hoisted_2$
|
|
185
|
-
_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)
|
|
186
186
|
]);
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
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
|
|
191
220
|
//#region src/app/LocaleSheet.vue?vue&type=script&setup=true&lang.ts
|
|
192
|
-
var _hoisted_1$
|
|
193
|
-
var _hoisted_2$
|
|
194
|
-
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" };
|
|
195
224
|
//#endregion
|
|
196
225
|
//#region src/app/LocaleSheet.vue
|
|
197
226
|
var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
@@ -227,7 +256,7 @@ var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
|
227
256
|
interactive: "",
|
|
228
257
|
onClick: _cache[0] || (_cache[0] = ($event) => open.value = true)
|
|
229
258
|
}, {
|
|
230
|
-
default: withCtx(() => [createElementVNode("span", _hoisted_1$
|
|
259
|
+
default: withCtx(() => [createElementVNode("span", _hoisted_1$3, toDisplayString(current.value), 1)]),
|
|
231
260
|
_: 1
|
|
232
261
|
}, 8, [
|
|
233
262
|
"label",
|
|
@@ -240,14 +269,14 @@ var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
|
240
269
|
subtitle: __props.hint,
|
|
241
270
|
"close-label": __props.closeLabel
|
|
242
271
|
}, {
|
|
243
|
-
default: withCtx(() => [createElementVNode("ul", _hoisted_2$
|
|
272
|
+
default: withCtx(() => [createElementVNode("ul", _hoisted_2$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (row) => {
|
|
244
273
|
return openBlock(), createElementBlock("li", { key: row.value }, [createVNode(BaseButton_default, {
|
|
245
274
|
variant: "row",
|
|
246
275
|
class: "rounded-xl",
|
|
247
276
|
pressed: model.value === row.value,
|
|
248
277
|
onClick: ($event) => select(row.value)
|
|
249
278
|
}, {
|
|
250
|
-
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), {
|
|
251
280
|
key: 0,
|
|
252
281
|
class: "text-primary size-5 shrink-0",
|
|
253
282
|
"aria-hidden": "true"
|
|
@@ -266,6 +295,81 @@ var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
|
266
295
|
}
|
|
267
296
|
});
|
|
268
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
|
|
269
373
|
//#region src/app/TourShell.vue?vue&type=script&setup=true&lang.ts
|
|
270
374
|
var _hoisted_1 = ["aria-label"];
|
|
271
375
|
var _hoisted_2 = { class: "shell-frame md:rounded-shell bg-canvas relative flex flex-col overflow-hidden" };
|
|
@@ -403,6 +507,142 @@ var TourShell_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PU
|
|
|
403
507
|
}
|
|
404
508
|
}), [["__scopeId", "data-v-1b9e7f4d"]]);
|
|
405
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
|
|
406
646
|
//#region src/app/field-errors.ts
|
|
407
647
|
/**
|
|
408
648
|
* Field errors keyed by field name, or null when the values are valid.
|
|
@@ -470,6 +710,18 @@ function createTabTransition(order) {
|
|
|
470
710
|
/** Direction of the current tab change. Read by the route transition. */
|
|
471
711
|
direction: readonly(direction),
|
|
472
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
|
+
/**
|
|
473
725
|
* Resolves the direction for a navigation. Call once per route change.
|
|
474
726
|
*
|
|
475
727
|
* @param to - Tab being entered, if the route has one.
|
|
@@ -533,6 +785,6 @@ function useThemeSync(stored) {
|
|
|
533
785
|
}, { immediate: true });
|
|
534
786
|
}
|
|
535
787
|
//#endregion
|
|
536
|
-
export { AuthForm_default as AuthForm, AuthShell_default as AuthShell, LocaleSheet_default as LocaleSheet, TourShell_default as TourShell, createTabTransition, fieldErrors, 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 };
|
|
537
789
|
|
|
538
790
|
//# sourceMappingURL=app.js.map
|