rei-kit 0.2.4 → 0.3.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"app-error-DF9cijE0.js","names":[],"sources":["../src/utils/app-error.ts"],"sourcesContent":["/** Error categories the UI can branch on, independent of Postgres or Supabase. */\nexport type AppErrorKind = 'conflict' | 'not-found' | 'network' | 'unknown'\n\n/**\n * A normalised error thrown by the data layer.\n *\n * Extends `Error` so it can be thrown, caught and logged like any other error,\n * and keeps the original in `cause` for debugging.\n *\n * @example\n * ```ts\n * try {\n * await createHabit(input)\n * } catch (e) {\n * const err = toAppError(e)\n * if (err.kind === 'conflict') return // already exists, not a real failure\n * showToast(err.message)\n * }\n * ```\n */\nexport class AppError extends Error {\n readonly kind: AppErrorKind\n\n constructor(kind: AppErrorKind, message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = 'AppError'\n this.kind = kind\n }\n}\n\n/**\n * Normalises anything thrown by Supabase into an {@link AppError}.\n *\n * Idempotent: an `AppError` is returned as-is, so wrapping twice is safe.\n *\n * @param error - Anything caught from the data layer.\n * @returns An `AppError` with a user-facing message and a `kind` to branch on.\n */\n/**\n * Turns a backend-specific error into an `AppError`, or returns `null` to let\n * the next mapper try.\n */\nexport type ErrorMapper = (error: unknown) => AppError | null\n\nconst mappers: ErrorMapper[] = []\n\n/**\n * Teaches `toAppError` about a backend it does not import.\n *\n * The core has no database dependency; `rei-kit/supabase` registers the\n * Postgrest mapping when it is imported, so an app that never touches Supabase\n * never downloads the code that knows about it.\n *\n * @example\n * ```ts\n * registerErrorMapper((error) =>\n * isPrismaConflict(error) ? new AppError('conflict', 'Already exists.') : null,\n * )\n * ```\n */\nexport function registerErrorMapper(mapper: ErrorMapper): void {\n mappers.push(mapper)\n}\n\n/**\n * Normalises anything thrown by the data layer.\n *\n * @param error - Whatever was caught.\n * @returns An `AppError`, never a rethrow.\n */\nexport function toAppError(error: unknown): AppError {\n if (error instanceof AppError) return error\n\n for (const map of mappers) {\n const mapped = map(error)\n if (mapped) return mapped\n }\n\n // A failed fetch surfaces as a TypeError, which is the only reliable signal\n // the browser gives that the request never left.\n if (error instanceof TypeError) {\n return new AppError('network', 'Could not reach the server.', { cause: error })\n }\n\n return new AppError('unknown', 'Something went wrong.', { cause: error })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,IAAa,WAAb,cAA8B,MAAM;CAClC;CAEA,YAAY,MAAoB,SAAiB,SAAwB;EACvE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAgBA,IAAM,UAAyB,CAAC;;;;;;;;;;;;;;;AAgBhC,SAAgB,oBAAoB,QAA2B;CAC7D,QAAQ,KAAK,MAAM;AACrB;;;;;;;AAQA,SAAgB,WAAW,OAA0B;CACnD,IAAI,iBAAiB,UAAU,OAAO;CAEtC,KAAK,MAAM,OAAO,SAAS;EACzB,MAAM,SAAS,IAAI,KAAK;EACxB,IAAI,QAAQ,OAAO;CACrB;CAIA,IAAI,iBAAiB,WACnB,OAAO,IAAI,SAAS,WAAW,+BAA+B,EAAE,OAAO,MAAM,CAAC;CAGhF,OAAO,IAAI,SAAS,WAAW,yBAAyB,EAAE,OAAO,MAAM,CAAC;AAC1E"}
1
+ {"version":3,"file":"app-error-DF9cijE0.js","names":[],"sources":["../src/utils/app-error.ts"],"sourcesContent":["/**\n * Error categories the UI can branch on, independent of Postgres or Supabase.\n *\n * `denied` is the one that is not a fault: the request was understood, well\n * formed and refused. A screen that treats it as a failure tells the reader\n * something is broken and sends them to support, when what they need is to\n * sign in again or to be told the thing is not theirs.\n */\nexport type AppErrorKind = 'conflict' | 'not-found' | 'network' | 'denied' | 'unknown'\n\n/**\n * A normalised error thrown by the data layer.\n *\n * Extends `Error` so it can be thrown, caught and logged like any other error,\n * and keeps the original in `cause` for debugging.\n *\n * @example\n * ```ts\n * try {\n * await createHabit(input)\n * } catch (e) {\n * const err = toAppError(e)\n * if (err.kind === 'conflict') return // already exists, not a real failure\n * showToast(err.message)\n * }\n * ```\n */\nexport class AppError extends Error {\n readonly kind: AppErrorKind\n\n constructor(kind: AppErrorKind, message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = 'AppError'\n this.kind = kind\n }\n}\n\n/**\n * Normalises anything thrown by Supabase into an {@link AppError}.\n *\n * Idempotent: an `AppError` is returned as-is, so wrapping twice is safe.\n *\n * @param error - Anything caught from the data layer.\n * @returns An `AppError` with a user-facing message and a `kind` to branch on.\n */\n/**\n * Turns a backend-specific error into an `AppError`, or returns `null` to let\n * the next mapper try.\n */\nexport type ErrorMapper = (error: unknown) => AppError | null\n\nconst mappers: ErrorMapper[] = []\n\n/**\n * Teaches `toAppError` about a backend it does not import.\n *\n * The core has no database dependency; `rei-kit/supabase` registers the\n * Postgrest mapping when it is imported, so an app that never touches Supabase\n * never downloads the code that knows about it.\n *\n * @example\n * ```ts\n * registerErrorMapper((error) =>\n * isPrismaConflict(error) ? new AppError('conflict', 'Already exists.') : null,\n * )\n * ```\n */\nexport function registerErrorMapper(mapper: ErrorMapper): void {\n mappers.push(mapper)\n}\n\n/**\n * Normalises anything thrown by the data layer.\n *\n * @param error - Whatever was caught.\n * @returns An `AppError`, never a rethrow.\n */\nexport function toAppError(error: unknown): AppError {\n if (error instanceof AppError) return error\n\n for (const map of mappers) {\n const mapped = map(error)\n if (mapped) return mapped\n }\n\n // A failed fetch surfaces as a TypeError, which is the only reliable signal\n // the browser gives that the request never left.\n if (error instanceof TypeError) {\n return new AppError('network', 'Could not reach the server.', { cause: error })\n }\n\n return new AppError('unknown', 'Something went wrong.', { cause: error })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2BA,IAAa,WAAb,cAA8B,MAAM;CAClC;CAEA,YAAY,MAAoB,SAAiB,SAAwB;EACvE,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;AAgBA,IAAM,UAAyB,CAAC;;;;;;;;;;;;;;;AAgBhC,SAAgB,oBAAoB,QAA2B;CAC7D,QAAQ,KAAK,MAAM;AACrB;;;;;;;AAQA,SAAgB,WAAW,OAA0B;CACnD,IAAI,iBAAiB,UAAU,OAAO;CAEtC,KAAK,MAAM,OAAO,SAAS;EACzB,MAAM,SAAS,IAAI,KAAK;EACxB,IAAI,QAAQ,OAAO;CACrB;CAIA,IAAI,iBAAiB,WACnB,OAAO,IAAI,SAAS,WAAW,+BAA+B,EAAE,OAAO,MAAM,CAAC;CAGhF,OAAO,IAAI,SAAS,WAAW,yBAAyB,EAAE,OAAO,MAAM,CAAC;AAC1E"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * One plan in a pricing table.
3
+ *
4
+ * Every string arrives as a prop. A component in a kit that reaches for its
5
+ * consumer's translations is not shared, it is one app's furniture parked
6
+ * somewhere else — and the second app to want it would have to fork it.
7
+ *
8
+ * The tone is semantic rather than named after a colour. "Gold" and "diamond"
9
+ * are one product's tiers; `warm` and `cool` are what a pricing table actually
10
+ * needs, which is for three columns to be distinguishable at a glance without
11
+ * any of them shouting. A table where every column is a different hue reads as
12
+ * three products from three companies.
13
+ */
14
+ type __VLS_Props = {
15
+ name: string;
16
+ lead?: string;
17
+ /** Already formatted, or whatever stands in while there is no price. */
18
+ price: string;
19
+ period?: string;
20
+ note?: string;
21
+ features: readonly string[];
22
+ tone?: 'neutral' | 'warm' | 'cool';
23
+ /** Rides on the card's edge, e.g. "Recommended". */
24
+ badge?: string;
25
+ /** Sits inside, e.g. "30% cheaper" or "Your plan". */
26
+ chip?: string;
27
+ /** Raises the card and lets the badge show. */
28
+ recommended?: boolean;
29
+ };
30
+ declare var __VLS_1: {}, __VLS_3: {};
31
+ type __VLS_Slots = {} & {
32
+ icon?: (props: typeof __VLS_1) => any;
33
+ } & {
34
+ action?: (props: typeof __VLS_3) => any;
35
+ };
36
+ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
37
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
38
+ declare const _default: typeof __VLS_export;
39
+ export default _default;
40
+ type __VLS_WithSlots<T, S> = T & {
41
+ new (): {
42
+ $slots: S;
43
+ };
44
+ };
package/dist/index.d.ts CHANGED
@@ -34,6 +34,7 @@ export { default as BaseInput } from './components/BaseInput.vue';
34
34
  export { default as BaseSheet } from './components/BaseSheet.vue';
35
35
  export { default as EmptyState } from './components/EmptyState.vue';
36
36
  export { default as PageHeader } from './components/PageHeader.vue';
37
+ export { default as PriceCard } from './components/PriceCard.vue';
37
38
  export { default as SectionHeading } from './components/SectionHeading.vue';
38
39
  export { default as SegmentedControl } from './components/SegmentedControl.vue';
39
40
  export { default as SettingsGroup } from './components/SettingsGroup.vue';
package/dist/index.js CHANGED
@@ -691,12 +691,12 @@ function useVisualViewport() {
691
691
  }
692
692
  //#endregion
693
693
  //#region src/components/BaseButton.vue?vue&type=script&setup=true&lang.ts
694
- var _hoisted_1$13 = [
694
+ var _hoisted_1$14 = [
695
695
  "type",
696
696
  "disabled",
697
697
  "aria-busy"
698
698
  ];
699
- var _hoisted_2$12 = {
699
+ var _hoisted_2$13 = {
700
700
  key: 0,
701
701
  class: "size-4 animate-spin rounded-full border-2 border-current border-t-transparent",
702
702
  "aria-hidden": "true"
@@ -734,15 +734,15 @@ var BaseButton_default = /* @__PURE__ */ defineComponent({
734
734
  disabled: __props.disabled || __props.loading,
735
735
  "aria-busy": __props.loading,
736
736
  class: normalizeClass(["rounded-card focus-visible:outline-primary inline-flex items-center justify-center gap-2 font-medium transition-transform duration-100 select-none focus-visible:outline-2 focus-visible:outline-offset-2 active:scale-95 disabled:pointer-events-none disabled:opacity-50", [VARIANT_CLASS[__props.variant], SIZE_CLASS[__props.size]]])
737
- }, [__props.loading ? (openBlock(), createElementBlock("span", _hoisted_2$12)) : createCommentVNode("", true), renderSlot(_ctx.$slots, "default")], 10, _hoisted_1$13);
737
+ }, [__props.loading ? (openBlock(), createElementBlock("span", _hoisted_2$13)) : createCommentVNode("", true), renderSlot(_ctx.$slots, "default")], 10, _hoisted_1$14);
738
738
  };
739
739
  }
740
740
  });
741
741
  //#endregion
742
742
  //#region src/components/BaseInput.vue?vue&type=script&setup=true&lang.ts
743
- var _hoisted_1$12 = { class: "flex flex-col gap-1.5" };
744
- var _hoisted_2$11 = ["for"];
745
- var _hoisted_3$7 = [
743
+ var _hoisted_1$13 = { class: "flex flex-col gap-1.5" };
744
+ var _hoisted_2$12 = ["for"];
745
+ var _hoisted_3$8 = [
746
746
  "id",
747
747
  "type",
748
748
  "aria-invalid",
@@ -777,18 +777,18 @@ var BaseInput_default = /* @__PURE__ */ defineComponent({
777
777
  if (__props.hint) return hintId;
778
778
  });
779
779
  return (_ctx, _cache) => {
780
- return openBlock(), createElementBlock("div", _hoisted_1$12, [
780
+ return openBlock(), createElementBlock("div", _hoisted_1$13, [
781
781
  createElementVNode("label", {
782
782
  for: unref(id),
783
783
  class: normalizeClass(["text-ink text-sm font-medium", __props.labelHidden ? "sr-only" : ""])
784
- }, toDisplayString(__props.label), 11, _hoisted_2$11),
784
+ }, toDisplayString(__props.label), 11, _hoisted_2$12),
785
785
  withDirectives(createElementVNode("input", mergeProps({
786
786
  id: unref(id),
787
787
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => model.value = $event),
788
788
  type: __props.type,
789
789
  "aria-invalid": Boolean(__props.error),
790
790
  "aria-describedby": describedBy.value
791
- }, _ctx.$attrs, { class: ["border-hair bg-surface text-ink rounded-card focus-visible:outline-primary h-11 border px-3 focus-visible:outline-2 focus-visible:outline-offset-1", __props.error ? "border-negative" : ""] }), null, 16, _hoisted_3$7), [[vModelDynamic, model.value]]),
791
+ }, _ctx.$attrs, { class: ["border-hair bg-surface text-ink rounded-card focus-visible:outline-primary h-11 border px-3 focus-visible:outline-2 focus-visible:outline-offset-1", __props.error ? "border-negative" : ""] }), null, 16, _hoisted_3$8), [[vModelDynamic, model.value]]),
792
792
  __props.error ? (openBlock(), createElementBlock("p", {
793
793
  key: 0,
794
794
  id: errorId,
@@ -804,17 +804,17 @@ var BaseInput_default = /* @__PURE__ */ defineComponent({
804
804
  });
805
805
  //#endregion
806
806
  //#region src/components/BaseSheet.vue?vue&type=script&setup=true&lang.ts
807
- var _hoisted_1$11 = { class: "shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden" };
808
- var _hoisted_2$10 = ["aria-label"];
809
- var _hoisted_3$6 = { class: "flex shrink-0 items-start gap-3 px-6 pt-4 pb-5" };
810
- var _hoisted_4$5 = { class: "min-w-0 flex-1" };
811
- var _hoisted_5$3 = { class: "text-ink text-xl leading-tight font-semibold" };
812
- var _hoisted_6$1 = {
807
+ var _hoisted_1$12 = { class: "shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden" };
808
+ var _hoisted_2$11 = ["aria-label"];
809
+ var _hoisted_3$7 = { class: "flex shrink-0 items-start gap-3 px-6 pt-4 pb-5" };
810
+ var _hoisted_4$6 = { class: "min-w-0 flex-1" };
811
+ var _hoisted_5$4 = { class: "text-ink text-xl leading-tight font-semibold" };
812
+ var _hoisted_6$2 = {
813
813
  key: 0,
814
814
  class: "text-ink-soft mt-1 text-sm leading-snug"
815
815
  };
816
- var _hoisted_7$1 = ["aria-label"];
817
- var _hoisted_8 = { class: "min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]" };
816
+ var _hoisted_7$2 = ["aria-label"];
817
+ var _hoisted_8$1 = { class: "min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]" };
818
818
  var BaseSheet_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
819
819
  __name: "BaseSheet",
820
820
  props: /*@__PURE__*/ mergeModels({
@@ -885,7 +885,7 @@ var BaseSheet_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
885
885
  key: 0,
886
886
  class: "fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center",
887
887
  style: normalizeStyle(viewportStyle.value)
888
- }, [createElementVNode("div", _hoisted_1$11, [createElementVNode("div", {
888
+ }, [createElementVNode("div", _hoisted_1$12, [createElementVNode("div", {
889
889
  class: "bg-ink/45 absolute inset-0 backdrop-blur-[2px]",
890
890
  onClick: close
891
891
  }), createElementVNode("section", {
@@ -901,14 +901,14 @@ var BaseSheet_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ define
901
901
  class: "flex shrink-0 justify-center pt-3",
902
902
  "aria-hidden": "true"
903
903
  }, [createElementVNode("span", { class: "bg-hair h-1.5 w-10 rounded-full" })], -1)),
904
- createElementVNode("header", _hoisted_3$6, [createElementVNode("div", _hoisted_4$5, [createElementVNode("h2", _hoisted_5$3, toDisplayString(__props.title), 1), __props.subtitle ? (openBlock(), createElementBlock("p", _hoisted_6$1, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true)]), createElementVNode("button", {
904
+ createElementVNode("header", _hoisted_3$7, [createElementVNode("div", _hoisted_4$6, [createElementVNode("h2", _hoisted_5$4, toDisplayString(__props.title), 1), __props.subtitle ? (openBlock(), createElementBlock("p", _hoisted_6$2, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true)]), createElementVNode("button", {
905
905
  type: "button",
906
906
  class: "text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90",
907
907
  "aria-label": __props.closeLabel,
908
908
  onClick: close
909
- }, [createVNode(unref(X), { class: "size-5" })], 8, _hoisted_7$1)]),
910
- createElementVNode("div", _hoisted_8, [renderSlot(_ctx.$slots, "default", {}, void 0, true)])
911
- ], 8, _hoisted_2$10)])], 4)) : createCommentVNode("", true)]),
909
+ }, [createVNode(unref(X), { class: "size-5" })], 8, _hoisted_7$2)]),
910
+ createElementVNode("div", _hoisted_8$1, [renderSlot(_ctx.$slots, "default", {}, void 0, true)])
911
+ ], 8, _hoisted_2$11)])], 4)) : createCommentVNode("", true)]),
912
912
  _: 3
913
913
  })]);
914
914
  };
@@ -926,17 +926,17 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
926
926
  var BaseSheet_default = /*#__PURE__*/ _plugin_vue_export_helper_default(BaseSheet_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-3f2e9ce4"]]);
927
927
  //#endregion
928
928
  //#region src/components/EmptyState.vue?vue&type=script&setup=true&lang.ts
929
- var _hoisted_1$10 = { class: "flex flex-col items-center gap-3 px-6 py-10 text-center" };
930
- var _hoisted_2$9 = {
929
+ var _hoisted_1$11 = { class: "flex flex-col items-center gap-3 px-6 py-10 text-center" };
930
+ var _hoisted_2$10 = {
931
931
  key: 0,
932
932
  class: "bg-muted text-primary rounded-card flex size-12 items-center justify-center"
933
933
  };
934
- var _hoisted_3$5 = { class: "text-ink text-base font-semibold" };
935
- var _hoisted_4$4 = {
934
+ var _hoisted_3$6 = { class: "text-ink text-base font-semibold" };
935
+ var _hoisted_4$5 = {
936
936
  key: 1,
937
937
  class: "text-ink-soft max-w-[36ch] text-sm"
938
938
  };
939
- var _hoisted_5$2 = {
939
+ var _hoisted_5$3 = {
940
940
  key: 2,
941
941
  class: "mt-2 flex w-full flex-col gap-2"
942
942
  };
@@ -950,22 +950,22 @@ var EmptyState_default = /* @__PURE__ */ defineComponent({
950
950
  },
951
951
  setup(__props) {
952
952
  return (_ctx, _cache) => {
953
- return openBlock(), createElementBlock("div", _hoisted_1$10, [
954
- _ctx.$slots.icon ? (openBlock(), createElementBlock("div", _hoisted_2$9, [renderSlot(_ctx.$slots, "icon")])) : createCommentVNode("", true),
955
- createElementVNode("h3", _hoisted_3$5, toDisplayString(__props.title), 1),
956
- __props.description ? (openBlock(), createElementBlock("p", _hoisted_4$4, toDisplayString(__props.description), 1)) : createCommentVNode("", true),
957
- _ctx.$slots.action ? (openBlock(), createElementBlock("div", _hoisted_5$2, [renderSlot(_ctx.$slots, "action")])) : createCommentVNode("", true)
953
+ return openBlock(), createElementBlock("div", _hoisted_1$11, [
954
+ _ctx.$slots.icon ? (openBlock(), createElementBlock("div", _hoisted_2$10, [renderSlot(_ctx.$slots, "icon")])) : createCommentVNode("", true),
955
+ createElementVNode("h3", _hoisted_3$6, toDisplayString(__props.title), 1),
956
+ __props.description ? (openBlock(), createElementBlock("p", _hoisted_4$5, toDisplayString(__props.description), 1)) : createCommentVNode("", true),
957
+ _ctx.$slots.action ? (openBlock(), createElementBlock("div", _hoisted_5$3, [renderSlot(_ctx.$slots, "action")])) : createCommentVNode("", true)
958
958
  ]);
959
959
  };
960
960
  }
961
961
  });
962
962
  //#endregion
963
963
  //#region src/components/PageHeader.vue?vue&type=script&setup=true&lang.ts
964
- var _hoisted_1$9 = { class: "grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center" };
965
- var _hoisted_2$8 = { class: "justify-self-start" };
966
- var _hoisted_3$4 = { class: "text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums" };
967
- var _hoisted_4$3 = { class: "truncate" };
968
- var _hoisted_5$1 = { class: "justify-self-end" };
964
+ var _hoisted_1$10 = { class: "grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center" };
965
+ var _hoisted_2$9 = { class: "justify-self-start" };
966
+ var _hoisted_3$5 = { class: "text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums" };
967
+ var _hoisted_4$4 = { class: "truncate" };
968
+ var _hoisted_5$2 = { class: "justify-self-end" };
969
969
  //#endregion
970
970
  //#region src/components/PageHeader.vue
971
971
  var PageHeader_default = /* @__PURE__ */ defineComponent({
@@ -973,15 +973,109 @@ var PageHeader_default = /* @__PURE__ */ defineComponent({
973
973
  props: { title: {} },
974
974
  setup(__props) {
975
975
  return (_ctx, _cache) => {
976
- return openBlock(), createElementBlock("header", _hoisted_1$9, [
977
- createElementVNode("div", _hoisted_2$8, [renderSlot(_ctx.$slots, "left")]),
978
- createElementVNode("h1", _hoisted_3$4, [renderSlot(_ctx.$slots, "title", {}, () => [createElementVNode("span", _hoisted_4$3, toDisplayString(__props.title), 1)])]),
979
- createElementVNode("div", _hoisted_5$1, [renderSlot(_ctx.$slots, "right")])
976
+ return openBlock(), createElementBlock("header", _hoisted_1$10, [
977
+ createElementVNode("div", _hoisted_2$9, [renderSlot(_ctx.$slots, "left")]),
978
+ createElementVNode("h1", _hoisted_3$5, [renderSlot(_ctx.$slots, "title", {}, () => [createElementVNode("span", _hoisted_4$4, toDisplayString(__props.title), 1)])]),
979
+ createElementVNode("div", _hoisted_5$2, [renderSlot(_ctx.$slots, "right")])
980
980
  ]);
981
981
  };
982
982
  }
983
983
  });
984
984
  //#endregion
985
+ //#region src/components/PriceCard.vue?vue&type=script&setup=true&lang.ts
986
+ var _hoisted_1$9 = {
987
+ key: 0,
988
+ class: "bg-primary rounded-cell absolute -top-3 left-7 px-3 py-1 text-[0.7rem] font-semibold text-white"
989
+ };
990
+ var _hoisted_2$8 = { class: "flex items-start justify-between gap-4" };
991
+ var _hoisted_3$4 = { class: "text-ink mt-5 text-lg font-semibold" };
992
+ var _hoisted_4$3 = {
993
+ key: 1,
994
+ class: "text-ink-soft mt-1.5 text-sm leading-relaxed"
995
+ };
996
+ var _hoisted_5$1 = { class: "mt-6 flex items-baseline gap-1.5" };
997
+ var _hoisted_6$1 = { class: "text-ink text-3xl font-semibold tracking-tight tabular-nums" };
998
+ var _hoisted_7$1 = {
999
+ key: 0,
1000
+ class: "text-ink-soft text-sm"
1001
+ };
1002
+ var _hoisted_8 = {
1003
+ key: 2,
1004
+ class: "text-ink-soft mt-1 text-xs"
1005
+ };
1006
+ var _hoisted_9 = { class: "mt-7 flex-1 space-y-3" };
1007
+ var _hoisted_10 = { class: "text-ink-soft leading-relaxed" };
1008
+ var _hoisted_11 = {
1009
+ key: 3,
1010
+ class: "mt-8"
1011
+ };
1012
+ //#endregion
1013
+ //#region src/components/PriceCard.vue
1014
+ var PriceCard_default = /* @__PURE__ */ defineComponent({
1015
+ __name: "PriceCard",
1016
+ props: {
1017
+ name: {},
1018
+ lead: {},
1019
+ price: {},
1020
+ period: {},
1021
+ note: {},
1022
+ features: {},
1023
+ tone: { default: "neutral" },
1024
+ badge: {},
1025
+ chip: {},
1026
+ recommended: {
1027
+ type: Boolean,
1028
+ default: false
1029
+ }
1030
+ },
1031
+ setup(__props) {
1032
+ const TONE = {
1033
+ neutral: {
1034
+ ring: "border-hair/70",
1035
+ soft: "bg-muted text-ink-soft",
1036
+ icon: "bg-primary/10 text-primary"
1037
+ },
1038
+ warm: {
1039
+ ring: "border-[color-mix(in_oklab,#b8862c_35%,transparent)]",
1040
+ soft: "bg-[color-mix(in_oklab,#b8862c_14%,transparent)] text-[#8a6318] dark:text-[#d9ad5c]",
1041
+ icon: "bg-[color-mix(in_oklab,#b8862c_16%,transparent)] text-[#8a6318] dark:text-[#d9ad5c]"
1042
+ },
1043
+ cool: {
1044
+ ring: "border-[color-mix(in_oklab,#4a86a8_38%,transparent)]",
1045
+ soft: "bg-[color-mix(in_oklab,#4a86a8_14%,transparent)] text-[#2f6079] dark:text-[#8fc6de]",
1046
+ icon: "bg-[color-mix(in_oklab,#4a86a8_16%,transparent)] text-[#2f6079] dark:text-[#8fc6de]"
1047
+ }
1048
+ };
1049
+ const palette = computed(() => TONE[__props.tone]);
1050
+ return (_ctx, _cache) => {
1051
+ return openBlock(), createElementBlock("article", { class: normalizeClass(["bg-surface rounded-card relative flex h-full flex-col border p-7 shadow-[var(--shadow-card)] transition-[border-color,box-shadow,transform] duration-[420ms] hover:border-[color-mix(in_oklab,var(--color-primary)_60%,transparent)] hover:shadow-[var(--shadow-lift)] sm:p-8", [palette.value.ring, __props.recommended ? "shadow-[var(--shadow-lift)]" : "hover:-translate-y-0.5"]]) }, [
1052
+ __props.badge && __props.recommended ? (openBlock(), createElementBlock("span", _hoisted_1$9, toDisplayString(__props.badge), 1)) : createCommentVNode("", true),
1053
+ createElementVNode("div", _hoisted_2$8, [_ctx.$slots.icon ? (openBlock(), createElementBlock("span", {
1054
+ key: 0,
1055
+ class: normalizeClass(["rounded-card grid size-11 place-items-center text-xl", palette.value.icon])
1056
+ }, [renderSlot(_ctx.$slots, "icon")], 2)) : createCommentVNode("", true), __props.chip ? (openBlock(), createElementBlock("span", {
1057
+ key: 1,
1058
+ class: normalizeClass(["rounded-cell ml-auto px-2.5 py-1 text-[0.7rem] font-medium", palette.value.soft])
1059
+ }, toDisplayString(__props.chip), 3)) : createCommentVNode("", true)]),
1060
+ createElementVNode("h3", _hoisted_3$4, toDisplayString(__props.name), 1),
1061
+ __props.lead ? (openBlock(), createElementBlock("p", _hoisted_4$3, toDisplayString(__props.lead), 1)) : createCommentVNode("", true),
1062
+ createElementVNode("p", _hoisted_5$1, [createElementVNode("span", _hoisted_6$1, toDisplayString(__props.price), 1), __props.period ? (openBlock(), createElementBlock("span", _hoisted_7$1, toDisplayString(__props.period), 1)) : createCommentVNode("", true)]),
1063
+ __props.note ? (openBlock(), createElementBlock("p", _hoisted_8, toDisplayString(__props.note), 1)) : createCommentVNode("", true),
1064
+ createElementVNode("ul", _hoisted_9, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.features, (feature) => {
1065
+ return openBlock(), createElementBlock("li", {
1066
+ key: feature,
1067
+ class: "flex gap-3 text-sm"
1068
+ }, [_cache[0] || (_cache[0] = createElementVNode("span", {
1069
+ class: "bg-primary/45 mt-[0.45rem] size-1.5 shrink-0 rounded-full",
1070
+ "aria-hidden": "true"
1071
+ }, null, -1)), createElementVNode("span", _hoisted_10, toDisplayString(feature), 1)]);
1072
+ }), 128))]),
1073
+ _ctx.$slots.action ? (openBlock(), createElementBlock("div", _hoisted_11, [renderSlot(_ctx.$slots, "action")])) : createCommentVNode("", true)
1074
+ ], 2);
1075
+ };
1076
+ }
1077
+ });
1078
+ //#endregion
985
1079
  //#region src/components/ToneDot.vue?vue&type=script&setup=true&lang.ts
986
1080
  var _hoisted_1$8 = { class: "inline-flex items-center gap-1.5" };
987
1081
  var _hoisted_2$7 = {
@@ -1444,6 +1538,6 @@ function createI18nRuntime(options) {
1444
1538
  */
1445
1539
  var VERSION = "0.0.0";
1446
1540
  //#endregion
1447
- export { AppError, BaseButton_default as BaseButton, BaseInput_default as BaseInput, BaseSheet_default as BaseSheet, EmptyState_default as EmptyState, GoogleButton_default as GoogleButton, LocaleLinks_default as LocaleLinks, PageHeader_default as PageHeader, 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, 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, useOnline, useTheme, useToday, useVisualViewport };
1541
+ export { AppError, BaseButton_default as BaseButton, BaseInput_default as BaseInput, BaseSheet_default as BaseSheet, EmptyState_default as EmptyState, GoogleButton_default as GoogleButton, LocaleLinks_default as LocaleLinks, PageHeader_default as PageHeader, PriceCard_default as PriceCard, 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, 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, useOnline, useTheme, useToday, useVisualViewport };
1448
1542
 
1449
1543
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["$attrs","$slots"],"sources":["../src/utils/date.ts","../src/utils/format.ts","../src/utils/day-label.ts","../src/utils/download.ts","../src/utils/redirect.ts","../src/utils/haptics.ts","../src/utils/platform.ts","../src/composables/use-theme.ts","../src/composables/use-today.ts","../src/composables/use-online.ts","../src/composables/use-debounced-callback.ts","../src/composables/use-drag-scroll.ts","../src/composables/use-visual-viewport.ts","../src/components/BaseButton.vue","../src/components/BaseButton.vue","../src/components/BaseInput.vue","../src/components/BaseInput.vue","../src/components/BaseSheet.vue","../src/components/BaseSheet.vue","../src/components/EmptyState.vue","../src/components/EmptyState.vue","../src/components/PageHeader.vue","../src/components/PageHeader.vue","../src/components/ToneDot.vue","../src/components/ToneDot.vue","../src/components/SectionHeading.vue","../src/components/SectionHeading.vue","../src/components/SegmentedControl.vue","../src/components/SegmentedControl.vue","../src/components/SettingsGroup.vue","../src/components/SettingsGroup.vue","../src/components/SettingsRow.vue","../src/components/SettingsRow.vue","../src/components/SkeletonList.vue","../src/components/SkeletonList.vue","../src/components/StatCard.vue","../src/components/StatCard.vue","../src/components/LocaleLinks.vue","../src/components/LocaleLinks.vue","../src/components/GoogleButton.vue","../src/components/GoogleButton.vue","../src/components/TabBar.vue","../src/components/TabBar.vue","../src/i18n/runtime.ts","../src/index.ts"],"sourcesContent":["/**\n * Local calendar-day helpers.\n *\n * Every function is pure and works on `YYYY-MM-DD` keys, the same shape as the\n * `date` columns in Postgres. Nothing here calls `toISOString`: that converts to\n * UTC, so in a UTC+9 timezone every entry made between midnight and 09:00 would\n * be written to the previous day.\n */\n\n/**\n * Formats a `Date` as a local `YYYY-MM-DD` key.\n *\n * @param date - Any `Date`; only its local year, month and day are read.\n * @returns The calendar day in the runtime's own timezone.\n *\n * @example\n * ```ts\n * // 2026-08-23 01:30 in Tokyo\n * toDateKey(new Date()) // '2026-08-23'\n * new Date().toISOString() // '2026-08-22T16:30…' ← the bug\n * ```\n */\nexport function toDateKey(date: Date): string {\n const year = String(date.getFullYear()).padStart(4, '0')\n const month = String(date.getMonth() + 1).padStart(2, '0')\n const day = String(date.getDate()).padStart(2, '0')\n\n return `${year}-${month}-${day}`\n}\n\n/** Today's key in the user's own timezone. */\nexport function todayKey(): string {\n return toDateKey(new Date())\n}\n\n/**\n * Parses a `YYYY-MM-DD` key into a `Date` at local midnight.\n *\n * @param key - A key produced by {@link toDateKey}.\n * @returns Local midnight of that calendar day.\n * @throws If the key is not three numeric parts.\n *\n * @example\n * ```ts\n * fromDateKey('2026-08-23') // local midnight, correct\n * new Date('2026-08-23') // UTC midnight — shifts a day in some zones\n * ```\n */\nexport function fromDateKey(key: string): Date {\n const [year, month, day] = key.split('-').map(Number)\n\n if (year === undefined || month === undefined || day === undefined) {\n throw new Error(`Invalid date key: ${key}`)\n }\n\n return new Date(year, month - 1, day)\n}\n\n/**\n * Shifts a date key by whole calendar days.\n *\n * Uses `setDate`, which is calendar-aware: it rolls over month and year ends,\n * and stays correct across daylight-saving transitions. Adding\n * `days * 86_400_000` milliseconds would not — a DST day is 23 or 25 hours long.\n *\n * @param key - Starting `YYYY-MM-DD` key.\n * @param days - Days to add; negative goes back.\n * @returns The resulting key.\n *\n * @example\n * ```ts\n * addDays('2026-01-31', 1) // '2026-02-01'\n * addDays('2026-01-01', -1) // '2025-12-31'\n * addDays('2028-02-28', 1) // '2028-02-29' — leap year\n * ```\n */\nexport function addDays(key: string, days: number): string {\n const date = fromDateKey(key)\n date.setDate(date.getDate() + days)\n\n return toDateKey(date)\n}\n\n/**\n * The last `count` days ending today, oldest first.\n *\n * `today` is a parameter so the function stays pure and testable; call sites\n * normally omit it.\n *\n * @param count - How many days to return, including `today`.\n * @param today - End of the range. Defaults to the real today.\n * @returns Keys in ascending order.\n *\n * @example\n * ```ts\n * lastNDays(3, '2026-08-23') // ['2026-08-21', '2026-08-22', '2026-08-23']\n * ```\n */\nexport function lastNDays(count: number, today: string = todayKey()): string[] {\n const keys: string[] = []\n\n for (let offset = count - 1; offset >= 0; offset -= 1) {\n keys.push(addDays(today, -offset))\n }\n\n return keys\n}\n\n/** 0 = week starts on Sunday, 1 = on Monday. Mirrors `profiles.week_starts_on`. */\nexport type WeekStart = 0 | 1\n\n/**\n * The first day of the week containing `key`.\n *\n * The user's preference is a parameter, not a module-level setting: changing it\n * in Profile has to re-render the week grid and the year heatmap immediately,\n * and a global would make that a hidden dependency.\n *\n * @param key - Any day in the week.\n * @param weekStartsOn - 0 for Sunday, 1 for Monday.\n * @returns Key of that week's first day.\n *\n * @example\n * ```ts\n * // 2026-08-23 is a Sunday\n * startOfWeek('2026-08-23', 1) // '2026-08-17' — previous Monday\n * startOfWeek('2026-08-23', 0) // '2026-08-23' — already Sunday\n * ```\n */\nexport function startOfWeek(key: string, weekStartsOn: WeekStart): string {\n const weekday = fromDateKey(key).getDay()\n const offset = (weekday - weekStartsOn + 7) % 7\n\n return addDays(key, -offset)\n}\n\n/**\n * Every day of a calendar year, in order.\n *\n * Leap years fall out of the loop for free: it walks day by day until the year\n * rolls over, so February 29 is included when it exists.\n *\n * @param year - Four-digit year.\n * @returns 365 or 366 keys, oldest first.\n */\nexport function eachDayOfYear(year: number): string[] {\n const keys: string[] = []\n const date = new Date(year, 0, 1)\n\n while (date.getFullYear() === year) {\n keys.push(toDateKey(date))\n date.setDate(date.getDate() + 1)\n }\n\n return keys\n}\n\n/**\n * Empty cells before a block's first day in a seven-row column grid.\n *\n * The grid fills column by column, so the first column is only partly used\n * unless the block starts exactly on the week's first day. An off-by-one here\n * shifts the whole block by a row, so this is unit tested.\n *\n * @param firstDayKey - First day of the block, e.g. `'2026-02-01'`.\n * @param weekStartsOn - 0 for Sunday, 1 for Monday.\n * @returns 0-6 blank cells.\n *\n * @example\n * ```ts\n * leadingBlanks('2026-01-01', 1) // 3 — a Thursday, Mon-Wed are blank\n * leadingBlanks('2024-01-01', 1) // 0 — a Monday\n * ```\n */\nexport function leadingBlanks(firstDayKey: string, weekStartsOn: WeekStart): number {\n return (fromDateKey(firstDayKey).getDay() - weekStartsOn + 7) % 7\n}\n","import { ref } from 'vue'\n\n/**\n * The locale `Intl` formatting uses.\n *\n * Held here rather than imported from an i18n runtime so the utilities have no\n * i18n dependency at all: an app that never installs vue-i18n still gets dates\n * in the right language. `createI18nRuntime` sets this when it is used.\n */\nconst locale = ref<string>(typeof navigator === 'undefined' ? 'en' : (navigator.language ?? 'en'))\n\n/**\n * Points every formatter at a new locale.\n *\n * @example\n * ```ts\n * setFormatLocale('tr-TR')\n * ```\n */\nexport function setFormatLocale(next: string): void {\n locale.value = next\n}\n\n/**\n * `Intl.DateTimeFormat` is expensive to construct, so instances are cached per\n * locale and option set. The key includes the locale, which is what lets the\n * cache survive a language change instead of returning stale formatters.\n */\nconst cache = new Map<string, Intl.DateTimeFormat>()\n\n/**\n * Formats a date in the active locale.\n *\n * Reading the locale ref here is deliberate: called from a `computed`, the\n * result re-evaluates when the language changes.\n *\n * @param date - Date to format.\n * @param options - Passed straight to `Intl.DateTimeFormat`.\n *\n * @example\n * ```ts\n * formatDate(new Date(), { weekday: 'narrow' }) // 'T'\n * ```\n */\nexport function formatDate(date: Date, options: Intl.DateTimeFormatOptions): string {\n const tag = locale.value\n const key = `${tag}:${JSON.stringify(options)}`\n\n let formatter = cache.get(key)\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(tag, options)\n cache.set(key, formatter)\n }\n\n return formatter.format(date)\n}\n","import { addDays, fromDateKey } from './date'\nimport { formatDate } from './format'\n\n/** The two days worth naming rather than numbering. */\nexport interface DayLabels {\n today: string\n yesterday: string\n}\n\n/**\n * A short name for a day, relative to today.\n *\n * \"Today\" and \"Yesterday\" are worth spelling out — they are the two a user\n * actually reaches for. Anything older gets its weekday, which inside a\n * five-day window is unambiguous and stays two or three characters in every\n * language.\n *\n * The two words are arguments rather than translated here: a library that calls\n * `t()` forces every consumer onto one i18n setup.\n *\n * @param dateKey - The day to label (`YYYY-MM-DD`).\n * @param today - Today's key, passed in so the caller controls the clock.\n * @param labels - What to call today and yesterday.\n *\n * @example\n * ```ts\n * relativeDayLabel('2026-08-28', '2026-08-31', { today: 'Today', yesterday: 'Yesterday' })\n * // 'Fri'\n * ```\n */\nexport function relativeDayLabel(dateKey: string, today: string, labels: DayLabels): string {\n if (dateKey === today) return labels.today\n if (dateKey === addDays(today, -1)) return labels.yesterday\n\n return formatDate(fromDateKey(dateKey), { weekday: 'short' })\n}\n","/**\n * Hands the user a file without a server round trip.\n *\n * @param data - Anything `JSON.stringify` can serialise.\n * @param filename - Suggested name, e.g. `hibi-export-2026-08-24.json`.\n */\nexport function downloadJson(data: unknown, filename: string): void {\n const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n\n link.href = url\n link.download = filename\n link.click()\n\n URL.revokeObjectURL(url)\n}\n","/**\n * What a router hands back for one query key.\n *\n * Inlined rather than imported from vue-router: the shape is `string | null`\n * either way, and a helper this small should not drag a router into the\n * package's dependencies.\n */\nexport type QueryValue = string | null\n\n/**\n * Resolves a `?redirect=` query value into a safe in-app path.\n *\n * Only same-origin paths are accepted. Anything else falls back to `/`,\n * so a crafted link cannot bounce a user from the real login page to a\n * phishing clone.\n *\n * Pure: takes the query value instead of reading the router, so it also\n * works inside navigation guards and can be unit tested.\n *\n * @param target - Raw `route.query.redirect` value. May be a string, an\n * array (repeated query key), `null`, or `undefined`.\n * @returns A path starting with a single `/`. Defaults to `/`.\n *\n * @example\n * ```ts\n * // in a view\n * await router.push(safeRedirect(route.query.redirect))\n *\n * // in a guard\n * return safeRedirect(to.query.redirect)\n * ```\n *\n * @example\n * ```ts\n * safeRedirect('/week') // '/week'\n * safeRedirect('https://evil.com') // '/'\n * safeRedirect('//evil.com') // '/' (protocol-relative URL)\n * safeRedirect(['/a', '/b']) // '/'\n * safeRedirect(undefined) // '/'\n * ```\n */\nexport function safeRedirect(target: QueryValue | QueryValue[] | undefined): string {\n if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {\n return target\n }\n\n return '/'\n}\n","/**\n * A short vibration for a confirmed tap.\n *\n * Optional chaining is not decoration: iOS Safari has no `vibrate` at all, and\n * calling it unguarded would throw on every marked day.\n *\n * @param duration - Milliseconds. Keep it under ~15ms; longer reads as an alert.\n */\nexport function tapFeedback(duration = 10): void {\n navigator.vibrate?.(duration)\n}\n","/**\n * Whether the app is running from the Home Screen rather than a browser tab.\n *\n * Two checks because iOS predates the standard one: `display-mode: standalone`\n * is the modern signal, `navigator.standalone` is Safari's own.\n */\nexport function isInstalled(): boolean {\n if (typeof window === 'undefined') return false\n\n return (\n window.matchMedia('(display-mode: standalone)').matches ||\n (navigator as Navigator & { standalone?: boolean }).standalone === true\n )\n}\n\n/** iPhone and iPad, including iPadOS reporting itself as a Mac. */\nexport function isApplePortable(): boolean {\n if (typeof window === 'undefined') return false\n\n return (\n /iPad|iPhone|iPod/.test(navigator.userAgent) ||\n (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)\n )\n}\n\n/**\n * Whether this device can only receive notifications once the app is installed.\n *\n * Safari on iOS grants notification permission to an installed web app and to\n * nothing else — in a normal tab the request does not even prompt. Telling the\n * user to allow notifications there is asking for something the browser will\n * not offer, so the UI has to say \"add to Home Screen\" instead.\n *\n * @example\n * ```ts\n * if (needsIosInstall()) // show the Home Screen instruction, not the button\n * ```\n */\nexport function needsIosInstall(): boolean {\n return isApplePortable() && !isInstalled()\n}\n","import { ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** What the user asked for; `system` follows the OS. */\nexport type ThemePreference = 'system' | 'light' | 'dark'\n\n/**\n * Namespaced by the app, not by this package.\n *\n * Two rei-kit apps served from the same origin would otherwise share one theme\n * setting — and during development on localhost, they will be.\n */\nlet storageKey = 'rei-theme'\n\nexport function isThemePreference(value: unknown): value is ThemePreference {\n return value === 'system' || value === 'light' || value === 'dark'\n}\n\n/** Reads the stored preference, falling back to `system`. */\nexport function readStoredTheme(): ThemePreference {\n try {\n const stored = localStorage.getItem(storageKey)\n\n return isThemePreference(stored) ? stored : 'system'\n } catch {\n return 'system'\n }\n}\n\nfunction storeTheme(preference: ThemePreference): void {\n try {\n localStorage.setItem(storageKey, preference)\n } catch {\n // Private mode or blocked storage: the choice just will not persist.\n }\n}\n\n/**\n * Does the environment prefer a dark scheme?\n *\n * `matchMedia` is checked for on its own rather than inferred from `document`.\n * Having one does not imply having the other: jsdom supplies a document and no\n * `matchMedia`, so a consumer's component test that so much as mounts something\n * calling `useTheme` threw — and some embedded webviews are the same. Where\n * there is nothing to ask, the answer is no rather than an exception.\n */\nfunction prefersDarkScheme(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)').matches\n : false\n}\n\n/**\n * Adds or removes `.dark` on `<html>`, resolving `system` against the OS.\n *\n * A no-op without a document. There is no OS preference to read on a server and\n * no `<html>` to write to, so a prerender leaves the class off and the app\n * decides the theme before hydration — see the note in the README.\n */\nexport function applyTheme(preference: ThemePreference): void {\n if (typeof document === 'undefined') return\n\n const isDark = preference === 'dark' || (preference === 'system' && prefersDarkScheme())\n\n document.documentElement.classList.toggle('dark', isDark)\n}\n\n/**\n * The shared preference, created on first use rather than at import.\n *\n * Lazy on purpose: reading storage at import time would lock in the default key\n * before an app had a chance to set its own, leaving the controller reading one\n * key and writing another.\n */\nlet preference: Ref<ThemePreference> | null = null\n\nfunction controller(): Ref<ThemePreference> {\n if (preference) return preference\n\n preference = ref<ThemePreference>(readStoredTheme())\n\n watch(\n preference,\n (next) => {\n storeTheme(next)\n applyTheme(next)\n },\n { immediate: true },\n )\n\n // While on `system`, follow the OS if the user flips it at night. Only where\n // there is something to listen to; see `prefersDarkScheme`.\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {\n if (preference?.value === 'system') applyTheme('system')\n })\n }\n\n return preference\n}\n\n/**\n * Sets where the preference is stored.\n *\n * Safe in either order: called before the first `useTheme()` it simply changes\n * the key, and called after it re-reads under the new one, so the controller\n * never reads from one key while writing to another.\n *\n * @example\n * ```ts\n * setThemeStorageKey('hibi-theme') // once, at startup\n * ```\n */\nexport function setThemeStorageKey(key: string): void {\n storageKey = key\n if (preference) preference.value = readStoredTheme()\n}\n\n/** @returns The shared preference ref; assigning to it stores and applies it. */\nexport function useTheme(): Ref<ThemePreference> {\n return controller()\n}\n","import { readonly, ref } from 'vue'\n\nimport { todayKey } from '../utils/date'\n\n/**\n * Today's date key, kept current while the app stays open.\n *\n * `todayKey()` called once in `setup` freezes the date for the lifetime of the\n * component. Nobody notices in a session that lasts minutes, but a phone left\n * on the Today screen overnight would keep marking yesterday, and the Week grid\n * would disable the column that just became today.\n */\nconst current = ref(todayKey())\n\nlet timer: ReturnType<typeof setTimeout> | undefined\nlet watching = false\n\n/** A second past midnight, so a fast timer cannot fire on the old date. */\nfunction msUntilMidnight(): number {\n const now = new Date()\n const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 1)\n\n return next.getTime() - now.getTime()\n}\n\nfunction refresh() {\n current.value = todayKey()\n}\n\nfunction schedule() {\n clearTimeout(timer)\n timer = setTimeout(() => {\n refresh()\n schedule()\n }, msUntilMidnight())\n}\n\n/**\n * Starts the clock, once, and only where there is a clock to watch.\n *\n * This used to run at import time, which made the module impossible to load on\n * a server: `document` is not defined there, and a barrel export means one\n * `import { BaseButton } from 'rei-kit'` pulls this file in. Deferring it to\n * the first `useToday()` also means an app that never asks for today never\n * arms a timer.\n */\nfunction watchTheClock() {\n if (watching || typeof document === 'undefined') return\n\n watching = true\n schedule()\n\n // A sleeping phone does not run timers reliably, so the tab also re-checks\n // the moment it comes back — which is when the user would see a stale date.\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState !== 'visible') return\n\n refresh()\n schedule()\n })\n}\n\n/**\n * @returns Read-only ref holding today's `YYYY-MM-DD` key.\n *\n * Rendered on a server this is the *server's* today, which is a different day\n * from the visitor's either side of midnight. Anything prerendered from it\n * would hydrate to a different value; render it on the client.\n *\n * @example\n * ```ts\n * const today = useToday()\n * const isFuture = computed(() => day > today.value)\n * ```\n */\nexport function useToday() {\n watchTheClock()\n\n return readonly(current)\n}\n","import { onMounted, onUnmounted, readonly, ref } from 'vue'\n\n/**\n * Tracks whether the browser thinks it has a network connection.\n *\n * Note the limit: `navigator.onLine` only reports whether a network interface\n * is up, not whether requests actually succeed. Treat it as a hint for the UI,\n * never as a reason to skip error handling.\n *\n * Listeners are removed on unmount, so the composable is safe to call per view.\n *\n * @returns A readonly ref that flips with the browser's online/offline events.\n *\n * @example\n * ```ts\n * const isOnline = useOnline()\n * // <p v-if=\"!isOnline\">You're offline.</p>\n * ```\n */\nexport function useOnline() {\n const isOnline = ref(true)\n\n function update() {\n isOnline.value = navigator.onLine\n }\n\n onMounted(() => {\n update()\n window.addEventListener('online', update)\n window.addEventListener('offline', update)\n })\n\n onUnmounted(() => {\n window.removeEventListener('online', update)\n window.removeEventListener('offline', update)\n })\n\n return readonly(isOnline)\n}\n","import { onScopeDispose } from 'vue'\n\n/**\n * Delays a callback until the caller stops calling it.\n *\n * Used for note autosave: a request per keystroke would be wasteful, but losing\n * the last keystrokes when the user navigates away would be worse — so the\n * pending call is flushed on dispose, and `flush` is exposed for route guards.\n *\n * @param callback - Runs with the arguments of the most recent call.\n * @param delay - Quiet period in milliseconds.\n * @returns `run` to schedule, `flush` to run now, `cancel` to drop.\n *\n * @example\n * ```ts\n * const save = useDebouncedCallback((body: string) => mutate(body), 800)\n * watch(text, (value) => save.run(value))\n * onBeforeRouteLeave(() => save.flush())\n * ```\n */\nexport function useDebouncedCallback<A extends unknown[]>(\n callback: (...args: A) => void,\n delay = 800,\n) {\n let timer: ReturnType<typeof setTimeout> | null = null\n let pending: A | null = null\n\n /** Runs the pending call right now, if there is one. */\n function flush() {\n if (timer !== null) clearTimeout(timer)\n timer = null\n\n if (pending !== null) {\n const args = pending\n pending = null\n callback(...args)\n }\n }\n\n /** Drops the pending call without running it. */\n function cancel() {\n if (timer !== null) clearTimeout(timer)\n timer = null\n pending = null\n }\n\n function run(...args: A) {\n pending = args\n if (timer !== null) clearTimeout(timer)\n timer = setTimeout(flush, delay)\n }\n\n // A closing sheet or an unmounting view must not eat the last keystrokes.\n onScopeDispose(flush)\n\n return { run, flush, cancel }\n}\n","import { onScopeDispose, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** Movement before a press counts as a drag rather than a tap. */\nconst DRAG_THRESHOLD_PX = 6\n\n/**\n * Drag-to-scroll for a horizontally scrolling element.\n *\n * The app puts `touch-action: pan-y` on the page content so the tab-swipe\n * gesture keeps its pointer events — the browser never claims a horizontal\n * drag, which also means it never pans this element natively. Rather than give\n * that up, horizontal scrolling is driven here.\n *\n * @param target - The scroll container.\n * @returns `didDrag`, so a click handler can ignore the press that ended a drag.\n *\n * @example\n * ```ts\n * const scroller = ref<HTMLElement | null>(null)\n * const { didDrag } = useDragScroll(scroller)\n *\n * function onClick() {\n * if (didDrag()) return\n * // …treat as a tap\n * }\n * ```\n */\nexport function useDragScroll(target: Ref<HTMLElement | null>) {\n let pointerId: number | null = null\n let startX = 0\n let startScroll = 0\n let dragged = false\n\n function onPointerDown(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerType === 'mouse') return\n\n pointerId = event.pointerId\n startX = event.clientX\n startScroll = element.scrollLeft\n dragged = false\n }\n\n function onPointerMove(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerId !== pointerId) return\n\n const dx = event.clientX - startX\n if (!dragged && Math.abs(dx) < DRAG_THRESHOLD_PX) return\n\n // Capture only once the gesture is clearly horizontal, so a vertical scroll\n // that happens to start here still belongs to the page.\n if (!dragged) {\n dragged = true\n element.setPointerCapture(event.pointerId)\n }\n\n element.scrollLeft = startScroll - dx\n }\n\n function onPointerUp(event: PointerEvent) {\n const element = target.value\n if (element?.hasPointerCapture(event.pointerId)) {\n element.releasePointerCapture(event.pointerId)\n }\n\n pointerId = null\n }\n\n function bind(element: HTMLElement) {\n element.addEventListener('pointerdown', onPointerDown)\n element.addEventListener('pointermove', onPointerMove)\n element.addEventListener('pointerup', onPointerUp)\n element.addEventListener('pointercancel', onPointerUp)\n }\n\n function unbind(element: HTMLElement) {\n element.removeEventListener('pointerdown', onPointerDown)\n element.removeEventListener('pointermove', onPointerMove)\n element.removeEventListener('pointerup', onPointerUp)\n element.removeEventListener('pointercancel', onPointerUp)\n }\n\n watch(\n target,\n (element, previous) => {\n if (previous) unbind(previous)\n if (element) bind(element)\n },\n { immediate: true },\n )\n\n onScopeDispose(() => {\n if (target.value) unbind(target.value)\n })\n\n return { didDrag: () => dragged }\n}\n","import { onScopeDispose, readonly, ref } from 'vue'\n\n/** The visible area, once the on-screen keyboard has taken its share. */\nexport interface VisualViewportRect {\n height: number\n offsetTop: number\n}\n\n/**\n * Tracks the visual viewport.\n *\n * Chrome and Android browsers honour `interactive-widget=resizes-content`, so\n * the layout viewport already shrinks for the keyboard there. Safari on iOS\n * does not implement it: it shrinks only the *visual* viewport, leaving a sheet\n * sized in `dvh` sitting partly underneath the keyboard.\n *\n * `null` means the API is unavailable, which callers should read as \"trust the\n * layout viewport\" rather than as zero. A server has no viewport at all, so it\n * gets that same `null` — this runs during `setup`, and a component using it\n * has to survive being rendered there.\n *\n * @example\n * ```ts\n * const viewport = useVisualViewport()\n * // :style=\"viewport ? { height: `${viewport.height}px` } : undefined\"\n * ```\n */\nexport function useVisualViewport() {\n const rect = ref<VisualViewportRect | null>(null)\n\n const viewport = typeof window === 'undefined' ? undefined : window.visualViewport\n if (!viewport) return readonly(rect)\n\n function read() {\n if (!viewport) return\n\n rect.value = { height: viewport.height, offsetTop: viewport.offsetTop }\n }\n\n read()\n\n // `scroll` matters as much as `resize`: iOS shifts the visual viewport up to\n // keep the focused field visible, without changing its height.\n viewport.addEventListener('resize', read)\n viewport.addEventListener('scroll', read)\n\n onScopeDispose(() => {\n viewport.removeEventListener('resize', read)\n viewport.removeEventListener('scroll', read)\n })\n\n return readonly(rect)\n}\n","<script setup lang=\"ts\">\nconst {\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n type = 'button',\n} = defineProps<{\n variant?: 'primary' | 'ghost' | 'danger'\n size?: 'sm' | 'md'\n loading?: boolean\n disabled?: boolean\n type?: 'button' | 'submit'\n}>()\n\nconst VARIANT_CLASS = {\n primary: 'bg-primary text-white hover:bg-primary/90',\n ghost: 'bg-transparent text-ink hover:bg-muted',\n danger: 'bg-negative text-white hover:bg-negative/90',\n} as const\n\nconst SIZE_CLASS = {\n sm: 'h-9 px-3 text-sm',\n md: 'h-11 px-4 text-base',\n} as const\n</script>\n\n<template>\n <button\n :type=\"type\"\n :disabled=\"disabled || loading\"\n :aria-busy=\"loading\"\n class=\"rounded-card focus-visible:outline-primary inline-flex items-center justify-center gap-2 font-medium transition-transform duration-100 select-none focus-visible:outline-2 focus-visible:outline-offset-2 active:scale-95 disabled:pointer-events-none disabled:opacity-50\"\n :class=\"[VARIANT_CLASS[variant], SIZE_CLASS[size]]\"\n >\n <span\n v-if=\"loading\"\n class=\"size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n aria-hidden=\"true\"\n />\n <slot />\n </button>\n</template>\n","<script setup lang=\"ts\">\nconst {\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n type = 'button',\n} = defineProps<{\n variant?: 'primary' | 'ghost' | 'danger'\n size?: 'sm' | 'md'\n loading?: boolean\n disabled?: boolean\n type?: 'button' | 'submit'\n}>()\n\nconst VARIANT_CLASS = {\n primary: 'bg-primary text-white hover:bg-primary/90',\n ghost: 'bg-transparent text-ink hover:bg-muted',\n danger: 'bg-negative text-white hover:bg-negative/90',\n} as const\n\nconst SIZE_CLASS = {\n sm: 'h-9 px-3 text-sm',\n md: 'h-11 px-4 text-base',\n} as const\n</script>\n\n<template>\n <button\n :type=\"type\"\n :disabled=\"disabled || loading\"\n :aria-busy=\"loading\"\n class=\"rounded-card focus-visible:outline-primary inline-flex items-center justify-center gap-2 font-medium transition-transform duration-100 select-none focus-visible:outline-2 focus-visible:outline-offset-2 active:scale-95 disabled:pointer-events-none disabled:opacity-50\"\n :class=\"[VARIANT_CLASS[variant], SIZE_CLASS[size]]\"\n >\n <span\n v-if=\"loading\"\n class=\"size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n aria-hidden=\"true\"\n />\n <slot />\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\ndefineOptions({ inheritAttrs: false })\n\nconst {\n label,\n error = '',\n hint = '',\n type = 'text',\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the input with no accessible name at all.\n */\n labelHidden?: boolean\n type?: 'text' | 'email' | 'password' | 'number'\n}>()\n\nconst model = defineModel<string | undefined>()\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label :for=\"id\" class=\"text-ink text-sm font-medium\" :class=\"labelHidden ? 'sr-only' : ''\">\n {{ label }}\n </label>\n\n <input\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n v-bind=\"$attrs\"\n class=\"border-hair bg-surface text-ink rounded-card focus-visible:outline-primary h-11 border px-3 focus-visible:outline-2 focus-visible:outline-offset-1\"\n :class=\"error ? 'border-negative' : ''\"\n />\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\ndefineOptions({ inheritAttrs: false })\n\nconst {\n label,\n error = '',\n hint = '',\n type = 'text',\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the input with no accessible name at all.\n */\n labelHidden?: boolean\n type?: 'text' | 'email' | 'password' | 'number'\n}>()\n\nconst model = defineModel<string | undefined>()\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label :for=\"id\" class=\"text-ink text-sm font-medium\" :class=\"labelHidden ? 'sr-only' : ''\">\n {{ label }}\n </label>\n\n <input\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n v-bind=\"$attrs\"\n class=\"border-hair bg-surface text-ink rounded-card focus-visible:outline-primary h-11 border px-3 focus-visible:outline-2 focus-visible:outline-offset-1\"\n :class=\"error ? 'border-negative' : ''\"\n />\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string\n}>()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n lastFocused?.focus()\n lastFocused = null\n setBackgroundInert(false)\n }\n})\n\n/**\n * `inert` takes the whole app out of tab order and pointer events while the\n * sheet is open — a real focus trap without keydown bookkeeping.\n *\n * The sheet itself is teleported to `#sheet-root`, a sibling of `#app`, so it\n * stays interactive.\n */\nfunction setBackgroundInert(isInert: boolean) {\n document.getElementById('app')?.toggleAttribute('inert', isInert)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport to=\"#sheet-root\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel bg-surface relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[28px] shadow-2xl outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <button\n type=\"button\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </button>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity 200ms ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string\n}>()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n lastFocused?.focus()\n lastFocused = null\n setBackgroundInert(false)\n }\n})\n\n/**\n * `inert` takes the whole app out of tab order and pointer events while the\n * sheet is open — a real focus trap without keydown bookkeeping.\n *\n * The sheet itself is teleported to `#sheet-root`, a sibling of `#app`, so it\n * stays interactive.\n */\nfunction setBackgroundInert(isInert: boolean) {\n document.getElementById('app')?.toggleAttribute('inert', isInert)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport to=\"#sheet-root\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel bg-surface relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[28px] shadow-2xl outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <button\n type=\"button\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </button>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity 200ms ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script lang=\"ts\" setup>\nconst { title, description = '' } = defineProps<{\n title: string\n description?: string | undefined\n}>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-center gap-3 px-6 py-10 text-center\">\n <div\n v-if=\"$slots.icon\"\n class=\"bg-muted text-primary rounded-card flex size-12 items-center justify-center\"\n >\n <slot name=\"icon\" />\n </div>\n\n <h3 class=\"text-ink text-base font-semibold\">{{ title }}</h3>\n <p v-if=\"description\" class=\"text-ink-soft max-w-[36ch] text-sm\">\n {{ description }}\n </p>\n\n <div v-if=\"$slots.action\" class=\"mt-2 flex w-full flex-col gap-2\">\n <slot name=\"action\" />\n </div>\n </div>\n</template>\n\n<style></style>\n","<script lang=\"ts\" setup>\nconst { title, description = '' } = defineProps<{\n title: string\n description?: string | undefined\n}>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-center gap-3 px-6 py-10 text-center\">\n <div\n v-if=\"$slots.icon\"\n class=\"bg-muted text-primary rounded-card flex size-12 items-center justify-center\"\n >\n <slot name=\"icon\" />\n </div>\n\n <h3 class=\"text-ink text-base font-semibold\">{{ title }}</h3>\n <p v-if=\"description\" class=\"text-ink-soft max-w-[36ch] text-sm\">\n {{ description }}\n </p>\n\n <div v-if=\"$slots.action\" class=\"mt-2 flex w-full flex-col gap-2\">\n <slot name=\"action\" />\n </div>\n </div>\n</template>\n\n<style></style>\n","<script setup lang=\"ts\">\nconst { title } = defineProps<{ title: string }>()\n</script>\n\n<template>\n <header class=\"grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center\">\n <div class=\"justify-self-start\"><slot name=\"left\" /></div>\n\n <h1 class=\"text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums\">\n <slot name=\"title\">\n <span class=\"truncate\">{{ title }}</span>\n </slot>\n </h1>\n\n <div class=\"justify-self-end\"><slot name=\"right\" /></div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nconst { title } = defineProps<{ title: string }>()\n</script>\n\n<template>\n <header class=\"grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center\">\n <div class=\"justify-self-start\"><slot name=\"left\" /></div>\n\n <h1 class=\"text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums\">\n <slot name=\"title\">\n <span class=\"truncate\">{{ title }}</span>\n </slot>\n </h1>\n\n <div class=\"justify-self-end\"><slot name=\"right\" /></div>\n </header>\n</template>\n","<script setup lang=\"ts\">\n/**\n * A small coloured dot, optionally labelled.\n *\n * Takes the colour as a class rather than a category, so an app can key it off\n * whatever its own domain calls a category — habit kinds, expense types,\n * priorities — without this component knowing about any of them.\n */\nconst { fill, label = '' } = defineProps<{\n /** Background utility for the dot, e.g. `bg-positive`. */\n fill: string\n /** Optional text after the dot. Omit for a bare marker. */\n label?: string\n}>()\n</script>\n\n<template>\n <span class=\"inline-flex items-center gap-1.5\">\n <span class=\"size-2 rounded-full\" :class=\"fill\" />\n <span v-if=\"label\" class=\"text-ink-soft text-xs font-medium\">{{ label }}</span>\n </span>\n</template>\n","<script setup lang=\"ts\">\n/**\n * A small coloured dot, optionally labelled.\n *\n * Takes the colour as a class rather than a category, so an app can key it off\n * whatever its own domain calls a category — habit kinds, expense types,\n * priorities — without this component knowing about any of them.\n */\nconst { fill, label = '' } = defineProps<{\n /** Background utility for the dot, e.g. `bg-positive`. */\n fill: string\n /** Optional text after the dot. Omit for a bare marker. */\n label?: string\n}>()\n</script>\n\n<template>\n <span class=\"inline-flex items-center gap-1.5\">\n <span class=\"size-2 rounded-full\" :class=\"fill\" />\n <span v-if=\"label\" class=\"text-ink-soft text-xs font-medium\">{{ label }}</span>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport ToneDot from './ToneDot.vue'\n\n/** The three classes a category needs to colour a heading. */\nexport interface Tone {\n /** Solid background for the dot, e.g. `bg-positive`. */\n fill: string\n /** Tinted surface for the pill, e.g. `bg-positive/5 border-positive/25`. */\n card: string\n /** Foreground that pairs with the surface, e.g. `text-positive`. */\n text: string\n}\n\n/**\n * A pill heading for a group of things.\n *\n * The tone arrives as three class strings rather than a category name: Tailwind\n * reads source files as plain text, so a class assembled at runtime never\n * reaches the stylesheet — the app has to write them out, and it is the app\n * that knows its own categories anyway.\n */\nconst {\n tone,\n label,\n count = 0,\n} = defineProps<{\n tone: Tone\n label: string\n /** Hidden when zero, so an empty group's heading stays quiet. */\n count?: number\n}>()\n</script>\n\n<template>\n <h2 class=\"flex items-center gap-2 self-start rounded-full border px-3 py-1\" :class=\"tone.card\">\n <ToneDot :fill=\"tone.fill\" />\n <span class=\"text-xs font-semibold tracking-wide uppercase\" :class=\"tone.text\">\n {{ label }}\n </span>\n <span v-if=\"count > 0\" class=\"text-ink-soft text-xs tabular-nums\">{{ count }}</span>\n </h2>\n</template>\n","<script setup lang=\"ts\">\nimport ToneDot from './ToneDot.vue'\n\n/** The three classes a category needs to colour a heading. */\nexport interface Tone {\n /** Solid background for the dot, e.g. `bg-positive`. */\n fill: string\n /** Tinted surface for the pill, e.g. `bg-positive/5 border-positive/25`. */\n card: string\n /** Foreground that pairs with the surface, e.g. `text-positive`. */\n text: string\n}\n\n/**\n * A pill heading for a group of things.\n *\n * The tone arrives as three class strings rather than a category name: Tailwind\n * reads source files as plain text, so a class assembled at runtime never\n * reaches the stylesheet — the app has to write them out, and it is the app\n * that knows its own categories anyway.\n */\nconst {\n tone,\n label,\n count = 0,\n} = defineProps<{\n tone: Tone\n label: string\n /** Hidden when zero, so an empty group's heading stays quiet. */\n count?: number\n}>()\n</script>\n\n<template>\n <h2 class=\"flex items-center gap-2 self-start rounded-full border px-3 py-1\" :class=\"tone.card\">\n <ToneDot :fill=\"tone.fill\" />\n <span class=\"text-xs font-semibold tracking-wide uppercase\" :class=\"tone.text\">\n {{ label }}\n </span>\n <span v-if=\"count > 0\" class=\"text-ink-soft text-xs tabular-nums\">{{ count }}</span>\n </h2>\n</template>\n","<script setup lang=\"ts\" generic=\"T extends string | number\">\nimport { useId } from 'vue'\n\n/**\n * A row of mutually exclusive choices.\n *\n * Radio inputs rather than buttons: it is a single choice out of a small set,\n * so arrow-key navigation and the \"one of N selected\" announcement come free.\n */\nconst { options } = defineProps<{\n options: readonly { value: T; label: string }[]\n}>()\n\nconst model = defineModel<T>({ required: true })\n\nconst name = useId()\n</script>\n\n<template>\n <div class=\"bg-muted rounded-card flex w-full gap-1 p-1\">\n <label v-for=\"option in options\" :key=\"String(option.value)\" class=\"flex-1 cursor-pointer\">\n <input v-model=\"model\" type=\"radio\" :value=\"option.value\" :name=\"name\" class=\"sr-only\" />\n <span\n class=\"flex h-10 items-center justify-center rounded-xl px-2 text-sm font-medium transition-colors select-none\"\n :class=\"model === option.value ? 'bg-surface text-ink shadow-sm' : 'text-ink-soft'\"\n >\n {{ option.label }}\n </span>\n </label>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"T extends string | number\">\nimport { useId } from 'vue'\n\n/**\n * A row of mutually exclusive choices.\n *\n * Radio inputs rather than buttons: it is a single choice out of a small set,\n * so arrow-key navigation and the \"one of N selected\" announcement come free.\n */\nconst { options } = defineProps<{\n options: readonly { value: T; label: string }[]\n}>()\n\nconst model = defineModel<T>({ required: true })\n\nconst name = useId()\n</script>\n\n<template>\n <div class=\"bg-muted rounded-card flex w-full gap-1 p-1\">\n <label v-for=\"option in options\" :key=\"String(option.value)\" class=\"flex-1 cursor-pointer\">\n <input v-model=\"model\" type=\"radio\" :value=\"option.value\" :name=\"name\" class=\"sr-only\" />\n <span\n class=\"flex h-10 items-center justify-center rounded-xl px-2 text-sm font-medium transition-colors select-none\"\n :class=\"model === option.value ? 'bg-surface text-ink shadow-sm' : 'text-ink-soft'\"\n >\n {{ option.label }}\n </span>\n </label>\n </div>\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{ title: string }>()\n</script>\n\n<template>\n <section class=\"flex flex-col gap-2\">\n <h2 class=\"text-ink-soft px-1 text-xs font-semibold tracking-wide uppercase\">{{ title }}</h2>\n\n <!-- One card per group, rows divided by hairlines. Loose fields floating on\n the page gave no sense of what belonged with what. -->\n <div class=\"border-hair bg-surface rounded-card divide-hair divide-y overflow-hidden border\">\n <slot />\n </div>\n </section>\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{ title: string }>()\n</script>\n\n<template>\n <section class=\"flex flex-col gap-2\">\n <h2 class=\"text-ink-soft px-1 text-xs font-semibold tracking-wide uppercase\">{{ title }}</h2>\n\n <!-- One card per group, rows divided by hairlines. Loose fields floating on\n the page gave no sense of what belonged with what. -->\n <div class=\"border-hair bg-surface rounded-card divide-hair divide-y overflow-hidden border\">\n <slot />\n </div>\n </section>\n</template>\n","<script setup lang=\"ts\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport type { Component } from 'vue'\n\n/**\n * One line in a settings card.\n *\n * `as` decides the element: a row that navigates has to be a button, and a row\n * that merely holds a control must not be, or the control becomes unreachable.\n */\nconst {\n label,\n description = '',\n icon = undefined,\n interactive = false,\n stacked = false,\n} = defineProps<{\n label: string\n description?: string\n icon?: Component | undefined\n /** Renders the row as a button with a chevron. */\n interactive?: boolean\n /** Puts the control on its own line below the label, for wide controls. */\n stacked?: boolean\n}>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <component\n :is=\"interactive ? 'button' : 'div'\"\n :type=\"interactive ? 'button' : undefined\"\n class=\"flex w-full items-center gap-3 px-4 py-3 text-left\"\n :class=\"[\n interactive ? 'hover:bg-muted/60 transition-colors active:scale-[0.99]' : '',\n stacked ? 'flex-col items-stretch gap-3' : '',\n ]\"\n @click=\"interactive && emit('click')\"\n >\n <div class=\"flex items-center gap-3\">\n <span\n v-if=\"icon\"\n class=\"bg-muted text-ink-soft flex size-9 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"icon\" class=\"size-[18px]\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-medium\">{{ label }}</p>\n <p v-if=\"description\" class=\"text-ink-soft mt-0.5 text-xs leading-snug\">\n {{ description }}\n </p>\n </div>\n\n <div v-if=\"!stacked\" class=\"shrink-0\"><slot /></div>\n\n <ChevronRight v-if=\"interactive\" class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <div v-if=\"stacked\"><slot /></div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport type { Component } from 'vue'\n\n/**\n * One line in a settings card.\n *\n * `as` decides the element: a row that navigates has to be a button, and a row\n * that merely holds a control must not be, or the control becomes unreachable.\n */\nconst {\n label,\n description = '',\n icon = undefined,\n interactive = false,\n stacked = false,\n} = defineProps<{\n label: string\n description?: string\n icon?: Component | undefined\n /** Renders the row as a button with a chevron. */\n interactive?: boolean\n /** Puts the control on its own line below the label, for wide controls. */\n stacked?: boolean\n}>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <component\n :is=\"interactive ? 'button' : 'div'\"\n :type=\"interactive ? 'button' : undefined\"\n class=\"flex w-full items-center gap-3 px-4 py-3 text-left\"\n :class=\"[\n interactive ? 'hover:bg-muted/60 transition-colors active:scale-[0.99]' : '',\n stacked ? 'flex-col items-stretch gap-3' : '',\n ]\"\n @click=\"interactive && emit('click')\"\n >\n <div class=\"flex items-center gap-3\">\n <span\n v-if=\"icon\"\n class=\"bg-muted text-ink-soft flex size-9 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"icon\" class=\"size-[18px]\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-medium\">{{ label }}</p>\n <p v-if=\"description\" class=\"text-ink-soft mt-0.5 text-xs leading-snug\">\n {{ description }}\n </p>\n </div>\n\n <div v-if=\"!stacked\" class=\"shrink-0\"><slot /></div>\n\n <ChevronRight v-if=\"interactive\" class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <div v-if=\"stacked\"><slot /></div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst {\n rows = 3,\n rowHeight = 'h-14',\n label = 'Loading…',\n} = defineProps<{\n rows?: number\n /**\n * How tall each row is, as either a utility class (`h-20`) or a CSS length\n * (`5rem`, `72px`, `var(--row)`).\n *\n * Both are accepted because the class-only version failed silently: a length\n * passed here landed in `class` as `5rem`, which is not a class, so the rows\n * had no height and the placeholder rendered as nothing at all. A loading\n * state that shows an empty page is worse than no loading state, because it\n * looks like the page is finished and empty.\n */\n rowHeight?: string\n label?: string\n}>()\n\n/** A length starts with a digit, a dot, or opens a CSS function. */\nconst isLength = computed(() => /^(?:[.\\d]|calc\\(|var\\(|clamp\\(|min\\(|max\\()/.test(rowHeight))\n</script>\n\n<template>\n <div role=\"status\" class=\"flex flex-col gap-1\">\n <span class=\"sr-only\">{{ label }}</span>\n\n <div\n v-for=\"row in rows\"\n :key=\"row\"\n class=\"bg-muted rounded-card animate-pulse\"\n :class=\"isLength ? undefined : rowHeight\"\n :style=\"isLength ? { height: rowHeight } : undefined\"\n aria-hidden=\"true\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst {\n rows = 3,\n rowHeight = 'h-14',\n label = 'Loading…',\n} = defineProps<{\n rows?: number\n /**\n * How tall each row is, as either a utility class (`h-20`) or a CSS length\n * (`5rem`, `72px`, `var(--row)`).\n *\n * Both are accepted because the class-only version failed silently: a length\n * passed here landed in `class` as `5rem`, which is not a class, so the rows\n * had no height and the placeholder rendered as nothing at all. A loading\n * state that shows an empty page is worse than no loading state, because it\n * looks like the page is finished and empty.\n */\n rowHeight?: string\n label?: string\n}>()\n\n/** A length starts with a digit, a dot, or opens a CSS function. */\nconst isLength = computed(() => /^(?:[.\\d]|calc\\(|var\\(|clamp\\(|min\\(|max\\()/.test(rowHeight))\n</script>\n\n<template>\n <div role=\"status\" class=\"flex flex-col gap-1\">\n <span class=\"sr-only\">{{ label }}</span>\n\n <div\n v-for=\"row in rows\"\n :key=\"row\"\n class=\"bg-muted rounded-card animate-pulse\"\n :class=\"isLength ? undefined : rowHeight\"\n :style=\"isLength ? { height: rowHeight } : undefined\"\n aria-hidden=\"true\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ArrowDown, ArrowRight, ArrowUp } from 'lucide-vue-next'\n\nconst {\n value,\n label,\n trend = null,\n} = defineProps<{\n value: string\n label: string\n trend?: 'up' | 'down' | 'flat' | null\n}>()\n\nconst TREND_ICON = { up: ArrowUp, down: ArrowDown, flat: ArrowRight } as const\n</script>\n\n<template>\n <div class=\"border-hair rounded-card flex flex-1 flex-col gap-0.5 border p-3\">\n <div class=\"flex items-baseline gap-1\">\n <span class=\"text-ink text-xl font-semibold tabular-nums\">{{ value }}</span>\n <component :is=\"TREND_ICON[trend]\" v-if=\"trend\" class=\"text-ink-soft size-3\" />\n </div>\n <span class=\"text-ink-soft text-xs\">{{ label }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ArrowDown, ArrowRight, ArrowUp } from 'lucide-vue-next'\n\nconst {\n value,\n label,\n trend = null,\n} = defineProps<{\n value: string\n label: string\n trend?: 'up' | 'down' | 'flat' | null\n}>()\n\nconst TREND_ICON = { up: ArrowUp, down: ArrowDown, flat: ArrowRight } as const\n</script>\n\n<template>\n <div class=\"border-hair rounded-card flex flex-1 flex-col gap-0.5 border p-3\">\n <div class=\"flex items-baseline gap-1\">\n <span class=\"text-ink text-xl font-semibold tabular-nums\">{{ value }}</span>\n <component :is=\"TREND_ICON[trend]\" v-if=\"trend\" class=\"text-ink-soft size-3\" />\n </div>\n <span class=\"text-ink-soft text-xs\">{{ label }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"L extends string\">\n/**\n * A flat language switcher for screens with no Settings behind them.\n *\n * The list and the labels are props: only the app knows which languages it\n * ships, and endonyms — each language written in itself — are what make the\n * right option legible to someone who cannot read the current interface.\n */\nconst {\n locales,\n labels,\n label = '',\n} = defineProps<{\n locales: readonly L[]\n /** Endonyms, e.g. `{ en: 'English', tr: 'Türkçe' }`. */\n labels: Record<L, string>\n /** Accessible name for the group. */\n label?: string\n}>()\n\n/**\n * Two-way bound rather than taking the runtime's ref as a prop: props are not\n * unwrapped in a template and cannot be assigned to, so the ref would compare\n * against itself and the click handler would not compile.\n */\nconst preference = defineModel<'system' | L>({ required: true })\n</script>\n\n<template>\n <nav class=\"flex flex-wrap items-center justify-center gap-1\" :aria-label=\"label || undefined\">\n <button\n v-for=\"locale in locales\"\n :key=\"locale\"\n type=\"button\"\n :lang=\"locale\"\n class=\"rounded-full px-2.5 py-1.5 text-xs transition-colors\"\n :class=\"\n preference === locale ? 'bg-muted text-ink font-semibold' : 'text-ink-soft hover:text-ink'\n \"\n :aria-pressed=\"preference === locale\"\n @click=\"preference = locale\"\n >\n {{ labels[locale] }}\n </button>\n </nav>\n</template>\n","<script setup lang=\"ts\" generic=\"L extends string\">\n/**\n * A flat language switcher for screens with no Settings behind them.\n *\n * The list and the labels are props: only the app knows which languages it\n * ships, and endonyms — each language written in itself — are what make the\n * right option legible to someone who cannot read the current interface.\n */\nconst {\n locales,\n labels,\n label = '',\n} = defineProps<{\n locales: readonly L[]\n /** Endonyms, e.g. `{ en: 'English', tr: 'Türkçe' }`. */\n labels: Record<L, string>\n /** Accessible name for the group. */\n label?: string\n}>()\n\n/**\n * Two-way bound rather than taking the runtime's ref as a prop: props are not\n * unwrapped in a template and cannot be assigned to, so the ref would compare\n * against itself and the click handler would not compile.\n */\nconst preference = defineModel<'system' | L>({ required: true })\n</script>\n\n<template>\n <nav class=\"flex flex-wrap items-center justify-center gap-1\" :aria-label=\"label || undefined\">\n <button\n v-for=\"locale in locales\"\n :key=\"locale\"\n type=\"button\"\n :lang=\"locale\"\n class=\"rounded-full px-2.5 py-1.5 text-xs transition-colors\"\n :class=\"\n preference === locale ? 'bg-muted text-ink font-semibold' : 'text-ink-soft hover:text-ink'\n \"\n :aria-pressed=\"preference === locale\"\n @click=\"preference = locale\"\n >\n {{ labels[locale] }}\n </button>\n </nav>\n</template>\n","<script setup lang=\"ts\">\nconst { label } = defineProps<{ label: string }>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <button\n type=\"button\"\n class=\"border-hair bg-surface text-ink rounded-card hover:bg-muted flex h-11 w-full items-center justify-center gap-2 border text-sm font-medium transition-colors active:scale-95\"\n @click=\"emit('click')\"\n >\n <!-- Google asks for its own mark, so it is inlined rather than themed. -->\n <svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n <path\n fill=\"#EA4335\"\n d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"\n />\n <path\n fill=\"#4285F4\"\n d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"\n />\n </svg>\n {{ label }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nconst { label } = defineProps<{ label: string }>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <button\n type=\"button\"\n class=\"border-hair bg-surface text-ink rounded-card hover:bg-muted flex h-11 w-full items-center justify-center gap-2 border text-sm font-medium transition-colors active:scale-95\"\n @click=\"emit('click')\"\n >\n <!-- Google asks for its own mark, so it is inlined rather than themed. -->\n <svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n <path\n fill=\"#EA4335\"\n d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"\n />\n <path\n fill=\"#4285F4\"\n d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"\n />\n </svg>\n {{ label }}\n </button>\n</template>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\nimport type { Component } from 'vue'\n\nimport { tapFeedback } from '../utils/haptics'\n\nexport interface TabItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Text under the icon. Already translated. */\n label: string\n icon: Component\n}\n\n/**\n * The floating bottom bar.\n *\n * Items and the active key are props: the package has no opinion about how an\n * app names its screens, and reading `route.meta` here would force one.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly TabItem<K>[]\n /** Which item is current. Usually from `route.meta`. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string\n}>()\n</script>\n\n<template>\n <header class=\"tab-bar\">\n <nav class=\"tab-bar-inner\" :aria-label=\"label || undefined\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"tab-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n @click=\"tapFeedback()\"\n >\n <span class=\"tab-icon-slot\">\n <component :is=\"item.icon\" class=\"tab-icon\" />\n </span>\n <span class=\"tab-label\">{{ item.label }}</span>\n </RouterLink>\n </nav>\n </header>\n</template>\n\n<style scoped>\n@reference \"../styles/_reference.css\";\n\n/* absolute, not fixed: the bar hangs inside the app shell. Fixed would pin it\n to the browser window, which on a desktop is nowhere near the app. */\n.tab-bar {\n @apply absolute left-1/2 z-40 w-full max-w-[360px] -translate-x-1/2 px-4;\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.tab-bar-inner {\n @apply border-hair bg-surface/85 flex items-center justify-between gap-1 border p-1.5 shadow-lg backdrop-blur-md;\n border-radius: var(--radius-shell);\n}\n\n.tab-link {\n @apply text-ink-soft flex min-h-[52px] flex-1 cursor-pointer flex-col items-center justify-center gap-1 py-1.5;\n border-radius: calc(var(--radius-shell) - 6px);\n /* Only the icon reacts to a press. Scaling the whole link drags the label and\n the pill with it, which reads as the bar wobbling. */\n transition: color 200ms ease;\n}\n\n.tab-link:hover {\n @apply text-ink;\n}\n\n/* The pill sits behind the icon rather than the link, so the active tab grows a\n marker instead of the row changing shape. */\n.tab-icon-slot {\n @apply flex h-7 w-12 items-center justify-center rounded-full transition-all duration-200 ease-out;\n}\n\n.tab-link:active .tab-icon-slot {\n transform: scale(0.88);\n}\n\n.is-active {\n @apply text-primary;\n}\n\n.is-active .tab-icon-slot {\n @apply bg-muted;\n}\n\n.tab-icon {\n @apply size-[18px] stroke-2 transition-transform duration-200;\n}\n\n.is-active .tab-icon {\n @apply scale-110 stroke-[2.5px];\n}\n\n.tab-label {\n @apply text-[10px] leading-none font-medium;\n}\n\n.is-active .tab-label {\n @apply font-semibold;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tab-icon-slot,\n .tab-icon {\n transition: none;\n }\n .tab-link:active .tab-icon-slot {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\nimport type { Component } from 'vue'\n\nimport { tapFeedback } from '../utils/haptics'\n\nexport interface TabItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Text under the icon. Already translated. */\n label: string\n icon: Component\n}\n\n/**\n * The floating bottom bar.\n *\n * Items and the active key are props: the package has no opinion about how an\n * app names its screens, and reading `route.meta` here would force one.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly TabItem<K>[]\n /** Which item is current. Usually from `route.meta`. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string\n}>()\n</script>\n\n<template>\n <header class=\"tab-bar\">\n <nav class=\"tab-bar-inner\" :aria-label=\"label || undefined\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"tab-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n @click=\"tapFeedback()\"\n >\n <span class=\"tab-icon-slot\">\n <component :is=\"item.icon\" class=\"tab-icon\" />\n </span>\n <span class=\"tab-label\">{{ item.label }}</span>\n </RouterLink>\n </nav>\n </header>\n</template>\n\n<style scoped>\n@reference \"../styles/_reference.css\";\n\n/* absolute, not fixed: the bar hangs inside the app shell. Fixed would pin it\n to the browser window, which on a desktop is nowhere near the app. */\n.tab-bar {\n @apply absolute left-1/2 z-40 w-full max-w-[360px] -translate-x-1/2 px-4;\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.tab-bar-inner {\n @apply border-hair bg-surface/85 flex items-center justify-between gap-1 border p-1.5 shadow-lg backdrop-blur-md;\n border-radius: var(--radius-shell);\n}\n\n.tab-link {\n @apply text-ink-soft flex min-h-[52px] flex-1 cursor-pointer flex-col items-center justify-center gap-1 py-1.5;\n border-radius: calc(var(--radius-shell) - 6px);\n /* Only the icon reacts to a press. Scaling the whole link drags the label and\n the pill with it, which reads as the bar wobbling. */\n transition: color 200ms ease;\n}\n\n.tab-link:hover {\n @apply text-ink;\n}\n\n/* The pill sits behind the icon rather than the link, so the active tab grows a\n marker instead of the row changing shape. */\n.tab-icon-slot {\n @apply flex h-7 w-12 items-center justify-center rounded-full transition-all duration-200 ease-out;\n}\n\n.tab-link:active .tab-icon-slot {\n transform: scale(0.88);\n}\n\n.is-active {\n @apply text-primary;\n}\n\n.is-active .tab-icon-slot {\n @apply bg-muted;\n}\n\n.tab-icon {\n @apply size-[18px] stroke-2 transition-transform duration-200;\n}\n\n.is-active .tab-icon {\n @apply scale-110 stroke-[2.5px];\n}\n\n.tab-label {\n @apply text-[10px] leading-none font-medium;\n}\n\n.is-active .tab-label {\n @apply font-semibold;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tab-icon-slot,\n .tab-icon {\n transition: none;\n }\n .tab-link:active .tab-icon-slot {\n transform: none;\n }\n}\n</style>\n","import { computed, ref, watchEffect } from 'vue'\nimport { createI18n } from 'vue-i18n'\n\nimport { setFormatLocale } from '../utils/format'\n\n/** What the user picked. `system` re-reads the browser on every launch. */\nexport type LocalePreference<L extends string> = 'system' | L\n\nexport interface I18nRuntimeOptions<L extends string, Schema> {\n /** Languages the app ships, in no particular order. */\n locales: readonly L[]\n /** The one that is always loaded, and the fallback when a load fails. */\n fallback: L\n /**\n * BCP 47 tag per locale, for `Intl`.\n *\n * Message lookup only needs the base language, but dates and numbers need a\n * region to be right — `zh` alone would leave the formatter to guess.\n */\n intlTags: Record<L, string>\n /** The fallback's messages, bundled. */\n messages: Schema\n /** The rest, fetched only when they are the one in use. */\n loaders?: Partial<Record<L, () => Promise<{ default: Schema }>>>\n /** Where the choice is stored. Namespace it per app. */\n storageKey?: string\n}\n\n/**\n * Builds an i18n runtime around an app's own catalogue.\n *\n * A factory rather than a module singleton because the schema is the app's:\n * typing every locale as `typeof en` is what makes a missing key a build error,\n * and this package has no `en` of its own to type against.\n *\n * @example\n * ```ts\n * export const { i18n, t, useLocalePreference, loadActiveLocale } =\n * createI18nRuntime({\n * locales: ['en', 'tr'] as const,\n * fallback: 'en',\n * intlTags: { en: 'en-GB', tr: 'tr-TR' },\n * messages: en,\n * loaders: { tr: () => import('./locales/tr') },\n * storageKey: 'myapp-locale',\n * })\n * ```\n */\nexport function createI18nRuntime<L extends string, Schema extends Record<string, unknown>>(\n options: I18nRuntimeOptions<L, Schema>,\n) {\n const { locales, fallback, intlTags, messages, storageKey = 'rei-locale' } = options\n\n // Typed rather than defaulted to `{}`, which erases the locale keys and makes\n // `loaders[locale]` an index into an empty object.\n const loaders: Partial<Record<L, () => Promise<{ default: Schema }>>> = options.loaders ?? {}\n\n function isSupported(value: string): value is L {\n return (locales as readonly string[]).includes(value)\n }\n\n /**\n * First browser language the app can actually speak.\n *\n * `navigator.languages` is ordered by the user's own preference, so the first\n * match is the best one — not simply the first entry.\n */\n function detectSystemLocale(): L {\n // No browser to ask. The fallback is the right answer on a server: it is\n // the locale whose messages are bundled, so it is the only one that could\n // render without a load.\n //\n // The test is `document`, not `navigator`. Node has shipped a global\n // `navigator` since v21, so `typeof navigator === 'undefined'` is false on\n // a server and this would read the *build machine's* language and bake it\n // into every prerendered page. `document` is the only one of the two that\n // still means \"a browser\".\n if (typeof document === 'undefined') return fallback\n\n for (const tag of navigator.languages ?? [navigator.language]) {\n const base = tag.split('-')[0]?.toLowerCase()\n if (base && isSupported(base)) return base\n }\n\n return fallback\n }\n\n function readStored(): LocalePreference<L> {\n try {\n const stored = localStorage.getItem(storageKey)\n if (stored === 'system' || (stored && isSupported(stored))) return stored\n } catch {\n // Storage blocked; fall through to the system language.\n }\n\n return 'system'\n }\n\n const preference = ref<LocalePreference<L>>(readStored())\n\n const activeLocale = computed<L>(() =>\n preference.value === 'system' ? detectSystemLocale() : (preference.value as L),\n )\n\n const intlLocale = computed(() => intlTags[activeLocale.value])\n\n // Only the fallback at construction; the rest arrive through\n // setLocaleMessage.\n const initial = { [fallback]: messages } as Record<string, Record<string, unknown>>\n\n const i18n = createI18n({\n legacy: false,\n locale: activeLocale.value as string,\n fallbackLocale: fallback as string,\n messages: initial,\n } as unknown as Parameters<typeof createI18n>[0])\n\n /**\n * A narrow view of the instance.\n *\n * vue-i18n infers its own generics from the messages it is handed, which\n * fights a runtime that is generic over the app's schema. Casting once, here,\n * keeps that fight out of every call site — and the surface below is the\n * whole of what this runtime uses.\n */\n const core = i18n.global as unknown as {\n locale: { value: string }\n setLocaleMessage: (locale: string, messages: Schema) => void\n t: (key: string, named?: Record<string, unknown>) => string\n }\n\n const loaded = new Set<L>([fallback])\n\n /**\n * Makes sure a locale's messages are in place before it becomes active.\n *\n * Awaited rather than fired and forgotten: setting the locale first paints one\n * frame of the fallback at every other user, which is the flash a fallback\n * exists to prevent, not cause.\n */\n async function ensureMessages(locale: L): Promise<void> {\n if (loaded.has(locale)) return\n\n const load = loaders[locale]\n if (!load) return\n\n try {\n const module = await load()\n core.setLocaleMessage(locale, module.default)\n loaded.add(locale)\n } catch {\n // Offline, or a stale chunk after a deploy. The fallback is loaded and\n // will carry the UI, which beats a blank screen.\n }\n }\n\n /** Loads whatever the stored preference resolves to. Call before mounting. */\n function loadActiveLocale(): Promise<void> {\n return ensureMessages(activeLocale.value)\n }\n\n // Keeps vue-i18n, `Intl` and the document in step. `lang` matters beyond\n // tidiness: it drives hyphenation, font fallback and screen readers.\n watchEffect(() => {\n core.locale.value = activeLocale.value\n setFormatLocale(intlLocale.value)\n\n if (typeof document !== 'undefined') {\n document.documentElement.lang = activeLocale.value\n }\n })\n\n /** Read and write the language preference. */\n function useLocalePreference() {\n return computed<LocalePreference<L>>({\n get: () => preference.value,\n set: (next) => {\n const resolved = next === 'system' ? detectSystemLocale() : (next as L)\n\n // Messages first, then the switch — the other order shows the fallback\n // for a frame on the way to the language the user just picked.\n void ensureMessages(resolved).then(() => {\n preference.value = next\n })\n\n try {\n localStorage.setItem(storageKey, next)\n } catch {\n // Storage blocked; the choice lasts for this session only.\n }\n },\n })\n }\n\n return {\n i18n,\n /** `t` for code outside a component. Tracks the locale inside a computed. */\n t: core.t,\n activeLocale,\n intlLocale,\n ensureMessages,\n loadActiveLocale,\n useLocalePreference,\n }\n}\n","/**\n * rei-kit — the layer every app starts from.\n *\n * Everything here is free of any backend, router or i18n choice. Components\n * take strings rather than calling a translator, and utilities take the clock\n * rather than reading it, so nothing in this package can force a decision on\n * the app that installs it.\n *\n * @see https://github.com/ramazandogna/rei-kit\n */\n\nexport const VERSION = '0.0.0'\n\n// ── Utilities ──────────────────────────────────────────────────────────────\nexport {\n addDays,\n eachDayOfYear,\n fromDateKey,\n lastNDays,\n leadingBlanks,\n startOfWeek,\n toDateKey,\n todayKey,\n} from './utils/date'\nexport type { WeekStart } from './utils/date'\n\nexport { formatDate, setFormatLocale } from './utils/format'\nexport { relativeDayLabel } from './utils/day-label'\nexport type { DayLabels } from './utils/day-label'\n\nexport { downloadJson } from './utils/download'\nexport { safeRedirect } from './utils/redirect'\nexport type { QueryValue } from './utils/redirect'\nexport { tapFeedback } from './utils/haptics'\nexport { isApplePortable, isInstalled, needsIosInstall } from './utils/platform'\n\nexport { AppError, registerErrorMapper, toAppError } from './utils/app-error'\nexport type { AppErrorKind, ErrorMapper } from './utils/app-error'\n\n// ── Composables ────────────────────────────────────────────────────────────\nexport {\n applyTheme,\n isThemePreference,\n readStoredTheme,\n setThemeStorageKey,\n useTheme,\n} from './composables/use-theme'\nexport type { ThemePreference } from './composables/use-theme'\n\nexport { useToday } from './composables/use-today'\nexport { useOnline } from './composables/use-online'\nexport { useDebouncedCallback } from './composables/use-debounced-callback'\nexport { useDragScroll } from './composables/use-drag-scroll'\nexport { useVisualViewport } from './composables/use-visual-viewport'\nexport type { VisualViewportRect } from './composables/use-visual-viewport'\n\n// ── Components ─────────────────────────────────────────────────────────────\nexport { default as BaseButton } from './components/BaseButton.vue'\nexport { default as BaseInput } from './components/BaseInput.vue'\nexport { default as BaseSheet } from './components/BaseSheet.vue'\nexport { default as EmptyState } from './components/EmptyState.vue'\nexport { default as PageHeader } from './components/PageHeader.vue'\nexport { default as SectionHeading } from './components/SectionHeading.vue'\nexport { default as SegmentedControl } from './components/SegmentedControl.vue'\nexport { default as SettingsGroup } from './components/SettingsGroup.vue'\nexport { default as SettingsRow } from './components/SettingsRow.vue'\nexport { default as SkeletonList } from './components/SkeletonList.vue'\nexport { default as StatCard } from './components/StatCard.vue'\nexport { default as ToneDot } from './components/ToneDot.vue'\nexport type { Tone } from './components/SectionHeading.vue'\nexport { default as LocaleLinks } from './components/LocaleLinks.vue'\nexport { default as GoogleButton } from './components/GoogleButton.vue'\nexport { default as TabBar } from './components/TabBar.vue'\nexport type { TabItem } from './components/TabBar.vue'\n\n// ── i18n ───────────────────────────────────────────────────────────────────\nexport { createI18nRuntime } from './i18n/runtime'\nexport type { I18nRuntimeOptions, LocalePreference } from './i18n/runtime'\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["$attrs","$slots","$slots"],"sources":["../src/utils/date.ts","../src/utils/format.ts","../src/utils/day-label.ts","../src/utils/download.ts","../src/utils/redirect.ts","../src/utils/haptics.ts","../src/utils/platform.ts","../src/composables/use-theme.ts","../src/composables/use-today.ts","../src/composables/use-online.ts","../src/composables/use-debounced-callback.ts","../src/composables/use-drag-scroll.ts","../src/composables/use-visual-viewport.ts","../src/components/BaseButton.vue","../src/components/BaseButton.vue","../src/components/BaseInput.vue","../src/components/BaseInput.vue","../src/components/BaseSheet.vue","../src/components/BaseSheet.vue","../src/components/EmptyState.vue","../src/components/EmptyState.vue","../src/components/PageHeader.vue","../src/components/PageHeader.vue","../src/components/PriceCard.vue","../src/components/PriceCard.vue","../src/components/ToneDot.vue","../src/components/ToneDot.vue","../src/components/SectionHeading.vue","../src/components/SectionHeading.vue","../src/components/SegmentedControl.vue","../src/components/SegmentedControl.vue","../src/components/SettingsGroup.vue","../src/components/SettingsGroup.vue","../src/components/SettingsRow.vue","../src/components/SettingsRow.vue","../src/components/SkeletonList.vue","../src/components/SkeletonList.vue","../src/components/StatCard.vue","../src/components/StatCard.vue","../src/components/LocaleLinks.vue","../src/components/LocaleLinks.vue","../src/components/GoogleButton.vue","../src/components/GoogleButton.vue","../src/components/TabBar.vue","../src/components/TabBar.vue","../src/i18n/runtime.ts","../src/index.ts"],"sourcesContent":["/**\n * Local calendar-day helpers.\n *\n * Every function is pure and works on `YYYY-MM-DD` keys, the same shape as the\n * `date` columns in Postgres. Nothing here calls `toISOString`: that converts to\n * UTC, so in a UTC+9 timezone every entry made between midnight and 09:00 would\n * be written to the previous day.\n */\n\n/**\n * Formats a `Date` as a local `YYYY-MM-DD` key.\n *\n * @param date - Any `Date`; only its local year, month and day are read.\n * @returns The calendar day in the runtime's own timezone.\n *\n * @example\n * ```ts\n * // 2026-08-23 01:30 in Tokyo\n * toDateKey(new Date()) // '2026-08-23'\n * new Date().toISOString() // '2026-08-22T16:30…' ← the bug\n * ```\n */\nexport function toDateKey(date: Date): string {\n const year = String(date.getFullYear()).padStart(4, '0')\n const month = String(date.getMonth() + 1).padStart(2, '0')\n const day = String(date.getDate()).padStart(2, '0')\n\n return `${year}-${month}-${day}`\n}\n\n/** Today's key in the user's own timezone. */\nexport function todayKey(): string {\n return toDateKey(new Date())\n}\n\n/**\n * Parses a `YYYY-MM-DD` key into a `Date` at local midnight.\n *\n * @param key - A key produced by {@link toDateKey}.\n * @returns Local midnight of that calendar day.\n * @throws If the key is not three numeric parts.\n *\n * @example\n * ```ts\n * fromDateKey('2026-08-23') // local midnight, correct\n * new Date('2026-08-23') // UTC midnight — shifts a day in some zones\n * ```\n */\nexport function fromDateKey(key: string): Date {\n const [year, month, day] = key.split('-').map(Number)\n\n if (year === undefined || month === undefined || day === undefined) {\n throw new Error(`Invalid date key: ${key}`)\n }\n\n return new Date(year, month - 1, day)\n}\n\n/**\n * Shifts a date key by whole calendar days.\n *\n * Uses `setDate`, which is calendar-aware: it rolls over month and year ends,\n * and stays correct across daylight-saving transitions. Adding\n * `days * 86_400_000` milliseconds would not — a DST day is 23 or 25 hours long.\n *\n * @param key - Starting `YYYY-MM-DD` key.\n * @param days - Days to add; negative goes back.\n * @returns The resulting key.\n *\n * @example\n * ```ts\n * addDays('2026-01-31', 1) // '2026-02-01'\n * addDays('2026-01-01', -1) // '2025-12-31'\n * addDays('2028-02-28', 1) // '2028-02-29' — leap year\n * ```\n */\nexport function addDays(key: string, days: number): string {\n const date = fromDateKey(key)\n date.setDate(date.getDate() + days)\n\n return toDateKey(date)\n}\n\n/**\n * The last `count` days ending today, oldest first.\n *\n * `today` is a parameter so the function stays pure and testable; call sites\n * normally omit it.\n *\n * @param count - How many days to return, including `today`.\n * @param today - End of the range. Defaults to the real today.\n * @returns Keys in ascending order.\n *\n * @example\n * ```ts\n * lastNDays(3, '2026-08-23') // ['2026-08-21', '2026-08-22', '2026-08-23']\n * ```\n */\nexport function lastNDays(count: number, today: string = todayKey()): string[] {\n const keys: string[] = []\n\n for (let offset = count - 1; offset >= 0; offset -= 1) {\n keys.push(addDays(today, -offset))\n }\n\n return keys\n}\n\n/** 0 = week starts on Sunday, 1 = on Monday. Mirrors `profiles.week_starts_on`. */\nexport type WeekStart = 0 | 1\n\n/**\n * The first day of the week containing `key`.\n *\n * The user's preference is a parameter, not a module-level setting: changing it\n * in Profile has to re-render the week grid and the year heatmap immediately,\n * and a global would make that a hidden dependency.\n *\n * @param key - Any day in the week.\n * @param weekStartsOn - 0 for Sunday, 1 for Monday.\n * @returns Key of that week's first day.\n *\n * @example\n * ```ts\n * // 2026-08-23 is a Sunday\n * startOfWeek('2026-08-23', 1) // '2026-08-17' — previous Monday\n * startOfWeek('2026-08-23', 0) // '2026-08-23' — already Sunday\n * ```\n */\nexport function startOfWeek(key: string, weekStartsOn: WeekStart): string {\n const weekday = fromDateKey(key).getDay()\n const offset = (weekday - weekStartsOn + 7) % 7\n\n return addDays(key, -offset)\n}\n\n/**\n * Every day of a calendar year, in order.\n *\n * Leap years fall out of the loop for free: it walks day by day until the year\n * rolls over, so February 29 is included when it exists.\n *\n * @param year - Four-digit year.\n * @returns 365 or 366 keys, oldest first.\n */\nexport function eachDayOfYear(year: number): string[] {\n const keys: string[] = []\n const date = new Date(year, 0, 1)\n\n while (date.getFullYear() === year) {\n keys.push(toDateKey(date))\n date.setDate(date.getDate() + 1)\n }\n\n return keys\n}\n\n/**\n * Empty cells before a block's first day in a seven-row column grid.\n *\n * The grid fills column by column, so the first column is only partly used\n * unless the block starts exactly on the week's first day. An off-by-one here\n * shifts the whole block by a row, so this is unit tested.\n *\n * @param firstDayKey - First day of the block, e.g. `'2026-02-01'`.\n * @param weekStartsOn - 0 for Sunday, 1 for Monday.\n * @returns 0-6 blank cells.\n *\n * @example\n * ```ts\n * leadingBlanks('2026-01-01', 1) // 3 — a Thursday, Mon-Wed are blank\n * leadingBlanks('2024-01-01', 1) // 0 — a Monday\n * ```\n */\nexport function leadingBlanks(firstDayKey: string, weekStartsOn: WeekStart): number {\n return (fromDateKey(firstDayKey).getDay() - weekStartsOn + 7) % 7\n}\n","import { ref } from 'vue'\n\n/**\n * The locale `Intl` formatting uses.\n *\n * Held here rather than imported from an i18n runtime so the utilities have no\n * i18n dependency at all: an app that never installs vue-i18n still gets dates\n * in the right language. `createI18nRuntime` sets this when it is used.\n */\nconst locale = ref<string>(typeof navigator === 'undefined' ? 'en' : (navigator.language ?? 'en'))\n\n/**\n * Points every formatter at a new locale.\n *\n * @example\n * ```ts\n * setFormatLocale('tr-TR')\n * ```\n */\nexport function setFormatLocale(next: string): void {\n locale.value = next\n}\n\n/**\n * `Intl.DateTimeFormat` is expensive to construct, so instances are cached per\n * locale and option set. The key includes the locale, which is what lets the\n * cache survive a language change instead of returning stale formatters.\n */\nconst cache = new Map<string, Intl.DateTimeFormat>()\n\n/**\n * Formats a date in the active locale.\n *\n * Reading the locale ref here is deliberate: called from a `computed`, the\n * result re-evaluates when the language changes.\n *\n * @param date - Date to format.\n * @param options - Passed straight to `Intl.DateTimeFormat`.\n *\n * @example\n * ```ts\n * formatDate(new Date(), { weekday: 'narrow' }) // 'T'\n * ```\n */\nexport function formatDate(date: Date, options: Intl.DateTimeFormatOptions): string {\n const tag = locale.value\n const key = `${tag}:${JSON.stringify(options)}`\n\n let formatter = cache.get(key)\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(tag, options)\n cache.set(key, formatter)\n }\n\n return formatter.format(date)\n}\n","import { addDays, fromDateKey } from './date'\nimport { formatDate } from './format'\n\n/** The two days worth naming rather than numbering. */\nexport interface DayLabels {\n today: string\n yesterday: string\n}\n\n/**\n * A short name for a day, relative to today.\n *\n * \"Today\" and \"Yesterday\" are worth spelling out — they are the two a user\n * actually reaches for. Anything older gets its weekday, which inside a\n * five-day window is unambiguous and stays two or three characters in every\n * language.\n *\n * The two words are arguments rather than translated here: a library that calls\n * `t()` forces every consumer onto one i18n setup.\n *\n * @param dateKey - The day to label (`YYYY-MM-DD`).\n * @param today - Today's key, passed in so the caller controls the clock.\n * @param labels - What to call today and yesterday.\n *\n * @example\n * ```ts\n * relativeDayLabel('2026-08-28', '2026-08-31', { today: 'Today', yesterday: 'Yesterday' })\n * // 'Fri'\n * ```\n */\nexport function relativeDayLabel(dateKey: string, today: string, labels: DayLabels): string {\n if (dateKey === today) return labels.today\n if (dateKey === addDays(today, -1)) return labels.yesterday\n\n return formatDate(fromDateKey(dateKey), { weekday: 'short' })\n}\n","/**\n * Hands the user a file without a server round trip.\n *\n * @param data - Anything `JSON.stringify` can serialise.\n * @param filename - Suggested name, e.g. `hibi-export-2026-08-24.json`.\n */\nexport function downloadJson(data: unknown, filename: string): void {\n const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n\n link.href = url\n link.download = filename\n link.click()\n\n URL.revokeObjectURL(url)\n}\n","/**\n * What a router hands back for one query key.\n *\n * Inlined rather than imported from vue-router: the shape is `string | null`\n * either way, and a helper this small should not drag a router into the\n * package's dependencies.\n */\nexport type QueryValue = string | null\n\n/**\n * Resolves a `?redirect=` query value into a safe in-app path.\n *\n * Only same-origin paths are accepted. Anything else falls back to `/`,\n * so a crafted link cannot bounce a user from the real login page to a\n * phishing clone.\n *\n * Pure: takes the query value instead of reading the router, so it also\n * works inside navigation guards and can be unit tested.\n *\n * @param target - Raw `route.query.redirect` value. May be a string, an\n * array (repeated query key), `null`, or `undefined`.\n * @returns A path starting with a single `/`. Defaults to `/`.\n *\n * @example\n * ```ts\n * // in a view\n * await router.push(safeRedirect(route.query.redirect))\n *\n * // in a guard\n * return safeRedirect(to.query.redirect)\n * ```\n *\n * @example\n * ```ts\n * safeRedirect('/week') // '/week'\n * safeRedirect('https://evil.com') // '/'\n * safeRedirect('//evil.com') // '/' (protocol-relative URL)\n * safeRedirect(['/a', '/b']) // '/'\n * safeRedirect(undefined) // '/'\n * ```\n */\nexport function safeRedirect(target: QueryValue | QueryValue[] | undefined): string {\n if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {\n return target\n }\n\n return '/'\n}\n","/**\n * A short vibration for a confirmed tap.\n *\n * Optional chaining is not decoration: iOS Safari has no `vibrate` at all, and\n * calling it unguarded would throw on every marked day.\n *\n * @param duration - Milliseconds. Keep it under ~15ms; longer reads as an alert.\n */\nexport function tapFeedback(duration = 10): void {\n navigator.vibrate?.(duration)\n}\n","/**\n * Whether the app is running from the Home Screen rather than a browser tab.\n *\n * Two checks because iOS predates the standard one: `display-mode: standalone`\n * is the modern signal, `navigator.standalone` is Safari's own.\n */\nexport function isInstalled(): boolean {\n if (typeof window === 'undefined') return false\n\n return (\n window.matchMedia('(display-mode: standalone)').matches ||\n (navigator as Navigator & { standalone?: boolean }).standalone === true\n )\n}\n\n/** iPhone and iPad, including iPadOS reporting itself as a Mac. */\nexport function isApplePortable(): boolean {\n if (typeof window === 'undefined') return false\n\n return (\n /iPad|iPhone|iPod/.test(navigator.userAgent) ||\n (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)\n )\n}\n\n/**\n * Whether this device can only receive notifications once the app is installed.\n *\n * Safari on iOS grants notification permission to an installed web app and to\n * nothing else — in a normal tab the request does not even prompt. Telling the\n * user to allow notifications there is asking for something the browser will\n * not offer, so the UI has to say \"add to Home Screen\" instead.\n *\n * @example\n * ```ts\n * if (needsIosInstall()) // show the Home Screen instruction, not the button\n * ```\n */\nexport function needsIosInstall(): boolean {\n return isApplePortable() && !isInstalled()\n}\n","import { ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** What the user asked for; `system` follows the OS. */\nexport type ThemePreference = 'system' | 'light' | 'dark'\n\n/**\n * Namespaced by the app, not by this package.\n *\n * Two rei-kit apps served from the same origin would otherwise share one theme\n * setting — and during development on localhost, they will be.\n */\nlet storageKey = 'rei-theme'\n\nexport function isThemePreference(value: unknown): value is ThemePreference {\n return value === 'system' || value === 'light' || value === 'dark'\n}\n\n/** Reads the stored preference, falling back to `system`. */\nexport function readStoredTheme(): ThemePreference {\n try {\n const stored = localStorage.getItem(storageKey)\n\n return isThemePreference(stored) ? stored : 'system'\n } catch {\n return 'system'\n }\n}\n\nfunction storeTheme(preference: ThemePreference): void {\n try {\n localStorage.setItem(storageKey, preference)\n } catch {\n // Private mode or blocked storage: the choice just will not persist.\n }\n}\n\n/**\n * Does the environment prefer a dark scheme?\n *\n * `matchMedia` is checked for on its own rather than inferred from `document`.\n * Having one does not imply having the other: jsdom supplies a document and no\n * `matchMedia`, so a consumer's component test that so much as mounts something\n * calling `useTheme` threw — and some embedded webviews are the same. Where\n * there is nothing to ask, the answer is no rather than an exception.\n */\nfunction prefersDarkScheme(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)').matches\n : false\n}\n\n/**\n * Adds or removes `.dark` on `<html>`, resolving `system` against the OS.\n *\n * A no-op without a document. There is no OS preference to read on a server and\n * no `<html>` to write to, so a prerender leaves the class off and the app\n * decides the theme before hydration — see the note in the README.\n */\nexport function applyTheme(preference: ThemePreference): void {\n if (typeof document === 'undefined') return\n\n const isDark = preference === 'dark' || (preference === 'system' && prefersDarkScheme())\n\n document.documentElement.classList.toggle('dark', isDark)\n}\n\n/**\n * The shared preference, created on first use rather than at import.\n *\n * Lazy on purpose: reading storage at import time would lock in the default key\n * before an app had a chance to set its own, leaving the controller reading one\n * key and writing another.\n */\nlet preference: Ref<ThemePreference> | null = null\n\nfunction controller(): Ref<ThemePreference> {\n if (preference) return preference\n\n preference = ref<ThemePreference>(readStoredTheme())\n\n watch(\n preference,\n (next) => {\n storeTheme(next)\n applyTheme(next)\n },\n { immediate: true },\n )\n\n // While on `system`, follow the OS if the user flips it at night. Only where\n // there is something to listen to; see `prefersDarkScheme`.\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {\n if (preference?.value === 'system') applyTheme('system')\n })\n }\n\n return preference\n}\n\n/**\n * Sets where the preference is stored.\n *\n * Safe in either order: called before the first `useTheme()` it simply changes\n * the key, and called after it re-reads under the new one, so the controller\n * never reads from one key while writing to another.\n *\n * @example\n * ```ts\n * setThemeStorageKey('hibi-theme') // once, at startup\n * ```\n */\nexport function setThemeStorageKey(key: string): void {\n storageKey = key\n if (preference) preference.value = readStoredTheme()\n}\n\n/** @returns The shared preference ref; assigning to it stores and applies it. */\nexport function useTheme(): Ref<ThemePreference> {\n return controller()\n}\n","import { readonly, ref } from 'vue'\n\nimport { todayKey } from '../utils/date'\n\n/**\n * Today's date key, kept current while the app stays open.\n *\n * `todayKey()` called once in `setup` freezes the date for the lifetime of the\n * component. Nobody notices in a session that lasts minutes, but a phone left\n * on the Today screen overnight would keep marking yesterday, and the Week grid\n * would disable the column that just became today.\n */\nconst current = ref(todayKey())\n\nlet timer: ReturnType<typeof setTimeout> | undefined\nlet watching = false\n\n/** A second past midnight, so a fast timer cannot fire on the old date. */\nfunction msUntilMidnight(): number {\n const now = new Date()\n const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 1)\n\n return next.getTime() - now.getTime()\n}\n\nfunction refresh() {\n current.value = todayKey()\n}\n\nfunction schedule() {\n clearTimeout(timer)\n timer = setTimeout(() => {\n refresh()\n schedule()\n }, msUntilMidnight())\n}\n\n/**\n * Starts the clock, once, and only where there is a clock to watch.\n *\n * This used to run at import time, which made the module impossible to load on\n * a server: `document` is not defined there, and a barrel export means one\n * `import { BaseButton } from 'rei-kit'` pulls this file in. Deferring it to\n * the first `useToday()` also means an app that never asks for today never\n * arms a timer.\n */\nfunction watchTheClock() {\n if (watching || typeof document === 'undefined') return\n\n watching = true\n schedule()\n\n // A sleeping phone does not run timers reliably, so the tab also re-checks\n // the moment it comes back — which is when the user would see a stale date.\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState !== 'visible') return\n\n refresh()\n schedule()\n })\n}\n\n/**\n * @returns Read-only ref holding today's `YYYY-MM-DD` key.\n *\n * Rendered on a server this is the *server's* today, which is a different day\n * from the visitor's either side of midnight. Anything prerendered from it\n * would hydrate to a different value; render it on the client.\n *\n * @example\n * ```ts\n * const today = useToday()\n * const isFuture = computed(() => day > today.value)\n * ```\n */\nexport function useToday() {\n watchTheClock()\n\n return readonly(current)\n}\n","import { onMounted, onUnmounted, readonly, ref } from 'vue'\n\n/**\n * Tracks whether the browser thinks it has a network connection.\n *\n * Note the limit: `navigator.onLine` only reports whether a network interface\n * is up, not whether requests actually succeed. Treat it as a hint for the UI,\n * never as a reason to skip error handling.\n *\n * Listeners are removed on unmount, so the composable is safe to call per view.\n *\n * @returns A readonly ref that flips with the browser's online/offline events.\n *\n * @example\n * ```ts\n * const isOnline = useOnline()\n * // <p v-if=\"!isOnline\">You're offline.</p>\n * ```\n */\nexport function useOnline() {\n const isOnline = ref(true)\n\n function update() {\n isOnline.value = navigator.onLine\n }\n\n onMounted(() => {\n update()\n window.addEventListener('online', update)\n window.addEventListener('offline', update)\n })\n\n onUnmounted(() => {\n window.removeEventListener('online', update)\n window.removeEventListener('offline', update)\n })\n\n return readonly(isOnline)\n}\n","import { onScopeDispose } from 'vue'\n\n/**\n * Delays a callback until the caller stops calling it.\n *\n * Used for note autosave: a request per keystroke would be wasteful, but losing\n * the last keystrokes when the user navigates away would be worse — so the\n * pending call is flushed on dispose, and `flush` is exposed for route guards.\n *\n * @param callback - Runs with the arguments of the most recent call.\n * @param delay - Quiet period in milliseconds.\n * @returns `run` to schedule, `flush` to run now, `cancel` to drop.\n *\n * @example\n * ```ts\n * const save = useDebouncedCallback((body: string) => mutate(body), 800)\n * watch(text, (value) => save.run(value))\n * onBeforeRouteLeave(() => save.flush())\n * ```\n */\nexport function useDebouncedCallback<A extends unknown[]>(\n callback: (...args: A) => void,\n delay = 800,\n) {\n let timer: ReturnType<typeof setTimeout> | null = null\n let pending: A | null = null\n\n /** Runs the pending call right now, if there is one. */\n function flush() {\n if (timer !== null) clearTimeout(timer)\n timer = null\n\n if (pending !== null) {\n const args = pending\n pending = null\n callback(...args)\n }\n }\n\n /** Drops the pending call without running it. */\n function cancel() {\n if (timer !== null) clearTimeout(timer)\n timer = null\n pending = null\n }\n\n function run(...args: A) {\n pending = args\n if (timer !== null) clearTimeout(timer)\n timer = setTimeout(flush, delay)\n }\n\n // A closing sheet or an unmounting view must not eat the last keystrokes.\n onScopeDispose(flush)\n\n return { run, flush, cancel }\n}\n","import { onScopeDispose, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** Movement before a press counts as a drag rather than a tap. */\nconst DRAG_THRESHOLD_PX = 6\n\n/**\n * Drag-to-scroll for a horizontally scrolling element.\n *\n * The app puts `touch-action: pan-y` on the page content so the tab-swipe\n * gesture keeps its pointer events — the browser never claims a horizontal\n * drag, which also means it never pans this element natively. Rather than give\n * that up, horizontal scrolling is driven here.\n *\n * @param target - The scroll container.\n * @returns `didDrag`, so a click handler can ignore the press that ended a drag.\n *\n * @example\n * ```ts\n * const scroller = ref<HTMLElement | null>(null)\n * const { didDrag } = useDragScroll(scroller)\n *\n * function onClick() {\n * if (didDrag()) return\n * // …treat as a tap\n * }\n * ```\n */\nexport function useDragScroll(target: Ref<HTMLElement | null>) {\n let pointerId: number | null = null\n let startX = 0\n let startScroll = 0\n let dragged = false\n\n function onPointerDown(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerType === 'mouse') return\n\n pointerId = event.pointerId\n startX = event.clientX\n startScroll = element.scrollLeft\n dragged = false\n }\n\n function onPointerMove(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerId !== pointerId) return\n\n const dx = event.clientX - startX\n if (!dragged && Math.abs(dx) < DRAG_THRESHOLD_PX) return\n\n // Capture only once the gesture is clearly horizontal, so a vertical scroll\n // that happens to start here still belongs to the page.\n if (!dragged) {\n dragged = true\n element.setPointerCapture(event.pointerId)\n }\n\n element.scrollLeft = startScroll - dx\n }\n\n function onPointerUp(event: PointerEvent) {\n const element = target.value\n if (element?.hasPointerCapture(event.pointerId)) {\n element.releasePointerCapture(event.pointerId)\n }\n\n pointerId = null\n }\n\n function bind(element: HTMLElement) {\n element.addEventListener('pointerdown', onPointerDown)\n element.addEventListener('pointermove', onPointerMove)\n element.addEventListener('pointerup', onPointerUp)\n element.addEventListener('pointercancel', onPointerUp)\n }\n\n function unbind(element: HTMLElement) {\n element.removeEventListener('pointerdown', onPointerDown)\n element.removeEventListener('pointermove', onPointerMove)\n element.removeEventListener('pointerup', onPointerUp)\n element.removeEventListener('pointercancel', onPointerUp)\n }\n\n watch(\n target,\n (element, previous) => {\n if (previous) unbind(previous)\n if (element) bind(element)\n },\n { immediate: true },\n )\n\n onScopeDispose(() => {\n if (target.value) unbind(target.value)\n })\n\n return { didDrag: () => dragged }\n}\n","import { onScopeDispose, readonly, ref } from 'vue'\n\n/** The visible area, once the on-screen keyboard has taken its share. */\nexport interface VisualViewportRect {\n height: number\n offsetTop: number\n}\n\n/**\n * Tracks the visual viewport.\n *\n * Chrome and Android browsers honour `interactive-widget=resizes-content`, so\n * the layout viewport already shrinks for the keyboard there. Safari on iOS\n * does not implement it: it shrinks only the *visual* viewport, leaving a sheet\n * sized in `dvh` sitting partly underneath the keyboard.\n *\n * `null` means the API is unavailable, which callers should read as \"trust the\n * layout viewport\" rather than as zero. A server has no viewport at all, so it\n * gets that same `null` — this runs during `setup`, and a component using it\n * has to survive being rendered there.\n *\n * @example\n * ```ts\n * const viewport = useVisualViewport()\n * // :style=\"viewport ? { height: `${viewport.height}px` } : undefined\"\n * ```\n */\nexport function useVisualViewport() {\n const rect = ref<VisualViewportRect | null>(null)\n\n const viewport = typeof window === 'undefined' ? undefined : window.visualViewport\n if (!viewport) return readonly(rect)\n\n function read() {\n if (!viewport) return\n\n rect.value = { height: viewport.height, offsetTop: viewport.offsetTop }\n }\n\n read()\n\n // `scroll` matters as much as `resize`: iOS shifts the visual viewport up to\n // keep the focused field visible, without changing its height.\n viewport.addEventListener('resize', read)\n viewport.addEventListener('scroll', read)\n\n onScopeDispose(() => {\n viewport.removeEventListener('resize', read)\n viewport.removeEventListener('scroll', read)\n })\n\n return readonly(rect)\n}\n","<script setup lang=\"ts\">\nconst {\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n type = 'button',\n} = defineProps<{\n variant?: 'primary' | 'ghost' | 'danger'\n size?: 'sm' | 'md'\n loading?: boolean\n disabled?: boolean\n type?: 'button' | 'submit'\n}>()\n\nconst VARIANT_CLASS = {\n primary: 'bg-primary text-white hover:bg-primary/90',\n ghost: 'bg-transparent text-ink hover:bg-muted',\n danger: 'bg-negative text-white hover:bg-negative/90',\n} as const\n\nconst SIZE_CLASS = {\n sm: 'h-9 px-3 text-sm',\n md: 'h-11 px-4 text-base',\n} as const\n</script>\n\n<template>\n <button\n :type=\"type\"\n :disabled=\"disabled || loading\"\n :aria-busy=\"loading\"\n class=\"rounded-card focus-visible:outline-primary inline-flex items-center justify-center gap-2 font-medium transition-transform duration-100 select-none focus-visible:outline-2 focus-visible:outline-offset-2 active:scale-95 disabled:pointer-events-none disabled:opacity-50\"\n :class=\"[VARIANT_CLASS[variant], SIZE_CLASS[size]]\"\n >\n <span\n v-if=\"loading\"\n class=\"size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n aria-hidden=\"true\"\n />\n <slot />\n </button>\n</template>\n","<script setup lang=\"ts\">\nconst {\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n type = 'button',\n} = defineProps<{\n variant?: 'primary' | 'ghost' | 'danger'\n size?: 'sm' | 'md'\n loading?: boolean\n disabled?: boolean\n type?: 'button' | 'submit'\n}>()\n\nconst VARIANT_CLASS = {\n primary: 'bg-primary text-white hover:bg-primary/90',\n ghost: 'bg-transparent text-ink hover:bg-muted',\n danger: 'bg-negative text-white hover:bg-negative/90',\n} as const\n\nconst SIZE_CLASS = {\n sm: 'h-9 px-3 text-sm',\n md: 'h-11 px-4 text-base',\n} as const\n</script>\n\n<template>\n <button\n :type=\"type\"\n :disabled=\"disabled || loading\"\n :aria-busy=\"loading\"\n class=\"rounded-card focus-visible:outline-primary inline-flex items-center justify-center gap-2 font-medium transition-transform duration-100 select-none focus-visible:outline-2 focus-visible:outline-offset-2 active:scale-95 disabled:pointer-events-none disabled:opacity-50\"\n :class=\"[VARIANT_CLASS[variant], SIZE_CLASS[size]]\"\n >\n <span\n v-if=\"loading\"\n class=\"size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n aria-hidden=\"true\"\n />\n <slot />\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\ndefineOptions({ inheritAttrs: false })\n\nconst {\n label,\n error = '',\n hint = '',\n type = 'text',\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the input with no accessible name at all.\n */\n labelHidden?: boolean\n type?: 'text' | 'email' | 'password' | 'number'\n}>()\n\nconst model = defineModel<string | undefined>()\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label :for=\"id\" class=\"text-ink text-sm font-medium\" :class=\"labelHidden ? 'sr-only' : ''\">\n {{ label }}\n </label>\n\n <input\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n v-bind=\"$attrs\"\n class=\"border-hair bg-surface text-ink rounded-card focus-visible:outline-primary h-11 border px-3 focus-visible:outline-2 focus-visible:outline-offset-1\"\n :class=\"error ? 'border-negative' : ''\"\n />\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\ndefineOptions({ inheritAttrs: false })\n\nconst {\n label,\n error = '',\n hint = '',\n type = 'text',\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n /**\n * Hides the label visually but keeps it for assistive tech. For fields whose\n * surrounding row already names them — dropping the label entirely would\n * leave the input with no accessible name at all.\n */\n labelHidden?: boolean\n type?: 'text' | 'email' | 'password' | 'number'\n}>()\n\nconst model = defineModel<string | undefined>()\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label :for=\"id\" class=\"text-ink text-sm font-medium\" :class=\"labelHidden ? 'sr-only' : ''\">\n {{ label }}\n </label>\n\n <input\n :id=\"id\"\n v-model=\"model\"\n :type=\"type\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n v-bind=\"$attrs\"\n class=\"border-hair bg-surface text-ink rounded-card focus-visible:outline-primary h-11 border px-3 focus-visible:outline-2 focus-visible:outline-offset-1\"\n :class=\"error ? 'border-negative' : ''\"\n />\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string\n}>()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n lastFocused?.focus()\n lastFocused = null\n setBackgroundInert(false)\n }\n})\n\n/**\n * `inert` takes the whole app out of tab order and pointer events while the\n * sheet is open — a real focus trap without keydown bookkeeping.\n *\n * The sheet itself is teleported to `#sheet-root`, a sibling of `#app`, so it\n * stays interactive.\n */\nfunction setBackgroundInert(isInert: boolean) {\n document.getElementById('app')?.toggleAttribute('inert', isInert)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport to=\"#sheet-root\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel bg-surface relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[28px] shadow-2xl outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <button\n type=\"button\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </button>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity 200ms ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string\n}>()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n lastFocused?.focus()\n lastFocused = null\n setBackgroundInert(false)\n }\n})\n\n/**\n * `inert` takes the whole app out of tab order and pointer events while the\n * sheet is open — a real focus trap without keydown bookkeeping.\n *\n * The sheet itself is teleported to `#sheet-root`, a sibling of `#app`, so it\n * stays interactive.\n */\nfunction setBackgroundInert(isInert: boolean) {\n document.getElementById('app')?.toggleAttribute('inert', isInert)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport to=\"#sheet-root\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel bg-surface relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[28px] shadow-2xl outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <button\n type=\"button\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </button>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity 200ms ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script lang=\"ts\" setup>\nconst { title, description = '' } = defineProps<{\n title: string\n description?: string | undefined\n}>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-center gap-3 px-6 py-10 text-center\">\n <div\n v-if=\"$slots.icon\"\n class=\"bg-muted text-primary rounded-card flex size-12 items-center justify-center\"\n >\n <slot name=\"icon\" />\n </div>\n\n <h3 class=\"text-ink text-base font-semibold\">{{ title }}</h3>\n <p v-if=\"description\" class=\"text-ink-soft max-w-[36ch] text-sm\">\n {{ description }}\n </p>\n\n <div v-if=\"$slots.action\" class=\"mt-2 flex w-full flex-col gap-2\">\n <slot name=\"action\" />\n </div>\n </div>\n</template>\n\n<style></style>\n","<script lang=\"ts\" setup>\nconst { title, description = '' } = defineProps<{\n title: string\n description?: string | undefined\n}>()\n</script>\n\n<template>\n <div class=\"flex flex-col items-center gap-3 px-6 py-10 text-center\">\n <div\n v-if=\"$slots.icon\"\n class=\"bg-muted text-primary rounded-card flex size-12 items-center justify-center\"\n >\n <slot name=\"icon\" />\n </div>\n\n <h3 class=\"text-ink text-base font-semibold\">{{ title }}</h3>\n <p v-if=\"description\" class=\"text-ink-soft max-w-[36ch] text-sm\">\n {{ description }}\n </p>\n\n <div v-if=\"$slots.action\" class=\"mt-2 flex w-full flex-col gap-2\">\n <slot name=\"action\" />\n </div>\n </div>\n</template>\n\n<style></style>\n","<script setup lang=\"ts\">\nconst { title } = defineProps<{ title: string }>()\n</script>\n\n<template>\n <header class=\"grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center\">\n <div class=\"justify-self-start\"><slot name=\"left\" /></div>\n\n <h1 class=\"text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums\">\n <slot name=\"title\">\n <span class=\"truncate\">{{ title }}</span>\n </slot>\n </h1>\n\n <div class=\"justify-self-end\"><slot name=\"right\" /></div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nconst { title } = defineProps<{ title: string }>()\n</script>\n\n<template>\n <header class=\"grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center\">\n <div class=\"justify-self-start\"><slot name=\"left\" /></div>\n\n <h1 class=\"text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums\">\n <slot name=\"title\">\n <span class=\"truncate\">{{ title }}</span>\n </slot>\n </h1>\n\n <div class=\"justify-self-end\"><slot name=\"right\" /></div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * One plan in a pricing table.\n *\n * Every string arrives as a prop. A component in a kit that reaches for its\n * consumer's translations is not shared, it is one app's furniture parked\n * somewhere else — and the second app to want it would have to fork it.\n *\n * The tone is semantic rather than named after a colour. \"Gold\" and \"diamond\"\n * are one product's tiers; `warm` and `cool` are what a pricing table actually\n * needs, which is for three columns to be distinguishable at a glance without\n * any of them shouting. A table where every column is a different hue reads as\n * three products from three companies.\n */\nconst {\n name,\n lead,\n price,\n period,\n note,\n features,\n tone = 'neutral',\n badge,\n chip,\n recommended = false,\n} = defineProps<{\n name: string\n lead?: string\n /** Already formatted, or whatever stands in while there is no price. */\n price: string\n period?: string\n note?: string\n features: readonly string[]\n tone?: 'neutral' | 'warm' | 'cool'\n /** Rides on the card's edge, e.g. \"Recommended\". */\n badge?: string\n /** Sits inside, e.g. \"30% cheaper\" or \"Your plan\". */\n chip?: string\n /** Raises the card and lets the badge show. */\n recommended?: boolean\n}>()\n\nconst TONE = {\n neutral: {\n ring: 'border-hair/70',\n soft: 'bg-muted text-ink-soft',\n icon: 'bg-primary/10 text-primary',\n },\n warm: {\n ring: 'border-[color-mix(in_oklab,#b8862c_35%,transparent)]',\n soft: 'bg-[color-mix(in_oklab,#b8862c_14%,transparent)] text-[#8a6318] dark:text-[#d9ad5c]',\n icon: 'bg-[color-mix(in_oklab,#b8862c_16%,transparent)] text-[#8a6318] dark:text-[#d9ad5c]',\n },\n cool: {\n ring: 'border-[color-mix(in_oklab,#4a86a8_38%,transparent)]',\n soft: 'bg-[color-mix(in_oklab,#4a86a8_14%,transparent)] text-[#2f6079] dark:text-[#8fc6de]',\n icon: 'bg-[color-mix(in_oklab,#4a86a8_16%,transparent)] text-[#2f6079] dark:text-[#8fc6de]',\n },\n} as const\n\nconst palette = computed(() => TONE[tone])\n</script>\n\n<template>\n <article\n class=\"bg-surface rounded-card relative flex h-full flex-col border p-7 shadow-[var(--shadow-card)] transition-[border-color,box-shadow,transform] duration-[420ms] hover:border-[color-mix(in_oklab,var(--color-primary)_60%,transparent)] hover:shadow-[var(--shadow-lift)] sm:p-8\"\n :class=\"[palette.ring, recommended ? 'shadow-[var(--shadow-lift)]' : 'hover:-translate-y-0.5']\"\n >\n <!-- On the edge rather than inside, so it cannot be mistaken for one of\n the plan's own features. -->\n <span\n v-if=\"badge && recommended\"\n class=\"bg-primary rounded-cell absolute -top-3 left-7 px-3 py-1 text-[0.7rem] font-semibold text-white\"\n >\n {{ badge }}\n </span>\n\n <div class=\"flex items-start justify-between gap-4\">\n <span\n v-if=\"$slots.icon\"\n class=\"rounded-card grid size-11 place-items-center text-xl\"\n :class=\"palette.icon\"\n >\n <slot name=\"icon\" />\n </span>\n\n <span\n v-if=\"chip\"\n class=\"rounded-cell ml-auto px-2.5 py-1 text-[0.7rem] font-medium\"\n :class=\"palette.soft\"\n >\n {{ chip }}\n </span>\n </div>\n\n <h3 class=\"text-ink mt-5 text-lg font-semibold\">{{ name }}</h3>\n <p v-if=\"lead\" class=\"text-ink-soft mt-1.5 text-sm leading-relaxed\">{{ lead }}</p>\n\n <p class=\"mt-6 flex items-baseline gap-1.5\">\n <span class=\"text-ink text-3xl font-semibold tracking-tight tabular-nums\">{{ price }}</span>\n <span v-if=\"period\" class=\"text-ink-soft text-sm\">{{ period }}</span>\n </p>\n <p v-if=\"note\" class=\"text-ink-soft mt-1 text-xs\">{{ note }}</p>\n\n <ul class=\"mt-7 flex-1 space-y-3\">\n <li v-for=\"feature in features\" :key=\"feature\" class=\"flex gap-3 text-sm\">\n <span\n class=\"bg-primary/45 mt-[0.45rem] size-1.5 shrink-0 rounded-full\"\n aria-hidden=\"true\"\n />\n <span class=\"text-ink-soft leading-relaxed\">{{ feature }}</span>\n </li>\n </ul>\n\n <div v-if=\"$slots.action\" class=\"mt-8\"><slot name=\"action\" /></div>\n </article>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * One plan in a pricing table.\n *\n * Every string arrives as a prop. A component in a kit that reaches for its\n * consumer's translations is not shared, it is one app's furniture parked\n * somewhere else — and the second app to want it would have to fork it.\n *\n * The tone is semantic rather than named after a colour. \"Gold\" and \"diamond\"\n * are one product's tiers; `warm` and `cool` are what a pricing table actually\n * needs, which is for three columns to be distinguishable at a glance without\n * any of them shouting. A table where every column is a different hue reads as\n * three products from three companies.\n */\nconst {\n name,\n lead,\n price,\n period,\n note,\n features,\n tone = 'neutral',\n badge,\n chip,\n recommended = false,\n} = defineProps<{\n name: string\n lead?: string\n /** Already formatted, or whatever stands in while there is no price. */\n price: string\n period?: string\n note?: string\n features: readonly string[]\n tone?: 'neutral' | 'warm' | 'cool'\n /** Rides on the card's edge, e.g. \"Recommended\". */\n badge?: string\n /** Sits inside, e.g. \"30% cheaper\" or \"Your plan\". */\n chip?: string\n /** Raises the card and lets the badge show. */\n recommended?: boolean\n}>()\n\nconst TONE = {\n neutral: {\n ring: 'border-hair/70',\n soft: 'bg-muted text-ink-soft',\n icon: 'bg-primary/10 text-primary',\n },\n warm: {\n ring: 'border-[color-mix(in_oklab,#b8862c_35%,transparent)]',\n soft: 'bg-[color-mix(in_oklab,#b8862c_14%,transparent)] text-[#8a6318] dark:text-[#d9ad5c]',\n icon: 'bg-[color-mix(in_oklab,#b8862c_16%,transparent)] text-[#8a6318] dark:text-[#d9ad5c]',\n },\n cool: {\n ring: 'border-[color-mix(in_oklab,#4a86a8_38%,transparent)]',\n soft: 'bg-[color-mix(in_oklab,#4a86a8_14%,transparent)] text-[#2f6079] dark:text-[#8fc6de]',\n icon: 'bg-[color-mix(in_oklab,#4a86a8_16%,transparent)] text-[#2f6079] dark:text-[#8fc6de]',\n },\n} as const\n\nconst palette = computed(() => TONE[tone])\n</script>\n\n<template>\n <article\n class=\"bg-surface rounded-card relative flex h-full flex-col border p-7 shadow-[var(--shadow-card)] transition-[border-color,box-shadow,transform] duration-[420ms] hover:border-[color-mix(in_oklab,var(--color-primary)_60%,transparent)] hover:shadow-[var(--shadow-lift)] sm:p-8\"\n :class=\"[palette.ring, recommended ? 'shadow-[var(--shadow-lift)]' : 'hover:-translate-y-0.5']\"\n >\n <!-- On the edge rather than inside, so it cannot be mistaken for one of\n the plan's own features. -->\n <span\n v-if=\"badge && recommended\"\n class=\"bg-primary rounded-cell absolute -top-3 left-7 px-3 py-1 text-[0.7rem] font-semibold text-white\"\n >\n {{ badge }}\n </span>\n\n <div class=\"flex items-start justify-between gap-4\">\n <span\n v-if=\"$slots.icon\"\n class=\"rounded-card grid size-11 place-items-center text-xl\"\n :class=\"palette.icon\"\n >\n <slot name=\"icon\" />\n </span>\n\n <span\n v-if=\"chip\"\n class=\"rounded-cell ml-auto px-2.5 py-1 text-[0.7rem] font-medium\"\n :class=\"palette.soft\"\n >\n {{ chip }}\n </span>\n </div>\n\n <h3 class=\"text-ink mt-5 text-lg font-semibold\">{{ name }}</h3>\n <p v-if=\"lead\" class=\"text-ink-soft mt-1.5 text-sm leading-relaxed\">{{ lead }}</p>\n\n <p class=\"mt-6 flex items-baseline gap-1.5\">\n <span class=\"text-ink text-3xl font-semibold tracking-tight tabular-nums\">{{ price }}</span>\n <span v-if=\"period\" class=\"text-ink-soft text-sm\">{{ period }}</span>\n </p>\n <p v-if=\"note\" class=\"text-ink-soft mt-1 text-xs\">{{ note }}</p>\n\n <ul class=\"mt-7 flex-1 space-y-3\">\n <li v-for=\"feature in features\" :key=\"feature\" class=\"flex gap-3 text-sm\">\n <span\n class=\"bg-primary/45 mt-[0.45rem] size-1.5 shrink-0 rounded-full\"\n aria-hidden=\"true\"\n />\n <span class=\"text-ink-soft leading-relaxed\">{{ feature }}</span>\n </li>\n </ul>\n\n <div v-if=\"$slots.action\" class=\"mt-8\"><slot name=\"action\" /></div>\n </article>\n</template>\n","<script setup lang=\"ts\">\n/**\n * A small coloured dot, optionally labelled.\n *\n * Takes the colour as a class rather than a category, so an app can key it off\n * whatever its own domain calls a category — habit kinds, expense types,\n * priorities — without this component knowing about any of them.\n */\nconst { fill, label = '' } = defineProps<{\n /** Background utility for the dot, e.g. `bg-positive`. */\n fill: string\n /** Optional text after the dot. Omit for a bare marker. */\n label?: string\n}>()\n</script>\n\n<template>\n <span class=\"inline-flex items-center gap-1.5\">\n <span class=\"size-2 rounded-full\" :class=\"fill\" />\n <span v-if=\"label\" class=\"text-ink-soft text-xs font-medium\">{{ label }}</span>\n </span>\n</template>\n","<script setup lang=\"ts\">\n/**\n * A small coloured dot, optionally labelled.\n *\n * Takes the colour as a class rather than a category, so an app can key it off\n * whatever its own domain calls a category — habit kinds, expense types,\n * priorities — without this component knowing about any of them.\n */\nconst { fill, label = '' } = defineProps<{\n /** Background utility for the dot, e.g. `bg-positive`. */\n fill: string\n /** Optional text after the dot. Omit for a bare marker. */\n label?: string\n}>()\n</script>\n\n<template>\n <span class=\"inline-flex items-center gap-1.5\">\n <span class=\"size-2 rounded-full\" :class=\"fill\" />\n <span v-if=\"label\" class=\"text-ink-soft text-xs font-medium\">{{ label }}</span>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport ToneDot from './ToneDot.vue'\n\n/** The three classes a category needs to colour a heading. */\nexport interface Tone {\n /** Solid background for the dot, e.g. `bg-positive`. */\n fill: string\n /** Tinted surface for the pill, e.g. `bg-positive/5 border-positive/25`. */\n card: string\n /** Foreground that pairs with the surface, e.g. `text-positive`. */\n text: string\n}\n\n/**\n * A pill heading for a group of things.\n *\n * The tone arrives as three class strings rather than a category name: Tailwind\n * reads source files as plain text, so a class assembled at runtime never\n * reaches the stylesheet — the app has to write them out, and it is the app\n * that knows its own categories anyway.\n */\nconst {\n tone,\n label,\n count = 0,\n} = defineProps<{\n tone: Tone\n label: string\n /** Hidden when zero, so an empty group's heading stays quiet. */\n count?: number\n}>()\n</script>\n\n<template>\n <h2 class=\"flex items-center gap-2 self-start rounded-full border px-3 py-1\" :class=\"tone.card\">\n <ToneDot :fill=\"tone.fill\" />\n <span class=\"text-xs font-semibold tracking-wide uppercase\" :class=\"tone.text\">\n {{ label }}\n </span>\n <span v-if=\"count > 0\" class=\"text-ink-soft text-xs tabular-nums\">{{ count }}</span>\n </h2>\n</template>\n","<script setup lang=\"ts\">\nimport ToneDot from './ToneDot.vue'\n\n/** The three classes a category needs to colour a heading. */\nexport interface Tone {\n /** Solid background for the dot, e.g. `bg-positive`. */\n fill: string\n /** Tinted surface for the pill, e.g. `bg-positive/5 border-positive/25`. */\n card: string\n /** Foreground that pairs with the surface, e.g. `text-positive`. */\n text: string\n}\n\n/**\n * A pill heading for a group of things.\n *\n * The tone arrives as three class strings rather than a category name: Tailwind\n * reads source files as plain text, so a class assembled at runtime never\n * reaches the stylesheet — the app has to write them out, and it is the app\n * that knows its own categories anyway.\n */\nconst {\n tone,\n label,\n count = 0,\n} = defineProps<{\n tone: Tone\n label: string\n /** Hidden when zero, so an empty group's heading stays quiet. */\n count?: number\n}>()\n</script>\n\n<template>\n <h2 class=\"flex items-center gap-2 self-start rounded-full border px-3 py-1\" :class=\"tone.card\">\n <ToneDot :fill=\"tone.fill\" />\n <span class=\"text-xs font-semibold tracking-wide uppercase\" :class=\"tone.text\">\n {{ label }}\n </span>\n <span v-if=\"count > 0\" class=\"text-ink-soft text-xs tabular-nums\">{{ count }}</span>\n </h2>\n</template>\n","<script setup lang=\"ts\" generic=\"T extends string | number\">\nimport { useId } from 'vue'\n\n/**\n * A row of mutually exclusive choices.\n *\n * Radio inputs rather than buttons: it is a single choice out of a small set,\n * so arrow-key navigation and the \"one of N selected\" announcement come free.\n */\nconst { options } = defineProps<{\n options: readonly { value: T; label: string }[]\n}>()\n\nconst model = defineModel<T>({ required: true })\n\nconst name = useId()\n</script>\n\n<template>\n <div class=\"bg-muted rounded-card flex w-full gap-1 p-1\">\n <label v-for=\"option in options\" :key=\"String(option.value)\" class=\"flex-1 cursor-pointer\">\n <input v-model=\"model\" type=\"radio\" :value=\"option.value\" :name=\"name\" class=\"sr-only\" />\n <span\n class=\"flex h-10 items-center justify-center rounded-xl px-2 text-sm font-medium transition-colors select-none\"\n :class=\"model === option.value ? 'bg-surface text-ink shadow-sm' : 'text-ink-soft'\"\n >\n {{ option.label }}\n </span>\n </label>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"T extends string | number\">\nimport { useId } from 'vue'\n\n/**\n * A row of mutually exclusive choices.\n *\n * Radio inputs rather than buttons: it is a single choice out of a small set,\n * so arrow-key navigation and the \"one of N selected\" announcement come free.\n */\nconst { options } = defineProps<{\n options: readonly { value: T; label: string }[]\n}>()\n\nconst model = defineModel<T>({ required: true })\n\nconst name = useId()\n</script>\n\n<template>\n <div class=\"bg-muted rounded-card flex w-full gap-1 p-1\">\n <label v-for=\"option in options\" :key=\"String(option.value)\" class=\"flex-1 cursor-pointer\">\n <input v-model=\"model\" type=\"radio\" :value=\"option.value\" :name=\"name\" class=\"sr-only\" />\n <span\n class=\"flex h-10 items-center justify-center rounded-xl px-2 text-sm font-medium transition-colors select-none\"\n :class=\"model === option.value ? 'bg-surface text-ink shadow-sm' : 'text-ink-soft'\"\n >\n {{ option.label }}\n </span>\n </label>\n </div>\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{ title: string }>()\n</script>\n\n<template>\n <section class=\"flex flex-col gap-2\">\n <h2 class=\"text-ink-soft px-1 text-xs font-semibold tracking-wide uppercase\">{{ title }}</h2>\n\n <!-- One card per group, rows divided by hairlines. Loose fields floating on\n the page gave no sense of what belonged with what. -->\n <div class=\"border-hair bg-surface rounded-card divide-hair divide-y overflow-hidden border\">\n <slot />\n </div>\n </section>\n</template>\n","<script setup lang=\"ts\">\ndefineProps<{ title: string }>()\n</script>\n\n<template>\n <section class=\"flex flex-col gap-2\">\n <h2 class=\"text-ink-soft px-1 text-xs font-semibold tracking-wide uppercase\">{{ title }}</h2>\n\n <!-- One card per group, rows divided by hairlines. Loose fields floating on\n the page gave no sense of what belonged with what. -->\n <div class=\"border-hair bg-surface rounded-card divide-hair divide-y overflow-hidden border\">\n <slot />\n </div>\n </section>\n</template>\n","<script setup lang=\"ts\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport type { Component } from 'vue'\n\n/**\n * One line in a settings card.\n *\n * `as` decides the element: a row that navigates has to be a button, and a row\n * that merely holds a control must not be, or the control becomes unreachable.\n */\nconst {\n label,\n description = '',\n icon = undefined,\n interactive = false,\n stacked = false,\n} = defineProps<{\n label: string\n description?: string\n icon?: Component | undefined\n /** Renders the row as a button with a chevron. */\n interactive?: boolean\n /** Puts the control on its own line below the label, for wide controls. */\n stacked?: boolean\n}>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <component\n :is=\"interactive ? 'button' : 'div'\"\n :type=\"interactive ? 'button' : undefined\"\n class=\"flex w-full items-center gap-3 px-4 py-3 text-left\"\n :class=\"[\n interactive ? 'hover:bg-muted/60 transition-colors active:scale-[0.99]' : '',\n stacked ? 'flex-col items-stretch gap-3' : '',\n ]\"\n @click=\"interactive && emit('click')\"\n >\n <div class=\"flex items-center gap-3\">\n <span\n v-if=\"icon\"\n class=\"bg-muted text-ink-soft flex size-9 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"icon\" class=\"size-[18px]\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-medium\">{{ label }}</p>\n <p v-if=\"description\" class=\"text-ink-soft mt-0.5 text-xs leading-snug\">\n {{ description }}\n </p>\n </div>\n\n <div v-if=\"!stacked\" class=\"shrink-0\"><slot /></div>\n\n <ChevronRight v-if=\"interactive\" class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <div v-if=\"stacked\"><slot /></div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport type { Component } from 'vue'\n\n/**\n * One line in a settings card.\n *\n * `as` decides the element: a row that navigates has to be a button, and a row\n * that merely holds a control must not be, or the control becomes unreachable.\n */\nconst {\n label,\n description = '',\n icon = undefined,\n interactive = false,\n stacked = false,\n} = defineProps<{\n label: string\n description?: string\n icon?: Component | undefined\n /** Renders the row as a button with a chevron. */\n interactive?: boolean\n /** Puts the control on its own line below the label, for wide controls. */\n stacked?: boolean\n}>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <component\n :is=\"interactive ? 'button' : 'div'\"\n :type=\"interactive ? 'button' : undefined\"\n class=\"flex w-full items-center gap-3 px-4 py-3 text-left\"\n :class=\"[\n interactive ? 'hover:bg-muted/60 transition-colors active:scale-[0.99]' : '',\n stacked ? 'flex-col items-stretch gap-3' : '',\n ]\"\n @click=\"interactive && emit('click')\"\n >\n <div class=\"flex items-center gap-3\">\n <span\n v-if=\"icon\"\n class=\"bg-muted text-ink-soft flex size-9 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"icon\" class=\"size-[18px]\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-medium\">{{ label }}</p>\n <p v-if=\"description\" class=\"text-ink-soft mt-0.5 text-xs leading-snug\">\n {{ description }}\n </p>\n </div>\n\n <div v-if=\"!stacked\" class=\"shrink-0\"><slot /></div>\n\n <ChevronRight v-if=\"interactive\" class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <div v-if=\"stacked\"><slot /></div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst {\n rows = 3,\n rowHeight = 'h-14',\n label = 'Loading…',\n} = defineProps<{\n rows?: number\n /**\n * How tall each row is, as either a utility class (`h-20`) or a CSS length\n * (`5rem`, `72px`, `var(--row)`).\n *\n * Both are accepted because the class-only version failed silently: a length\n * passed here landed in `class` as `5rem`, which is not a class, so the rows\n * had no height and the placeholder rendered as nothing at all. A loading\n * state that shows an empty page is worse than no loading state, because it\n * looks like the page is finished and empty.\n */\n rowHeight?: string\n label?: string\n}>()\n\n/** A length starts with a digit, a dot, or opens a CSS function. */\nconst isLength = computed(() => /^(?:[.\\d]|calc\\(|var\\(|clamp\\(|min\\(|max\\()/.test(rowHeight))\n</script>\n\n<template>\n <div role=\"status\" class=\"flex flex-col gap-1\">\n <span class=\"sr-only\">{{ label }}</span>\n\n <div\n v-for=\"row in rows\"\n :key=\"row\"\n class=\"bg-muted rounded-card animate-pulse\"\n :class=\"isLength ? undefined : rowHeight\"\n :style=\"isLength ? { height: rowHeight } : undefined\"\n aria-hidden=\"true\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nconst {\n rows = 3,\n rowHeight = 'h-14',\n label = 'Loading…',\n} = defineProps<{\n rows?: number\n /**\n * How tall each row is, as either a utility class (`h-20`) or a CSS length\n * (`5rem`, `72px`, `var(--row)`).\n *\n * Both are accepted because the class-only version failed silently: a length\n * passed here landed in `class` as `5rem`, which is not a class, so the rows\n * had no height and the placeholder rendered as nothing at all. A loading\n * state that shows an empty page is worse than no loading state, because it\n * looks like the page is finished and empty.\n */\n rowHeight?: string\n label?: string\n}>()\n\n/** A length starts with a digit, a dot, or opens a CSS function. */\nconst isLength = computed(() => /^(?:[.\\d]|calc\\(|var\\(|clamp\\(|min\\(|max\\()/.test(rowHeight))\n</script>\n\n<template>\n <div role=\"status\" class=\"flex flex-col gap-1\">\n <span class=\"sr-only\">{{ label }}</span>\n\n <div\n v-for=\"row in rows\"\n :key=\"row\"\n class=\"bg-muted rounded-card animate-pulse\"\n :class=\"isLength ? undefined : rowHeight\"\n :style=\"isLength ? { height: rowHeight } : undefined\"\n aria-hidden=\"true\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ArrowDown, ArrowRight, ArrowUp } from 'lucide-vue-next'\n\nconst {\n value,\n label,\n trend = null,\n} = defineProps<{\n value: string\n label: string\n trend?: 'up' | 'down' | 'flat' | null\n}>()\n\nconst TREND_ICON = { up: ArrowUp, down: ArrowDown, flat: ArrowRight } as const\n</script>\n\n<template>\n <div class=\"border-hair rounded-card flex flex-1 flex-col gap-0.5 border p-3\">\n <div class=\"flex items-baseline gap-1\">\n <span class=\"text-ink text-xl font-semibold tabular-nums\">{{ value }}</span>\n <component :is=\"TREND_ICON[trend]\" v-if=\"trend\" class=\"text-ink-soft size-3\" />\n </div>\n <span class=\"text-ink-soft text-xs\">{{ label }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ArrowDown, ArrowRight, ArrowUp } from 'lucide-vue-next'\n\nconst {\n value,\n label,\n trend = null,\n} = defineProps<{\n value: string\n label: string\n trend?: 'up' | 'down' | 'flat' | null\n}>()\n\nconst TREND_ICON = { up: ArrowUp, down: ArrowDown, flat: ArrowRight } as const\n</script>\n\n<template>\n <div class=\"border-hair rounded-card flex flex-1 flex-col gap-0.5 border p-3\">\n <div class=\"flex items-baseline gap-1\">\n <span class=\"text-ink text-xl font-semibold tabular-nums\">{{ value }}</span>\n <component :is=\"TREND_ICON[trend]\" v-if=\"trend\" class=\"text-ink-soft size-3\" />\n </div>\n <span class=\"text-ink-soft text-xs\">{{ label }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\" generic=\"L extends string\">\n/**\n * A flat language switcher for screens with no Settings behind them.\n *\n * The list and the labels are props: only the app knows which languages it\n * ships, and endonyms — each language written in itself — are what make the\n * right option legible to someone who cannot read the current interface.\n */\nconst {\n locales,\n labels,\n label = '',\n} = defineProps<{\n locales: readonly L[]\n /** Endonyms, e.g. `{ en: 'English', tr: 'Türkçe' }`. */\n labels: Record<L, string>\n /** Accessible name for the group. */\n label?: string\n}>()\n\n/**\n * Two-way bound rather than taking the runtime's ref as a prop: props are not\n * unwrapped in a template and cannot be assigned to, so the ref would compare\n * against itself and the click handler would not compile.\n */\nconst preference = defineModel<'system' | L>({ required: true })\n</script>\n\n<template>\n <nav class=\"flex flex-wrap items-center justify-center gap-1\" :aria-label=\"label || undefined\">\n <button\n v-for=\"locale in locales\"\n :key=\"locale\"\n type=\"button\"\n :lang=\"locale\"\n class=\"rounded-full px-2.5 py-1.5 text-xs transition-colors\"\n :class=\"\n preference === locale ? 'bg-muted text-ink font-semibold' : 'text-ink-soft hover:text-ink'\n \"\n :aria-pressed=\"preference === locale\"\n @click=\"preference = locale\"\n >\n {{ labels[locale] }}\n </button>\n </nav>\n</template>\n","<script setup lang=\"ts\" generic=\"L extends string\">\n/**\n * A flat language switcher for screens with no Settings behind them.\n *\n * The list and the labels are props: only the app knows which languages it\n * ships, and endonyms — each language written in itself — are what make the\n * right option legible to someone who cannot read the current interface.\n */\nconst {\n locales,\n labels,\n label = '',\n} = defineProps<{\n locales: readonly L[]\n /** Endonyms, e.g. `{ en: 'English', tr: 'Türkçe' }`. */\n labels: Record<L, string>\n /** Accessible name for the group. */\n label?: string\n}>()\n\n/**\n * Two-way bound rather than taking the runtime's ref as a prop: props are not\n * unwrapped in a template and cannot be assigned to, so the ref would compare\n * against itself and the click handler would not compile.\n */\nconst preference = defineModel<'system' | L>({ required: true })\n</script>\n\n<template>\n <nav class=\"flex flex-wrap items-center justify-center gap-1\" :aria-label=\"label || undefined\">\n <button\n v-for=\"locale in locales\"\n :key=\"locale\"\n type=\"button\"\n :lang=\"locale\"\n class=\"rounded-full px-2.5 py-1.5 text-xs transition-colors\"\n :class=\"\n preference === locale ? 'bg-muted text-ink font-semibold' : 'text-ink-soft hover:text-ink'\n \"\n :aria-pressed=\"preference === locale\"\n @click=\"preference = locale\"\n >\n {{ labels[locale] }}\n </button>\n </nav>\n</template>\n","<script setup lang=\"ts\">\nconst { label } = defineProps<{ label: string }>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <button\n type=\"button\"\n class=\"border-hair bg-surface text-ink rounded-card hover:bg-muted flex h-11 w-full items-center justify-center gap-2 border text-sm font-medium transition-colors active:scale-95\"\n @click=\"emit('click')\"\n >\n <!-- Google asks for its own mark, so it is inlined rather than themed. -->\n <svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n <path\n fill=\"#EA4335\"\n d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"\n />\n <path\n fill=\"#4285F4\"\n d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"\n />\n </svg>\n {{ label }}\n </button>\n</template>\n","<script setup lang=\"ts\">\nconst { label } = defineProps<{ label: string }>()\n\nconst emit = defineEmits<{ click: [] }>()\n</script>\n\n<template>\n <button\n type=\"button\"\n class=\"border-hair bg-surface text-ink rounded-card hover:bg-muted flex h-11 w-full items-center justify-center gap-2 border text-sm font-medium transition-colors active:scale-95\"\n @click=\"emit('click')\"\n >\n <!-- Google asks for its own mark, so it is inlined rather than themed. -->\n <svg class=\"size-4\" viewBox=\"0 0 48 48\" aria-hidden=\"true\">\n <path\n fill=\"#EA4335\"\n d=\"M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.7 30.2.5 24 .5 14.6.5 6.5 5.9 2.6 13.7l7.8 6.1C12.3 13.7 17.7 9.5 24 9.5z\"\n />\n <path\n fill=\"#4285F4\"\n d=\"M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.6-4.9 7.3l7.6 5.9c4.4-4.1 7.1-10.2 7.1-17.7z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M10.4 28.2a14.6 14.6 0 0 1 0-8.4l-7.8-6.1a24 24 0 0 0 0 20.6l7.8-6.1z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M24 47.5c6.2 0 11.5-2 15.4-5.6l-7.6-5.9c-2.1 1.4-4.8 2.3-7.8 2.3-6.3 0-11.7-4.2-13.6-10l-7.8 6.1C6.5 42.1 14.6 47.5 24 47.5z\"\n />\n </svg>\n {{ label }}\n </button>\n</template>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\nimport type { Component } from 'vue'\n\nimport { tapFeedback } from '../utils/haptics'\n\nexport interface TabItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Text under the icon. Already translated. */\n label: string\n icon: Component\n}\n\n/**\n * The floating bottom bar.\n *\n * Items and the active key are props: the package has no opinion about how an\n * app names its screens, and reading `route.meta` here would force one.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly TabItem<K>[]\n /** Which item is current. Usually from `route.meta`. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string\n}>()\n</script>\n\n<template>\n <header class=\"tab-bar\">\n <nav class=\"tab-bar-inner\" :aria-label=\"label || undefined\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"tab-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n @click=\"tapFeedback()\"\n >\n <span class=\"tab-icon-slot\">\n <component :is=\"item.icon\" class=\"tab-icon\" />\n </span>\n <span class=\"tab-label\">{{ item.label }}</span>\n </RouterLink>\n </nav>\n </header>\n</template>\n\n<style scoped>\n@reference \"../styles/_reference.css\";\n\n/* absolute, not fixed: the bar hangs inside the app shell. Fixed would pin it\n to the browser window, which on a desktop is nowhere near the app. */\n.tab-bar {\n @apply absolute left-1/2 z-40 w-full max-w-[360px] -translate-x-1/2 px-4;\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.tab-bar-inner {\n @apply border-hair bg-surface/85 flex items-center justify-between gap-1 border p-1.5 shadow-lg backdrop-blur-md;\n border-radius: var(--radius-shell);\n}\n\n.tab-link {\n @apply text-ink-soft flex min-h-[52px] flex-1 cursor-pointer flex-col items-center justify-center gap-1 py-1.5;\n border-radius: calc(var(--radius-shell) - 6px);\n /* Only the icon reacts to a press. Scaling the whole link drags the label and\n the pill with it, which reads as the bar wobbling. */\n transition: color 200ms ease;\n}\n\n.tab-link:hover {\n @apply text-ink;\n}\n\n/* The pill sits behind the icon rather than the link, so the active tab grows a\n marker instead of the row changing shape. */\n.tab-icon-slot {\n @apply flex h-7 w-12 items-center justify-center rounded-full transition-all duration-200 ease-out;\n}\n\n.tab-link:active .tab-icon-slot {\n transform: scale(0.88);\n}\n\n.is-active {\n @apply text-primary;\n}\n\n.is-active .tab-icon-slot {\n @apply bg-muted;\n}\n\n.tab-icon {\n @apply size-[18px] stroke-2 transition-transform duration-200;\n}\n\n.is-active .tab-icon {\n @apply scale-110 stroke-[2.5px];\n}\n\n.tab-label {\n @apply text-[10px] leading-none font-medium;\n}\n\n.is-active .tab-label {\n @apply font-semibold;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tab-icon-slot,\n .tab-icon {\n transition: none;\n }\n .tab-link:active .tab-icon-slot {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\nimport type { Component } from 'vue'\n\nimport { tapFeedback } from '../utils/haptics'\n\nexport interface TabItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Text under the icon. Already translated. */\n label: string\n icon: Component\n}\n\n/**\n * The floating bottom bar.\n *\n * Items and the active key are props: the package has no opinion about how an\n * app names its screens, and reading `route.meta` here would force one.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly TabItem<K>[]\n /** Which item is current. Usually from `route.meta`. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string\n}>()\n</script>\n\n<template>\n <header class=\"tab-bar\">\n <nav class=\"tab-bar-inner\" :aria-label=\"label || undefined\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"tab-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n @click=\"tapFeedback()\"\n >\n <span class=\"tab-icon-slot\">\n <component :is=\"item.icon\" class=\"tab-icon\" />\n </span>\n <span class=\"tab-label\">{{ item.label }}</span>\n </RouterLink>\n </nav>\n </header>\n</template>\n\n<style scoped>\n@reference \"../styles/_reference.css\";\n\n/* absolute, not fixed: the bar hangs inside the app shell. Fixed would pin it\n to the browser window, which on a desktop is nowhere near the app. */\n.tab-bar {\n @apply absolute left-1/2 z-40 w-full max-w-[360px] -translate-x-1/2 px-4;\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.tab-bar-inner {\n @apply border-hair bg-surface/85 flex items-center justify-between gap-1 border p-1.5 shadow-lg backdrop-blur-md;\n border-radius: var(--radius-shell);\n}\n\n.tab-link {\n @apply text-ink-soft flex min-h-[52px] flex-1 cursor-pointer flex-col items-center justify-center gap-1 py-1.5;\n border-radius: calc(var(--radius-shell) - 6px);\n /* Only the icon reacts to a press. Scaling the whole link drags the label and\n the pill with it, which reads as the bar wobbling. */\n transition: color 200ms ease;\n}\n\n.tab-link:hover {\n @apply text-ink;\n}\n\n/* The pill sits behind the icon rather than the link, so the active tab grows a\n marker instead of the row changing shape. */\n.tab-icon-slot {\n @apply flex h-7 w-12 items-center justify-center rounded-full transition-all duration-200 ease-out;\n}\n\n.tab-link:active .tab-icon-slot {\n transform: scale(0.88);\n}\n\n.is-active {\n @apply text-primary;\n}\n\n.is-active .tab-icon-slot {\n @apply bg-muted;\n}\n\n.tab-icon {\n @apply size-[18px] stroke-2 transition-transform duration-200;\n}\n\n.is-active .tab-icon {\n @apply scale-110 stroke-[2.5px];\n}\n\n.tab-label {\n @apply text-[10px] leading-none font-medium;\n}\n\n.is-active .tab-label {\n @apply font-semibold;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tab-icon-slot,\n .tab-icon {\n transition: none;\n }\n .tab-link:active .tab-icon-slot {\n transform: none;\n }\n}\n</style>\n","import { computed, ref, watchEffect } from 'vue'\nimport { createI18n } from 'vue-i18n'\n\nimport { setFormatLocale } from '../utils/format'\n\n/** What the user picked. `system` re-reads the browser on every launch. */\nexport type LocalePreference<L extends string> = 'system' | L\n\nexport interface I18nRuntimeOptions<L extends string, Schema> {\n /** Languages the app ships, in no particular order. */\n locales: readonly L[]\n /** The one that is always loaded, and the fallback when a load fails. */\n fallback: L\n /**\n * BCP 47 tag per locale, for `Intl`.\n *\n * Message lookup only needs the base language, but dates and numbers need a\n * region to be right — `zh` alone would leave the formatter to guess.\n */\n intlTags: Record<L, string>\n /** The fallback's messages, bundled. */\n messages: Schema\n /** The rest, fetched only when they are the one in use. */\n loaders?: Partial<Record<L, () => Promise<{ default: Schema }>>>\n /** Where the choice is stored. Namespace it per app. */\n storageKey?: string\n}\n\n/**\n * Builds an i18n runtime around an app's own catalogue.\n *\n * A factory rather than a module singleton because the schema is the app's:\n * typing every locale as `typeof en` is what makes a missing key a build error,\n * and this package has no `en` of its own to type against.\n *\n * @example\n * ```ts\n * export const { i18n, t, useLocalePreference, loadActiveLocale } =\n * createI18nRuntime({\n * locales: ['en', 'tr'] as const,\n * fallback: 'en',\n * intlTags: { en: 'en-GB', tr: 'tr-TR' },\n * messages: en,\n * loaders: { tr: () => import('./locales/tr') },\n * storageKey: 'myapp-locale',\n * })\n * ```\n */\nexport function createI18nRuntime<L extends string, Schema extends Record<string, unknown>>(\n options: I18nRuntimeOptions<L, Schema>,\n) {\n const { locales, fallback, intlTags, messages, storageKey = 'rei-locale' } = options\n\n // Typed rather than defaulted to `{}`, which erases the locale keys and makes\n // `loaders[locale]` an index into an empty object.\n const loaders: Partial<Record<L, () => Promise<{ default: Schema }>>> = options.loaders ?? {}\n\n function isSupported(value: string): value is L {\n return (locales as readonly string[]).includes(value)\n }\n\n /**\n * First browser language the app can actually speak.\n *\n * `navigator.languages` is ordered by the user's own preference, so the first\n * match is the best one — not simply the first entry.\n */\n function detectSystemLocale(): L {\n // No browser to ask. The fallback is the right answer on a server: it is\n // the locale whose messages are bundled, so it is the only one that could\n // render without a load.\n //\n // The test is `document`, not `navigator`. Node has shipped a global\n // `navigator` since v21, so `typeof navigator === 'undefined'` is false on\n // a server and this would read the *build machine's* language and bake it\n // into every prerendered page. `document` is the only one of the two that\n // still means \"a browser\".\n if (typeof document === 'undefined') return fallback\n\n for (const tag of navigator.languages ?? [navigator.language]) {\n const base = tag.split('-')[0]?.toLowerCase()\n if (base && isSupported(base)) return base\n }\n\n return fallback\n }\n\n function readStored(): LocalePreference<L> {\n try {\n const stored = localStorage.getItem(storageKey)\n if (stored === 'system' || (stored && isSupported(stored))) return stored\n } catch {\n // Storage blocked; fall through to the system language.\n }\n\n return 'system'\n }\n\n const preference = ref<LocalePreference<L>>(readStored())\n\n const activeLocale = computed<L>(() =>\n preference.value === 'system' ? detectSystemLocale() : (preference.value as L),\n )\n\n const intlLocale = computed(() => intlTags[activeLocale.value])\n\n // Only the fallback at construction; the rest arrive through\n // setLocaleMessage.\n const initial = { [fallback]: messages } as Record<string, Record<string, unknown>>\n\n const i18n = createI18n({\n legacy: false,\n locale: activeLocale.value as string,\n fallbackLocale: fallback as string,\n messages: initial,\n } as unknown as Parameters<typeof createI18n>[0])\n\n /**\n * A narrow view of the instance.\n *\n * vue-i18n infers its own generics from the messages it is handed, which\n * fights a runtime that is generic over the app's schema. Casting once, here,\n * keeps that fight out of every call site — and the surface below is the\n * whole of what this runtime uses.\n */\n const core = i18n.global as unknown as {\n locale: { value: string }\n setLocaleMessage: (locale: string, messages: Schema) => void\n t: (key: string, named?: Record<string, unknown>) => string\n }\n\n const loaded = new Set<L>([fallback])\n\n /**\n * Makes sure a locale's messages are in place before it becomes active.\n *\n * Awaited rather than fired and forgotten: setting the locale first paints one\n * frame of the fallback at every other user, which is the flash a fallback\n * exists to prevent, not cause.\n */\n async function ensureMessages(locale: L): Promise<void> {\n if (loaded.has(locale)) return\n\n const load = loaders[locale]\n if (!load) return\n\n try {\n const module = await load()\n core.setLocaleMessage(locale, module.default)\n loaded.add(locale)\n } catch {\n // Offline, or a stale chunk after a deploy. The fallback is loaded and\n // will carry the UI, which beats a blank screen.\n }\n }\n\n /** Loads whatever the stored preference resolves to. Call before mounting. */\n function loadActiveLocale(): Promise<void> {\n return ensureMessages(activeLocale.value)\n }\n\n // Keeps vue-i18n, `Intl` and the document in step. `lang` matters beyond\n // tidiness: it drives hyphenation, font fallback and screen readers.\n watchEffect(() => {\n core.locale.value = activeLocale.value\n setFormatLocale(intlLocale.value)\n\n if (typeof document !== 'undefined') {\n document.documentElement.lang = activeLocale.value\n }\n })\n\n /** Read and write the language preference. */\n function useLocalePreference() {\n return computed<LocalePreference<L>>({\n get: () => preference.value,\n set: (next) => {\n const resolved = next === 'system' ? detectSystemLocale() : (next as L)\n\n // Messages first, then the switch — the other order shows the fallback\n // for a frame on the way to the language the user just picked.\n void ensureMessages(resolved).then(() => {\n preference.value = next\n })\n\n try {\n localStorage.setItem(storageKey, next)\n } catch {\n // Storage blocked; the choice lasts for this session only.\n }\n },\n })\n }\n\n return {\n i18n,\n /** `t` for code outside a component. Tracks the locale inside a computed. */\n t: core.t,\n activeLocale,\n intlLocale,\n ensureMessages,\n loadActiveLocale,\n useLocalePreference,\n }\n}\n","/**\n * rei-kit — the layer every app starts from.\n *\n * Everything here is free of any backend, router or i18n choice. Components\n * take strings rather than calling a translator, and utilities take the clock\n * rather than reading it, so nothing in this package can force a decision on\n * the app that installs it.\n *\n * @see https://github.com/ramazandogna/rei-kit\n */\n\nexport const VERSION = '0.0.0'\n\n// ── Utilities ──────────────────────────────────────────────────────────────\nexport {\n addDays,\n eachDayOfYear,\n fromDateKey,\n lastNDays,\n leadingBlanks,\n startOfWeek,\n toDateKey,\n todayKey,\n} from './utils/date'\nexport type { WeekStart } from './utils/date'\n\nexport { formatDate, setFormatLocale } from './utils/format'\nexport { relativeDayLabel } from './utils/day-label'\nexport type { DayLabels } from './utils/day-label'\n\nexport { downloadJson } from './utils/download'\nexport { safeRedirect } from './utils/redirect'\nexport type { QueryValue } from './utils/redirect'\nexport { tapFeedback } from './utils/haptics'\nexport { isApplePortable, isInstalled, needsIosInstall } from './utils/platform'\n\nexport { AppError, registerErrorMapper, toAppError } from './utils/app-error'\nexport type { AppErrorKind, ErrorMapper } from './utils/app-error'\n\n// ── Composables ────────────────────────────────────────────────────────────\nexport {\n applyTheme,\n isThemePreference,\n readStoredTheme,\n setThemeStorageKey,\n useTheme,\n} from './composables/use-theme'\nexport type { ThemePreference } from './composables/use-theme'\n\nexport { useToday } from './composables/use-today'\nexport { useOnline } from './composables/use-online'\nexport { useDebouncedCallback } from './composables/use-debounced-callback'\nexport { useDragScroll } from './composables/use-drag-scroll'\nexport { useVisualViewport } from './composables/use-visual-viewport'\nexport type { VisualViewportRect } from './composables/use-visual-viewport'\n\n// ── Components ─────────────────────────────────────────────────────────────\nexport { default as BaseButton } from './components/BaseButton.vue'\nexport { default as BaseInput } from './components/BaseInput.vue'\nexport { default as BaseSheet } from './components/BaseSheet.vue'\nexport { default as EmptyState } from './components/EmptyState.vue'\nexport { default as PageHeader } from './components/PageHeader.vue'\nexport { default as PriceCard } from './components/PriceCard.vue'\nexport { default as SectionHeading } from './components/SectionHeading.vue'\nexport { default as SegmentedControl } from './components/SegmentedControl.vue'\nexport { default as SettingsGroup } from './components/SettingsGroup.vue'\nexport { default as SettingsRow } from './components/SettingsRow.vue'\nexport { default as SkeletonList } from './components/SkeletonList.vue'\nexport { default as StatCard } from './components/StatCard.vue'\nexport { default as ToneDot } from './components/ToneDot.vue'\nexport type { Tone } from './components/SectionHeading.vue'\nexport { default as LocaleLinks } from './components/LocaleLinks.vue'\nexport { default as GoogleButton } from './components/GoogleButton.vue'\nexport { default as TabBar } from './components/TabBar.vue'\nexport type { TabItem } from './components/TabBar.vue'\n\n// ── i18n ───────────────────────────────────────────────────────────────────\nexport { createI18nRuntime } from './i18n/runtime'\nexport type { I18nRuntimeOptions, LocalePreference } from './i18n/runtime'\n"],"mappings":"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"}
package/dist/supabase.js CHANGED
@@ -88,10 +88,36 @@ function createSupabaseClient(url, anonKey) {
88
88
  * Registered on import rather than exported as a step to remember: importing
89
89
  * this module is already the decision to use Supabase.
90
90
  */
91
+ /**
92
+ * A PostgREST failure, however it reached us.
93
+ *
94
+ * `instanceof PostgrestError` is the obvious test and it is not enough. The
95
+ * error returned in `{ data, error }` is a plain object — supabase-js builds
96
+ * the class only on the paths that throw — and even where it does construct
97
+ * one, a project holding two copies of `@supabase/postgrest-js` gets two
98
+ * different classes and an `instanceof` that is false against a genuine error.
99
+ *
100
+ * The consequence was silent and total: every database failure fell past this
101
+ * mapper into the generic branch, so `permission denied for table enrollments`
102
+ * — a message naming the table and the missing grant — reached the screen as
103
+ * "Something went wrong." The information was there the whole time.
104
+ *
105
+ * So: the class where it holds, and the shape where it does not. `code` and
106
+ * `message` are what PostgREST always sends; `details` and `hint` are always
107
+ * present as keys, null when empty, which is what separates this from any
108
+ * other object carrying a `code`.
109
+ */
110
+ function isPostgrestError(error) {
111
+ if (error instanceof PostgrestError) return true;
112
+ if (typeof error !== "object" || error === null) return false;
113
+ const candidate = error;
114
+ return typeof candidate["message"] === "string" && typeof candidate["code"] === "string" && "details" in candidate && "hint" in candidate;
115
+ }
91
116
  registerErrorMapper((error) => {
92
- if (!(error instanceof PostgrestError)) return null;
117
+ if (!isPostgrestError(error)) return null;
93
118
  if (error.code === "23505") return new AppError("conflict", "That already exists.", { cause: error });
94
119
  if (error.code === "PGRST116") return new AppError("not-found", "That could not be found.", { cause: error });
120
+ if (error.code === "42501" || error.code === "PGRST301" || error.code === "PGRST302") return new AppError("denied", "You are not allowed to do that.", { cause: error });
95
121
  return new AppError("unknown", error.message, { cause: error });
96
122
  });
97
123
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"supabase.js","names":[],"sources":["../src/supabase/index.ts"],"sourcesContent":["import { createClient } from '@supabase/supabase-js'\nimport { PostgrestError } from '@supabase/supabase-js'\nimport type { SupabaseClient, SupportedStorage } from '@supabase/supabase-js'\n\nimport { AppError, registerErrorMapper } from '../utils/app-error'\n\n/**\n * The optional Supabase entry.\n *\n * Behind its own export so an app that never touches Supabase downloads none of\n * it — importing this module is what opts in, including to the error mapping\n * registered at the bottom.\n */\n\nconst REMEMBER_KEY = 'rei-remember'\n\n/**\n * Records whether the next session should outlive the tab.\n *\n * Call before signing in: the SDK writes the session as soon as the request\n * succeeds, and this decides where it lands.\n */\nexport function setRememberMe(remember: boolean): void {\n try {\n localStorage.setItem(REMEMBER_KEY, String(remember))\n } catch {\n // Storage blocked; the session will simply not persist.\n }\n}\n\nfunction activeStore(): Storage {\n try {\n return localStorage.getItem(REMEMBER_KEY) === 'false' ? sessionStorage : localStorage\n } catch {\n return sessionStorage\n }\n}\n\n/**\n * Session storage that follows the \"remember me\" choice.\n *\n * Supabase issues a short-lived access token plus a long-lived refresh token.\n * Where the refresh token is kept decides how long a login survives:\n * `localStorage` outlives the browser, `sessionStorage` dies with the tab. On a\n * shared machine that difference is the whole point, so the choice switches the\n * store rather than the token lifetime.\n *\n * Removal clears both, so signing out cannot leave a copy behind.\n */\nconst rememberAwareStorage: SupportedStorage = {\n getItem: (key) => {\n try {\n return activeStore().getItem(key)\n } catch {\n return null\n }\n },\n setItem: (key, value) => {\n try {\n activeStore().setItem(key, value)\n } catch {\n // Storage blocked.\n }\n },\n removeItem: (key) => {\n try {\n localStorage.removeItem(key)\n sessionStorage.removeItem(key)\n } catch {\n // Storage blocked.\n }\n },\n}\n\n/**\n * Builds a typed Supabase client with the remember-me storage wired in.\n *\n * A factory, not a module singleton reading `import.meta.env`: a package cannot\n * know what an app calls its environment variables, and a second app would have\n * different ones.\n *\n * @param url - Project URL. Public; it is the API endpoint.\n * @param anonKey - Anon key. Also public — row-level security is the boundary,\n * not the key.\n *\n * @example\n * ```ts\n * export const supabase = createSupabaseClient<Database>(\n * import.meta.env.VITE_SUPABASE_URL,\n * import.meta.env.VITE_SUPABASE_ANON_KEY,\n * )\n * ```\n */\nexport function createSupabaseClient<Database>(\n url: string,\n anonKey: string,\n): SupabaseClient<Database> {\n if (!url) throw new Error('createSupabaseClient: the project URL is missing.')\n if (!anonKey) throw new Error('createSupabaseClient: the anon key is missing.')\n\n return createClient<Database>(url, anonKey, { auth: { storage: rememberAwareStorage } })\n}\n\n/**\n * Teaches `toAppError` to read Postgres.\n *\n * Registered on import rather than exported as a step to remember: importing\n * this module is already the decision to use Supabase.\n */\nregisterErrorMapper((error) => {\n if (!(error instanceof PostgrestError)) return null\n\n // 23505 is unique_violation — a second row where the schema allows one. It is\n // a normal outcome of a double tap, not a failure worth an error screen.\n if (error.code === '23505') {\n return new AppError('conflict', 'That already exists.', { cause: error })\n }\n\n if (error.code === 'PGRST116') {\n return new AppError('not-found', 'That could not be found.', { cause: error })\n }\n\n return new AppError('unknown', error.message, { cause: error })\n})\n"],"mappings":";;;;;;;;;;AAcA,IAAM,eAAe;;;;;;;AAQrB,SAAgB,cAAc,UAAyB;CACrD,IAAI;EACF,aAAa,QAAQ,cAAc,OAAO,QAAQ,CAAC;CACrD,QAAQ,CAER;AACF;AAEA,SAAS,cAAuB;CAC9B,IAAI;EACF,OAAO,aAAa,QAAQ,YAAY,MAAM,UAAU,iBAAiB;CAC3E,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;AAaA,IAAM,uBAAyC;CAC7C,UAAU,QAAQ;EAChB,IAAI;GACF,OAAO,YAAY,CAAC,CAAC,QAAQ,GAAG;EAClC,QAAQ;GACN,OAAO;EACT;CACF;CACA,UAAU,KAAK,UAAU;EACvB,IAAI;GACF,YAAY,CAAC,CAAC,QAAQ,KAAK,KAAK;EAClC,QAAQ,CAER;CACF;CACA,aAAa,QAAQ;EACnB,IAAI;GACF,aAAa,WAAW,GAAG;GAC3B,eAAe,WAAW,GAAG;EAC/B,QAAQ,CAER;CACF;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,qBACd,KACA,SAC0B;CAC1B,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,mDAAmD;CAC7E,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gDAAgD;CAE9E,OAAO,aAAuB,KAAK,SAAS,EAAE,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;AACzF;;;;;;;AAQA,qBAAqB,UAAU;CAC7B,IAAI,EAAE,iBAAiB,iBAAiB,OAAO;CAI/C,IAAI,MAAM,SAAS,SACjB,OAAO,IAAI,SAAS,YAAY,wBAAwB,EAAE,OAAO,MAAM,CAAC;CAG1E,IAAI,MAAM,SAAS,YACjB,OAAO,IAAI,SAAS,aAAa,4BAA4B,EAAE,OAAO,MAAM,CAAC;CAG/E,OAAO,IAAI,SAAS,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AAChE,CAAC"}
1
+ {"version":3,"file":"supabase.js","names":[],"sources":["../src/supabase/index.ts"],"sourcesContent":["import { createClient } from '@supabase/supabase-js'\nimport { PostgrestError } from '@supabase/supabase-js'\nimport type { SupabaseClient, SupportedStorage } from '@supabase/supabase-js'\n\nimport { AppError, registerErrorMapper } from '../utils/app-error'\n\n/**\n * The optional Supabase entry.\n *\n * Behind its own export so an app that never touches Supabase downloads none of\n * it — importing this module is what opts in, including to the error mapping\n * registered at the bottom.\n */\n\nconst REMEMBER_KEY = 'rei-remember'\n\n/**\n * Records whether the next session should outlive the tab.\n *\n * Call before signing in: the SDK writes the session as soon as the request\n * succeeds, and this decides where it lands.\n */\nexport function setRememberMe(remember: boolean): void {\n try {\n localStorage.setItem(REMEMBER_KEY, String(remember))\n } catch {\n // Storage blocked; the session will simply not persist.\n }\n}\n\nfunction activeStore(): Storage {\n try {\n return localStorage.getItem(REMEMBER_KEY) === 'false' ? sessionStorage : localStorage\n } catch {\n return sessionStorage\n }\n}\n\n/**\n * Session storage that follows the \"remember me\" choice.\n *\n * Supabase issues a short-lived access token plus a long-lived refresh token.\n * Where the refresh token is kept decides how long a login survives:\n * `localStorage` outlives the browser, `sessionStorage` dies with the tab. On a\n * shared machine that difference is the whole point, so the choice switches the\n * store rather than the token lifetime.\n *\n * Removal clears both, so signing out cannot leave a copy behind.\n */\nconst rememberAwareStorage: SupportedStorage = {\n getItem: (key) => {\n try {\n return activeStore().getItem(key)\n } catch {\n return null\n }\n },\n setItem: (key, value) => {\n try {\n activeStore().setItem(key, value)\n } catch {\n // Storage blocked.\n }\n },\n removeItem: (key) => {\n try {\n localStorage.removeItem(key)\n sessionStorage.removeItem(key)\n } catch {\n // Storage blocked.\n }\n },\n}\n\n/**\n * Builds a typed Supabase client with the remember-me storage wired in.\n *\n * A factory, not a module singleton reading `import.meta.env`: a package cannot\n * know what an app calls its environment variables, and a second app would have\n * different ones.\n *\n * @param url - Project URL. Public; it is the API endpoint.\n * @param anonKey - Anon key. Also public — row-level security is the boundary,\n * not the key.\n *\n * @example\n * ```ts\n * export const supabase = createSupabaseClient<Database>(\n * import.meta.env.VITE_SUPABASE_URL,\n * import.meta.env.VITE_SUPABASE_ANON_KEY,\n * )\n * ```\n */\nexport function createSupabaseClient<Database>(\n url: string,\n anonKey: string,\n): SupabaseClient<Database> {\n if (!url) throw new Error('createSupabaseClient: the project URL is missing.')\n if (!anonKey) throw new Error('createSupabaseClient: the anon key is missing.')\n\n return createClient<Database>(url, anonKey, { auth: { storage: rememberAwareStorage } })\n}\n\n/**\n * Teaches `toAppError` to read Postgres.\n *\n * Registered on import rather than exported as a step to remember: importing\n * this module is already the decision to use Supabase.\n */\n/**\n * A PostgREST failure, however it reached us.\n *\n * `instanceof PostgrestError` is the obvious test and it is not enough. The\n * error returned in `{ data, error }` is a plain object — supabase-js builds\n * the class only on the paths that throw — and even where it does construct\n * one, a project holding two copies of `@supabase/postgrest-js` gets two\n * different classes and an `instanceof` that is false against a genuine error.\n *\n * The consequence was silent and total: every database failure fell past this\n * mapper into the generic branch, so `permission denied for table enrollments`\n * — a message naming the table and the missing grant — reached the screen as\n * \"Something went wrong.\" The information was there the whole time.\n *\n * So: the class where it holds, and the shape where it does not. `code` and\n * `message` are what PostgREST always sends; `details` and `hint` are always\n * present as keys, null when empty, which is what separates this from any\n * other object carrying a `code`.\n */\nfunction isPostgrestError(error: unknown): error is PostgrestError {\n if (error instanceof PostgrestError) return true\n if (typeof error !== 'object' || error === null) return false\n\n const candidate = error as Record<string, unknown>\n\n return (\n typeof candidate['message'] === 'string' &&\n typeof candidate['code'] === 'string' &&\n 'details' in candidate &&\n 'hint' in candidate\n )\n}\n\nregisterErrorMapper((error) => {\n if (!isPostgrestError(error)) return null\n\n // 23505 is unique_violation — a second row where the schema allows one. It is\n // a normal outcome of a double tap, not a failure worth an error screen.\n if (error.code === '23505') {\n return new AppError('conflict', 'That already exists.', { cause: error })\n }\n\n if (error.code === 'PGRST116') {\n return new AppError('not-found', 'That could not be found.', { cause: error })\n }\n\n // 42501 is insufficient_privilege and PGRST301/302 are an absent or expired\n // token. All three mean the same thing to a reader — you are not allowed to\n // do this — and none of them mean the app is broken, which is what a generic\n // failure message implies and what sends somebody to the error log.\n if (error.code === '42501' || error.code === 'PGRST301' || error.code === 'PGRST302') {\n return new AppError('denied', 'You are not allowed to do that.', { cause: error })\n }\n\n return new AppError('unknown', error.message, { cause: error })\n})\n"],"mappings":";;;;;;;;;;AAcA,IAAM,eAAe;;;;;;;AAQrB,SAAgB,cAAc,UAAyB;CACrD,IAAI;EACF,aAAa,QAAQ,cAAc,OAAO,QAAQ,CAAC;CACrD,QAAQ,CAER;AACF;AAEA,SAAS,cAAuB;CAC9B,IAAI;EACF,OAAO,aAAa,QAAQ,YAAY,MAAM,UAAU,iBAAiB;CAC3E,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;AAaA,IAAM,uBAAyC;CAC7C,UAAU,QAAQ;EAChB,IAAI;GACF,OAAO,YAAY,CAAC,CAAC,QAAQ,GAAG;EAClC,QAAQ;GACN,OAAO;EACT;CACF;CACA,UAAU,KAAK,UAAU;EACvB,IAAI;GACF,YAAY,CAAC,CAAC,QAAQ,KAAK,KAAK;EAClC,QAAQ,CAER;CACF;CACA,aAAa,QAAQ;EACnB,IAAI;GACF,aAAa,WAAW,GAAG;GAC3B,eAAe,WAAW,GAAG;EAC/B,QAAQ,CAER;CACF;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,qBACd,KACA,SAC0B;CAC1B,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,mDAAmD;CAC7E,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,gDAAgD;CAE9E,OAAO,aAAuB,KAAK,SAAS,EAAE,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;AACzF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAS,iBAAiB,OAAyC;CACjE,IAAI,iBAAiB,gBAAgB,OAAO;CAC5C,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CAExD,MAAM,YAAY;CAElB,OACE,OAAO,UAAU,eAAe,YAChC,OAAO,UAAU,YAAY,YAC7B,aAAa,aACb,UAAU;AAEd;AAEA,qBAAqB,UAAU;CAC7B,IAAI,CAAC,iBAAiB,KAAK,GAAG,OAAO;CAIrC,IAAI,MAAM,SAAS,SACjB,OAAO,IAAI,SAAS,YAAY,wBAAwB,EAAE,OAAO,MAAM,CAAC;CAG1E,IAAI,MAAM,SAAS,YACjB,OAAO,IAAI,SAAS,aAAa,4BAA4B,EAAE,OAAO,MAAM,CAAC;CAO/E,IAAI,MAAM,SAAS,WAAW,MAAM,SAAS,cAAc,MAAM,SAAS,YACxE,OAAO,IAAI,SAAS,UAAU,mCAAmC,EAAE,OAAO,MAAM,CAAC;CAGnF,OAAO,IAAI,SAAS,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AAChE,CAAC"}
@@ -1,5 +1,12 @@
1
- /** Error categories the UI can branch on, independent of Postgres or Supabase. */
2
- export type AppErrorKind = 'conflict' | 'not-found' | 'network' | 'unknown';
1
+ /**
2
+ * Error categories the UI can branch on, independent of Postgres or Supabase.
3
+ *
4
+ * `denied` is the one that is not a fault: the request was understood, well
5
+ * formed and refused. A screen that treats it as a failure tells the reader
6
+ * something is broken and sends them to support, when what they need is to
7
+ * sign in again or to be told the thing is not theirs.
8
+ */
9
+ export type AppErrorKind = 'conflict' | 'not-found' | 'network' | 'denied' | 'unknown';
3
10
  /**
4
11
  * A normalised error thrown by the data layer.
5
12
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rei-kit",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Vue 3 and Tailwind 4 design system and shared runtime. Extracted from Hibi.",
5
5
  "license": "MIT",
6
6
  "author": "Ramazan Doğan",