@dreamtree-org/landing-kit 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.js CHANGED
@@ -2,7 +2,109 @@
2
2
 
3
3
  // src/AuthShell.jsx
4
4
  import { useId } from "react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ // src/DemoAccounts.jsx
7
+ import { useEffect, useRef, useState } from "react";
8
+
9
+ // src/demoFill.js
10
+ var DEMO_ACCOUNTS_URL = "/api/auth/demo-accounts";
11
+ var FILL_KEY = "dt-demo-fill";
12
+ async function fetchDemoAccounts(url = DEMO_ACCOUNTS_URL, init = {}) {
13
+ try {
14
+ const res = await fetch(url, { credentials: "include", cache: "no-store", ...init, headers: { Accept: "application/json", ...init.headers } });
15
+ if (!res.ok) return { accounts: [] };
16
+ return normalizeDemoAccounts(await res.json());
17
+ } catch {
18
+ return { accounts: [] };
19
+ }
20
+ }
21
+ function normalizeDemoAccounts(data) {
22
+ const list = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.accounts) ? data.accounts : [];
23
+ const accounts = list.filter((a) => a && typeof a.label === "string" && typeof a.identifier === "string");
24
+ let sharedPassword = typeof (data == null ? void 0 : data.sharedPassword) === "string" ? data.sharedPassword : void 0;
25
+ if (!sharedPassword && accounts.length > 1 && accounts.every((a) => a.password && a.password === accounts[0].password)) {
26
+ sharedPassword = accounts[0].password;
27
+ }
28
+ return sharedPassword ? { accounts, sharedPassword } : { accounts };
29
+ }
30
+ function stashDemoFill(account) {
31
+ var _a;
32
+ try {
33
+ sessionStorage.setItem(FILL_KEY, JSON.stringify({ identifier: account.identifier, password: (_a = account.password) != null ? _a : "", company: account.company }));
34
+ } catch {
35
+ }
36
+ }
37
+ function takeDemoFill() {
38
+ try {
39
+ const raw = sessionStorage.getItem(FILL_KEY);
40
+ if (!raw) return null;
41
+ sessionStorage.removeItem(FILL_KEY);
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ // src/DemoAccounts.jsx
49
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
50
+ function DemoAccounts({ load, onSelect, className = "" }) {
51
+ const [state, setState] = useState({ status: "loading", accounts: [] });
52
+ const [picked, setPicked] = useState(null);
53
+ const loadRef = useRef(load);
54
+ useEffect(() => {
55
+ let live = true;
56
+ Promise.resolve().then(() => loadRef.current()).then((data) => live && setState({ status: "ready", ...normalizeDemoAccounts(data) })).catch(() => live && setState({ status: "ready", accounts: [] }));
57
+ return () => {
58
+ live = false;
59
+ };
60
+ }, []);
61
+ if (state.status === "ready" && !state.accounts.length) return null;
62
+ const pick = (account, i) => {
63
+ setPicked(i);
64
+ onSelect == null ? void 0 : onSelect(account);
65
+ };
66
+ return /* @__PURE__ */ jsxs("div", { className: `dt-auth-demo ${className}`, "aria-busy": state.status === "loading" || void 0, children: [
67
+ /* @__PURE__ */ jsx("p", { className: "dt-auth-demo-label", children: "Demo accounts" }),
68
+ state.status === "loading" ? /* @__PURE__ */ jsx("div", { className: "dt-auth-demo-chips", "aria-hidden": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("span", { className: "dt-auth-demo-chip dt-auth-demo-chip--skeleton" }, i)) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
69
+ /* @__PURE__ */ jsx("div", { className: "dt-auth-demo-chips", children: state.accounts.map((a, i) => /* @__PURE__ */ jsx(
70
+ "button",
71
+ {
72
+ type: "button",
73
+ className: "dt-auth-demo-chip",
74
+ "aria-pressed": picked === i,
75
+ title: a.company ? `${a.identifier} \xB7 ${a.company}` : a.identifier,
76
+ onClick: () => pick(a, i),
77
+ children: a.label
78
+ },
79
+ `${a.label}-${a.identifier}`
80
+ )) }),
81
+ /* @__PURE__ */ jsx("p", { className: "dt-auth-demo-note", children: state.sharedPassword ? /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ "Shared password ",
83
+ /* @__PURE__ */ jsx("code", { children: state.sharedPassword }),
84
+ " \u2014 tap a role to fill."
85
+ ] }) : "Tap a role to fill." })
86
+ ] })
87
+ ] });
88
+ }
89
+
90
+ // src/useTapReveal.js
91
+ import { useCallback, useRef as useRef2, useState as useState2 } from "react";
92
+ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
93
+ const [revealed, setRevealed] = useState2(false);
94
+ const count = useRef2(0);
95
+ const last = useRef2(0);
96
+ const onClick = useCallback(() => {
97
+ if (revealed) return;
98
+ const now = Date.now();
99
+ count.current = now - last.current <= windowMs ? count.current + 1 : 1;
100
+ last.current = now;
101
+ if (count.current >= taps) setRevealed(true);
102
+ }, [revealed, taps, windowMs]);
103
+ return { revealed, tapProps: { onClick, style: { userSelect: "none", WebkitUserSelect: "none" } } };
104
+ }
105
+
106
+ // src/AuthShell.jsx
107
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
6
108
  var DEFAULT_LEGAL = [
7
109
  { label: "Privacy Policy", href: "/privacy" },
8
110
  { label: "Terms", href: "/terms" },
@@ -10,10 +112,10 @@ var DEFAULT_LEGAL = [
10
112
  ];
11
113
  function BrandMark({ brand, size }) {
12
114
  if (brand.logoSrc) {
13
- return /* @__PURE__ */ jsx("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
115
+ return /* @__PURE__ */ jsx2("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
14
116
  }
15
- if (brand.mark) return /* @__PURE__ */ jsx("span", { className: "dt-auth-mark-node", children: brand.mark });
16
- return /* @__PURE__ */ jsx("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
117
+ if (brand.mark) return /* @__PURE__ */ jsx2("span", { className: "dt-auth-mark-node", children: brand.mark });
118
+ return /* @__PURE__ */ jsx2("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
17
119
  }
18
120
  function brandStyle(brand) {
19
121
  return {
@@ -31,50 +133,53 @@ function AuthShell({
31
133
  variant = "tenant",
32
134
  legalLinks = DEFAULT_LEGAL,
33
135
  linkComponent: LinkComponent,
34
- className = ""
136
+ className = "",
137
+ demoAccounts
35
138
  }) {
36
- var _a;
139
+ var _a, _b;
37
140
  const headingId = useId();
141
+ const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
38
142
  const isPlatform = variant === "platform";
39
143
  const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
40
144
  const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
41
- const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
42
- return /* @__PURE__ */ jsxs(
145
+ const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx2(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx2("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
146
+ return /* @__PURE__ */ jsxs2(
43
147
  "div",
44
148
  {
45
149
  className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
46
150
  style: brandStyle(brand),
47
151
  "data-product": brand.id,
48
152
  children: [
49
- /* @__PURE__ */ jsxs("aside", { className: "dt-auth-panel", children: [
50
- /* @__PURE__ */ jsxs("a", { href: "/", className: "dt-auth-brand", children: [
51
- /* @__PURE__ */ jsx(BrandMark, { brand, size: 28 }),
52
- /* @__PURE__ */ jsx("span", { className: "dt-auth-brand-name", children: brand.name }),
53
- isPlatform ? /* @__PURE__ */ jsx("span", { className: "dt-auth-badge", children: "Platform" }) : null
153
+ /* @__PURE__ */ jsxs2("aside", { className: "dt-auth-panel", children: [
154
+ /* @__PURE__ */ jsxs2("a", { href: "/", className: "dt-auth-brand", children: [
155
+ /* @__PURE__ */ jsx2(BrandMark, { brand, size: 28 }),
156
+ /* @__PURE__ */ jsx2("span", { className: "dt-auth-brand-name", children: brand.name }),
157
+ isPlatform ? /* @__PURE__ */ jsx2("span", { className: "dt-auth-badge", children: "Platform" }) : null
54
158
  ] }),
55
- /* @__PURE__ */ jsxs("div", { className: "dt-auth-pitch", children: [
56
- /* @__PURE__ */ jsx("h2", { children: headline }),
57
- pitch ? /* @__PURE__ */ jsx("p", { children: pitch }) : null,
58
- !isPlatform && ((_a = brand.points) == null ? void 0 : _a.length) ? /* @__PURE__ */ jsx("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx("li", { children: p }, p)) }) : null
159
+ /* @__PURE__ */ jsxs2("div", { className: "dt-auth-pitch", children: [
160
+ /* @__PURE__ */ jsx2("h2", { children: headline }),
161
+ pitch ? /* @__PURE__ */ jsx2("p", { children: pitch }) : null,
162
+ !isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx2("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx2("li", { children: p }, p)) }) : null
59
163
  ] }),
60
- /* @__PURE__ */ jsx("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
164
+ /* @__PURE__ */ jsx2("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
61
165
  ] }),
62
- /* @__PURE__ */ jsx("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs("div", { className: "dt-auth-column", children: [
63
- /* @__PURE__ */ jsxs("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
64
- /* @__PURE__ */ jsx(BrandMark, { brand, size: 24 }),
65
- /* @__PURE__ */ jsx("span", { className: "dt-auth-brand-name", children: brand.name }),
66
- isPlatform ? /* @__PURE__ */ jsx("span", { className: "dt-auth-badge", children: "Platform" }) : null
166
+ /* @__PURE__ */ jsx2("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs2("div", { className: "dt-auth-column", children: [
167
+ /* @__PURE__ */ jsxs2("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
168
+ /* @__PURE__ */ jsx2(BrandMark, { brand, size: 24 }),
169
+ /* @__PURE__ */ jsx2("span", { className: "dt-auth-brand-name", children: brand.name }),
170
+ isPlatform ? /* @__PURE__ */ jsx2("span", { className: "dt-auth-badge", children: "Platform" }) : null
67
171
  ] }),
68
- /* @__PURE__ */ jsxs("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
69
- /* @__PURE__ */ jsxs("header", { className: "dt-auth-head", children: [
70
- /* @__PURE__ */ jsx("h1", { id: headingId, children: title }),
71
- subtitle ? /* @__PURE__ */ jsx("p", { children: subtitle }) : null
172
+ /* @__PURE__ */ jsxs2("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
173
+ /* @__PURE__ */ jsxs2("header", { className: "dt-auth-head", children: [
174
+ /* @__PURE__ */ jsx2("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
175
+ subtitle ? /* @__PURE__ */ jsx2("p", { children: subtitle }) : null
72
176
  ] }),
73
- children
177
+ children,
178
+ demoAccounts && revealed ? /* @__PURE__ */ jsx2(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
74
179
  ] }),
75
- footer ? /* @__PURE__ */ jsx("div", { className: "dt-auth-footer", children: footer }) : null,
76
- (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs("span", { className: "dt-auth-legal-item", children: [
77
- i > 0 ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }) : null,
180
+ footer ? /* @__PURE__ */ jsx2("div", { className: "dt-auth-footer", children: footer }) : null,
181
+ (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx2("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs2("span", { className: "dt-auth-legal-item", children: [
182
+ i > 0 ? /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "\xB7" }) : null,
78
183
  renderLink(l)
79
184
  ] }, l.href)) }) : null
80
185
  ] }) })
@@ -198,14 +303,53 @@ var brands = {
198
303
  category: "by Dreamtree-Org"
199
304
  }
200
305
  };
306
+ var warned = false;
201
307
  function getBrand(id) {
308
+ if (!warned && typeof console !== "undefined") {
309
+ warned = true;
310
+ console.warn('landing-kit: getBrand() and the built-in brands are deprecated (removed in 0.5.0). Keep your brand in dreamtree.brand.json and use toLandingBrand(require("./dreamtree.brand.json"), copy).');
311
+ }
202
312
  const b = brands[id];
203
313
  if (!b) throw new Error(`landing-kit: unknown brand "${id}" (known: ${Object.keys(brands).join(", ")})`);
204
314
  return b;
205
315
  }
316
+
317
+ // src/brandFile.js
318
+ var shellOf = (role) => {
319
+ const shell = role && role.shell;
320
+ return typeof shell === "string" ? shell : shell && shell.light;
321
+ };
322
+ function toLandingBrand(file, copy = {}) {
323
+ if (!file || typeof file !== "object") throw new Error("landing-kit: toLandingBrand needs the parsed dreamtree.brand.json object.");
324
+ const id = file.id || file.name;
325
+ if (!id) throw new Error('landing-kit: the brand file has no "id". Run `npx @dreamtree-org/twreact-ui brand validate`.');
326
+ const scale = file.color && file.color.primary || {};
327
+ const marketing = file.marketing || {};
328
+ const primary = marketing.primary || scale["600"] || scale["700"] || scale["500"];
329
+ if (!primary) throw new Error(`landing-kit: brand "${id}" sets no primary colour (color.primary.600 or marketing.primary).`);
330
+ const brand = {
331
+ id,
332
+ name: (file.id ? file.name : file.label) || id,
333
+ primary,
334
+ accent: marketing.accent || scale["300"] || void 0,
335
+ panel: marketing.panel || shellOf(file.role) || void 0,
336
+ logoSrc: file.logo && file.logo.mark || void 0,
337
+ headline: "",
338
+ ...copy
339
+ };
340
+ return Object.fromEntries(Object.entries(brand).filter(([, v]) => v !== void 0));
341
+ }
206
342
  export {
207
343
  AuthShell,
344
+ DEMO_ACCOUNTS_URL,
345
+ DemoAccounts,
208
346
  brandStyle,
209
347
  brands,
210
- getBrand
348
+ fetchDemoAccounts,
349
+ getBrand,
350
+ normalizeDemoAccounts,
351
+ stashDemoFill,
352
+ takeDemoFill,
353
+ toLandingBrand,
354
+ useTapReveal
211
355
  };
@@ -0,0 +1,75 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/demo-server.js
20
+ var demo_server_exports = {};
21
+ __export(demo_server_exports, {
22
+ DEMO_ACCOUNTS_HEADERS: () => DEMO_ACCOUNTS_HEADERS,
23
+ demoAccountsEnabled: () => demoAccountsEnabled,
24
+ demoAccountsExpress: () => demoAccountsExpress,
25
+ demoAccountsResponse: () => demoAccountsResponse,
26
+ resolveDemoAccounts: () => resolveDemoAccounts
27
+ });
28
+ module.exports = __toCommonJS(demo_server_exports);
29
+ function fromEnvJson(raw) {
30
+ if (!raw) return null;
31
+ try {
32
+ const data = JSON.parse(raw);
33
+ return Array.isArray(data) ? data : Array.isArray(data?.accounts) ? data.accounts : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function clean(accounts) {
39
+ return (accounts || []).filter((a) => a && a.label && a.identifier && a.password).map(({ label, identifier, password, company, role }) => ({
40
+ label: String(label),
41
+ identifier: String(identifier),
42
+ password: String(password),
43
+ ...company ? { company: String(company) } : {},
44
+ ...role ? { role: String(role) } : {}
45
+ }));
46
+ }
47
+ function demoAccountsEnabled({ enabled = true, env = process.env } = {}) {
48
+ return Boolean(enabled) && env.DEMO_ACCOUNTS === "1";
49
+ }
50
+ function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
51
+ if (!demoAccountsEnabled({ enabled, env })) return { status: 404, body: { error: "Not found" } };
52
+ const list = clean(fromEnvJson(env.DEMO_ACCOUNTS_JSON) ?? (typeof accounts === "function" ? accounts(env) : accounts));
53
+ if (!list.length) return { status: 404, body: { error: "Not found" } };
54
+ return { status: 200, body: { accounts: list } };
55
+ }
56
+ var DEMO_ACCOUNTS_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
57
+ function demoAccountsExpress(opts = {}) {
58
+ return (req, res) => {
59
+ const { status, body } = resolveDemoAccounts(opts);
60
+ res.set(DEMO_ACCOUNTS_HEADERS);
61
+ res.status(status).json(body);
62
+ };
63
+ }
64
+ function demoAccountsResponse(opts = {}) {
65
+ const { status, body } = resolveDemoAccounts(opts);
66
+ return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
67
+ }
68
+ // Annotate the CommonJS export names for ESM import in node:
69
+ 0 && (module.exports = {
70
+ DEMO_ACCOUNTS_HEADERS,
71
+ demoAccountsEnabled,
72
+ demoAccountsExpress,
73
+ demoAccountsResponse,
74
+ resolveDemoAccounts
75
+ });
@@ -0,0 +1,47 @@
1
+ // src/demo-server.js
2
+ function fromEnvJson(raw) {
3
+ if (!raw) return null;
4
+ try {
5
+ const data = JSON.parse(raw);
6
+ return Array.isArray(data) ? data : Array.isArray(data?.accounts) ? data.accounts : null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+ function clean(accounts) {
12
+ return (accounts || []).filter((a) => a && a.label && a.identifier && a.password).map(({ label, identifier, password, company, role }) => ({
13
+ label: String(label),
14
+ identifier: String(identifier),
15
+ password: String(password),
16
+ ...company ? { company: String(company) } : {},
17
+ ...role ? { role: String(role) } : {}
18
+ }));
19
+ }
20
+ function demoAccountsEnabled({ enabled = true, env = process.env } = {}) {
21
+ return Boolean(enabled) && env.DEMO_ACCOUNTS === "1";
22
+ }
23
+ function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
24
+ if (!demoAccountsEnabled({ enabled, env })) return { status: 404, body: { error: "Not found" } };
25
+ const list = clean(fromEnvJson(env.DEMO_ACCOUNTS_JSON) ?? (typeof accounts === "function" ? accounts(env) : accounts));
26
+ if (!list.length) return { status: 404, body: { error: "Not found" } };
27
+ return { status: 200, body: { accounts: list } };
28
+ }
29
+ var DEMO_ACCOUNTS_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
30
+ function demoAccountsExpress(opts = {}) {
31
+ return (req, res) => {
32
+ const { status, body } = resolveDemoAccounts(opts);
33
+ res.set(DEMO_ACCOUNTS_HEADERS);
34
+ res.status(status).json(body);
35
+ };
36
+ }
37
+ function demoAccountsResponse(opts = {}) {
38
+ const { status, body } = resolveDemoAccounts(opts);
39
+ return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
40
+ }
41
+ export {
42
+ DEMO_ACCOUNTS_HEADERS,
43
+ demoAccountsEnabled,
44
+ demoAccountsExpress,
45
+ demoAccountsResponse,
46
+ resolveDemoAccounts
47
+ };