@dreamtree-org/landing-kit 0.2.0 → 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/MIGRATION.md CHANGED
@@ -85,3 +85,12 @@ Both moved over on branch `adopt/landing-kit`. What they needed that 0.1.0 lacke
85
85
  illustrations, icon lists), `media` + `reverse` for Floor Plan Studio and the collections table, `HowItWorks cta`,
86
86
  `DemoCta note`, `intro` `cta` / `glanceNote`, an async `isPublicHost` (registry lookup) and `onPrivateHost: 'app-shell'`.
87
87
  - `/seo` gained `product.screenshot`, FAQPage `isPartOf` and `og:image:type`, so both JSON-LD graphs match what they had.
88
+
89
+ ## 0.3.0: the brand moves into your repo
90
+
91
+ The built-in `brands` / `getBrand(id)` registry is deprecated: it warns once and is removed in 0.5.0. Keep your brand next to your code:
92
+
93
+ 1. Run `npx @dreamtree-org/twreact-ui brand init --id billpro --name BillPro`. Copy your colours in: `color.primary` (the full 50–950 scale), plus optional `marketing.accent` / `marketing.panel` and `logo.mark`.
94
+ 2. Replace `getBrand('billpro')` with `toLandingBrand(require('./dreamtree.brand.json'), { headline, pitch, points, category })`. The copy that used to live in `brands.js` moves into your code.
95
+ 3. Run `npx @dreamtree-org/twreact-ui brand build --out app/brand.css` so the app's twreact-ui components use the same colours.
96
+
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @dreamtree-org/landing-kit
2
2
 
3
- Replaces the marketing landing setup copied across billpro, taskpro and venturepro (page chrome,
4
- sections, "Book a demo" dialog, SEO files, Express routes and prerender) with one content-driven
5
- package that looks like the same family as the `@dreamtree-org/auth-kit` login screens.
3
+ The shared marketing landing pages **and** authentication frame (`<AuthShell>`, `brands`, `getBrand`) for every Dreamtree product: replaces the landing setup copied across billpro, taskpro and venturepro (page chrome, sections, "Book a demo" dialog, SEO files, Express routes and prerender) and the login/signup/reset/platform-admin frame in one package.
6
4
 
7
- - One facade: `<LandingPage brand={getBrand('billpro')} content={content} />`. A product's landing page is a content file.
8
- - Same brand objects and `--dt-*` colours as auth-kit (`brands` / `getBrand` are re-exported from it).
9
- - Plain CSS. It looks the same under Tailwind 3, Tailwind 4 (preflight or not) or no Tailwind. Dark mode via `[data-theme="dark"]` or `.dark`.
5
+ - Two facades, both taking the product's brand from **its own repo** (`dreamtree.brand.json`, twreact-ui's brand format) through `toLandingBrand()`:
6
+ - `<LandingPage brand={brand} content={content} />` — a product's landing page is a content file.
7
+ - `<AuthShell brand={brand} title="Welcome back">…</AuthShell>` (also available at `@dreamtree-org/landing-kit/auth` + `@dreamtree-org/landing-kit/auth.css`).
8
+ - The built-in `brands` / `getBrand(id)` registry is **deprecated** (warns once, removed in 0.5.0).
9
+ - Plain CSS (`--dt-*` custom properties). It looks the same under Tailwind 3, Tailwind 4 (preflight or not) or no Tailwind. Dark mode via `[data-theme="dark"]` or `.dark`.
10
10
  - Built for prerendered, **un-hydrated** pages (fast and crawlable): `enhanceLanding()` wires the menu and the demo button in a few hundred bytes. It also works hydrated in an SPA or Next.js (`'use client'`, `onBookDemo`, `linkComponent`).
11
11
  - Responsive from 320px up. The nav folds into a menu below 1024px. Checked with Playwright at 320/375/768/1024/1440, light and dark.
12
12
 
@@ -18,12 +18,13 @@ npm install @dreamtree-org/landing-kit
18
18
 
19
19
  ```jsx
20
20
  // Landing.jsx
21
- import { LandingPage, getBrand } from '@dreamtree-org/landing-kit';
21
+ import { LandingPage, toLandingBrand } from '@dreamtree-org/landing-kit';
22
+ import brandFile from '../dreamtree.brand.json'; // the product's brand, kept in its repo
22
23
  import '@dreamtree-org/landing-kit/styles.css';
23
24
  import { content } from './content';
24
25
 
25
26
  export default function Landing() {
26
- return <LandingPage brand={getBrand('billpro')} content={content} />;
27
+ return <LandingPage brand={toLandingBrand(brandFile, { headline: 'Billing, inventory and GST — done right.' })} content={content} />;
27
28
  }
28
29
  ```
29
30
 
@@ -59,6 +60,20 @@ Then prerender it and serve it (Express recipe below), or render it in your SPA
59
60
 
60
61
  ## Reference
61
62
 
63
+ ### `toLandingBrand(file, copy?)`
64
+
65
+ Maps the product's `dreamtree.brand.json` (create it with `npx @dreamtree-org/twreact-ui brand init`) to a `Brand`, so the landing and sign-in pages share the app's colours:
66
+
67
+ | Brand field | From the brand file |
68
+ |---|---|
69
+ | `id`, `name` | `id`, `name` |
70
+ | `primary` | `marketing.primary`, else `color.primary.600` |
71
+ | `accent` | `marketing.accent`, else `color.primary.300` |
72
+ | `panel` | `marketing.panel`, else `role.shell` |
73
+ | `logoSrc` | `logo.mark` |
74
+
75
+ `copy` is the product's content (`headline`, `pitch`, `points`, `category`, `platformHeadline`, `platformPitch`). It stays in product code, not the brand file.
76
+
62
77
  ### `<LandingPage>`
63
78
 
64
79
  | Prop | Type | Default | Meaning |
@@ -93,6 +108,81 @@ Then prerender it and serve it (Express recipe below), or render it in your SPA
93
108
  | `createDemoIsland(props)` | mounts `DemoDialog` on demand for un-hydrated pages; returns `{ openDemo }` |
94
109
  | `BookDemoButton`, `BrandLockup`, `Section`, `SectionIntro`, `TextLink`, `SmartLink` | building blocks |
95
110
 
111
+ ### `<AuthShell>` & `@dreamtree-org/landing-kit/auth`
112
+
113
+ The shared auth frame for login, signup, password reset, consent and platform-admin sign-in (`desktop [ brand panel 40% | form card ]`, `mobile [ logo · form card · footer · legal links ]`). Import from `@dreamtree-org/landing-kit` or the lightweight `@dreamtree-org/landing-kit/auth` subpath (with `@dreamtree-org/landing-kit/auth.css` or `styles.css`).
114
+
115
+ ```jsx
116
+ import { AuthShell, getBrand } from '@dreamtree-org/landing-kit/auth';
117
+ import '@dreamtree-org/landing-kit/auth.css';
118
+
119
+ export default function Login() {
120
+ return (
121
+ <AuthShell
122
+ brand={getBrand('venturepro')}
123
+ linkComponent={Link}
124
+ title="Welcome back"
125
+ subtitle="Sign in to manage your ventures and units."
126
+ footer={<>New here? <Link to="/register">Register now</Link></>}
127
+ >
128
+ {/* your form */}
129
+ </AuthShell>
130
+ );
131
+ }
132
+ ```
133
+
134
+ | Prop | Type | Default | Meaning |
135
+ |---|---|---|---|
136
+ | `brand` | `Brand` | required | from `getBrand(id)` (`billpro`, `taskpro`, `venturepro`, `campaignpro`, `firepro`, `procurementpro`, `fitweavepro`, `aiagentbuilder`, `runloompro`, `appforge`) or your own object |
137
+ | `title` | node | required | card heading |
138
+ | `subtitle` | node | | under the heading |
139
+ | `footer` | node | | below the card (e.g. "Register now") |
140
+ | `variant` | `'tenant' \| 'platform'` | `'tenant'` | `'platform'` renders a darker panel + `"Platform"` badge |
141
+ | `legalLinks` | `{label, href}[]` | Privacy, Terms, Data Policy | `[]` hides them |
142
+ | `linkComponent` | component | `<a>` | receives both `to` and `href` |
143
+ | `demoAccounts` | `{ load, onSelect, taps? }` | | hidden demo-accounts panel (below) |
144
+
145
+ #### Hidden demo accounts (every product, same behaviour)
146
+
147
+ Tapping the card heading (**"Sign in"**) 4 times quickly reveals a **Demo accounts** row of role chips under the
148
+ form; a chip fills the form (it never submits). Credentials are never bundled: `load` asks the product's API,
149
+ which answers **404 unless the deployment sets `DEMO_ACCOUNTS=1`**, and a 404 renders nothing.
150
+
151
+ ```jsx
152
+ import { AuthShell, fetchDemoAccounts } from '@dreamtree-org/landing-kit/auth';
153
+
154
+ <AuthShell
155
+ brand={brand}
156
+ title="Sign in"
157
+ demoAccounts={{
158
+ load: fetchDemoAccounts, // GET /api/auth/demo-accounts (or fetchDemoAccounts('/other/url'))
159
+ onSelect: (a) => { setEmail(a.identifier); setPassword(a.password); if (a.company) setCompany(a.company); },
160
+ }}
161
+ >
162
+ ```
163
+
164
+ Server (`@dreamtree-org/landing-kit/demo-server`, no dependencies):
165
+
166
+ ```js
167
+ const { demoAccountsExpress } = require('@dreamtree-org/landing-kit/demo-server');
168
+ app.get('/api/auth/demo-accounts', demoAccountsExpress({
169
+ // read passwords from the same env vars the seed script uses, so they never drift
170
+ accounts: (env) => [{ label: 'Admin', identifier: 'admin@demo.test', password: env.SEED_PASSWORD || 'demo1234', company: 'demo' }],
171
+ }));
172
+
173
+ // Next.js: app/api/auth/demo-accounts/route.ts
174
+ export const GET = () => demoAccountsResponse({ accounts: [...] });
175
+ ```
176
+
177
+ | Env | Meaning |
178
+ |---|---|
179
+ | `DEMO_ACCOUNTS=1` | turn the endpoint on (off by default; leave it off in production) |
180
+ | `DEMO_ACCOUNTS_JSON` | `[{ label, identifier, password, company? }]`, replaces the built-in list (accounts a deployment created itself) |
181
+
182
+ Company-first logins pick the chip on the company step: call `stashDemoFill(account)` there and
183
+ `takeDemoFill()` when the password step mounts (sessionStorage, read once). Outside AuthShell, render
184
+ `<DemoAccounts load onSelect />` behind `useTapReveal()` yourself.
185
+
96
186
  ### `@dreamtree-org/landing-kit/enhance`
97
187
 
98
188
  `enhanceLanding(root = document, { openDemo | loadDemo, warmDemo, hash = '#book-demo' })`: wires
package/auth.css CHANGED
@@ -167,6 +167,48 @@
167
167
  .dt-auth-legal-link { color: inherit; text-decoration: none; }
168
168
  .dt-auth-legal-link:hover { color: var(--dt-brand); text-decoration: underline; }
169
169
 
170
+ /* ---- hidden demo-accounts panel (revealed by tapping the heading) ---- */
171
+ .dt-auth-demo { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--dt-auth-border); }
172
+ .dt-auth-demo-label {
173
+ margin: 0 0 0.625rem;
174
+ font-size: 0.6875rem;
175
+ font-weight: 500;
176
+ letter-spacing: 0.14em;
177
+ text-transform: uppercase;
178
+ color: var(--dt-auth-muted);
179
+ }
180
+ .dt-auth-demo-chips { display: flex; flex-wrap: wrap; gap: 0.375rem; }
181
+ .dt-auth-demo-chip {
182
+ appearance: none;
183
+ margin: 0;
184
+ padding: 0.3125rem 0.625rem;
185
+ min-height: 1.75rem;
186
+ border: 1px solid color-mix(in oklab, var(--dt-brand) 45%, transparent);
187
+ border-radius: 999px;
188
+ background: color-mix(in oklab, var(--dt-brand) 8%, transparent);
189
+ color: color-mix(in oklab, var(--dt-brand) 80%, var(--dt-auth-text));
190
+ font: inherit;
191
+ font-size: 0.8125rem;
192
+ font-weight: 500;
193
+ line-height: 1.2;
194
+ cursor: pointer;
195
+ transition: background-color 120ms, border-color 120ms;
196
+ }
197
+ .dt-auth-demo-chip:hover { background: color-mix(in oklab, var(--dt-brand) 16%, transparent); }
198
+ .dt-auth-demo-chip:focus-visible { outline: 2px solid var(--dt-brand); outline-offset: 2px; }
199
+ .dt-auth-demo-chip[aria-pressed='true'] { background: var(--dt-brand); border-color: var(--dt-brand); color: #fff; }
200
+ .dt-auth-demo-chip--skeleton {
201
+ width: 4.5rem;
202
+ border-color: var(--dt-auth-border);
203
+ background: var(--dt-auth-border);
204
+ cursor: default;
205
+ animation: dt-auth-demo-pulse 1.2s ease-in-out infinite;
206
+ }
207
+ @keyframes dt-auth-demo-pulse { 50% { opacity: 0.5; } }
208
+ @media (prefers-reduced-motion: reduce) { .dt-auth-demo-chip--skeleton { animation: none; } }
209
+ .dt-auth-demo-note { margin: 0.625rem 0 0; font-size: 0.75rem; color: var(--dt-auth-muted); }
210
+ .dt-auth-demo-note code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.75rem; color: inherit; }
211
+
170
212
  /* ---- phones ---- */
171
213
  .dt-auth-column,
172
214
  .dt-auth-card { min-width: 0; }
package/auth.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export type { BrandId, Brand, LegalLink, AuthShellProps } from './index.js';
2
- export { AuthShell, brandStyle, brands, getBrand } from './index.js';
1
+ export type { BrandId, Brand, LegalLink, AuthShellProps, AuthDemoAccounts, DemoAccount, DemoAccountsResponse, DemoAccountsProps } from './index.js';
2
+ export { AuthShell, brandStyle, brands, getBrand, toLandingBrand, DemoAccounts, useTapReveal, DEMO_ACCOUNTS_URL, fetchDemoAccounts, normalizeDemoAccounts, stashDemoFill, takeDemoFill } from './index.js';
3
+ export type { BrandFile, BrandCopy } from './index.js';
@@ -0,0 +1,24 @@
1
+ import type { DemoAccount } from './index.js';
2
+
3
+ type Env = Record<string, string | undefined>;
4
+
5
+ export interface DemoAccountsOptions {
6
+ /** The product's own gate (e.g. not a production tenant). DEMO_ACCOUNTS=1 is required as well. */
7
+ enabled?: boolean | undefined;
8
+ /** Built-in list, or a function of env (read seed passwords from the same vars the seed script uses). */
9
+ accounts?: DemoAccount[] | ((env: Env) => DemoAccount[]) | undefined;
10
+ env?: Env | undefined;
11
+ }
12
+
13
+ export type DemoAccountsResult =
14
+ | { status: 200; body: { accounts: DemoAccount[] } }
15
+ | { status: 404; body: { error: string } };
16
+
17
+ export declare const DEMO_ACCOUNTS_HEADERS: Record<string, string>;
18
+ export declare function demoAccountsEnabled(opts?: Pick<DemoAccountsOptions, 'enabled' | 'env'>): boolean;
19
+ /** 404 unless enabled and DEMO_ACCOUNTS=1; DEMO_ACCOUNTS_JSON replaces `accounts`. */
20
+ export declare function resolveDemoAccounts(opts?: DemoAccountsOptions): DemoAccountsResult;
21
+ /** Express handler for `GET /api/auth/demo-accounts`. */
22
+ export declare function demoAccountsExpress(opts?: DemoAccountsOptions): (req: any, res: any) => void;
23
+ /** Fetch-API Response for a Next.js route handler. */
24
+ export declare function demoAccountsResponse(opts?: DemoAccountsOptions): Response;
package/dist/auth.cjs CHANGED
@@ -21,15 +21,125 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var auth_exports = {};
22
22
  __export(auth_exports, {
23
23
  AuthShell: () => AuthShell,
24
+ DEMO_ACCOUNTS_URL: () => DEMO_ACCOUNTS_URL,
25
+ DemoAccounts: () => DemoAccounts,
24
26
  brandStyle: () => brandStyle,
25
27
  brands: () => brands,
26
- getBrand: () => getBrand
28
+ fetchDemoAccounts: () => fetchDemoAccounts,
29
+ getBrand: () => getBrand,
30
+ normalizeDemoAccounts: () => normalizeDemoAccounts,
31
+ stashDemoFill: () => stashDemoFill,
32
+ takeDemoFill: () => takeDemoFill,
33
+ toLandingBrand: () => toLandingBrand,
34
+ useTapReveal: () => useTapReveal
27
35
  });
28
36
  module.exports = __toCommonJS(auth_exports);
29
37
 
30
38
  // src/AuthShell.jsx
39
+ var import_react3 = require("react");
40
+
41
+ // src/DemoAccounts.jsx
31
42
  var import_react = require("react");
43
+
44
+ // src/demoFill.js
45
+ var DEMO_ACCOUNTS_URL = "/api/auth/demo-accounts";
46
+ var FILL_KEY = "dt-demo-fill";
47
+ async function fetchDemoAccounts(url = DEMO_ACCOUNTS_URL, init = {}) {
48
+ try {
49
+ const res = await fetch(url, { credentials: "include", cache: "no-store", ...init, headers: { Accept: "application/json", ...init.headers } });
50
+ if (!res.ok) return { accounts: [] };
51
+ return normalizeDemoAccounts(await res.json());
52
+ } catch {
53
+ return { accounts: [] };
54
+ }
55
+ }
56
+ function normalizeDemoAccounts(data) {
57
+ const list = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.accounts) ? data.accounts : [];
58
+ const accounts = list.filter((a) => a && typeof a.label === "string" && typeof a.identifier === "string");
59
+ let sharedPassword = typeof (data == null ? void 0 : data.sharedPassword) === "string" ? data.sharedPassword : void 0;
60
+ if (!sharedPassword && accounts.length > 1 && accounts.every((a) => a.password && a.password === accounts[0].password)) {
61
+ sharedPassword = accounts[0].password;
62
+ }
63
+ return sharedPassword ? { accounts, sharedPassword } : { accounts };
64
+ }
65
+ function stashDemoFill(account) {
66
+ var _a;
67
+ try {
68
+ sessionStorage.setItem(FILL_KEY, JSON.stringify({ identifier: account.identifier, password: (_a = account.password) != null ? _a : "", company: account.company }));
69
+ } catch {
70
+ }
71
+ }
72
+ function takeDemoFill() {
73
+ try {
74
+ const raw = sessionStorage.getItem(FILL_KEY);
75
+ if (!raw) return null;
76
+ sessionStorage.removeItem(FILL_KEY);
77
+ return JSON.parse(raw);
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ // src/DemoAccounts.jsx
32
84
  var import_jsx_runtime = require("react/jsx-runtime");
85
+ function DemoAccounts({ load, onSelect, className = "" }) {
86
+ const [state, setState] = (0, import_react.useState)({ status: "loading", accounts: [] });
87
+ const [picked, setPicked] = (0, import_react.useState)(null);
88
+ const loadRef = (0, import_react.useRef)(load);
89
+ (0, import_react.useEffect)(() => {
90
+ let live = true;
91
+ Promise.resolve().then(() => loadRef.current()).then((data) => live && setState({ status: "ready", ...normalizeDemoAccounts(data) })).catch(() => live && setState({ status: "ready", accounts: [] }));
92
+ return () => {
93
+ live = false;
94
+ };
95
+ }, []);
96
+ if (state.status === "ready" && !state.accounts.length) return null;
97
+ const pick = (account, i) => {
98
+ setPicked(i);
99
+ onSelect == null ? void 0 : onSelect(account);
100
+ };
101
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `dt-auth-demo ${className}`, "aria-busy": state.status === "loading" || void 0, children: [
102
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dt-auth-demo-label", children: "Demo accounts" }),
103
+ state.status === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dt-auth-demo-chips", "aria-hidden": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-demo-chip dt-auth-demo-chip--skeleton" }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
104
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dt-auth-demo-chips", children: state.accounts.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
105
+ "button",
106
+ {
107
+ type: "button",
108
+ className: "dt-auth-demo-chip",
109
+ "aria-pressed": picked === i,
110
+ title: a.company ? `${a.identifier} \xB7 ${a.company}` : a.identifier,
111
+ onClick: () => pick(a, i),
112
+ children: a.label
113
+ },
114
+ `${a.label}-${a.identifier}`
115
+ )) }),
116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dt-auth-demo-note", children: state.sharedPassword ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
117
+ "Shared password ",
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: state.sharedPassword }),
119
+ " \u2014 tap a role to fill."
120
+ ] }) : "Tap a role to fill." })
121
+ ] })
122
+ ] });
123
+ }
124
+
125
+ // src/useTapReveal.js
126
+ var import_react2 = require("react");
127
+ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
128
+ const [revealed, setRevealed] = (0, import_react2.useState)(false);
129
+ const count = (0, import_react2.useRef)(0);
130
+ const last = (0, import_react2.useRef)(0);
131
+ const onClick = (0, import_react2.useCallback)(() => {
132
+ if (revealed) return;
133
+ const now = Date.now();
134
+ count.current = now - last.current <= windowMs ? count.current + 1 : 1;
135
+ last.current = now;
136
+ if (count.current >= taps) setRevealed(true);
137
+ }, [revealed, taps, windowMs]);
138
+ return { revealed, tapProps: { onClick, style: { userSelect: "none", WebkitUserSelect: "none" } } };
139
+ }
140
+
141
+ // src/AuthShell.jsx
142
+ var import_jsx_runtime2 = require("react/jsx-runtime");
33
143
  var DEFAULT_LEGAL = [
34
144
  { label: "Privacy Policy", href: "/privacy" },
35
145
  { label: "Terms", href: "/terms" },
@@ -37,10 +147,10 @@ var DEFAULT_LEGAL = [
37
147
  ];
38
148
  function BrandMark({ brand, size }) {
39
149
  if (brand.logoSrc) {
40
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
150
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
41
151
  }
42
- if (brand.mark) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-mark-node", children: brand.mark });
43
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
152
+ if (brand.mark) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dt-auth-mark-node", children: brand.mark });
153
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
44
154
  }
45
155
  function brandStyle(brand) {
46
156
  return {
@@ -58,50 +168,53 @@ function AuthShell({
58
168
  variant = "tenant",
59
169
  legalLinks = DEFAULT_LEGAL,
60
170
  linkComponent: LinkComponent,
61
- className = ""
171
+ className = "",
172
+ demoAccounts
62
173
  }) {
63
- var _a;
64
- const headingId = (0, import_react.useId)();
174
+ var _a, _b;
175
+ const headingId = (0, import_react3.useId)();
176
+ const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
65
177
  const isPlatform = variant === "platform";
66
178
  const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
67
179
  const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
68
- const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
69
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
180
+ const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
181
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
70
182
  "div",
71
183
  {
72
184
  className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
73
185
  style: brandStyle(brand),
74
186
  "data-product": brand.id,
75
187
  children: [
76
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", { className: "dt-auth-panel", children: [
77
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: "/", className: "dt-auth-brand", children: [
78
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BrandMark, { brand, size: 28 }),
79
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
80
- isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
188
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("aside", { className: "dt-auth-panel", children: [
189
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("a", { href: "/", className: "dt-auth-brand", children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BrandMark, { brand, size: 28 }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
192
+ isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
81
193
  ] }),
82
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dt-auth-pitch", children: [
83
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: headline }),
84
- pitch ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: pitch }) : null,
85
- !isPlatform && ((_a = brand.points) == null ? void 0 : _a.length) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: brand.points.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: p }, p)) }) : null
194
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dt-auth-pitch", children: [
195
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: headline }),
196
+ pitch ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: pitch }) : null,
197
+ !isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { children: brand.points.map((p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { children: p }, p)) }) : null
86
198
  ] }),
87
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
199
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
88
200
  ] }),
89
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("main", { className: "dt-auth-main", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dt-auth-column", children: [
90
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
91
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BrandMark, { brand, size: 24 }),
92
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
93
- isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
201
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("main", { className: "dt-auth-main", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dt-auth-column", children: [
202
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
203
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BrandMark, { brand, size: 24 }),
204
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
205
+ isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
94
206
  ] }),
95
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
96
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "dt-auth-head", children: [
97
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { id: headingId, children: title }),
98
- subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: subtitle }) : null
207
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
208
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "dt-auth-head", children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
210
+ subtitle ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: subtitle }) : null
99
211
  ] }),
100
- children
212
+ children,
213
+ demoAccounts && revealed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
101
214
  ] }),
102
- footer ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dt-auth-footer", children: footer }) : null,
103
- (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dt-auth-legal-item", children: [
104
- i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\xB7" }) : null,
215
+ footer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dt-auth-footer", children: footer }) : null,
216
+ (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dt-auth-legal-item", children: [
217
+ i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", children: "\xB7" }) : null,
105
218
  renderLink(l)
106
219
  ] }, l.href)) }) : null
107
220
  ] }) })
@@ -225,8 +338,39 @@ var brands = {
225
338
  category: "by Dreamtree-Org"
226
339
  }
227
340
  };
341
+ var warned = false;
228
342
  function getBrand(id) {
343
+ if (!warned && typeof console !== "undefined") {
344
+ warned = true;
345
+ 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).');
346
+ }
229
347
  const b = brands[id];
230
348
  if (!b) throw new Error(`landing-kit: unknown brand "${id}" (known: ${Object.keys(brands).join(", ")})`);
231
349
  return b;
232
350
  }
351
+
352
+ // src/brandFile.js
353
+ var shellOf = (role) => {
354
+ const shell = role && role.shell;
355
+ return typeof shell === "string" ? shell : shell && shell.light;
356
+ };
357
+ function toLandingBrand(file, copy = {}) {
358
+ if (!file || typeof file !== "object") throw new Error("landing-kit: toLandingBrand needs the parsed dreamtree.brand.json object.");
359
+ const id = file.id || file.name;
360
+ if (!id) throw new Error('landing-kit: the brand file has no "id". Run `npx @dreamtree-org/twreact-ui brand validate`.');
361
+ const scale = file.color && file.color.primary || {};
362
+ const marketing = file.marketing || {};
363
+ const primary = marketing.primary || scale["600"] || scale["700"] || scale["500"];
364
+ if (!primary) throw new Error(`landing-kit: brand "${id}" sets no primary colour (color.primary.600 or marketing.primary).`);
365
+ const brand = {
366
+ id,
367
+ name: (file.id ? file.name : file.label) || id,
368
+ primary,
369
+ accent: marketing.accent || scale["300"] || void 0,
370
+ panel: marketing.panel || shellOf(file.role) || void 0,
371
+ logoSrc: file.logo && file.logo.mark || void 0,
372
+ headline: "",
373
+ ...copy
374
+ };
375
+ return Object.fromEntries(Object.entries(brand).filter(([, v]) => v !== void 0));
376
+ }