@spacesync/client 0.1.7 → 0.1.9

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,5 +1,4 @@
1
- export { AnalogClockPicker } from './chunk-34332W2P.js';
2
- import './chunk-H3CNZCYU.js';
1
+ import { parseHm, toTimeStr, formatTimeForDisplay } from './chunk-F7MU5T46.js';
3
2
  import './chunk-RV5PMZYZ.js';
4
3
  import './chunk-L65F4EMK.js';
5
4
  import './chunk-PBNBLCC2.js';
@@ -10,6 +9,383 @@ import './chunk-LUB7QWGU.js';
10
9
  import './chunk-VHINHHZQ.js';
11
10
  import './chunk-FCY5FYJB.js';
12
11
  import './chunk-OWNUMPUC.js';
13
- import './chunk-DMPSQ22Q.js';
12
+ import { cn } from './chunk-DMPSQ22Q.js';
13
+ import { useRef, useState, useCallback, useMemo } from 'react';
14
+ import { jsxs, jsx } from 'react/jsx-runtime';
15
+
16
+ var SIZE = 248;
17
+ var CX = SIZE / 2;
18
+ var CY = SIZE / 2;
19
+ var FACE_R = 98;
20
+ var HOUR_LABEL_R = 72;
21
+ var MINUTE_TICK_R = 84;
22
+ function pointToAngleDeg(x, y) {
23
+ const dx = x - CX;
24
+ const dy = y - CY;
25
+ let deg = Math.atan2(dy, dx) * 180 / Math.PI + 90;
26
+ if (deg < 0) deg += 360;
27
+ return deg;
28
+ }
29
+ function angleToHour12(deg) {
30
+ const h = Math.round(deg / 30) % 12;
31
+ return h === 0 ? 12 : h;
32
+ }
33
+ function angleToHour24(deg) {
34
+ return Math.round(deg / 15) % 24;
35
+ }
36
+ function angleToMinute(deg) {
37
+ return Math.round(deg / 6) % 60;
38
+ }
39
+ function hour12To24(h12, ap) {
40
+ if (ap === "am") return h12 === 12 ? 0 : h12;
41
+ return h12 === 12 ? 12 : h12 + 12;
42
+ }
43
+ function hour24To12(h24) {
44
+ return { h12: h24 % 12 || 12, ap: h24 >= 12 ? "pm" : "am" };
45
+ }
46
+ function handEnd(deg, length) {
47
+ const rad = (deg - 90) * Math.PI / 180;
48
+ return { x: CX + Math.cos(rad) * length, y: CY + Math.sin(rad) * length };
49
+ }
50
+ function hourCenterDeg(h, timeFormat) {
51
+ if (timeFormat === "24h") return h * 15;
52
+ return h % 12 * 30;
53
+ }
54
+ function sectorPath(centerDeg, spread, radius) {
55
+ const start = handEnd(centerDeg - spread, radius);
56
+ const end = handEnd(centerDeg + spread, radius);
57
+ return `M ${CX} ${CY} L ${start.x} ${start.y} A ${radius} ${radius} 0 0 1 ${end.x} ${end.y} Z`;
58
+ }
59
+ function pointerTarget(clientX, clientY, svg, phase, timeFormat) {
60
+ const rect = svg.getBoundingClientRect();
61
+ const scale = SIZE / rect.width;
62
+ const x = (clientX - rect.left) * scale;
63
+ const y = (clientY - rect.top) * scale;
64
+ const dist = Math.hypot(x - CX, y - CY);
65
+ if (dist < 24 || dist > FACE_R + 8) return null;
66
+ const deg = pointToAngleDeg(x, y);
67
+ if (phase === "hour") {
68
+ return timeFormat === "24h" ? angleToHour24(deg) : angleToHour12(deg);
69
+ }
70
+ return angleToMinute(deg);
71
+ }
72
+ function AnalogClockPicker({
73
+ value,
74
+ onChange,
75
+ timeFormat = "24h",
76
+ className
77
+ }) {
78
+ const svgRef = useRef(null);
79
+ const parsed = parseHm(value);
80
+ const editor = parsed ?? { h: 9, m: 0 };
81
+ const { h12, ap } = hour24To12(editor.h);
82
+ const [phase, setPhase] = useState("hour");
83
+ const [hoverTarget, setHoverTarget] = useState(null);
84
+ const setTime = useCallback(
85
+ (h, m) => {
86
+ onChange(toTimeStr(h, m));
87
+ },
88
+ [onChange]
89
+ );
90
+ const handlePointer = useCallback(
91
+ (clientX, clientY) => {
92
+ const svg = svgRef.current;
93
+ if (!svg) return;
94
+ const rect = svg.getBoundingClientRect();
95
+ const scale = SIZE / rect.width;
96
+ const x = (clientX - rect.left) * scale;
97
+ const y = (clientY - rect.top) * scale;
98
+ const deg = pointToAngleDeg(x, y);
99
+ if (phase === "hour") {
100
+ if (timeFormat === "24h") {
101
+ const h24 = angleToHour24(deg);
102
+ setTime(h24, editor.m);
103
+ } else {
104
+ const h = angleToHour12(deg);
105
+ setTime(hour12To24(h, ap), editor.m);
106
+ }
107
+ setPhase("minute");
108
+ } else {
109
+ const m = angleToMinute(deg);
110
+ setTime(editor.h, m);
111
+ }
112
+ },
113
+ [phase, timeFormat, ap, editor.h, editor.m, setTime]
114
+ );
115
+ const updateHover = useCallback(
116
+ (clientX, clientY) => {
117
+ const svg = svgRef.current;
118
+ if (!svg) return;
119
+ setHoverTarget(pointerTarget(clientX, clientY, svg, phase, timeFormat));
120
+ },
121
+ [phase, timeFormat]
122
+ );
123
+ const previewHour = phase === "hour" && hoverTarget != null ? timeFormat === "24h" ? hoverTarget : hour12To24(hoverTarget, ap) : editor.h;
124
+ const previewMinute = phase === "minute" && hoverTarget != null ? hoverTarget : editor.m;
125
+ const hourHandDeg = previewHour % 12 * 30 + previewMinute * 0.5;
126
+ const minuteHandDeg = previewMinute * 6;
127
+ const hourEnd = handEnd(hourHandDeg, 46);
128
+ const minuteEnd = handEnd(minuteHandDeg, 64);
129
+ const selectedHourKey = timeFormat === "24h" ? editor.h : h12;
130
+ const hoveredHourKey = phase === "hour" ? hoverTarget : null;
131
+ const hourMarkers = useMemo(() => {
132
+ if (timeFormat === "24h") {
133
+ return Array.from({ length: 24 }, (_, h) => {
134
+ const deg = h * 15;
135
+ const rad = (deg - 90) * Math.PI / 180;
136
+ const showLabel = h % 3 === 0;
137
+ const r = showLabel ? HOUR_LABEL_R - 6 : HOUR_LABEL_R + 4;
138
+ return {
139
+ key: h,
140
+ label: showLabel ? String(h).padStart(2, "0") : null,
141
+ x: CX + Math.cos(rad) * r,
142
+ y: CY + Math.sin(rad) * r,
143
+ deg,
144
+ h
145
+ };
146
+ });
147
+ }
148
+ return Array.from({ length: 12 }, (_, i) => {
149
+ const h = i === 0 ? 12 : i;
150
+ const deg = h * 30;
151
+ const rad = (deg - 90) * Math.PI / 180;
152
+ return {
153
+ key: h,
154
+ label: String(h),
155
+ x: CX + Math.cos(rad) * HOUR_LABEL_R,
156
+ y: CY + Math.sin(rad) * HOUR_LABEL_R,
157
+ deg,
158
+ h
159
+ };
160
+ });
161
+ }, [timeFormat]);
162
+ const minuteMarkers = useMemo(
163
+ () => Array.from({ length: 12 }, (_, i) => {
164
+ const m = i * 5;
165
+ const deg = m * 6;
166
+ const rad = (deg - 90) * Math.PI / 180;
167
+ const selected = editor.m === m;
168
+ const hovered = hoverTarget != null && Math.round(hoverTarget / 5) * 5 === m;
169
+ return {
170
+ m,
171
+ x: CX + Math.cos(rad) * MINUTE_TICK_R,
172
+ y: CY + Math.sin(rad) * MINUTE_TICK_R,
173
+ selected,
174
+ hovered
175
+ };
176
+ }),
177
+ [editor.m, hoverTarget]
178
+ );
179
+ const displayTime = formatTimeForDisplay(value || toTimeStr(editor.h, editor.m), timeFormat);
180
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-center gap-4", className), children: [
181
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-2xl border border-border bg-muted/60 px-4 py-2.5", children: [
182
+ /* @__PURE__ */ jsx(
183
+ "button",
184
+ {
185
+ type: "button",
186
+ onClick: () => setPhase("hour"),
187
+ className: cn(
188
+ "rounded-lg px-3 py-1 text-2xl font-semibold tabular-nums transition-colors",
189
+ phase === "hour" ? "bg-blue-500 text-white shadow-sm" : "text-foreground hover:bg-accent"
190
+ ),
191
+ children: timeFormat === "24h" ? String(editor.h).padStart(2, "0") : String(h12).padStart(2, "0")
192
+ }
193
+ ),
194
+ /* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold text-muted-foreground", children: ":" }),
195
+ /* @__PURE__ */ jsx(
196
+ "button",
197
+ {
198
+ type: "button",
199
+ onClick: () => setPhase("minute"),
200
+ className: cn(
201
+ "rounded-lg px-3 py-1 text-2xl font-semibold tabular-nums transition-colors",
202
+ phase === "minute" ? "bg-blue-500 text-white shadow-sm" : "text-foreground hover:bg-accent"
203
+ ),
204
+ children: String(editor.m).padStart(2, "0")
205
+ }
206
+ ),
207
+ timeFormat === "12h" ? /* @__PURE__ */ jsx("div", { className: "ml-2 flex flex-col gap-0.5", children: ["am", "pm"].map((p) => /* @__PURE__ */ jsx(
208
+ "button",
209
+ {
210
+ type: "button",
211
+ onClick: () => setTime(hour12To24(h12, p), editor.m),
212
+ className: cn(
213
+ "rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide transition-colors",
214
+ ap === p ? "bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300" : "text-muted-foreground hover:bg-accent"
215
+ ),
216
+ children: p
217
+ },
218
+ p
219
+ )) }) : null
220
+ ] }),
221
+ /* @__PURE__ */ jsx("div", { className: "relative", children: /* @__PURE__ */ jsxs(
222
+ "svg",
223
+ {
224
+ ref: svgRef,
225
+ width: SIZE,
226
+ height: SIZE,
227
+ viewBox: `0 0 ${SIZE} ${SIZE}`,
228
+ className: "cursor-pointer touch-none select-none",
229
+ onClick: (e) => handlePointer(e.clientX, e.clientY),
230
+ onPointerMove: (e) => updateHover(e.clientX, e.clientY),
231
+ onPointerLeave: () => setHoverTarget(null),
232
+ onPointerDown: (e) => {
233
+ e.preventDefault();
234
+ handlePointer(e.clientX, e.clientY);
235
+ },
236
+ children: [
237
+ /* @__PURE__ */ jsx("circle", { cx: CX, cy: CY, r: FACE_R, className: "fill-blue-50/80 dark:fill-blue-950/30" }),
238
+ /* @__PURE__ */ jsx(
239
+ "circle",
240
+ {
241
+ cx: CX,
242
+ cy: CY,
243
+ r: FACE_R,
244
+ className: "fill-none stroke-border",
245
+ strokeWidth: 1.5
246
+ }
247
+ ),
248
+ phase === "hour" && hoverTarget != null ? /* @__PURE__ */ jsx(
249
+ "path",
250
+ {
251
+ d: sectorPath(
252
+ hourCenterDeg(hoverTarget, timeFormat),
253
+ timeFormat === "24h" ? 7.5 : 15,
254
+ FACE_R - 2
255
+ ),
256
+ className: "fill-blue-200/70 transition-opacity dark:fill-blue-500/25"
257
+ }
258
+ ) : null,
259
+ phase === "minute" && hoverTarget != null ? /* @__PURE__ */ jsx(
260
+ "path",
261
+ {
262
+ d: sectorPath(hoverTarget * 6, 3, FACE_R - 2),
263
+ className: "fill-blue-200/70 transition-opacity dark:fill-blue-500/25"
264
+ }
265
+ ) : null,
266
+ phase === "hour" ? hourMarkers.map((m) => {
267
+ const isSelected = m.h === selectedHourKey || timeFormat === "12h" && m.h === h12;
268
+ const isHovered = m.h === hoveredHourKey;
269
+ const active = isHovered || isSelected && hoveredHourKey == null;
270
+ return /* @__PURE__ */ jsxs("g", { children: [
271
+ active ? /* @__PURE__ */ jsx(
272
+ "circle",
273
+ {
274
+ cx: m.x,
275
+ cy: m.y,
276
+ r: isHovered ? 18 : 16,
277
+ className: cn(
278
+ "transition-all duration-100",
279
+ isHovered ? "fill-blue-500 shadow-sm" : "fill-blue-400/90"
280
+ )
281
+ }
282
+ ) : null,
283
+ m.label ? /* @__PURE__ */ jsx(
284
+ "text",
285
+ {
286
+ x: m.x,
287
+ y: m.y,
288
+ textAnchor: "middle",
289
+ dominantBaseline: "central",
290
+ className: cn(
291
+ "pointer-events-none text-[11px] font-semibold transition-colors duration-100",
292
+ active ? "fill-white" : "fill-muted-foreground",
293
+ isHovered && "text-[12px] font-bold"
294
+ ),
295
+ children: m.label
296
+ }
297
+ ) : /* @__PURE__ */ jsx(
298
+ "circle",
299
+ {
300
+ cx: m.x,
301
+ cy: m.y,
302
+ r: isHovered ? 5 : 2,
303
+ className: cn(
304
+ "pointer-events-none transition-all duration-100",
305
+ isHovered ? "fill-white" : "fill-muted-foreground/40"
306
+ )
307
+ }
308
+ )
309
+ ] }, m.key);
310
+ }) : minuteMarkers.map((m) => {
311
+ const active = m.selected || m.hovered;
312
+ return /* @__PURE__ */ jsxs("g", { children: [
313
+ /* @__PURE__ */ jsx(
314
+ "circle",
315
+ {
316
+ cx: m.x,
317
+ cy: m.y,
318
+ r: m.hovered ? 16 : m.selected ? 14 : 4,
319
+ className: cn(
320
+ "transition-all duration-100",
321
+ m.hovered ? "fill-blue-400" : m.selected ? "fill-blue-500" : "fill-blue-200/80 dark:fill-blue-800/60"
322
+ )
323
+ }
324
+ ),
325
+ active || m.m % 15 === 0 ? /* @__PURE__ */ jsx(
326
+ "text",
327
+ {
328
+ x: m.x,
329
+ y: m.y,
330
+ textAnchor: "middle",
331
+ dominantBaseline: "central",
332
+ className: cn(
333
+ "pointer-events-none text-[10px] font-semibold transition-colors duration-100",
334
+ active ? "fill-white" : "fill-blue-700 dark:fill-blue-300"
335
+ ),
336
+ children: String(m.m).padStart(2, "0")
337
+ }
338
+ ) : null
339
+ ] }, m.m);
340
+ }),
341
+ /* @__PURE__ */ jsx(
342
+ "line",
343
+ {
344
+ x1: CX,
345
+ y1: CY,
346
+ x2: hourEnd.x,
347
+ y2: hourEnd.y,
348
+ className: cn(
349
+ "stroke-blue-600 transition-all duration-100 dark:stroke-blue-400",
350
+ phase === "hour" && hoverTarget != null && "stroke-blue-500 opacity-90"
351
+ ),
352
+ strokeWidth: 3.5,
353
+ strokeLinecap: "round"
354
+ }
355
+ ),
356
+ /* @__PURE__ */ jsx(
357
+ "line",
358
+ {
359
+ x1: CX,
360
+ y1: CY,
361
+ x2: minuteEnd.x,
362
+ y2: minuteEnd.y,
363
+ className: cn(
364
+ "stroke-blue-500 transition-all duration-100 dark:stroke-blue-300",
365
+ phase === "minute" && hoverTarget != null && "stroke-blue-400 opacity-90"
366
+ ),
367
+ strokeWidth: 2.5,
368
+ strokeLinecap: "round"
369
+ }
370
+ ),
371
+ /* @__PURE__ */ jsx("circle", { cx: CX, cy: CY, r: 5, className: "fill-blue-600 dark:fill-blue-400" }),
372
+ /* @__PURE__ */ jsx("circle", { cx: CX, cy: CY, r: 2.5, className: "fill-white" })
373
+ ]
374
+ }
375
+ ) }),
376
+ /* @__PURE__ */ jsxs("p", { className: "text-center text-[11px] text-muted-foreground", children: [
377
+ phase === "hour" ? "Hover or tap an hour" : "Hover or tap minutes \xB7 ",
378
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: hoverTarget != null && phase === "hour" ? formatTimeForDisplay(
379
+ toTimeStr(
380
+ timeFormat === "24h" ? hoverTarget : hour12To24(hoverTarget, ap),
381
+ editor.m
382
+ ),
383
+ timeFormat
384
+ ) : hoverTarget != null && phase === "minute" ? formatTimeForDisplay(toTimeStr(editor.h, hoverTarget), timeFormat) : displayTime })
385
+ ] })
386
+ ] });
387
+ }
388
+
389
+ export { AnalogClockPicker };
14
390
  //# sourceMappingURL=analog-clock-picker.js.map
15
391
  //# sourceMappingURL=analog-clock-picker.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"analog-clock-picker.js"}
1
+ {"version":3,"sources":["../src/booking/analog-clock-picker.tsx"],"names":[],"mappings":"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","file":"analog-clock-picker.js","sourcesContent":["import { useCallback, useMemo, useRef, useState } from \"react\";\nimport {\n formatTimeForDisplay,\n parseHm,\n toTimeStr,\n} from \"../ui/time-field\";\nimport { cn } from \"../ui\";\n\nconst SIZE = 248;\nconst CX = SIZE / 2;\nconst CY = SIZE / 2;\nconst FACE_R = 98;\nconst HOUR_LABEL_R = 72;\nconst MINUTE_TICK_R = 84;\n\ntype ClockPhase = \"hour\" | \"minute\";\n\nfunction pointToAngleDeg(x: number, y: number): number {\n const dx = x - CX;\n const dy = y - CY;\n let deg = (Math.atan2(dy, dx) * 180) / Math.PI + 90;\n if (deg < 0) deg += 360;\n return deg;\n}\n\nfunction angleToHour12(deg: number): number {\n const h = Math.round(deg / 30) % 12;\n return h === 0 ? 12 : h;\n}\n\nfunction angleToHour24(deg: number): number {\n return Math.round(deg / 15) % 24;\n}\n\nfunction angleToMinute(deg: number): number {\n return Math.round(deg / 6) % 60;\n}\n\nfunction hour12To24(h12: number, ap: \"am\" | \"pm\"): number {\n if (ap === \"am\") return h12 === 12 ? 0 : h12;\n return h12 === 12 ? 12 : h12 + 12;\n}\n\nfunction hour24To12(h24: number): { h12: number; ap: \"am\" | \"pm\" } {\n return { h12: h24 % 12 || 12, ap: h24 >= 12 ? \"pm\" : \"am\" };\n}\n\nfunction handEnd(deg: number, length: number): { x: number; y: number } {\n const rad = ((deg - 90) * Math.PI) / 180;\n return { x: CX + Math.cos(rad) * length, y: CY + Math.sin(rad) * length };\n}\n\nfunction hourCenterDeg(h: number, timeFormat: \"12h\" | \"24h\"): number {\n if (timeFormat === \"24h\") return h * 15;\n return (h % 12) * 30;\n}\n\nfunction sectorPath(centerDeg: number, spread: number, radius: number): string {\n const start = handEnd(centerDeg - spread, radius);\n const end = handEnd(centerDeg + spread, radius);\n return `M ${CX} ${CY} L ${start.x} ${start.y} A ${radius} ${radius} 0 0 1 ${end.x} ${end.y} Z`;\n}\n\nfunction pointerTarget(\n clientX: number,\n clientY: number,\n svg: SVGSVGElement,\n phase: ClockPhase,\n timeFormat: \"12h\" | \"24h\",\n): number | null {\n const rect = svg.getBoundingClientRect();\n const scale = SIZE / rect.width;\n const x = (clientX - rect.left) * scale;\n const y = (clientY - rect.top) * scale;\n const dist = Math.hypot(x - CX, y - CY);\n if (dist < 24 || dist > FACE_R + 8) return null;\n\n const deg = pointToAngleDeg(x, y);\n if (phase === \"hour\") {\n return timeFormat === \"24h\" ? angleToHour24(deg) : angleToHour12(deg);\n }\n return angleToMinute(deg);\n}\n\nexport function AnalogClockPicker({\n value,\n onChange,\n timeFormat = \"24h\",\n className,\n}: {\n value: string;\n onChange: (v: string) => void;\n timeFormat?: \"12h\" | \"24h\";\n className?: string;\n}) {\n const svgRef = useRef<SVGSVGElement>(null);\n const parsed = parseHm(value);\n const editor = parsed ?? { h: 9, m: 0 };\n const { h12, ap } = hour24To12(editor.h);\n\n const [phase, setPhase] = useState<ClockPhase>(\"hour\");\n const [hoverTarget, setHoverTarget] = useState<number | null>(null);\n\n const setTime = useCallback(\n (h: number, m: number) => {\n onChange(toTimeStr(h, m));\n },\n [onChange],\n );\n\n const handlePointer = useCallback(\n (clientX: number, clientY: number) => {\n const svg = svgRef.current;\n if (!svg) return;\n const rect = svg.getBoundingClientRect();\n const scale = SIZE / rect.width;\n const x = (clientX - rect.left) * scale;\n const y = (clientY - rect.top) * scale;\n const deg = pointToAngleDeg(x, y);\n\n if (phase === \"hour\") {\n if (timeFormat === \"24h\") {\n const h24 = angleToHour24(deg);\n setTime(h24, editor.m);\n } else {\n const h = angleToHour12(deg);\n setTime(hour12To24(h, ap), editor.m);\n }\n setPhase(\"minute\");\n } else {\n const m = angleToMinute(deg);\n setTime(editor.h, m);\n }\n },\n [phase, timeFormat, ap, editor.h, editor.m, setTime],\n );\n\n const updateHover = useCallback(\n (clientX: number, clientY: number) => {\n const svg = svgRef.current;\n if (!svg) return;\n setHoverTarget(pointerTarget(clientX, clientY, svg, phase, timeFormat));\n },\n [phase, timeFormat],\n );\n\n const previewHour =\n phase === \"hour\" && hoverTarget != null\n ? timeFormat === \"24h\"\n ? hoverTarget\n : hour12To24(hoverTarget, ap)\n : editor.h;\n\n const previewMinute = phase === \"minute\" && hoverTarget != null ? hoverTarget : editor.m;\n\n const hourHandDeg = (previewHour % 12) * 30 + previewMinute * 0.5;\n const minuteHandDeg = previewMinute * 6;\n const hourEnd = handEnd(hourHandDeg, 46);\n const minuteEnd = handEnd(minuteHandDeg, 64);\n\n const selectedHourKey =\n timeFormat === \"24h\" ? editor.h : h12;\n const hoveredHourKey = phase === \"hour\" ? hoverTarget : null;\n\n const hourMarkers = useMemo(() => {\n if (timeFormat === \"24h\") {\n return Array.from({ length: 24 }, (_, h) => {\n const deg = h * 15;\n const rad = ((deg - 90) * Math.PI) / 180;\n const showLabel = h % 3 === 0;\n const r = showLabel ? HOUR_LABEL_R - 6 : HOUR_LABEL_R + 4;\n return {\n key: h,\n label: showLabel ? String(h).padStart(2, \"0\") : null,\n x: CX + Math.cos(rad) * r,\n y: CY + Math.sin(rad) * r,\n deg,\n h,\n };\n });\n }\n return Array.from({ length: 12 }, (_, i) => {\n const h = i === 0 ? 12 : i;\n const deg = h * 30;\n const rad = ((deg - 90) * Math.PI) / 180;\n return {\n key: h,\n label: String(h),\n x: CX + Math.cos(rad) * HOUR_LABEL_R,\n y: CY + Math.sin(rad) * HOUR_LABEL_R,\n deg,\n h,\n };\n });\n }, [timeFormat]);\n\n const minuteMarkers = useMemo(\n () =>\n Array.from({ length: 12 }, (_, i) => {\n const m = i * 5;\n const deg = m * 6;\n const rad = ((deg - 90) * Math.PI) / 180;\n const selected = editor.m === m;\n const hovered =\n hoverTarget != null && Math.round(hoverTarget / 5) * 5 === m;\n return {\n m,\n x: CX + Math.cos(rad) * MINUTE_TICK_R,\n y: CY + Math.sin(rad) * MINUTE_TICK_R,\n selected,\n hovered,\n };\n }),\n [editor.m, hoverTarget],\n );\n\n const displayTime = formatTimeForDisplay(value || toTimeStr(editor.h, editor.m), timeFormat);\n\n return (\n <div className={cn(\"flex flex-col items-center gap-4\", className)}>\n <div className=\"flex items-center gap-1 rounded-2xl border border-border bg-muted/60 px-4 py-2.5\">\n <button\n type=\"button\"\n onClick={() => setPhase(\"hour\")}\n className={cn(\n \"rounded-lg px-3 py-1 text-2xl font-semibold tabular-nums transition-colors\",\n phase === \"hour\"\n ? \"bg-blue-500 text-white shadow-sm\"\n : \"text-foreground hover:bg-accent\",\n )}\n >\n {timeFormat === \"24h\"\n ? String(editor.h).padStart(2, \"0\")\n : String(h12).padStart(2, \"0\")}\n </button>\n <span className=\"text-2xl font-semibold text-muted-foreground\">:</span>\n <button\n type=\"button\"\n onClick={() => setPhase(\"minute\")}\n className={cn(\n \"rounded-lg px-3 py-1 text-2xl font-semibold tabular-nums transition-colors\",\n phase === \"minute\"\n ? \"bg-blue-500 text-white shadow-sm\"\n : \"text-foreground hover:bg-accent\",\n )}\n >\n {String(editor.m).padStart(2, \"0\")}\n </button>\n {timeFormat === \"12h\" ? (\n <div className=\"ml-2 flex flex-col gap-0.5\">\n {([\"am\", \"pm\"] as const).map((p) => (\n <button\n key={p}\n type=\"button\"\n onClick={() => setTime(hour12To24(h12, p), editor.m)}\n className={cn(\n \"rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide transition-colors\",\n ap === p\n ? \"bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300\"\n : \"text-muted-foreground hover:bg-accent\",\n )}\n >\n {p}\n </button>\n ))}\n </div>\n ) : null}\n </div>\n\n <div className=\"relative\">\n <svg\n ref={svgRef}\n width={SIZE}\n height={SIZE}\n viewBox={`0 0 ${SIZE} ${SIZE}`}\n className=\"cursor-pointer touch-none select-none\"\n onClick={(e) => handlePointer(e.clientX, e.clientY)}\n onPointerMove={(e) => updateHover(e.clientX, e.clientY)}\n onPointerLeave={() => setHoverTarget(null)}\n onPointerDown={(e) => {\n e.preventDefault();\n handlePointer(e.clientX, e.clientY);\n }}\n >\n <circle cx={CX} cy={CY} r={FACE_R} className=\"fill-blue-50/80 dark:fill-blue-950/30\" />\n <circle\n cx={CX}\n cy={CY}\n r={FACE_R}\n className=\"fill-none stroke-border\"\n strokeWidth={1.5}\n />\n\n {phase === \"hour\" && hoverTarget != null ? (\n <path\n d={sectorPath(\n hourCenterDeg(hoverTarget, timeFormat),\n timeFormat === \"24h\" ? 7.5 : 15,\n FACE_R - 2,\n )}\n className=\"fill-blue-200/70 transition-opacity dark:fill-blue-500/25\"\n />\n ) : null}\n\n {phase === \"minute\" && hoverTarget != null ? (\n <path\n d={sectorPath(hoverTarget * 6, 3, FACE_R - 2)}\n className=\"fill-blue-200/70 transition-opacity dark:fill-blue-500/25\"\n />\n ) : null}\n\n {phase === \"hour\"\n ? hourMarkers.map((m) => {\n const isSelected = m.h === selectedHourKey || (timeFormat === \"12h\" && m.h === h12);\n const isHovered = m.h === hoveredHourKey;\n const active = isHovered || (isSelected && hoveredHourKey == null);\n return (\n <g key={m.key}>\n {active ? (\n <circle\n cx={m.x}\n cy={m.y}\n r={isHovered ? 18 : 16}\n className={cn(\n \"transition-all duration-100\",\n isHovered\n ? \"fill-blue-500 shadow-sm\"\n : \"fill-blue-400/90\",\n )}\n />\n ) : null}\n {m.label ? (\n <text\n x={m.x}\n y={m.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n className={cn(\n \"pointer-events-none text-[11px] font-semibold transition-colors duration-100\",\n active ? \"fill-white\" : \"fill-muted-foreground\",\n isHovered && \"text-[12px] font-bold\",\n )}\n >\n {m.label}\n </text>\n ) : (\n <circle\n cx={m.x}\n cy={m.y}\n r={isHovered ? 5 : 2}\n className={cn(\n \"pointer-events-none transition-all duration-100\",\n isHovered ? \"fill-white\" : \"fill-muted-foreground/40\",\n )}\n />\n )}\n </g>\n );\n })\n : minuteMarkers.map((m) => {\n const active = m.selected || m.hovered;\n return (\n <g key={m.m}>\n <circle\n cx={m.x}\n cy={m.y}\n r={m.hovered ? 16 : m.selected ? 14 : 4}\n className={cn(\n \"transition-all duration-100\",\n m.hovered\n ? \"fill-blue-400\"\n : m.selected\n ? \"fill-blue-500\"\n : \"fill-blue-200/80 dark:fill-blue-800/60\",\n )}\n />\n {active || m.m % 15 === 0 ? (\n <text\n x={m.x}\n y={m.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n className={cn(\n \"pointer-events-none text-[10px] font-semibold transition-colors duration-100\",\n active ? \"fill-white\" : \"fill-blue-700 dark:fill-blue-300\",\n )}\n >\n {String(m.m).padStart(2, \"0\")}\n </text>\n ) : null}\n </g>\n );\n })}\n\n <line\n x1={CX}\n y1={CY}\n x2={hourEnd.x}\n y2={hourEnd.y}\n className={cn(\n \"stroke-blue-600 transition-all duration-100 dark:stroke-blue-400\",\n phase === \"hour\" && hoverTarget != null && \"stroke-blue-500 opacity-90\",\n )}\n strokeWidth={3.5}\n strokeLinecap=\"round\"\n />\n <line\n x1={CX}\n y1={CY}\n x2={minuteEnd.x}\n y2={minuteEnd.y}\n className={cn(\n \"stroke-blue-500 transition-all duration-100 dark:stroke-blue-300\",\n phase === \"minute\" && hoverTarget != null && \"stroke-blue-400 opacity-90\",\n )}\n strokeWidth={2.5}\n strokeLinecap=\"round\"\n />\n <circle cx={CX} cy={CY} r={5} className=\"fill-blue-600 dark:fill-blue-400\" />\n <circle cx={CX} cy={CY} r={2.5} className=\"fill-white\" />\n </svg>\n </div>\n\n <p className=\"text-center text-[11px] text-muted-foreground\">\n {phase === \"hour\" ? \"Hover or tap an hour\" : \"Hover or tap minutes · \"}\n <span className=\"font-medium text-foreground\">\n {hoverTarget != null && phase === \"hour\"\n ? formatTimeForDisplay(\n toTimeStr(\n timeFormat === \"24h\" ? hoverTarget : hour12To24(hoverTarget, ap),\n editor.m,\n ),\n timeFormat,\n )\n : hoverTarget != null && phase === \"minute\"\n ? formatTimeForDisplay(toTimeStr(editor.h, hoverTarget), timeFormat)\n : displayTime}\n </span>\n </p>\n </div>\n );\n}\n"]}
package/dist/api.d.ts CHANGED
@@ -176,6 +176,14 @@ interface TokenStorage {
176
176
  */
177
177
  declare function createLocalStorageTokenStorage(namespace: string): TokenStorage;
178
178
 
179
+ type RememberedCredentials = {
180
+ email: string;
181
+ password: string;
182
+ };
183
+ declare function clearRememberedCredentials(namespace: string): void;
184
+ declare function saveRememberedCredentials(namespace: string, credentials: RememberedCredentials): Promise<void>;
185
+ declare function loadRememberedCredentials(namespace: string): Promise<RememberedCredentials | null>;
186
+
179
187
  declare module "axios" {
180
188
  interface AxiosRequestConfig {
181
189
  /** Set on requests (login/register/refresh) that should never get a Bearer header or trigger a refresh-retry. */
@@ -219,4 +227,4 @@ interface ApiRequestResult<TData> {
219
227
  */
220
228
  declare function apiRequest<TData>(client: AxiosInstance, requestConfig: AxiosRequestConfig, dataSchema: z.ZodType<TData>): Promise<ApiRequestResult<TData>>;
221
229
 
222
- export { type ApiErrorBody, type ApiRequestResult, type ApiResponseMeta, type ApiSuccessBody, type AuthTokens, type CreateApiClientConfig, type TokenStorage, apiErrorBodySchema, apiRequest, apiSuccessBodySchema, createApiClient, createLocalStorageTokenStorage, paginationMetaSchema };
230
+ export { type ApiErrorBody, type ApiRequestResult, type ApiResponseMeta, type ApiSuccessBody, type AuthTokens, type CreateApiClientConfig, type RememberedCredentials, type TokenStorage, apiErrorBodySchema, apiRequest, apiSuccessBodySchema, clearRememberedCredentials, createApiClient, createLocalStorageTokenStorage, loadRememberedCredentials, paginationMetaSchema, saveRememberedCredentials };
package/dist/api.js CHANGED
@@ -23,6 +23,89 @@ function createLocalStorageTokenStorage(namespace) {
23
23
  }
24
24
  };
25
25
  }
26
+
27
+ // src/api/rememberedCredentials.ts
28
+ var STORAGE_VERSION = 1;
29
+ function storageKey(namespace) {
30
+ return `coworking.${namespace}.rememberedLogin`;
31
+ }
32
+ function bytesToBase64(bytes) {
33
+ let binary = "";
34
+ for (const byte of bytes) binary += String.fromCharCode(byte);
35
+ return btoa(binary);
36
+ }
37
+ function base64ToArrayBuffer(value) {
38
+ const binary = atob(value);
39
+ const bytes = new Uint8Array(binary.length);
40
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
41
+ const copy = new ArrayBuffer(bytes.byteLength);
42
+ new Uint8Array(copy).set(bytes);
43
+ return copy;
44
+ }
45
+ async function deriveKey(namespace) {
46
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
47
+ const material = await crypto.subtle.digest(
48
+ "SHA-256",
49
+ new TextEncoder().encode(`spacesync.remember.${namespace}.${origin}`)
50
+ );
51
+ return crypto.subtle.importKey("raw", material, "AES-GCM", false, ["encrypt", "decrypt"]);
52
+ }
53
+ function isRememberedPayload(value) {
54
+ if (!value || typeof value !== "object") return false;
55
+ const record = value;
56
+ return record.v === STORAGE_VERSION && typeof record.email === "string" && typeof record.iv === "string" && typeof record.cipher === "string";
57
+ }
58
+ function clearRememberedCredentials(namespace) {
59
+ try {
60
+ localStorage.removeItem(storageKey(namespace));
61
+ } catch {
62
+ }
63
+ }
64
+ async function saveRememberedCredentials(namespace, credentials) {
65
+ const iv = crypto.getRandomValues(new Uint8Array(12));
66
+ const key = await deriveKey(namespace);
67
+ const cipherBuffer = await crypto.subtle.encrypt(
68
+ { name: "AES-GCM", iv },
69
+ key,
70
+ new TextEncoder().encode(credentials.password)
71
+ );
72
+ const payload = {
73
+ v: STORAGE_VERSION,
74
+ email: credentials.email,
75
+ iv: bytesToBase64(iv),
76
+ cipher: bytesToBase64(new Uint8Array(cipherBuffer))
77
+ };
78
+ localStorage.setItem(storageKey(namespace), JSON.stringify(payload));
79
+ }
80
+ async function loadRememberedCredentials(namespace) {
81
+ let raw = null;
82
+ try {
83
+ raw = localStorage.getItem(storageKey(namespace));
84
+ } catch {
85
+ return null;
86
+ }
87
+ if (!raw) return null;
88
+ try {
89
+ const parsed = JSON.parse(raw);
90
+ if (!isRememberedPayload(parsed) || !parsed.email) {
91
+ clearRememberedCredentials(namespace);
92
+ return null;
93
+ }
94
+ const key = await deriveKey(namespace);
95
+ const plainBuffer = await crypto.subtle.decrypt(
96
+ { name: "AES-GCM", iv: base64ToArrayBuffer(parsed.iv) },
97
+ key,
98
+ base64ToArrayBuffer(parsed.cipher)
99
+ );
100
+ return {
101
+ email: parsed.email,
102
+ password: new TextDecoder().decode(plainBuffer)
103
+ };
104
+ } catch {
105
+ clearRememberedCredentials(namespace);
106
+ return null;
107
+ }
108
+ }
26
109
  function createApiClient(config) {
27
110
  const { baseURL, tokenStorage, refreshPath, onAuthExpired } = config;
28
111
  const client = axios.create({ baseURL });
@@ -82,6 +165,6 @@ async function apiRequest(client, requestConfig, dataSchema) {
82
165
  }
83
166
  }
84
167
 
85
- export { apiRequest, createApiClient, createLocalStorageTokenStorage };
168
+ export { apiRequest, clearRememberedCredentials, createApiClient, createLocalStorageTokenStorage, loadRememberedCredentials, saveRememberedCredentials };
86
169
  //# sourceMappingURL=api.js.map
87
170
  //# sourceMappingURL=api.js.map
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api/tokenStorage.ts","../src/api/createApiClient.ts","../src/api/request.ts"],"names":[],"mappings":"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","file":"api.js","sourcesContent":["export interface AuthTokens {\n accessToken: string;\n refreshToken: string;\n}\n\nexport interface TokenStorage {\n getAccessToken(): string | null;\n getRefreshToken(): string | null;\n setTokens(tokens: AuthTokens): void;\n clearTokens(): void;\n}\n\n/**\n * localStorage-backed token storage, namespaced per app/scope (e.g. \"tenant\", \"platform\")\n * so multiple apps on the same origin (or a single app talking to two backends) never\n * collide on the same keys.\n */\nexport function createLocalStorageTokenStorage(namespace: string): TokenStorage {\n const accessKey = `coworking.${namespace}.accessToken`;\n const refreshKey = `coworking.${namespace}.refreshToken`;\n\n return {\n getAccessToken() {\n return localStorage.getItem(accessKey);\n },\n getRefreshToken() {\n return localStorage.getItem(refreshKey);\n },\n setTokens({ accessToken, refreshToken }) {\n localStorage.setItem(accessKey, accessToken);\n localStorage.setItem(refreshKey, refreshToken);\n },\n clearTokens() {\n localStorage.removeItem(accessKey);\n localStorage.removeItem(refreshKey);\n },\n };\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport type { AuthTokens, TokenStorage } from \"./tokenStorage\";\n\ndeclare module \"axios\" {\n export interface AxiosRequestConfig {\n /** Set on requests (login/register/refresh) that should never get a Bearer header or trigger a refresh-retry. */\n skipAuth?: boolean;\n /** Internal flag set by the response interceptor to avoid retrying the same request twice. */\n _retriedAfterRefresh?: boolean;\n }\n}\n\nexport interface CreateApiClientConfig {\n baseURL: string;\n tokenStorage: TokenStorage;\n /** Path (relative to `baseURL`) used to exchange a refresh token for a new token pair, e.g. \"/auth/refresh\". */\n refreshPath: string;\n /** Called once refreshing the session has failed (bad/expired refresh token) - e.g. navigate to /login. */\n onAuthExpired?: () => void;\n}\n\n/**\n * Creates an axios instance that:\n * - attaches `Authorization: Bearer <accessToken>` to every request (unless `skipAuth` is set on that request's config)\n * - on a 401, refreshes the session exactly once (concurrent 401s share the same in-flight refresh call via\n * `refreshPromise`, so a burst of requests never fires multiple refreshes) and retries the original request\n * - on refresh failure, clears stored tokens and calls `onAuthExpired` so the app can redirect to login\n *\n * Does NOT unwrap the `{ success, data, error }` envelope itself - see `request.ts` for that - so raw responses\n * (e.g. the 204 from `/auth/logout`) are still inspectable when needed.\n */\nexport function createApiClient(config: CreateApiClientConfig): AxiosInstance {\n const { baseURL, tokenStorage, refreshPath, onAuthExpired } = config;\n const client = axios.create({ baseURL });\n\n let refreshPromise: Promise<AuthTokens> | null = null;\n\n async function refreshTokens(): Promise<AuthTokens> {\n const refreshToken = tokenStorage.getRefreshToken();\n if (!refreshToken) throw new Error(\"No refresh token available\");\n\n // Uses the bare `axios` instance (not `client`) so this call never passes through the\n // interceptors below - it can't trigger its own refresh-retry loop.\n const response = await axios.post<{ data: AuthTokens }>(`${baseURL}${refreshPath}`, { refreshToken });\n const tokens = response.data.data;\n tokenStorage.setTokens(tokens);\n return tokens;\n }\n\n client.interceptors.request.use((requestConfig) => {\n if (requestConfig.skipAuth) return requestConfig;\n\n const accessToken = tokenStorage.getAccessToken();\n if (accessToken) {\n requestConfig.headers.set(\"Authorization\", `Bearer ${accessToken}`);\n }\n return requestConfig;\n });\n\n client.interceptors.response.use(\n (response) => response,\n async (error) => {\n if (!axios.isAxiosError(error) || error.response?.status !== 401) {\n throw error;\n }\n\n const originalRequest = error.config;\n if (!originalRequest || originalRequest.skipAuth || originalRequest._retriedAfterRefresh) {\n throw error;\n }\n\n try {\n refreshPromise ??= refreshTokens().finally(() => {\n refreshPromise = null;\n });\n const tokens = await refreshPromise;\n\n originalRequest._retriedAfterRefresh = true;\n originalRequest.headers.set(\"Authorization\", `Bearer ${tokens.accessToken}`);\n return client.request(originalRequest);\n } catch {\n tokenStorage.clearTokens();\n onAuthExpired?.();\n throw error; // surface the original 401, not the refresh failure\n }\n },\n );\n\n return client;\n}\n","import type { AxiosInstance, AxiosRequestConfig } from \"axios\";\nimport type { z } from \"zod\";\nimport { apiSuccessBodySchema, type ApiResponseMeta } from \"./envelope\";\nimport { normalizeAxiosError } from \"./errors\";\n\nexport interface ApiRequestResult<TData> {\n data: TData;\n meta?: ApiResponseMeta;\n}\n\n/**\n * Runs a request through the given client, validates + unwraps the `{ success, data, error, meta }`\n * envelope against `dataSchema`, and normalizes any failure (network, envelope-shape mismatch, or a\n * `{ success: false }` response) into an `ApiClientError`.\n *\n * This is the one place response validation + error normalization happens, so every service call\n * built on top of it gets a runtime-verified response \"for free\" - if the backend's actual response\n * doesn't match `dataSchema`, this throws instead of silently returning malformed data.\n */\nexport async function apiRequest<TData>(\n client: AxiosInstance,\n requestConfig: AxiosRequestConfig,\n dataSchema: z.ZodType<TData>,\n): Promise<ApiRequestResult<TData>> {\n try {\n const response = await client.request(requestConfig);\n const parsed = apiSuccessBodySchema(dataSchema).parse(response.data);\n // `parsed.data` is structurally `TData` by construction (it came from `dataSchema`), but TS\n // can't prove that through a generic zod composition - narrow it explicitly.\n return { data: parsed.data as TData, meta: parsed.meta };\n } catch (err) {\n throw normalizeAxiosError(err);\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/api/tokenStorage.ts","../src/api/rememberedCredentials.ts","../src/api/createApiClient.ts","../src/api/request.ts"],"names":[],"mappings":"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","file":"api.js","sourcesContent":["export interface AuthTokens {\n accessToken: string;\n refreshToken: string;\n}\n\nexport interface TokenStorage {\n getAccessToken(): string | null;\n getRefreshToken(): string | null;\n setTokens(tokens: AuthTokens): void;\n clearTokens(): void;\n}\n\n/**\n * localStorage-backed token storage, namespaced per app/scope (e.g. \"tenant\", \"platform\")\n * so multiple apps on the same origin (or a single app talking to two backends) never\n * collide on the same keys.\n */\nexport function createLocalStorageTokenStorage(namespace: string): TokenStorage {\n const accessKey = `coworking.${namespace}.accessToken`;\n const refreshKey = `coworking.${namespace}.refreshToken`;\n\n return {\n getAccessToken() {\n return localStorage.getItem(accessKey);\n },\n getRefreshToken() {\n return localStorage.getItem(refreshKey);\n },\n setTokens({ accessToken, refreshToken }) {\n localStorage.setItem(accessKey, accessToken);\n localStorage.setItem(refreshKey, refreshToken);\n },\n clearTokens() {\n localStorage.removeItem(accessKey);\n localStorage.removeItem(refreshKey);\n },\n };\n}\n","const STORAGE_VERSION = 1;\n\ntype RememberedPayload = {\n v: number;\n email: string;\n iv: string;\n cipher: string;\n};\n\nexport type RememberedCredentials = {\n email: string;\n password: string;\n};\n\nfunction storageKey(namespace: string): string {\n return `coworking.${namespace}.rememberedLogin`;\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction base64ToArrayBuffer(value: string): ArrayBuffer {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);\n const copy = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(copy).set(bytes);\n return copy;\n}\n\nasync function deriveKey(namespace: string): Promise<CryptoKey> {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n const material = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(`spacesync.remember.${namespace}.${origin}`),\n );\n return crypto.subtle.importKey(\"raw\", material, \"AES-GCM\", false, [\"encrypt\", \"decrypt\"]);\n}\n\nfunction isRememberedPayload(value: unknown): value is RememberedPayload {\n if (!value || typeof value !== \"object\") return false;\n const record = value as Record<string, unknown>;\n return (\n record.v === STORAGE_VERSION &&\n typeof record.email === \"string\" &&\n typeof record.iv === \"string\" &&\n typeof record.cipher === \"string\"\n );\n}\n\nexport function clearRememberedCredentials(namespace: string): void {\n try {\n localStorage.removeItem(storageKey(namespace));\n } catch {\n // localStorage may be unavailable (private mode / quota).\n }\n}\n\nexport async function saveRememberedCredentials(\n namespace: string,\n credentials: RememberedCredentials,\n): Promise<void> {\n const iv = crypto.getRandomValues(new Uint8Array(12));\n const key = await deriveKey(namespace);\n const cipherBuffer = await crypto.subtle.encrypt(\n { name: \"AES-GCM\", iv },\n key,\n new TextEncoder().encode(credentials.password),\n );\n const payload: RememberedPayload = {\n v: STORAGE_VERSION,\n email: credentials.email,\n iv: bytesToBase64(iv),\n cipher: bytesToBase64(new Uint8Array(cipherBuffer)),\n };\n localStorage.setItem(storageKey(namespace), JSON.stringify(payload));\n}\n\nexport async function loadRememberedCredentials(\n namespace: string,\n): Promise<RememberedCredentials | null> {\n let raw: string | null = null;\n try {\n raw = localStorage.getItem(storageKey(namespace));\n } catch {\n return null;\n }\n if (!raw) return null;\n\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!isRememberedPayload(parsed) || !parsed.email) {\n clearRememberedCredentials(namespace);\n return null;\n }\n const key = await deriveKey(namespace);\n const plainBuffer = await crypto.subtle.decrypt(\n { name: \"AES-GCM\", iv: base64ToArrayBuffer(parsed.iv) },\n key,\n base64ToArrayBuffer(parsed.cipher),\n );\n return {\n email: parsed.email,\n password: new TextDecoder().decode(plainBuffer),\n };\n } catch {\n clearRememberedCredentials(namespace);\n return null;\n }\n}\n","import axios, { type AxiosInstance } from \"axios\";\nimport type { AuthTokens, TokenStorage } from \"./tokenStorage\";\n\ndeclare module \"axios\" {\n export interface AxiosRequestConfig {\n /** Set on requests (login/register/refresh) that should never get a Bearer header or trigger a refresh-retry. */\n skipAuth?: boolean;\n /** Internal flag set by the response interceptor to avoid retrying the same request twice. */\n _retriedAfterRefresh?: boolean;\n }\n}\n\nexport interface CreateApiClientConfig {\n baseURL: string;\n tokenStorage: TokenStorage;\n /** Path (relative to `baseURL`) used to exchange a refresh token for a new token pair, e.g. \"/auth/refresh\". */\n refreshPath: string;\n /** Called once refreshing the session has failed (bad/expired refresh token) - e.g. navigate to /login. */\n onAuthExpired?: () => void;\n}\n\n/**\n * Creates an axios instance that:\n * - attaches `Authorization: Bearer <accessToken>` to every request (unless `skipAuth` is set on that request's config)\n * - on a 401, refreshes the session exactly once (concurrent 401s share the same in-flight refresh call via\n * `refreshPromise`, so a burst of requests never fires multiple refreshes) and retries the original request\n * - on refresh failure, clears stored tokens and calls `onAuthExpired` so the app can redirect to login\n *\n * Does NOT unwrap the `{ success, data, error }` envelope itself - see `request.ts` for that - so raw responses\n * (e.g. the 204 from `/auth/logout`) are still inspectable when needed.\n */\nexport function createApiClient(config: CreateApiClientConfig): AxiosInstance {\n const { baseURL, tokenStorage, refreshPath, onAuthExpired } = config;\n const client = axios.create({ baseURL });\n\n let refreshPromise: Promise<AuthTokens> | null = null;\n\n async function refreshTokens(): Promise<AuthTokens> {\n const refreshToken = tokenStorage.getRefreshToken();\n if (!refreshToken) throw new Error(\"No refresh token available\");\n\n // Uses the bare `axios` instance (not `client`) so this call never passes through the\n // interceptors below - it can't trigger its own refresh-retry loop.\n const response = await axios.post<{ data: AuthTokens }>(`${baseURL}${refreshPath}`, { refreshToken });\n const tokens = response.data.data;\n tokenStorage.setTokens(tokens);\n return tokens;\n }\n\n client.interceptors.request.use((requestConfig) => {\n if (requestConfig.skipAuth) return requestConfig;\n\n const accessToken = tokenStorage.getAccessToken();\n if (accessToken) {\n requestConfig.headers.set(\"Authorization\", `Bearer ${accessToken}`);\n }\n return requestConfig;\n });\n\n client.interceptors.response.use(\n (response) => response,\n async (error) => {\n if (!axios.isAxiosError(error) || error.response?.status !== 401) {\n throw error;\n }\n\n const originalRequest = error.config;\n if (!originalRequest || originalRequest.skipAuth || originalRequest._retriedAfterRefresh) {\n throw error;\n }\n\n try {\n refreshPromise ??= refreshTokens().finally(() => {\n refreshPromise = null;\n });\n const tokens = await refreshPromise;\n\n originalRequest._retriedAfterRefresh = true;\n originalRequest.headers.set(\"Authorization\", `Bearer ${tokens.accessToken}`);\n return client.request(originalRequest);\n } catch {\n tokenStorage.clearTokens();\n onAuthExpired?.();\n throw error; // surface the original 401, not the refresh failure\n }\n },\n );\n\n return client;\n}\n","import type { AxiosInstance, AxiosRequestConfig } from \"axios\";\nimport type { z } from \"zod\";\nimport { apiSuccessBodySchema, type ApiResponseMeta } from \"./envelope\";\nimport { normalizeAxiosError } from \"./errors\";\n\nexport interface ApiRequestResult<TData> {\n data: TData;\n meta?: ApiResponseMeta;\n}\n\n/**\n * Runs a request through the given client, validates + unwraps the `{ success, data, error, meta }`\n * envelope against `dataSchema`, and normalizes any failure (network, envelope-shape mismatch, or a\n * `{ success: false }` response) into an `ApiClientError`.\n *\n * This is the one place response validation + error normalization happens, so every service call\n * built on top of it gets a runtime-verified response \"for free\" - if the backend's actual response\n * doesn't match `dataSchema`, this throws instead of silently returning malformed data.\n */\nexport async function apiRequest<TData>(\n client: AxiosInstance,\n requestConfig: AxiosRequestConfig,\n dataSchema: z.ZodType<TData>,\n): Promise<ApiRequestResult<TData>> {\n try {\n const response = await client.request(requestConfig);\n const parsed = apiSuccessBodySchema(dataSchema).parse(response.data);\n // `parsed.data` is structurally `TData` by construction (it came from `dataSchema`), but TS\n // can't prove that through a generic zod composition - narrow it explicitly.\n return { data: parsed.data as TData, meta: parsed.meta };\n } catch (err) {\n throw normalizeAxiosError(err);\n }\n}\n"]}