rei-kit 0.12.1 → 0.13.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/app.js ADDED
@@ -0,0 +1,140 @@
1
+ import { a as useTheme, n as isThemePreference } from "./use-theme-_MnaDD5v.js";
2
+ import { createCommentVNode, createElementBlock, createElementVNode, defineComponent, openBlock, readonly, ref, renderSlot, watch } from "vue";
3
+ //#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 = {
7
+ key: 0,
8
+ class: "mt-auto"
9
+ };
10
+ //#endregion
11
+ //#region src/app/AuthShell.vue
12
+ var AuthShell_default = /* @__PURE__ */ defineComponent({
13
+ __name: "AuthShell",
14
+ setup(__props) {
15
+ /**
16
+ * The frame every sign-in screen sits in.
17
+ *
18
+ * A brand mark, a narrow column, and the language links pinned to the bottom.
19
+ * The two phone apps had this file character for character — thirty-seven
20
+ * lines, no difference at all — and the only thing either would want to change
21
+ * is what goes in the slots.
22
+ *
23
+ * The language links matter more than they look. Sign-in is the first screen a
24
+ * new user sees and Settings is behind it, so without a way to switch here,
25
+ * somebody who does not read the browser's language cannot get to one.
26
+ */
27
+ return (_ctx, _cache) => {
28
+ return openBlock(), createElementBlock("div", _hoisted_1, [
29
+ 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
+ ]);
33
+ };
34
+ }
35
+ });
36
+ //#endregion
37
+ //#region src/app/use-tab-transition.ts
38
+ /**
39
+ * Which way a tabbed app is moving.
40
+ *
41
+ * A phone app slides sideways between its tabs, and the direction has to come
42
+ * from somewhere: going from the second tab to the fourth is forward, the
43
+ * other way is back, and arriving from nowhere is neither. That is index
44
+ * arithmetic over the tab order, and it was written twice, identically, in the
45
+ * two phone apps this kit came from — thirty-four lines each, byte for byte
46
+ * the same.
47
+ *
48
+ * Generic over the tab key, so the app keeps its own union and the kit never
49
+ * learns what a tab is called.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * // shared/lib/tabs.ts
54
+ * export const tabs = createTabTransition(['today', 'week', 'year', 'profile'] as const)
55
+ *
56
+ * // the router guard
57
+ * router.afterEach((to, from) => tabs.resolve(to.meta.tab, from.meta.tab))
58
+ *
59
+ * // App.vue
60
+ * const name = computed(() =>
61
+ * tabs.direction.value === 'none' ? '' : `slide-${tabs.direction.value}`,
62
+ * )
63
+ * ```
64
+ *
65
+ * The `slide-forward-*` and `slide-backward-*` classes those names refer to
66
+ * ship in `rei-kit/shell/mobile.css`.
67
+ */
68
+ function createTabTransition(order) {
69
+ const direction = ref("none");
70
+ let override = null;
71
+ return {
72
+ /** Direction of the current tab change. Read by the route transition. */
73
+ direction: readonly(direction),
74
+ /**
75
+ * Resolves the direction for a navigation. Call once per route change.
76
+ *
77
+ * @param to - Tab being entered, if the route has one.
78
+ * @param from - Tab being left, if the route had one.
79
+ */
80
+ resolve(to, from) {
81
+ if (override) {
82
+ direction.value = override;
83
+ override = null;
84
+ return;
85
+ }
86
+ if (!to || !from || to === from) {
87
+ direction.value = "none";
88
+ return;
89
+ }
90
+ direction.value = order.indexOf(to) > order.indexOf(from) ? "forward" : "backward";
91
+ },
92
+ /**
93
+ * Forces the next navigation's direction, whatever the indices say.
94
+ *
95
+ * For the navigations that are not a tab change at heart: going back from
96
+ * a detail screen, or being sent to sign-in. Without it, leaving a detail
97
+ * page under the fourth tab for the first tab slides backward, which is
98
+ * right, and arriving there slides forward, which is not.
99
+ */
100
+ force(next) {
101
+ override = next;
102
+ }
103
+ };
104
+ }
105
+ //#endregion
106
+ //#region src/app/use-theme-sync.ts
107
+ /**
108
+ * Adopts the theme stored on the account, once, as soon as it arrives.
109
+ *
110
+ * Two things make this worth a component rather than four lines at a call
111
+ * site, and both are about *once*.
112
+ *
113
+ * It has to run at the app root rather than on the settings screen, or a user
114
+ * on a fresh device keeps the system theme until they happen to open Profile.
115
+ * And it has to run once and never again, or a later refetch of the profile
116
+ * undoes a choice the user has just made locally — the theme flips back under
117
+ * them a second after they set it, which reads as the app fighting them.
118
+ *
119
+ * The source is a ref rather than a query, so the kit never learns what a
120
+ * profile is or where it came from.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const { data: profile } = useProfile()
125
+ * useThemeSync(computed(() => profile.value?.theme))
126
+ * ```
127
+ */
128
+ function useThemeSync(stored) {
129
+ const theme = useTheme();
130
+ let adopted = false;
131
+ watch(stored, (next) => {
132
+ if (adopted || next === null || next === void 0) return;
133
+ adopted = true;
134
+ if (isThemePreference(next) && next !== theme.value) theme.value = next;
135
+ }, { immediate: true });
136
+ }
137
+ //#endregion
138
+ export { AuthShell_default as AuthShell, createTabTransition, useThemeSync };
139
+
140
+ //# sourceMappingURL=app.js.map
@@ -0,0 +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"}
package/dist/index.js CHANGED
@@ -1,158 +1,10 @@
1
+ import { a as needsIosInstall, c as fromDateKey, d as startOfWeek, f as toDateKey, i as isInstalled, l as lastNDays, n as BaseButton_default, o as addDays, p as todayKey, r as isApplePortable, s as eachDayOfYear, t as _plugin_vue_export_helper_default, u as leadingBlanks } from "./_plugin-vue_export-helper-3AcMDTtW.js";
1
2
  import { n as registerErrorMapper, r as toAppError, t as AppError } from "./app-error-DF9cijE0.js";
3
+ import { a as useTheme, i as setThemeStorageKey, n as isThemePreference, r as readStoredTheme, t as applyTheme } from "./use-theme-_MnaDD5v.js";
2
4
  import { Fragment, Teleport, Transition, TransitionGroup, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createStaticVNode, createTextVNode, createVNode, defineComponent, mergeModels, mergeProps, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onErrorCaptured, onMounted, onScopeDispose, onUnmounted, openBlock, readonly, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useId, useModel, vModelCheckbox, vModelDynamic, vModelRadio, vModelSelect, vModelText, watch, watchEffect, withCtx, withDirectives } from "vue";
3
5
  import { ArrowDown, ArrowRight, ArrowUp, CheckCircle2, ChevronDown, ChevronRight, Info, TriangleAlert, X, XCircle } from "lucide-vue-next";
4
6
  import { RouterLink } from "vue-router";
5
7
  import { createI18n } from "vue-i18n";
6
- //#region src/utils/date.ts
7
- /**
8
- * Local calendar-day helpers.
9
- *
10
- * Every function is pure and works on `YYYY-MM-DD` keys, the same shape as the
11
- * `date` columns in Postgres. Nothing here calls `toISOString`: that converts to
12
- * UTC, so in a UTC+9 timezone every entry made between midnight and 09:00 would
13
- * be written to the previous day.
14
- */
15
- /**
16
- * Formats a `Date` as a local `YYYY-MM-DD` key.
17
- *
18
- * @param date - Any `Date`; only its local year, month and day are read.
19
- * @returns The calendar day in the runtime's own timezone.
20
- *
21
- * @example
22
- * ```ts
23
- * // 2026-08-23 01:30 in Tokyo
24
- * toDateKey(new Date()) // '2026-08-23'
25
- * new Date().toISOString() // '2026-08-22T16:30…' ← the bug
26
- * ```
27
- */
28
- function toDateKey(date) {
29
- return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
30
- }
31
- /** Today's key in the user's own timezone. */
32
- function todayKey() {
33
- return toDateKey(/* @__PURE__ */ new Date());
34
- }
35
- /**
36
- * Parses a `YYYY-MM-DD` key into a `Date` at local midnight.
37
- *
38
- * @param key - A key produced by {@link toDateKey}.
39
- * @returns Local midnight of that calendar day.
40
- * @throws If the key is not three numeric parts.
41
- *
42
- * @example
43
- * ```ts
44
- * fromDateKey('2026-08-23') // local midnight, correct
45
- * new Date('2026-08-23') // UTC midnight — shifts a day in some zones
46
- * ```
47
- */
48
- function fromDateKey(key) {
49
- const [year, month, day] = key.split("-").map(Number);
50
- if (year === void 0 || month === void 0 || day === void 0) throw new Error(`Invalid date key: ${key}`);
51
- return new Date(year, month - 1, day);
52
- }
53
- /**
54
- * Shifts a date key by whole calendar days.
55
- *
56
- * Uses `setDate`, which is calendar-aware: it rolls over month and year ends,
57
- * and stays correct across daylight-saving transitions. Adding
58
- * `days * 86_400_000` milliseconds would not — a DST day is 23 or 25 hours long.
59
- *
60
- * @param key - Starting `YYYY-MM-DD` key.
61
- * @param days - Days to add; negative goes back.
62
- * @returns The resulting key.
63
- *
64
- * @example
65
- * ```ts
66
- * addDays('2026-01-31', 1) // '2026-02-01'
67
- * addDays('2026-01-01', -1) // '2025-12-31'
68
- * addDays('2028-02-28', 1) // '2028-02-29' — leap year
69
- * ```
70
- */
71
- function addDays(key, days) {
72
- const date = fromDateKey(key);
73
- date.setDate(date.getDate() + days);
74
- return toDateKey(date);
75
- }
76
- /**
77
- * The last `count` days ending today, oldest first.
78
- *
79
- * `today` is a parameter so the function stays pure and testable; call sites
80
- * normally omit it.
81
- *
82
- * @param count - How many days to return, including `today`.
83
- * @param today - End of the range. Defaults to the real today.
84
- * @returns Keys in ascending order.
85
- *
86
- * @example
87
- * ```ts
88
- * lastNDays(3, '2026-08-23') // ['2026-08-21', '2026-08-22', '2026-08-23']
89
- * ```
90
- */
91
- function lastNDays(count, today = todayKey()) {
92
- const keys = [];
93
- for (let offset = count - 1; offset >= 0; offset -= 1) keys.push(addDays(today, -offset));
94
- return keys;
95
- }
96
- /**
97
- * The first day of the week containing `key`.
98
- *
99
- * The user's preference is a parameter, not a module-level setting: changing it
100
- * in Profile has to re-render the week grid and the year heatmap immediately,
101
- * and a global would make that a hidden dependency.
102
- *
103
- * @param key - Any day in the week.
104
- * @param weekStartsOn - 0 for Sunday, 1 for Monday.
105
- * @returns Key of that week's first day.
106
- *
107
- * @example
108
- * ```ts
109
- * // 2026-08-23 is a Sunday
110
- * startOfWeek('2026-08-23', 1) // '2026-08-17' — previous Monday
111
- * startOfWeek('2026-08-23', 0) // '2026-08-23' — already Sunday
112
- * ```
113
- */
114
- function startOfWeek(key, weekStartsOn) {
115
- return addDays(key, -((fromDateKey(key).getDay() - weekStartsOn + 7) % 7));
116
- }
117
- /**
118
- * Every day of a calendar year, in order.
119
- *
120
- * Leap years fall out of the loop for free: it walks day by day until the year
121
- * rolls over, so February 29 is included when it exists.
122
- *
123
- * @param year - Four-digit year.
124
- * @returns 365 or 366 keys, oldest first.
125
- */
126
- function eachDayOfYear(year) {
127
- const keys = [];
128
- const date = new Date(year, 0, 1);
129
- while (date.getFullYear() === year) {
130
- keys.push(toDateKey(date));
131
- date.setDate(date.getDate() + 1);
132
- }
133
- return keys;
134
- }
135
- /**
136
- * Empty cells before a block's first day in a seven-row column grid.
137
- *
138
- * The grid fills column by column, so the first column is only partly used
139
- * unless the block starts exactly on the week's first day. An off-by-one here
140
- * shifts the whole block by a row, so this is unit tested.
141
- *
142
- * @param firstDayKey - First day of the block, e.g. `'2026-02-01'`.
143
- * @param weekStartsOn - 0 for Sunday, 1 for Monday.
144
- * @returns 0-6 blank cells.
145
- *
146
- * @example
147
- * ```ts
148
- * leadingBlanks('2026-01-01', 1) // 3 — a Thursday, Mon-Wed are blank
149
- * leadingBlanks('2024-01-01', 1) // 0 — a Monday
150
- * ```
151
- */
152
- function leadingBlanks(firstDayKey, weekStartsOn) {
153
- return (fromDateKey(firstDayKey).getDay() - weekStartsOn + 7) % 7;
154
- }
155
- //#endregion
156
8
  //#region src/utils/format.ts
157
9
  /**
158
10
  * The locale `Intl` formatting uses.
@@ -300,129 +152,6 @@ function tapFeedback(duration = 10) {
300
152
  navigator.vibrate?.(duration);
301
153
  }
302
154
  //#endregion
303
- //#region src/utils/platform.ts
304
- /**
305
- * Whether the app is running from the Home Screen rather than a browser tab.
306
- *
307
- * Two checks because iOS predates the standard one: `display-mode: standalone`
308
- * is the modern signal, `navigator.standalone` is Safari's own.
309
- */
310
- function isInstalled() {
311
- if (typeof window === "undefined") return false;
312
- return window.matchMedia("(display-mode: standalone)").matches || navigator.standalone === true;
313
- }
314
- /** iPhone and iPad, including iPadOS reporting itself as a Mac. */
315
- function isApplePortable() {
316
- if (typeof window === "undefined") return false;
317
- return /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
318
- }
319
- /**
320
- * Whether this device can only receive notifications once the app is installed.
321
- *
322
- * Safari on iOS grants notification permission to an installed web app and to
323
- * nothing else — in a normal tab the request does not even prompt. Telling the
324
- * user to allow notifications there is asking for something the browser will
325
- * not offer, so the UI has to say "add to Home Screen" instead.
326
- *
327
- * @example
328
- * ```ts
329
- * if (needsIosInstall()) // show the Home Screen instruction, not the button
330
- * ```
331
- */
332
- function needsIosInstall() {
333
- return isApplePortable() && !isInstalled();
334
- }
335
- //#endregion
336
- //#region src/composables/use-theme.ts
337
- /**
338
- * Namespaced by the app, not by this package.
339
- *
340
- * Two rei-kit apps served from the same origin would otherwise share one theme
341
- * setting — and during development on localhost, they will be.
342
- */
343
- var storageKey = "rei-theme";
344
- function isThemePreference(value) {
345
- return value === "system" || value === "light" || value === "dark";
346
- }
347
- /** Reads the stored preference, falling back to `system`. */
348
- function readStoredTheme() {
349
- try {
350
- const stored = localStorage.getItem(storageKey);
351
- return isThemePreference(stored) ? stored : "system";
352
- } catch {
353
- return "system";
354
- }
355
- }
356
- function storeTheme(preference) {
357
- try {
358
- localStorage.setItem(storageKey, preference);
359
- } catch {}
360
- }
361
- /**
362
- * Does the environment prefer a dark scheme?
363
- *
364
- * `matchMedia` is checked for on its own rather than inferred from `document`.
365
- * Having one does not imply having the other: jsdom supplies a document and no
366
- * `matchMedia`, so a consumer's component test that so much as mounts something
367
- * calling `useTheme` threw — and some embedded webviews are the same. Where
368
- * there is nothing to ask, the answer is no rather than an exception.
369
- */
370
- function prefersDarkScheme() {
371
- return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: dark)").matches : false;
372
- }
373
- /**
374
- * Adds or removes `.dark` on `<html>`, resolving `system` against the OS.
375
- *
376
- * A no-op without a document. There is no OS preference to read on a server and
377
- * no `<html>` to write to, so a prerender leaves the class off and the app
378
- * decides the theme before hydration — see the note in the README.
379
- */
380
- function applyTheme(preference) {
381
- if (typeof document === "undefined") return;
382
- const isDark = preference === "dark" || preference === "system" && prefersDarkScheme();
383
- document.documentElement.classList.toggle("dark", isDark);
384
- }
385
- /**
386
- * The shared preference, created on first use rather than at import.
387
- *
388
- * Lazy on purpose: reading storage at import time would lock in the default key
389
- * before an app had a chance to set its own, leaving the controller reading one
390
- * key and writing another.
391
- */
392
- var preference = null;
393
- function controller() {
394
- if (preference) return preference;
395
- preference = ref(readStoredTheme());
396
- watch(preference, (next) => {
397
- storeTheme(next);
398
- applyTheme(next);
399
- }, { immediate: true });
400
- if (typeof window !== "undefined" && typeof window.matchMedia === "function") window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
401
- if (preference?.value === "system") applyTheme("system");
402
- });
403
- return preference;
404
- }
405
- /**
406
- * Sets where the preference is stored.
407
- *
408
- * Safe in either order: called before the first `useTheme()` it simply changes
409
- * the key, and called after it re-reads under the new one, so the controller
410
- * never reads from one key while writing to another.
411
- *
412
- * @example
413
- * ```ts
414
- * setThemeStorageKey('hibi-theme') // once, at startup
415
- * ```
416
- */
417
- function setThemeStorageKey(key) {
418
- storageKey = key;
419
- if (preference) preference.value = readStoredTheme();
420
- }
421
- /** @returns The shared preference ref; assigning to it stores and applies it. */
422
- function useTheme() {
423
- return controller();
424
- }
425
- //#endregion
426
155
  //#region src/composables/use-today.ts
427
156
  /**
428
157
  * Today's date key, kept current while the app stays open.
@@ -838,7 +567,7 @@ function useToast() {
838
567
  }
839
568
  //#endregion
840
569
  //#region src/components/BaseAlert.vue?vue&type=script&setup=true&lang.ts
841
- var _hoisted_1$22 = ["role", "aria-live"];
570
+ var _hoisted_1$21 = ["role", "aria-live"];
842
571
  var _hoisted_2$16 = { class: "min-w-0 flex-1" };
843
572
  var _hoisted_3$11 = {
844
573
  key: 0,
@@ -883,7 +612,7 @@ var BaseAlert_default = /* @__PURE__ */ defineComponent({
883
612
  }, [renderSlot(_ctx.$slots, "mark")], 2)) : createCommentVNode("", true),
884
613
  createElementVNode("div", _hoisted_2$16, [_ctx.$slots.title ? (openBlock(), createElementBlock("p", _hoisted_3$11, [renderSlot(_ctx.$slots, "title")])) : createCommentVNode("", true), createElementVNode("div", { class: normalizeClass(_ctx.$slots.title ? "mt-1" : "") }, [renderSlot(_ctx.$slots, "default")], 2)]),
885
614
  renderSlot(_ctx.$slots, "action")
886
- ], 10, _hoisted_1$22);
615
+ ], 10, _hoisted_1$21);
887
616
  };
888
617
  }
889
618
  });
@@ -907,151 +636,6 @@ var BaseBadge_default = /* @__PURE__ */ defineComponent({
907
636
  }
908
637
  });
909
638
  //#endregion
910
- //#region src/components/BaseButton.vue?vue&type=script&setup=true&lang.ts
911
- var _hoisted_1$21 = {
912
- key: 0,
913
- class: "size-4 animate-spin rounded-full border-2 border-current border-t-transparent",
914
- "aria-hidden": "true"
915
- };
916
- //#endregion
917
- //#region src/components/BaseButton.vue
918
- var BaseButton_default = /* @__PURE__ */ defineComponent({
919
- __name: "BaseButton",
920
- props: {
921
- as: { default: "button" },
922
- variant: { default: "primary" },
923
- size: { default: "md" },
924
- loading: {
925
- type: Boolean,
926
- default: false
927
- },
928
- disabled: {
929
- type: Boolean,
930
- default: false
931
- },
932
- type: { default: "button" },
933
- icon: {
934
- type: Boolean,
935
- default: false
936
- },
937
- block: {
938
- type: Boolean,
939
- default: false
940
- },
941
- to: { default: () => void 0 },
942
- href: { default: () => void 0 },
943
- pill: {
944
- type: Boolean,
945
- default: false
946
- },
947
- pressed: {
948
- type: Boolean,
949
- default: () => void 0
950
- }
951
- },
952
- setup(__props) {
953
- const VARIANT_CLASS = {
954
- primary: "bg-primary text-white hover:bg-primary/90",
955
- secondary: "border-hair bg-surface text-ink border hover:bg-muted",
956
- ghost: "bg-transparent text-ink hover:bg-muted",
957
- destructive: "bg-transparent text-ink-soft hover:text-negative",
958
- row: "w-full justify-start text-left bg-transparent text-ink hover:bg-muted",
959
- quiet: "bg-transparent text-ink-soft hover:bg-muted hover:text-ink",
960
- danger: "bg-negative text-white hover:bg-negative/90",
961
- positive: "bg-positive text-white hover:bg-positive/90",
962
- warning: "bg-warning text-white hover:bg-warning/90",
963
- accent: "bg-accent text-white hover:bg-accent/90",
964
- link: "bg-transparent underline underline-offset-2 hover:opacity-80",
965
- unstyled: ""
966
- };
967
- const SIZE_CLASS = {
968
- xs: "h-8 px-3 text-xs",
969
- sm: "h-9 px-3 text-sm",
970
- md: "h-11 px-4 text-base",
971
- lg: "h-14 px-6 text-lg"
972
- };
973
- const ICON_SIZE_CLASS = {
974
- xs: "size-8 text-xs",
975
- sm: "size-9 text-sm",
976
- md: "size-11 text-base",
977
- lg: "size-14 text-lg"
978
- };
979
- const ROW_SIZE_CLASS = {
980
- xs: "px-2 py-1.5 text-xs",
981
- sm: "px-3 py-2 text-sm",
982
- md: "px-3 py-2.5 text-base",
983
- lg: "px-4 py-3 text-lg"
984
- };
985
- const LINK_SIZE_CLASS = {
986
- xs: "text-xs",
987
- sm: "text-sm",
988
- md: "text-base",
989
- lg: "text-lg"
990
- };
991
- const sizing = computed(() => {
992
- if (__props.variant === "unstyled") return "";
993
- if (__props.variant === "link") return LINK_SIZE_CLASS[__props.size];
994
- if (__props.variant === "row") return ROW_SIZE_CLASS[__props.size];
995
- return __props.icon ? ICON_SIZE_CLASS[__props.size] : SIZE_CLASS[__props.size];
996
- });
997
- const PRESSED_CLASS = {
998
- ghost: "bg-primary text-white hover:bg-primary/90",
999
- quiet: "bg-primary text-white hover:bg-primary/90",
1000
- secondary: "bg-primary border-primary text-white hover:bg-primary/90",
1001
- row: "w-full justify-start text-left bg-muted text-ink hover:bg-muted"
1002
- };
1003
- const surface = computed(() => {
1004
- if (__props.pressed === true) return PRESSED_CLASS[__props.variant] ?? VARIANT_CLASS[__props.variant];
1005
- if (__props.variant === "destructive") return `${VARIANT_CLASS.destructive} hover:bg-negative/10`;
1006
- return VARIANT_CLASS[__props.variant];
1007
- });
1008
- const shell = computed(() => {
1009
- if (__props.variant === "unstyled") return "";
1010
- const feel = "transition-[transform,color,background-color,border-color] duration-100 select-none";
1011
- if (__props.variant === "row") return `inline-flex items-center gap-2 font-medium ${feel}`;
1012
- return `inline-flex items-center justify-center gap-2 font-medium ${feel} active:scale-95`;
1013
- });
1014
- const radius = computed(() => {
1015
- if (__props.variant === "unstyled") return "";
1016
- if (__props.variant === "link") return "rounded-xs";
1017
- return __props.pill ? "rounded-full" : "rounded-card";
1018
- });
1019
- /** Anything that is not a `<button>` cannot be `disabled`; it has to be told. */
1020
- const inactive = computed(() => __props.disabled || __props.loading);
1021
- const linkProps = computed(() => {
1022
- if (__props.as === "router-link") return { to: __props.to };
1023
- if (__props.as === "a") return inactive.value ? {} : { href: __props.href };
1024
- return {};
1025
- });
1026
- return (_ctx, _cache) => {
1027
- return openBlock(), createBlock(resolveDynamicComponent(__props.as), mergeProps(linkProps.value, {
1028
- type: __props.as === "button" ? __props.type : void 0,
1029
- disabled: __props.as === "button" ? inactive.value : void 0,
1030
- "aria-disabled": __props.as !== "button" && inactive.value ? "true" : void 0,
1031
- "aria-busy": __props.loading,
1032
- "aria-pressed": __props.pressed === void 0 ? void 0 : String(__props.pressed),
1033
- class: ["focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50", [
1034
- shell.value,
1035
- surface.value,
1036
- sizing.value,
1037
- radius.value,
1038
- __props.block ? "w-full" : ""
1039
- ]]
1040
- }), {
1041
- default: withCtx(() => [__props.loading ? (openBlock(), createElementBlock("span", _hoisted_1$21)) : createCommentVNode("", true), renderSlot(_ctx.$slots, "default")]),
1042
- _: 3
1043
- }, 16, [
1044
- "type",
1045
- "disabled",
1046
- "aria-disabled",
1047
- "aria-busy",
1048
- "aria-pressed",
1049
- "class"
1050
- ]);
1051
- };
1052
- }
1053
- });
1054
- //#endregion
1055
639
  //#region src/components/FormField.vue?vue&type=script&setup=true&lang.ts
1056
640
  var _hoisted_1$20 = ["for"];
1057
641
  //#endregion
@@ -1175,7 +759,9 @@ var _hoisted_6$2 = {
1175
759
  class: "text-ink-soft mt-1 text-sm leading-snug"
1176
760
  };
1177
761
  var _hoisted_7$2 = { class: "min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]" };
1178
- var BaseSheet_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
762
+ //#endregion
763
+ //#region src/components/BaseSheet.vue
764
+ var BaseSheet_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
1179
765
  __name: "BaseSheet",
1180
766
  props: /*@__PURE__*/ mergeModels({
1181
767
  title: {},
@@ -1276,17 +862,7 @@ var BaseSheet_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
1276
862
  })]);
1277
863
  };
1278
864
  }
1279
- });
1280
- //#endregion
1281
- //#region \0plugin-vue:export-helper
1282
- var _plugin_vue_export_helper_default = (sfc, props) => {
1283
- const target = sfc.__vccOpts || sfc;
1284
- for (const [key, val] of props) target[key] = val;
1285
- return target;
1286
- };
1287
- //#endregion
1288
- //#region src/components/BaseSheet.vue
1289
- var BaseSheet_default = /*#__PURE__*/ _plugin_vue_export_helper_default(BaseSheet_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-51709579"]]);
865
+ }), [["__scopeId", "data-v-51709579"]]);
1290
866
  //#endregion
1291
867
  //#region src/components/BaseCard.vue
1292
868
  var BaseCard_default = /* @__PURE__ */ defineComponent({
@@ -2437,7 +2013,7 @@ function createI18nRuntime(options) {
2437
2013
  *
2438
2014
  * The fallback keeps `vitest` and `vite dev` honest, where no define runs.
2439
2015
  */
2440
- var VERSION = "0.12.1";
2016
+ var VERSION = "0.13.0";
2441
2017
  //#endregion
2442
2018
  export { AppError, BaseAlert_default as BaseAlert, BaseBadge_default as BaseBadge, BaseButton_default as BaseButton, BaseCard_default as BaseCard, BaseCheckbox_default as BaseCheckbox, BaseInput_default as BaseInput, BaseRadioGroup_default as BaseRadioGroup, BaseSelect_default as BaseSelect, BaseSheet_default as BaseSheet, BaseTextarea_default as BaseTextarea, EmptyState_default as EmptyState, ErrorBoundary_default as ErrorBoundary, FormField_default as FormField, GoogleButton_default as GoogleButton, LocaleLinks_default as LocaleLinks, PageContainer_default as PageContainer, PageHeader_default as PageHeader, PriceCard_default as PriceCard, ProgressBar_default as ProgressBar, SectionHeading_default as SectionHeading, SegmentedControl_default as SegmentedControl, SettingsGroup_default as SettingsGroup, SettingsRow_default as SettingsRow, SkeletonList_default as SkeletonList, StatCard_default as StatCard, TabBar_default as TabBar, ToastHost_default as ToastHost, ToneDot_default as ToneDot, VERSION, addDays, applyTheme, createI18nRuntime, downloadJson, eachDayOfYear, formatDate, fromDateKey, isApplePortable, isInstalled, isThemePreference, lastNDays, leadingBlanks, needsIosInstall, readStoredTheme, registerErrorMapper, relativeDayLabel, safeRedirect, setFormatLocale, setThemeStorageKey, startOfWeek, tapFeedback, toAppError, toDateKey, todayKey, useDebouncedCallback, useDragScroll, useMediaQuery, useOnline, useTheme, useToast, useToday, useVisualViewport };
2443
2019