@dreamtree-org/landing-kit 0.2.1 → 0.5.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 +9 -0
- package/README.md +81 -5
- package/auth.css +42 -0
- package/auth.d.ts +3 -2
- package/demo-server.d.ts +41 -0
- package/dist/auth.cjs +176 -32
- package/dist/auth.js +176 -32
- package/dist/demo-server.cjs +121 -0
- package/dist/demo-server.js +88 -0
- package/dist/index.cjs +484 -340
- package/dist/index.js +477 -333
- package/index.d.ts +74 -1
- package/package.json +7 -1
- package/styles.css +42 -0
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
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
-
- Two facades
|
|
6
|
-
- `<LandingPage brand={
|
|
7
|
-
- `<AuthShell brand={
|
|
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).
|
|
8
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`.
|
|
9
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`).
|
|
10
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.
|
|
@@ -17,12 +18,13 @@ npm install @dreamtree-org/landing-kit
|
|
|
17
18
|
|
|
18
19
|
```jsx
|
|
19
20
|
// Landing.jsx
|
|
20
|
-
import { LandingPage,
|
|
21
|
+
import { LandingPage, toLandingBrand } from '@dreamtree-org/landing-kit';
|
|
22
|
+
import brandFile from '../dreamtree.brand.json'; // the product's brand, kept in its repo
|
|
21
23
|
import '@dreamtree-org/landing-kit/styles.css';
|
|
22
24
|
import { content } from './content';
|
|
23
25
|
|
|
24
26
|
export default function Landing() {
|
|
25
|
-
return <LandingPage brand={
|
|
27
|
+
return <LandingPage brand={toLandingBrand(brandFile, { headline: 'Billing, inventory and GST — done right.' })} content={content} />;
|
|
26
28
|
}
|
|
27
29
|
```
|
|
28
30
|
|
|
@@ -58,6 +60,20 @@ Then prerender it and serve it (Express recipe below), or render it in your SPA
|
|
|
58
60
|
|
|
59
61
|
## Reference
|
|
60
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
|
+
|
|
61
77
|
### `<LandingPage>`
|
|
62
78
|
|
|
63
79
|
| Prop | Type | Default | Meaning |
|
|
@@ -124,6 +140,66 @@ export default function Login() {
|
|
|
124
140
|
| `variant` | `'tenant' \| 'platform'` | `'tenant'` | `'platform'` renders a darker panel + `"Platform"` badge |
|
|
125
141
|
| `legalLinks` | `{label, href}[]` | Privacy, Terms, Data Policy | `[]` hides them |
|
|
126
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
|
+
#### The `default` demo company (every pro app)
|
|
183
|
+
|
|
184
|
+
Every pro app seeds a company with slug **`default`** — its branches and **one user per role** —
|
|
185
|
+
so the whole product can be demoed end to end, and the chips list exactly those users:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const { demoUsers, demoAccountsExpress, requireDemoPassword } = require('@dreamtree-org/landing-kit/demo-server');
|
|
189
|
+
const manifest = [{ role: 'tenant-admin', label: 'Admin' }, { role: 'viewer', label: 'Viewer' }];
|
|
190
|
+
|
|
191
|
+
// demo:seed (idempotent): create `default`, upsert each user, password = requireDemoPassword()
|
|
192
|
+
// endpoint: one chip per role, emails demoEmail(product, role) → tenant-admin@taskpro.demo.test
|
|
193
|
+
app.get('/api/auth/demo-accounts', demoAccountsExpress({ accounts: (env) => demoUsers(manifest, { product: 'taskpro', env }) }));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
| Env | Meaning |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `DEMO_PASSWORD` | the demo users' password (≥ 12 chars); `demo:seed` resets every demo user to it, the chips show it. Unset → no chips |
|
|
199
|
+
|
|
200
|
+
Company-first logins pick the chip on the company step: call `stashDemoFill(account)` there and
|
|
201
|
+
`takeDemoFill()` when the password step mounts (sessionStorage, read once). Outside AuthShell, render
|
|
202
|
+
`<DemoAccounts load onSelect />` behind `useTapReveal()` yourself.
|
|
127
203
|
|
|
128
204
|
### `@dreamtree-org/landing-kit/enhance`
|
|
129
205
|
|
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';
|
package/demo-server.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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[] | Promise<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`. A Promise when `accounts` is async. */
|
|
20
|
+
export declare function resolveDemoAccounts(opts?: DemoAccountsOptions): DemoAccountsResult | Promise<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 | Promise<Response>;
|
|
25
|
+
|
|
26
|
+
/** One row per role in a product's demo manifest. */
|
|
27
|
+
export interface DemoRole {
|
|
28
|
+
role: string;
|
|
29
|
+
label: string;
|
|
30
|
+
/** Defaults to demoEmail(product, role). */
|
|
31
|
+
email?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Slug of the shared demo company every pro app seeds: 'default'. */
|
|
34
|
+
export declare const DEMO_COMPANY: 'default';
|
|
35
|
+
export declare const DEMO_PASSWORD_MIN: number;
|
|
36
|
+
/** `${role}@${product}.demo.test` (lower-case, dashes). */
|
|
37
|
+
export declare function demoEmail(product: string, role: string): string;
|
|
38
|
+
/** Manifest → chips in the `default` company with env.DEMO_PASSWORD; [] when DEMO_PASSWORD is unset. */
|
|
39
|
+
export declare function demoUsers(manifest: DemoRole[], opts: { product: string; env?: Env | undefined; company?: string | undefined }): DemoAccount[];
|
|
40
|
+
/** Throws unless DEMO_PASSWORD is set and at least DEMO_PASSWORD_MIN characters; returns it. */
|
|
41
|
+
export declare function requireDemoPassword(env?: Env): string;
|
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
|
-
|
|
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,
|
|
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,
|
|
43
|
-
return /* @__PURE__ */ (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,
|
|
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,
|
|
69
|
-
return /* @__PURE__ */ (0,
|
|
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,
|
|
77
|
-
/* @__PURE__ */ (0,
|
|
78
|
-
/* @__PURE__ */ (0,
|
|
79
|
-
/* @__PURE__ */ (0,
|
|
80
|
-
isPlatform ? /* @__PURE__ */ (0,
|
|
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,
|
|
83
|
-
/* @__PURE__ */ (0,
|
|
84
|
-
pitch ? /* @__PURE__ */ (0,
|
|
85
|
-
!isPlatform && ((
|
|
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,
|
|
199
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
|
|
88
200
|
] }),
|
|
89
|
-
/* @__PURE__ */ (0,
|
|
90
|
-
/* @__PURE__ */ (0,
|
|
91
|
-
/* @__PURE__ */ (0,
|
|
92
|
-
/* @__PURE__ */ (0,
|
|
93
|
-
isPlatform ? /* @__PURE__ */ (0,
|
|
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,
|
|
96
|
-
/* @__PURE__ */ (0,
|
|
97
|
-
/* @__PURE__ */ (0,
|
|
98
|
-
subtitle ? /* @__PURE__ */ (0,
|
|
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,
|
|
103
|
-
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0,
|
|
104
|
-
i > 0 ? /* @__PURE__ */ (0,
|
|
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
|
+
}
|