rei-kit 0.13.0 → 0.14.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/dist/BaseSheet-CLIhXDwe.js +253 -0
- package/dist/BaseSheet-CLIhXDwe.js.map +1 -0
- package/dist/{_plugin-vue_export-helper-3AcMDTtW.js → SettingsGroup-DtEB_Hrd.js} +12 -148
- package/dist/SettingsGroup-DtEB_Hrd.js.map +1 -0
- package/dist/SettingsRow-MnVleeTR.js +217 -0
- package/dist/SettingsRow-MnVleeTR.js.map +1 -0
- package/dist/app/LocaleSheet.vue.d.ts +36 -0
- package/dist/app/TourShell.vue.d.ts +74 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app.js +226 -9
- package/dist/app.js.map +1 -1
- package/dist/index.js +77 -309
- package/dist/index.js.map +1 -1
- package/dist/pwa/InstallSettings.vue.d.ts +26 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa.js +50 -3
- package/dist/pwa.js.map +1 -1
- package/dist/styles.css +45 -0
- package/package.json +1 -1
- package/dist/_plugin-vue_export-helper-3AcMDTtW.js.map +0 -1
- package/dist/use-theme-_MnaDD5v.js +0 -94
- package/dist/use-theme-_MnaDD5v.js.map +0 -1
package/dist/app.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as isThemePreference, s as useTheme, t as BaseSheet_default } from "./BaseSheet-CLIhXDwe.js";
|
|
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";
|
|
4
|
+
import { Check, Languages } from "lucide-vue-next";
|
|
3
5
|
//#region src/app/AuthShell.vue?vue&type=script&setup=true&lang.ts
|
|
4
|
-
var _hoisted_1 = { class: "flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10" };
|
|
5
|
-
var _hoisted_2 = { class: "w-full max-w-[22rem]" };
|
|
6
|
-
var _hoisted_3 = {
|
|
6
|
+
var _hoisted_1$2 = { class: "flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10" };
|
|
7
|
+
var _hoisted_2$2 = { class: "w-full max-w-[22rem]" };
|
|
8
|
+
var _hoisted_3$2 = {
|
|
7
9
|
key: 0,
|
|
8
10
|
class: "mt-auto"
|
|
9
11
|
};
|
|
@@ -25,15 +27,230 @@ var AuthShell_default = /* @__PURE__ */ defineComponent({
|
|
|
25
27
|
* somebody who does not read the browser's language cannot get to one.
|
|
26
28
|
*/
|
|
27
29
|
return (_ctx, _cache) => {
|
|
28
|
-
return openBlock(), createElementBlock("div", _hoisted_1, [
|
|
30
|
+
return openBlock(), createElementBlock("div", _hoisted_1$2, [
|
|
29
31
|
renderSlot(_ctx.$slots, "brand"),
|
|
30
|
-
createElementVNode("main", _hoisted_2, [renderSlot(_ctx.$slots, "default")]),
|
|
31
|
-
_ctx.$slots.foot ? (openBlock(), createElementBlock("div", _hoisted_3, [renderSlot(_ctx.$slots, "foot")])) : createCommentVNode("", true)
|
|
32
|
+
createElementVNode("main", _hoisted_2$2, [renderSlot(_ctx.$slots, "default")]),
|
|
33
|
+
_ctx.$slots.foot ? (openBlock(), createElementBlock("div", _hoisted_3$2, [renderSlot(_ctx.$slots, "foot")])) : createCommentVNode("", true)
|
|
32
34
|
]);
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
});
|
|
36
38
|
//#endregion
|
|
39
|
+
//#region src/app/LocaleSheet.vue?vue&type=script&setup=true&lang.ts
|
|
40
|
+
var _hoisted_1$1 = { class: "text-ink-soft text-sm" };
|
|
41
|
+
var _hoisted_2$1 = { class: "flex flex-col" };
|
|
42
|
+
var _hoisted_3$1 = { class: "flex-1 text-base" };
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/app/LocaleSheet.vue
|
|
45
|
+
var LocaleSheet_default = /* @__PURE__ */ defineComponent({
|
|
46
|
+
__name: "LocaleSheet",
|
|
47
|
+
props: /*@__PURE__*/ mergeModels({
|
|
48
|
+
label: {},
|
|
49
|
+
hint: { default: "" },
|
|
50
|
+
systemLabel: {},
|
|
51
|
+
options: {},
|
|
52
|
+
closeLabel: { default: "" }
|
|
53
|
+
}, {
|
|
54
|
+
"modelValue": { required: true },
|
|
55
|
+
"modelModifiers": {}
|
|
56
|
+
}),
|
|
57
|
+
emits: ["update:modelValue"],
|
|
58
|
+
setup(__props) {
|
|
59
|
+
const model = useModel(__props, "modelValue");
|
|
60
|
+
const open = ref(false);
|
|
61
|
+
const rows = computed(() => [{
|
|
62
|
+
value: "system",
|
|
63
|
+
label: __props.systemLabel
|
|
64
|
+
}, ...__props.options]);
|
|
65
|
+
const current = computed(() => rows.value.find((row) => row.value === model.value)?.label ?? __props.systemLabel);
|
|
66
|
+
function select(value) {
|
|
67
|
+
model.value = value;
|
|
68
|
+
open.value = false;
|
|
69
|
+
}
|
|
70
|
+
return (_ctx, _cache) => {
|
|
71
|
+
return openBlock(), createElementBlock(Fragment, null, [createVNode(SettingsRow_default, {
|
|
72
|
+
label: __props.label,
|
|
73
|
+
description: __props.hint,
|
|
74
|
+
icon: unref(Languages),
|
|
75
|
+
interactive: "",
|
|
76
|
+
onClick: _cache[0] || (_cache[0] = ($event) => open.value = true)
|
|
77
|
+
}, {
|
|
78
|
+
default: withCtx(() => [createElementVNode("span", _hoisted_1$1, toDisplayString(current.value), 1)]),
|
|
79
|
+
_: 1
|
|
80
|
+
}, 8, [
|
|
81
|
+
"label",
|
|
82
|
+
"description",
|
|
83
|
+
"icon"
|
|
84
|
+
]), createVNode(BaseSheet_default, {
|
|
85
|
+
modelValue: open.value,
|
|
86
|
+
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => open.value = $event),
|
|
87
|
+
title: __props.label,
|
|
88
|
+
subtitle: __props.hint,
|
|
89
|
+
"close-label": __props.closeLabel
|
|
90
|
+
}, {
|
|
91
|
+
default: withCtx(() => [createElementVNode("ul", _hoisted_2$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (row) => {
|
|
92
|
+
return openBlock(), createElementBlock("li", { key: row.value }, [createVNode(BaseButton_default, {
|
|
93
|
+
variant: "row",
|
|
94
|
+
class: "rounded-xl",
|
|
95
|
+
pressed: model.value === row.value,
|
|
96
|
+
onClick: ($event) => select(row.value)
|
|
97
|
+
}, {
|
|
98
|
+
default: withCtx(() => [createElementVNode("span", _hoisted_3$1, toDisplayString(row.label), 1), model.value === row.value ? (openBlock(), createBlock(unref(Check), {
|
|
99
|
+
key: 0,
|
|
100
|
+
class: "text-primary size-5 shrink-0",
|
|
101
|
+
"aria-hidden": "true"
|
|
102
|
+
})) : createCommentVNode("", true)]),
|
|
103
|
+
_: 2
|
|
104
|
+
}, 1032, ["pressed", "onClick"])]);
|
|
105
|
+
}), 128))])]),
|
|
106
|
+
_: 1
|
|
107
|
+
}, 8, [
|
|
108
|
+
"modelValue",
|
|
109
|
+
"title",
|
|
110
|
+
"subtitle",
|
|
111
|
+
"close-label"
|
|
112
|
+
])], 64);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/app/TourShell.vue?vue&type=script&setup=true&lang.ts
|
|
118
|
+
var _hoisted_1 = ["aria-label"];
|
|
119
|
+
var _hoisted_2 = { class: "shell-frame md:rounded-shell bg-canvas relative flex flex-col overflow-hidden" };
|
|
120
|
+
var _hoisted_3 = { class: "relative flex shrink-0 items-center justify-between gap-3 px-6 pt-6" };
|
|
121
|
+
var _hoisted_4 = { class: "text-ink-soft text-xs font-semibold tabular-nums" };
|
|
122
|
+
var _hoisted_5 = { class: "relative flex min-h-0 flex-1 flex-col justify-center overflow-y-auto px-6 py-6" };
|
|
123
|
+
var _hoisted_6 = { class: "relative flex shrink-0 flex-col gap-4 px-6 pt-4 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]" };
|
|
124
|
+
var _hoisted_7 = ["aria-label"];
|
|
125
|
+
var _hoisted_8 = { class: "flex gap-2" };
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/app/TourShell.vue
|
|
128
|
+
var TourShell_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
129
|
+
__name: "TourShell",
|
|
130
|
+
props: /*@__PURE__*/ mergeModels({
|
|
131
|
+
index: {},
|
|
132
|
+
total: {},
|
|
133
|
+
dialogLabel: {},
|
|
134
|
+
skipLabel: {},
|
|
135
|
+
backLabel: {},
|
|
136
|
+
nextLabel: {},
|
|
137
|
+
lastLabel: {},
|
|
138
|
+
stepLabel: { type: Function },
|
|
139
|
+
teleportTo: { default: "body" }
|
|
140
|
+
}, {
|
|
141
|
+
"modelValue": {
|
|
142
|
+
type: Boolean,
|
|
143
|
+
required: true
|
|
144
|
+
},
|
|
145
|
+
"modelModifiers": {}
|
|
146
|
+
}),
|
|
147
|
+
emits: /*@__PURE__*/ mergeModels([
|
|
148
|
+
"next",
|
|
149
|
+
"back",
|
|
150
|
+
"dismiss",
|
|
151
|
+
"goTo"
|
|
152
|
+
], ["update:modelValue"]),
|
|
153
|
+
setup(__props, { emit: __emit }) {
|
|
154
|
+
const open = useModel(__props, "modelValue");
|
|
155
|
+
const emit = __emit;
|
|
156
|
+
const dialog = ref(null);
|
|
157
|
+
/** Direction, so a jump backwards still animates backwards. */
|
|
158
|
+
const transitionName = ref("tour-forward");
|
|
159
|
+
watch(() => __props.index, (next, previous) => {
|
|
160
|
+
transitionName.value = next >= previous ? "tour-forward" : "tour-backward";
|
|
161
|
+
});
|
|
162
|
+
const isLast = computed(() => __props.index >= __props.total - 1);
|
|
163
|
+
watch(open, async (isOpen) => {
|
|
164
|
+
if (typeof document === "undefined") return;
|
|
165
|
+
document.getElementById("app")?.toggleAttribute("inert", isOpen);
|
|
166
|
+
if (!isOpen) return;
|
|
167
|
+
await nextTick();
|
|
168
|
+
dialog.value?.focus();
|
|
169
|
+
}, { immediate: true });
|
|
170
|
+
onUnmounted(() => {
|
|
171
|
+
if (typeof document !== "undefined") document.getElementById("app")?.removeAttribute("inert");
|
|
172
|
+
});
|
|
173
|
+
function onKeydown(event) {
|
|
174
|
+
if (event.key === "ArrowRight") emit("next");
|
|
175
|
+
if (event.key === "ArrowLeft") emit("back");
|
|
176
|
+
if (event.key === "Escape") emit("dismiss");
|
|
177
|
+
}
|
|
178
|
+
return (_ctx, _cache) => {
|
|
179
|
+
return openBlock(), createBlock(Teleport, { to: __props.teleportTo }, [createVNode(Transition, {
|
|
180
|
+
name: "tour",
|
|
181
|
+
appear: ""
|
|
182
|
+
}, {
|
|
183
|
+
default: withCtx(() => [open.value ? (openBlock(), createElementBlock("div", {
|
|
184
|
+
key: 0,
|
|
185
|
+
ref_key: "dialog",
|
|
186
|
+
ref: dialog,
|
|
187
|
+
class: "fixed inset-0 z-[60] flex items-center justify-center outline-none",
|
|
188
|
+
role: "dialog",
|
|
189
|
+
"aria-modal": "true",
|
|
190
|
+
"aria-label": __props.dialogLabel,
|
|
191
|
+
tabindex: "-1",
|
|
192
|
+
onKeydown
|
|
193
|
+
}, [createElementVNode("div", _hoisted_2, [
|
|
194
|
+
renderSlot(_ctx.$slots, "wash", {}, void 0, true),
|
|
195
|
+
createElementVNode("header", _hoisted_3, [createElementVNode("span", _hoisted_4, toDisplayString(__props.stepLabel(__props.index + 1)), 1), createVNode(BaseButton_default, {
|
|
196
|
+
pill: "",
|
|
197
|
+
size: "sm",
|
|
198
|
+
variant: "quiet",
|
|
199
|
+
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("dismiss"))
|
|
200
|
+
}, {
|
|
201
|
+
default: withCtx(() => [createTextVNode(toDisplayString(__props.skipLabel), 1)]),
|
|
202
|
+
_: 1
|
|
203
|
+
})]),
|
|
204
|
+
createElementVNode("div", _hoisted_5, [createVNode(Transition, {
|
|
205
|
+
name: transitionName.value,
|
|
206
|
+
mode: "out-in"
|
|
207
|
+
}, {
|
|
208
|
+
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { index: __props.index }, void 0, true)]),
|
|
209
|
+
_: 3
|
|
210
|
+
}, 8, ["name"])]),
|
|
211
|
+
createElementVNode("footer", _hoisted_6, [createElementVNode("div", {
|
|
212
|
+
class: "-my-2 flex items-center gap-1",
|
|
213
|
+
role: "tablist",
|
|
214
|
+
"aria-label": __props.stepLabel(__props.index + 1)
|
|
215
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.total, (position) => {
|
|
216
|
+
return openBlock(), createBlock(BaseButton_default, {
|
|
217
|
+
key: position,
|
|
218
|
+
variant: "unstyled",
|
|
219
|
+
role: "tab",
|
|
220
|
+
"aria-selected": position - 1 === __props.index,
|
|
221
|
+
"aria-label": __props.stepLabel(position),
|
|
222
|
+
class: "group flex flex-1 items-center py-2.5",
|
|
223
|
+
onClick: ($event) => _ctx.$emit("goTo", position - 1)
|
|
224
|
+
}, {
|
|
225
|
+
default: withCtx(() => [createElementVNode("span", { class: normalizeClass(["h-1 w-full rounded-full transition-colors duration-300", position - 1 <= __props.index ? "bg-primary" : "bg-hair group-hover:bg-ink-soft/40"]) }, null, 2)]),
|
|
226
|
+
_: 2
|
|
227
|
+
}, 1032, [
|
|
228
|
+
"aria-selected",
|
|
229
|
+
"aria-label",
|
|
230
|
+
"onClick"
|
|
231
|
+
]);
|
|
232
|
+
}), 128))], 8, _hoisted_7), createElementVNode("div", _hoisted_8, [__props.index > 0 ? (openBlock(), createBlock(BaseButton_default, {
|
|
233
|
+
key: 0,
|
|
234
|
+
variant: "ghost",
|
|
235
|
+
class: "shrink-0 px-5",
|
|
236
|
+
onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("back"))
|
|
237
|
+
}, {
|
|
238
|
+
default: withCtx(() => [createTextVNode(toDisplayString(__props.backLabel), 1)]),
|
|
239
|
+
_: 1
|
|
240
|
+
})) : createCommentVNode("", true), createVNode(BaseButton_default, {
|
|
241
|
+
class: "flex-1",
|
|
242
|
+
onClick: _cache[2] || (_cache[2] = ($event) => _ctx.$emit("next"))
|
|
243
|
+
}, {
|
|
244
|
+
default: withCtx(() => [createTextVNode(toDisplayString(isLast.value ? __props.lastLabel : __props.nextLabel), 1)]),
|
|
245
|
+
_: 1
|
|
246
|
+
})])])
|
|
247
|
+
])], 40, _hoisted_1)) : createCommentVNode("", true)]),
|
|
248
|
+
_: 3
|
|
249
|
+
})], 8, ["to"]);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}), [["__scopeId", "data-v-75292716"]]);
|
|
253
|
+
//#endregion
|
|
37
254
|
//#region src/app/use-tab-transition.ts
|
|
38
255
|
/**
|
|
39
256
|
* Which way a tabbed app is moving.
|
|
@@ -135,6 +352,6 @@ function useThemeSync(stored) {
|
|
|
135
352
|
}, { immediate: true });
|
|
136
353
|
}
|
|
137
354
|
//#endregion
|
|
138
|
-
export { AuthShell_default as AuthShell, createTabTransition, useThemeSync };
|
|
355
|
+
export { AuthShell_default as AuthShell, LocaleSheet_default as LocaleSheet, TourShell_default as TourShell, createTabTransition, useThemeSync };
|
|
139
356
|
|
|
140
357
|
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","names":["$slots"],"sources":["../src/app/AuthShell.vue","../src/app/AuthShell.vue","../src/app/use-tab-transition.ts","../src/app/use-theme-sync.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The frame every sign-in screen sits in.\n *\n * A brand mark, a narrow column, and the language links pinned to the bottom.\n * The two phone apps had this file character for character — thirty-seven\n * lines, no difference at all — and the only thing either would want to change\n * is what goes in the slots.\n *\n * The language links matter more than they look. Sign-in is the first screen a\n * new user sees and Settings is behind it, so without a way to switch here,\n * somebody who does not read the browser's language cannot get to one.\n */\ndefineSlots<{\n /** The brand mark. */\n brand?: () => unknown\n /** The form. */\n default: () => unknown\n /** The language links, or anything else that belongs at the foot. */\n foot?: () => unknown\n}>()\n</script>\n\n<template>\n <div\n class=\"flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10\"\n >\n <slot name=\"brand\" />\n\n <main class=\"w-full max-w-[22rem]\">\n <slot />\n </main>\n\n <div v-if=\"$slots.foot\" class=\"mt-auto\">\n <slot name=\"foot\" />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * The frame every sign-in screen sits in.\n *\n * A brand mark, a narrow column, and the language links pinned to the bottom.\n * The two phone apps had this file character for character — thirty-seven\n * lines, no difference at all — and the only thing either would want to change\n * is what goes in the slots.\n *\n * The language links matter more than they look. Sign-in is the first screen a\n * new user sees and Settings is behind it, so without a way to switch here,\n * somebody who does not read the browser's language cannot get to one.\n */\ndefineSlots<{\n /** The brand mark. */\n brand?: () => unknown\n /** The form. */\n default: () => unknown\n /** The language links, or anything else that belongs at the foot. */\n foot?: () => unknown\n}>()\n</script>\n\n<template>\n <div\n class=\"flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10\"\n >\n <slot name=\"brand\" />\n\n <main class=\"w-full max-w-[22rem]\">\n <slot />\n </main>\n\n <div v-if=\"$slots.foot\" class=\"mt-auto\">\n <slot name=\"foot\" />\n </div>\n </div>\n</template>\n","import { readonly, ref } from 'vue'\n\n/** Which way the screens slide during a tab change. */\nexport type SlideDirection = 'forward' | 'backward' | 'none'\n\n/**\n * Which way a tabbed app is moving.\n *\n * A phone app slides sideways between its tabs, and the direction has to come\n * from somewhere: going from the second tab to the fourth is forward, the\n * other way is back, and arriving from nowhere is neither. That is index\n * arithmetic over the tab order, and it was written twice, identically, in the\n * two phone apps this kit came from — thirty-four lines each, byte for byte\n * the same.\n *\n * Generic over the tab key, so the app keeps its own union and the kit never\n * learns what a tab is called.\n *\n * @example\n * ```ts\n * // shared/lib/tabs.ts\n * export const tabs = createTabTransition(['today', 'week', 'year', 'profile'] as const)\n *\n * // the router guard\n * router.afterEach((to, from) => tabs.resolve(to.meta.tab, from.meta.tab))\n *\n * // App.vue\n * const name = computed(() =>\n * tabs.direction.value === 'none' ? '' : `slide-${tabs.direction.value}`,\n * )\n * ```\n *\n * The `slide-forward-*` and `slide-backward-*` classes those names refer to\n * ship in `rei-kit/shell/mobile.css`.\n */\nexport function createTabTransition<K extends string>(order: readonly K[]) {\n const direction = ref<SlideDirection>('none')\n let override: SlideDirection | null = null\n\n return {\n /** Direction of the current tab change. Read by the route transition. */\n direction: readonly(direction),\n\n /**\n * Resolves the direction for a navigation. Call once per route change.\n *\n * @param to - Tab being entered, if the route has one.\n * @param from - Tab being left, if the route had one.\n */\n resolve(to: K | undefined, from: K | undefined): void {\n if (override) {\n direction.value = override\n override = null\n\n return\n }\n\n if (!to || !from || to === from) {\n direction.value = 'none'\n\n return\n }\n\n direction.value = order.indexOf(to) > order.indexOf(from) ? 'forward' : 'backward'\n },\n\n /**\n * Forces the next navigation's direction, whatever the indices say.\n *\n * For the navigations that are not a tab change at heart: going back from\n * a detail screen, or being sent to sign-in. Without it, leaving a detail\n * page under the fourth tab for the first tab slides backward, which is\n * right, and arriving there slides forward, which is not.\n */\n force(next: SlideDirection): void {\n override = next\n },\n }\n}\n","import { watch } from 'vue'\nimport type { Ref } from 'vue'\n\nimport { isThemePreference, useTheme } from '../composables/use-theme'\n\n/**\n * Adopts the theme stored on the account, once, as soon as it arrives.\n *\n * Two things make this worth a component rather than four lines at a call\n * site, and both are about *once*.\n *\n * It has to run at the app root rather than on the settings screen, or a user\n * on a fresh device keeps the system theme until they happen to open Profile.\n * And it has to run once and never again, or a later refetch of the profile\n * undoes a choice the user has just made locally — the theme flips back under\n * them a second after they set it, which reads as the app fighting them.\n *\n * The source is a ref rather than a query, so the kit never learns what a\n * profile is or where it came from.\n *\n * @example\n * ```ts\n * const { data: profile } = useProfile()\n * useThemeSync(computed(() => profile.value?.theme))\n * ```\n */\nexport function useThemeSync(stored: Ref<string | null | undefined>): void {\n const theme = useTheme()\n\n let adopted = false\n\n watch(\n stored,\n (next) => {\n if (adopted || next === null || next === undefined) return\n\n adopted = true\n\n if (isThemePreference(next) && next !== theme.value) {\n theme.value = next\n }\n },\n { immediate: true },\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;GAwBE,OAAA,UAAA,GAAA,mBAYM,OAZN,YAYM;IATJ,WAAqB,KAAA,QAAA,OAAA;IAErB,mBAEO,QAFP,YAEO,CADL,WAAQ,KAAA,QAAA,SAAA,CAAA,CAAA;IAGCA,KAAAA,OAAO,QAAlB,UAAA,GAAA,mBAEM,OAFN,YAEM,CADJ,WAAoB,KAAA,QAAA,MAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AEC1B,SAAgB,oBAAsC,OAAqB;CACzE,MAAM,YAAY,IAAoB,MAAM;CAC5C,IAAI,WAAkC;CAEtC,OAAO;;EAEL,WAAW,SAAS,SAAS;;;;;;;EAQ7B,QAAQ,IAAmB,MAA2B;GACpD,IAAI,UAAU;IACZ,UAAU,QAAQ;IAClB,WAAW;IAEX;GACF;GAEA,IAAI,CAAC,MAAM,CAAC,QAAQ,OAAO,MAAM;IAC/B,UAAU,QAAQ;IAElB;GACF;GAEA,UAAU,QAAQ,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,IAAI,IAAI,YAAY;EAC1E;;;;;;;;;EAUA,MAAM,MAA4B;GAChC,WAAW;EACb;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;ACpDA,SAAgB,aAAa,QAA8C;CACzE,MAAM,QAAQ,SAAS;CAEvB,IAAI,UAAU;CAEd,MACE,SACC,SAAS;EACR,IAAI,WAAW,SAAS,QAAQ,SAAS,KAAA,GAAW;EAEpD,UAAU;EAEV,IAAI,kBAAkB,IAAI,KAAK,SAAS,MAAM,OAC5C,MAAM,QAAQ;CAElB,GACA,EAAE,WAAW,KAAK,CACpB;AACF"}
|
|
1
|
+
{"version":3,"file":"app.js","names":["$slots","$emit"],"sources":["../src/app/AuthShell.vue","../src/app/AuthShell.vue","../src/app/LocaleSheet.vue","../src/app/LocaleSheet.vue","../src/app/TourShell.vue","../src/app/TourShell.vue","../src/app/use-tab-transition.ts","../src/app/use-theme-sync.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The frame every sign-in screen sits in.\n *\n * A brand mark, a narrow column, and the language links pinned to the bottom.\n * The two phone apps had this file character for character — thirty-seven\n * lines, no difference at all — and the only thing either would want to change\n * is what goes in the slots.\n *\n * The language links matter more than they look. Sign-in is the first screen a\n * new user sees and Settings is behind it, so without a way to switch here,\n * somebody who does not read the browser's language cannot get to one.\n */\ndefineSlots<{\n /** The brand mark. */\n brand?: () => unknown\n /** The form. */\n default: () => unknown\n /** The language links, or anything else that belongs at the foot. */\n foot?: () => unknown\n}>()\n</script>\n\n<template>\n <div\n class=\"flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10\"\n >\n <slot name=\"brand\" />\n\n <main class=\"w-full max-w-[22rem]\">\n <slot />\n </main>\n\n <div v-if=\"$slots.foot\" class=\"mt-auto\">\n <slot name=\"foot\" />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * The frame every sign-in screen sits in.\n *\n * A brand mark, a narrow column, and the language links pinned to the bottom.\n * The two phone apps had this file character for character — thirty-seven\n * lines, no difference at all — and the only thing either would want to change\n * is what goes in the slots.\n *\n * The language links matter more than they look. Sign-in is the first screen a\n * new user sees and Settings is behind it, so without a way to switch here,\n * somebody who does not read the browser's language cannot get to one.\n */\ndefineSlots<{\n /** The brand mark. */\n brand?: () => unknown\n /** The form. */\n default: () => unknown\n /** The language links, or anything else that belongs at the foot. */\n foot?: () => unknown\n}>()\n</script>\n\n<template>\n <div\n class=\"flex min-h-0 w-full flex-1 flex-col items-center gap-8 overflow-y-auto px-6 pt-10 pb-10\"\n >\n <slot name=\"brand\" />\n\n <main class=\"w-full max-w-[22rem]\">\n <slot />\n </main>\n\n <div v-if=\"$slots.foot\" class=\"mt-auto\">\n <slot name=\"foot\" />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Check, Languages } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport BaseSheet from '../components/BaseSheet.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\n\n/**\n * Choosing the interface language, from a settings row.\n *\n * A sheet rather than a segmented control: past four options a row of pills\n * stops being readable, and the list of languages only grows.\n *\n * Both phone apps had this, 96% identical. What differed was one colour class.\n *\n * The labels are the caller's, and they should be **endonyms** — a language is\n * always listed in its own language, so someone who cannot read the current\n * interface can still find theirs. The kit cannot know them.\n */\nconst {\n label,\n hint = '',\n systemLabel,\n options,\n closeLabel = '',\n} = defineProps<{\n /** The settings row's label, and the sheet's title. */\n label: string\n hint?: string | undefined\n /** What \"follow the device\" is called. Its value is `system`. */\n systemLabel: string\n /** In the order they should be listed. Labels are endonyms. */\n options: readonly { value: string; label: string }[]\n closeLabel?: string | undefined\n}>()\n\nconst model = defineModel<string>({ required: true })\n\nconst open = ref(false)\n\nconst rows = computed(() => [{ value: 'system', label: systemLabel }, ...options])\n\nconst current = computed(\n () => rows.value.find((row) => row.value === model.value)?.label ?? systemLabel,\n)\n\nfunction select(value: string) {\n model.value = value\n open.value = false\n}\n</script>\n\n<template>\n <SettingsRow\n :label=\"label\"\n :description=\"hint\"\n :icon=\"Languages\"\n interactive\n @click=\"open = true\"\n >\n <span class=\"text-ink-soft text-sm\">{{ current }}</span>\n </SettingsRow>\n\n <BaseSheet v-model=\"open\" :title=\"label\" :subtitle=\"hint\" :close-label=\"closeLabel\">\n <ul class=\"flex flex-col\">\n <li v-for=\"row in rows\" :key=\"row.value\">\n <BaseButton\n variant=\"row\"\n class=\"rounded-xl\"\n :pressed=\"model === row.value\"\n @click=\"select(row.value)\"\n >\n <span class=\"flex-1 text-base\">{{ row.label }}</span>\n <Check\n v-if=\"model === row.value\"\n class=\"text-primary size-5 shrink-0\"\n aria-hidden=\"true\"\n />\n </BaseButton>\n </li>\n </ul>\n </BaseSheet>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Check, Languages } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport BaseSheet from '../components/BaseSheet.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\n\n/**\n * Choosing the interface language, from a settings row.\n *\n * A sheet rather than a segmented control: past four options a row of pills\n * stops being readable, and the list of languages only grows.\n *\n * Both phone apps had this, 96% identical. What differed was one colour class.\n *\n * The labels are the caller's, and they should be **endonyms** — a language is\n * always listed in its own language, so someone who cannot read the current\n * interface can still find theirs. The kit cannot know them.\n */\nconst {\n label,\n hint = '',\n systemLabel,\n options,\n closeLabel = '',\n} = defineProps<{\n /** The settings row's label, and the sheet's title. */\n label: string\n hint?: string | undefined\n /** What \"follow the device\" is called. Its value is `system`. */\n systemLabel: string\n /** In the order they should be listed. Labels are endonyms. */\n options: readonly { value: string; label: string }[]\n closeLabel?: string | undefined\n}>()\n\nconst model = defineModel<string>({ required: true })\n\nconst open = ref(false)\n\nconst rows = computed(() => [{ value: 'system', label: systemLabel }, ...options])\n\nconst current = computed(\n () => rows.value.find((row) => row.value === model.value)?.label ?? systemLabel,\n)\n\nfunction select(value: string) {\n model.value = value\n open.value = false\n}\n</script>\n\n<template>\n <SettingsRow\n :label=\"label\"\n :description=\"hint\"\n :icon=\"Languages\"\n interactive\n @click=\"open = true\"\n >\n <span class=\"text-ink-soft text-sm\">{{ current }}</span>\n </SettingsRow>\n\n <BaseSheet v-model=\"open\" :title=\"label\" :subtitle=\"hint\" :close-label=\"closeLabel\">\n <ul class=\"flex flex-col\">\n <li v-for=\"row in rows\" :key=\"row.value\">\n <BaseButton\n variant=\"row\"\n class=\"rounded-xl\"\n :pressed=\"model === row.value\"\n @click=\"select(row.value)\"\n >\n <span class=\"flex-1 text-base\">{{ row.label }}</span>\n <Check\n v-if=\"model === row.value\"\n class=\"text-primary size-5 shrink-0\"\n aria-hidden=\"true\"\n />\n </BaseButton>\n </li>\n </ul>\n </BaseSheet>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The frame an onboarding guide runs inside.\n *\n * Both phone apps had this, 296 lines, 94% identical — and what differed was\n * every part that should: the slides, the wash colours, the illustrations. What\n * did not differ is here.\n *\n * The parts that are easy to get wrong and were written twice:\n *\n * - **`inert` on the app behind it.** Without it, Tab walks into a screen the\n * reader cannot see. The same trick `BaseSheet` uses, and it has to be undone\n * on unmount or the whole app stays inert forever.\n * - **Focusing the dialog**, which is the only reason the arrow keys work.\n * - **The direction.** A guide that can jump to slide two from slide seven has\n * to animate backwards, so the transition name follows the index rather than\n * the button that was pressed.\n * - **A segmented track, not dots.** Ten slides is a sequence with a length,\n * and the reader deserves to see how much is left.\n *\n * Every string is a prop and the slide is a slot: the kit renders the frame and\n * knows nothing about what is being explained.\n */\nconst {\n index,\n total,\n dialogLabel,\n skipLabel,\n backLabel,\n nextLabel,\n lastLabel,\n stepLabel,\n teleportTo = 'body',\n} = defineProps<{\n /** Which slide, zero-based. */\n index: number\n total: number\n /** The dialog's accessible name. */\n dialogLabel: string\n skipLabel: string\n backLabel: string\n nextLabel: string\n /** The button on the final slide — \"Start\", rather than \"Next\". */\n lastLabel: string\n /** Names one segment for a screen reader, e.g. `(n) => \\`Step ${n} of ${total}\\``. */\n stepLabel: (position: number) => string\n /**\n * Where the dialog goes. A phone shell clips its children, so the guide has\n * to leave the tree to cover the tab bar and the header alike.\n */\n teleportTo?: string | undefined\n}>()\n\nconst open = defineModel<boolean>({ required: true })\n\nconst emit = defineEmits<{ next: []; back: []; dismiss: []; goTo: [position: number] }>()\n\nconst dialog = ref<HTMLElement | null>(null)\n\n/** Direction, so a jump backwards still animates backwards. */\nconst transitionName = ref('tour-forward')\n\nwatch(\n () => index,\n (next, previous) => {\n transitionName.value = next >= previous ? 'tour-forward' : 'tour-backward'\n },\n)\n\nconst isLast = computed(() => index >= total - 1)\n\n/* `immediate`, because a layout that mounts this only once the guide has been\n asked for would otherwise have the first `true` predate the watcher. */\nwatch(\n open,\n async (isOpen) => {\n if (typeof document === 'undefined') return\n\n document.getElementById('app')?.toggleAttribute('inert', isOpen)\n if (!isOpen) return\n\n await nextTick()\n dialog.value?.focus()\n },\n { immediate: true },\n)\n\nonUnmounted(() => {\n if (typeof document !== 'undefined') document.getElementById('app')?.removeAttribute('inert')\n})\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowRight') emit('next')\n if (event.key === 'ArrowLeft') emit('back')\n if (event.key === 'Escape') emit('dismiss')\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTo\">\n <Transition name=\"tour\" appear>\n <div\n v-if=\"open\"\n ref=\"dialog\"\n class=\"fixed inset-0 z-[60] flex items-center justify-center outline-none\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"dialogLabel\"\n tabindex=\"-1\"\n @keydown=\"onKeydown\"\n >\n <div class=\"shell-frame md:rounded-shell bg-canvas relative flex flex-col overflow-hidden\">\n <!-- Behind everything: the app's mood for this slide. -->\n <slot name=\"wash\" />\n\n <header class=\"relative flex shrink-0 items-center justify-between gap-3 px-6 pt-6\">\n <span class=\"text-ink-soft text-xs font-semibold tabular-nums\">\n {{ stepLabel(index + 1) }}\n </span>\n\n <BaseButton pill size=\"sm\" variant=\"quiet\" @click=\"$emit('dismiss')\">\n {{ skipLabel }}\n </BaseButton>\n </header>\n\n <!-- min-h-0 keeps the body inside the shell, so a long slide scrolls\n here rather than pushing the buttons off the bottom. -->\n <div\n class=\"relative flex min-h-0 flex-1 flex-col justify-center overflow-y-auto px-6 py-6\"\n >\n <Transition :name=\"transitionName\" mode=\"out-in\">\n <slot :index=\"index\" />\n </Transition>\n </div>\n\n <footer\n class=\"relative flex shrink-0 flex-col gap-4 px-6 pt-4 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <!-- A segmented track rather than dots: ten slides is a sequence\n with a length, and the reader deserves to see how much is left. -->\n <div\n class=\"-my-2 flex items-center gap-1\"\n role=\"tablist\"\n :aria-label=\"stepLabel(index + 1)\"\n >\n <BaseButton\n v-for=\"position in total\"\n :key=\"position\"\n variant=\"unstyled\"\n role=\"tab\"\n :aria-selected=\"position - 1 === index\"\n :aria-label=\"stepLabel(position)\"\n class=\"group flex flex-1 items-center py-2.5\"\n @click=\"$emit('goTo', position - 1)\"\n >\n <span\n class=\"h-1 w-full rounded-full transition-colors duration-300\"\n :class=\"\n position - 1 <= index ? 'bg-primary' : 'bg-hair group-hover:bg-ink-soft/40'\n \"\n />\n </BaseButton>\n </div>\n\n <div class=\"flex gap-2\">\n <BaseButton\n v-if=\"index > 0\"\n variant=\"ghost\"\n class=\"shrink-0 px-5\"\n @click=\"$emit('back')\"\n >\n {{ backLabel }}\n </BaseButton>\n\n <BaseButton class=\"flex-1\" @click=\"$emit('next')\">\n {{ isLast ? lastLabel : nextLabel }}\n </BaseButton>\n </div>\n </footer>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.tour-enter-active,\n.tour-leave-active {\n transition: opacity 220ms ease;\n}\n\n.tour-enter-from,\n.tour-leave-to {\n opacity: 0;\n}\n\n.tour-forward-enter-active,\n.tour-backward-enter-active {\n transition:\n opacity 260ms ease-out,\n transform 260ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.tour-forward-leave-active,\n.tour-backward-leave-active {\n transition:\n opacity 130ms ease-in,\n transform 130ms ease-in;\n}\n\n.tour-forward-enter-from {\n opacity: 0;\n transform: translateX(1.25rem);\n}\n\n.tour-forward-leave-to {\n opacity: 0;\n transform: translateX(-0.75rem);\n}\n\n.tour-backward-enter-from {\n opacity: 0;\n transform: translateX(-1.25rem);\n}\n\n.tour-backward-leave-to {\n opacity: 0;\n transform: translateX(0.75rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tour-forward-enter-from,\n .tour-forward-leave-to,\n .tour-backward-enter-from,\n .tour-backward-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The frame an onboarding guide runs inside.\n *\n * Both phone apps had this, 296 lines, 94% identical — and what differed was\n * every part that should: the slides, the wash colours, the illustrations. What\n * did not differ is here.\n *\n * The parts that are easy to get wrong and were written twice:\n *\n * - **`inert` on the app behind it.** Without it, Tab walks into a screen the\n * reader cannot see. The same trick `BaseSheet` uses, and it has to be undone\n * on unmount or the whole app stays inert forever.\n * - **Focusing the dialog**, which is the only reason the arrow keys work.\n * - **The direction.** A guide that can jump to slide two from slide seven has\n * to animate backwards, so the transition name follows the index rather than\n * the button that was pressed.\n * - **A segmented track, not dots.** Ten slides is a sequence with a length,\n * and the reader deserves to see how much is left.\n *\n * Every string is a prop and the slide is a slot: the kit renders the frame and\n * knows nothing about what is being explained.\n */\nconst {\n index,\n total,\n dialogLabel,\n skipLabel,\n backLabel,\n nextLabel,\n lastLabel,\n stepLabel,\n teleportTo = 'body',\n} = defineProps<{\n /** Which slide, zero-based. */\n index: number\n total: number\n /** The dialog's accessible name. */\n dialogLabel: string\n skipLabel: string\n backLabel: string\n nextLabel: string\n /** The button on the final slide — \"Start\", rather than \"Next\". */\n lastLabel: string\n /** Names one segment for a screen reader, e.g. `(n) => \\`Step ${n} of ${total}\\``. */\n stepLabel: (position: number) => string\n /**\n * Where the dialog goes. A phone shell clips its children, so the guide has\n * to leave the tree to cover the tab bar and the header alike.\n */\n teleportTo?: string | undefined\n}>()\n\nconst open = defineModel<boolean>({ required: true })\n\nconst emit = defineEmits<{ next: []; back: []; dismiss: []; goTo: [position: number] }>()\n\nconst dialog = ref<HTMLElement | null>(null)\n\n/** Direction, so a jump backwards still animates backwards. */\nconst transitionName = ref('tour-forward')\n\nwatch(\n () => index,\n (next, previous) => {\n transitionName.value = next >= previous ? 'tour-forward' : 'tour-backward'\n },\n)\n\nconst isLast = computed(() => index >= total - 1)\n\n/* `immediate`, because a layout that mounts this only once the guide has been\n asked for would otherwise have the first `true` predate the watcher. */\nwatch(\n open,\n async (isOpen) => {\n if (typeof document === 'undefined') return\n\n document.getElementById('app')?.toggleAttribute('inert', isOpen)\n if (!isOpen) return\n\n await nextTick()\n dialog.value?.focus()\n },\n { immediate: true },\n)\n\nonUnmounted(() => {\n if (typeof document !== 'undefined') document.getElementById('app')?.removeAttribute('inert')\n})\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowRight') emit('next')\n if (event.key === 'ArrowLeft') emit('back')\n if (event.key === 'Escape') emit('dismiss')\n}\n</script>\n\n<template>\n <Teleport :to=\"teleportTo\">\n <Transition name=\"tour\" appear>\n <div\n v-if=\"open\"\n ref=\"dialog\"\n class=\"fixed inset-0 z-[60] flex items-center justify-center outline-none\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"dialogLabel\"\n tabindex=\"-1\"\n @keydown=\"onKeydown\"\n >\n <div class=\"shell-frame md:rounded-shell bg-canvas relative flex flex-col overflow-hidden\">\n <!-- Behind everything: the app's mood for this slide. -->\n <slot name=\"wash\" />\n\n <header class=\"relative flex shrink-0 items-center justify-between gap-3 px-6 pt-6\">\n <span class=\"text-ink-soft text-xs font-semibold tabular-nums\">\n {{ stepLabel(index + 1) }}\n </span>\n\n <BaseButton pill size=\"sm\" variant=\"quiet\" @click=\"$emit('dismiss')\">\n {{ skipLabel }}\n </BaseButton>\n </header>\n\n <!-- min-h-0 keeps the body inside the shell, so a long slide scrolls\n here rather than pushing the buttons off the bottom. -->\n <div\n class=\"relative flex min-h-0 flex-1 flex-col justify-center overflow-y-auto px-6 py-6\"\n >\n <Transition :name=\"transitionName\" mode=\"out-in\">\n <slot :index=\"index\" />\n </Transition>\n </div>\n\n <footer\n class=\"relative flex shrink-0 flex-col gap-4 px-6 pt-4 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <!-- A segmented track rather than dots: ten slides is a sequence\n with a length, and the reader deserves to see how much is left. -->\n <div\n class=\"-my-2 flex items-center gap-1\"\n role=\"tablist\"\n :aria-label=\"stepLabel(index + 1)\"\n >\n <BaseButton\n v-for=\"position in total\"\n :key=\"position\"\n variant=\"unstyled\"\n role=\"tab\"\n :aria-selected=\"position - 1 === index\"\n :aria-label=\"stepLabel(position)\"\n class=\"group flex flex-1 items-center py-2.5\"\n @click=\"$emit('goTo', position - 1)\"\n >\n <span\n class=\"h-1 w-full rounded-full transition-colors duration-300\"\n :class=\"\n position - 1 <= index ? 'bg-primary' : 'bg-hair group-hover:bg-ink-soft/40'\n \"\n />\n </BaseButton>\n </div>\n\n <div class=\"flex gap-2\">\n <BaseButton\n v-if=\"index > 0\"\n variant=\"ghost\"\n class=\"shrink-0 px-5\"\n @click=\"$emit('back')\"\n >\n {{ backLabel }}\n </BaseButton>\n\n <BaseButton class=\"flex-1\" @click=\"$emit('next')\">\n {{ isLast ? lastLabel : nextLabel }}\n </BaseButton>\n </div>\n </footer>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.tour-enter-active,\n.tour-leave-active {\n transition: opacity 220ms ease;\n}\n\n.tour-enter-from,\n.tour-leave-to {\n opacity: 0;\n}\n\n.tour-forward-enter-active,\n.tour-backward-enter-active {\n transition:\n opacity 260ms ease-out,\n transform 260ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.tour-forward-leave-active,\n.tour-backward-leave-active {\n transition:\n opacity 130ms ease-in,\n transform 130ms ease-in;\n}\n\n.tour-forward-enter-from {\n opacity: 0;\n transform: translateX(1.25rem);\n}\n\n.tour-forward-leave-to {\n opacity: 0;\n transform: translateX(-0.75rem);\n}\n\n.tour-backward-enter-from {\n opacity: 0;\n transform: translateX(-1.25rem);\n}\n\n.tour-backward-leave-to {\n opacity: 0;\n transform: translateX(0.75rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tour-forward-enter-from,\n .tour-forward-leave-to,\n .tour-backward-enter-from,\n .tour-backward-leave-to {\n transform: none;\n }\n}\n</style>\n","import { readonly, ref } from 'vue'\n\n/** Which way the screens slide during a tab change. */\nexport type SlideDirection = 'forward' | 'backward' | 'none'\n\n/**\n * Which way a tabbed app is moving.\n *\n * A phone app slides sideways between its tabs, and the direction has to come\n * from somewhere: going from the second tab to the fourth is forward, the\n * other way is back, and arriving from nowhere is neither. That is index\n * arithmetic over the tab order, and it was written twice, identically, in the\n * two phone apps this kit came from — thirty-four lines each, byte for byte\n * the same.\n *\n * Generic over the tab key, so the app keeps its own union and the kit never\n * learns what a tab is called.\n *\n * @example\n * ```ts\n * // shared/lib/tabs.ts\n * export const tabs = createTabTransition(['today', 'week', 'year', 'profile'] as const)\n *\n * // the router guard\n * router.afterEach((to, from) => tabs.resolve(to.meta.tab, from.meta.tab))\n *\n * // App.vue\n * const name = computed(() =>\n * tabs.direction.value === 'none' ? '' : `slide-${tabs.direction.value}`,\n * )\n * ```\n *\n * The `slide-forward-*` and `slide-backward-*` classes those names refer to\n * ship in `rei-kit/shell/mobile.css`.\n */\nexport function createTabTransition<K extends string>(order: readonly K[]) {\n const direction = ref<SlideDirection>('none')\n let override: SlideDirection | null = null\n\n return {\n /** Direction of the current tab change. Read by the route transition. */\n direction: readonly(direction),\n\n /**\n * Resolves the direction for a navigation. Call once per route change.\n *\n * @param to - Tab being entered, if the route has one.\n * @param from - Tab being left, if the route had one.\n */\n resolve(to: K | undefined, from: K | undefined): void {\n if (override) {\n direction.value = override\n override = null\n\n return\n }\n\n if (!to || !from || to === from) {\n direction.value = 'none'\n\n return\n }\n\n direction.value = order.indexOf(to) > order.indexOf(from) ? 'forward' : 'backward'\n },\n\n /**\n * Forces the next navigation's direction, whatever the indices say.\n *\n * For the navigations that are not a tab change at heart: going back from\n * a detail screen, or being sent to sign-in. Without it, leaving a detail\n * page under the fourth tab for the first tab slides backward, which is\n * right, and arriving there slides forward, which is not.\n */\n force(next: SlideDirection): void {\n override = next\n },\n }\n}\n","import { watch } from 'vue'\nimport type { Ref } from 'vue'\n\nimport { isThemePreference, useTheme } from '../composables/use-theme'\n\n/**\n * Adopts the theme stored on the account, once, as soon as it arrives.\n *\n * Two things make this worth a component rather than four lines at a call\n * site, and both are about *once*.\n *\n * It has to run at the app root rather than on the settings screen, or a user\n * on a fresh device keeps the system theme until they happen to open Profile.\n * And it has to run once and never again, or a later refetch of the profile\n * undoes a choice the user has just made locally — the theme flips back under\n * them a second after they set it, which reads as the app fighting them.\n *\n * The source is a ref rather than a query, so the kit never learns what a\n * profile is or where it came from.\n *\n * @example\n * ```ts\n * const { data: profile } = useProfile()\n * useThemeSync(computed(() => profile.value?.theme))\n * ```\n */\nexport function useThemeSync(stored: Ref<string | null | undefined>): void {\n const theme = useTheme()\n\n let adopted = false\n\n watch(\n stored,\n (next) => {\n if (adopted || next === null || next === undefined) return\n\n adopted = true\n\n if (isThemePreference(next) && next !== theme.value) {\n theme.value = next\n }\n },\n { immediate: true },\n )\n}\n"],"mappings":"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"}
|