@patientos/website-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ // src/patient-surface-theme.ts
2
+ var HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
3
+ var LENGTH_RE = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|%|pt|vh|vw)?$/;
4
+ var FONT_RE = /^[A-Za-z0-9 _,'"-]{1,120}$/;
5
+ function patientSurfaceTheme(themeTokens) {
6
+ if (!themeTokens || typeof themeTokens !== "object") return {};
7
+ const out = {};
8
+ const primary = pick(themeTokens, ["primary", "brandColor"], HEX_RE);
9
+ if (primary) out.primary = primary;
10
+ const accent = pick(themeTokens, ["accent", "accentColor"], HEX_RE);
11
+ if (accent) out.accent = accent;
12
+ const radius = pick(themeTokens, ["radius", "borderRadius"], LENGTH_RE);
13
+ if (radius) out.radius = radius;
14
+ const fontFamily = pick(themeTokens, ["fontFamily", "font"], FONT_RE);
15
+ if (fontFamily) out.fontFamily = fontFamily;
16
+ return out;
17
+ }
18
+ function pick(tokens, keys, re) {
19
+ for (const k of keys) {
20
+ const v = tokens[k];
21
+ if (typeof v !== "string") continue;
22
+ const trimmed = v.trim();
23
+ if (re.test(trimmed)) return trimmed;
24
+ }
25
+ return void 0;
26
+ }
27
+
28
+ export {
29
+ patientSurfaceTheme
30
+ };
@@ -0,0 +1,64 @@
1
+ // src/contrast.ts
2
+ function parseHexColor(hex) {
3
+ const raw = hex.trim().replace(/^#/, "");
4
+ if (!/^[0-9a-fA-F]+$/.test(raw)) return null;
5
+ if (raw.length === 3) {
6
+ const [r, g, b] = [...raw].map((c) => parseInt(c + c, 16));
7
+ return { r, g, b };
8
+ }
9
+ if (raw.length === 6) {
10
+ return {
11
+ r: parseInt(raw.slice(0, 2), 16),
12
+ g: parseInt(raw.slice(2, 4), 16),
13
+ b: parseInt(raw.slice(4, 6), 16)
14
+ };
15
+ }
16
+ return null;
17
+ }
18
+ function relativeLuminance(rgb) {
19
+ const lin = (channel) => {
20
+ const c = channel / 255;
21
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
22
+ };
23
+ return 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b);
24
+ }
25
+ function contrastRatio(a, b) {
26
+ const ca = parseHexColor(a);
27
+ const cb = parseHexColor(b);
28
+ if (!ca || !cb) return 1;
29
+ const la = relativeLuminance(ca);
30
+ const lb = relativeLuminance(cb);
31
+ const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
32
+ return (hi + 0.05) / (lo + 0.05);
33
+ }
34
+ var WHITE = "#ffffff";
35
+ var INK = "#1b2733";
36
+ function deriveForeground(bg) {
37
+ if (!parseHexColor(bg)) return INK;
38
+ return contrastRatio(bg, WHITE) >= contrastRatio(bg, INK) ? WHITE : INK;
39
+ }
40
+ var AA_NORMAL = 4.5;
41
+ var KIT_PRIMARY = "#0f766e";
42
+ function toHex({ r, g, b }) {
43
+ const hex = (v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0");
44
+ return `#${hex(r)}${hex(g)}${hex(b)}`;
45
+ }
46
+ function derivePrimaryPair(brand) {
47
+ const rgb = parseHexColor(brand);
48
+ if (!rgb) return { bg: KIT_PRIMARY, fg: WHITE };
49
+ const fg = deriveForeground(brand);
50
+ if (contrastRatio(brand, fg) >= AA_NORMAL) return { bg: brand, fg };
51
+ let cur = rgb;
52
+ for (let i = 0; i < 24 && contrastRatio(toHex(cur), WHITE) < AA_NORMAL; i++) {
53
+ cur = { r: cur.r * 0.96, g: cur.g * 0.96, b: cur.b * 0.96 };
54
+ }
55
+ return { bg: toHex(cur), fg: WHITE };
56
+ }
57
+
58
+ export {
59
+ parseHexColor,
60
+ relativeLuminance,
61
+ contrastRatio,
62
+ deriveForeground,
63
+ derivePrimaryPair
64
+ };
@@ -0,0 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
@@ -0,0 +1,36 @@
1
+ import {
2
+ BookingBlockClient,
3
+ CartClient,
4
+ CertificateFunnelClient,
5
+ CheckoutClient,
6
+ PortalAccountClient,
7
+ PortalPanelClient,
8
+ StoreClient
9
+ } from "./chunk-TOPPLOH2.js";
10
+ import {
11
+ mountIslandsWith
12
+ } from "./chunk-THMT43MV.js";
13
+
14
+ // src/islands-registry.tsx
15
+ var ISLANDS = {
16
+ booking: BookingBlockClient,
17
+ "certificate-funnel": CertificateFunnelClient,
18
+ "portal-panel": PortalPanelClient,
19
+ // PAT-783 — the FULL account surface (manage appointments, download documents, view
20
+ // orders, edit contact details) rendered INSIDE the clinic's own page. It reads and
21
+ // WRITES the patient's own record over the same session-authed `/portal/api/*` edge
22
+ // the teaser already uses; the reason it is permitted here is the reason one-origin
23
+ // exists — finishing the portal without bouncing the patient to another host.
24
+ "portal-account": PortalAccountClient,
25
+ store: StoreClient,
26
+ cart: CartClient,
27
+ checkout: CheckoutClient
28
+ };
29
+ function mountIslands(root, createRoot) {
30
+ return mountIslandsWith(ISLANDS, root, createRoot);
31
+ }
32
+
33
+ export {
34
+ ISLANDS,
35
+ mountIslands
36
+ };
@@ -0,0 +1,72 @@
1
+ // src/show-when.ts
2
+ function isAnswered(v) {
3
+ return v !== void 0 && v !== null && v !== "";
4
+ }
5
+ function combineAnd(states) {
6
+ if (states.includes("not_satisfied")) return "not_satisfied";
7
+ if (states.includes("unknown")) return "unknown";
8
+ return "satisfied";
9
+ }
10
+ function combineOr(states) {
11
+ if (states.includes("satisfied")) return "satisfied";
12
+ if (states.includes("unknown")) return "unknown";
13
+ return "not_satisfied";
14
+ }
15
+ function negateTri(s) {
16
+ if (s === "satisfied") return "not_satisfied";
17
+ if (s === "not_satisfied") return "satisfied";
18
+ return "unknown";
19
+ }
20
+ function evaluateLeaf(leaf, answers) {
21
+ const key = leaf.questionKey;
22
+ if (typeof key !== "string") return "unknown";
23
+ const answer = answers[key];
24
+ if (leaf.op === "answered") return isAnswered(answer) ? "satisfied" : "not_satisfied";
25
+ if (!isAnswered(answer)) return "unknown";
26
+ const value = leaf.value;
27
+ switch (leaf.op) {
28
+ case "equals":
29
+ return answer === value ? "satisfied" : "not_satisfied";
30
+ case "notEquals":
31
+ return answer !== value ? "satisfied" : "not_satisfied";
32
+ case "in":
33
+ if (!Array.isArray(value)) return "unknown";
34
+ return value.includes(answer) ? "satisfied" : "not_satisfied";
35
+ case "gt":
36
+ case "gte":
37
+ case "lt":
38
+ case "lte": {
39
+ if (typeof answer !== "number" || typeof value !== "number") return "unknown";
40
+ const ok = leaf.op === "gt" ? answer > value : leaf.op === "gte" ? answer >= value : leaf.op === "lt" ? answer < value : answer <= value;
41
+ return ok ? "satisfied" : "not_satisfied";
42
+ }
43
+ case "before":
44
+ case "after": {
45
+ if (typeof answer !== "string" || typeof value !== "string") return "unknown";
46
+ const ok = leaf.op === "before" ? answer < value : answer > value;
47
+ return ok ? "satisfied" : "not_satisfied";
48
+ }
49
+ default:
50
+ return "unknown";
51
+ }
52
+ }
53
+ function evaluateTri(cond, answers) {
54
+ if (!cond || typeof cond !== "object") return "unknown";
55
+ const node = cond;
56
+ if (Array.isArray(node.and)) return combineAnd(node.and.map((c) => evaluateTri(c, answers)));
57
+ if (Array.isArray(node.or)) return combineOr(node.or.map((c) => evaluateTri(c, answers)));
58
+ if ("not" in node) return negateTri(evaluateTri(node.not, answers));
59
+ return evaluateLeaf(node, answers);
60
+ }
61
+ function evaluateShowWhen(cond, answers) {
62
+ if (cond == null) return true;
63
+ return evaluateTri(cond, answers) === "satisfied";
64
+ }
65
+ function visibleQuestions(ordered, answers) {
66
+ return ordered.filter((q) => evaluateShowWhen(q.showWhen ?? null, answers));
67
+ }
68
+
69
+ export {
70
+ evaluateShowWhen,
71
+ visibleQuestions
72
+ };
@@ -0,0 +1,61 @@
1
+ // src/islands.tsx
2
+ import * as React from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var ISLAND_ATTR = "data-pos-island";
5
+ var ISLAND_PROPS_ATTR = "data-pos-island-props";
6
+ var MOUNTED_ATTR = "data-pos-island-mounted";
7
+ function Island({
8
+ name,
9
+ props,
10
+ children,
11
+ className,
12
+ style
13
+ }) {
14
+ const serialised = JSON.stringify(props ?? {});
15
+ return /* @__PURE__ */ jsx(
16
+ "div",
17
+ {
18
+ ...{ [ISLAND_ATTR]: name, [ISLAND_PROPS_ATTR]: serialised },
19
+ className,
20
+ style,
21
+ children
22
+ }
23
+ );
24
+ }
25
+ function readIslandProps(el) {
26
+ const raw = el.getAttribute(ISLAND_PROPS_ATTR);
27
+ if (!raw) return {};
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
31
+ return parsed;
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+ function mountIslandsWith(registry, root, createRoot) {
37
+ const markers = root.querySelectorAll(`[${ISLAND_ATTR}]`);
38
+ let mounted = 0;
39
+ for (const el of Array.from(markers)) {
40
+ if (el.hasAttribute(MOUNTED_ATTR)) continue;
41
+ const name = el.getAttribute(ISLAND_ATTR);
42
+ if (!name) continue;
43
+ const Impl = registry[name];
44
+ if (!Impl) {
45
+ console.warn(`[patientos] unknown island "${name}" \u2014 skipped`);
46
+ continue;
47
+ }
48
+ el.setAttribute(MOUNTED_ATTR, "");
49
+ createRoot(el).render(React.createElement(Impl, readIslandProps(el)));
50
+ mounted += 1;
51
+ }
52
+ return mounted;
53
+ }
54
+
55
+ export {
56
+ ISLAND_ATTR,
57
+ ISLAND_PROPS_ATTR,
58
+ Island,
59
+ readIslandProps,
60
+ mountIslandsWith
61
+ };