@dreamtree-org/landing-kit 0.10.0 → 0.10.1

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/README.md CHANGED
@@ -261,6 +261,7 @@ export default function Login() {
261
261
  | `legalLinks` | `{label, href}[]` | Privacy, Terms, Data Policy | `[]` hides them |
262
262
  | `linkComponent` | component | `<a>` | receives both `to` and `href` |
263
263
  | `demoAccounts` | `{ load, onSelect, taps? }` | | hidden demo-accounts panel (below) |
264
+ | `ssoProvider` | `{ startUrl, mode, label?, companySlug?, returnTo?, autoRedirect? }` | | "Continue with Dreamtree" (Dreamtree ID single sign-on, below) |
264
265
 
265
266
  #### Hidden demo accounts (every product, same behaviour)
266
267
 
@@ -321,6 +322,45 @@ Company-first logins pick the chip on the company step: call `stashDemoFill(acco
321
322
  `takeDemoFill()` when the password step mounts (sessionStorage, read once). Outside AuthShell, render
322
323
  `<DemoAccounts load onSelect />` behind `useTapReveal()` yourself.
323
324
 
325
+ #### Dreamtree ID single sign-on (`ssoProvider`)
326
+
327
+ Each app maps its `AUTH_MODE` env to `ssoProvider`: `local` → leave it out (the card is byte-for-byte what it
328
+ was), `hybrid` → a **Continue with Dreamtree** button and an "or" divider above your form, `sso` → no form, a
329
+ "Redirecting to Dreamtree ID…" status (`role="status"`) and `window.location.assign` after mount, with the
330
+ same button kept as the fallback if JS or the redirect fails.
331
+
332
+ ```jsx
333
+ // hybrid: the local form stays, Dreamtree goes on top
334
+ <AuthShell
335
+ brand={brand}
336
+ title="Sign in"
337
+ ssoProvider={{ startUrl: '/auth/sso/start', mode: 'hybrid', companySlug: slug, returnTo: '/dashboard' }}
338
+ >
339
+ <LoginForm />
340
+ </AuthShell>
341
+
342
+ // sso: Dreamtree only; children (and the demo-accounts panel) are not rendered
343
+ <AuthShell
344
+ brand={brand}
345
+ title="Sign in"
346
+ ssoProvider={{ startUrl: '/auth/sso/start', mode: 'sso', companySlug: params.slug, returnTo: searchParams.get('next') ?? '/' }}
347
+ />
348
+ ```
349
+
350
+ | Field | Type | Default | Meaning |
351
+ |---|---|---|---|
352
+ | `startUrl` | `string` | required | the app's SSO entry point (`/auth/sso/start`); may already have a query or `#hash` |
353
+ | `mode` | `'hybrid' \| 'sso'` | required | see above |
354
+ | `label` | `string` | `"Continue with Dreamtree"` | button text (its accessible name) |
355
+ | `companySlug` | `string` | | sent as `dt_company` (the `/c/[slug]/login` company) |
356
+ | `returnTo` | `string` | | sent as `return_to`; leave it out and the app's start route picks its default |
357
+ | `autoRedirect` | `boolean` | `true` in `sso` | `false` shows only the button (no status, no redirect); ignored in `hybrid` |
358
+
359
+ The link is `buildSsoUrl(startUrl, { returnTo, companySlug })` (also exported): both values are URI-encoded,
360
+ replace same-named params already on `startUrl`, and every other param and the `#hash` are kept. It is pure
361
+ string work, so server rendering never touches `window`; the redirect happens only in an effect, once per
362
+ mount. The spinner does not animate under `prefers-reduced-motion`.
363
+
324
364
  ### `@dreamtree-org/landing-kit/enhance`
325
365
 
326
366
  `enhanceLanding(root = document, { openDemo | loadDemo, warmDemo, hash = '#book-demo' })`: wires
package/auth.css CHANGED
@@ -209,6 +209,63 @@
209
209
  .dt-auth-demo-note { margin: 0.625rem 0 0; font-size: 0.75rem; color: var(--dt-auth-muted); }
210
210
  .dt-auth-demo-note code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.75rem; color: inherit; }
211
211
 
212
+ /* ---- Dreamtree ID single sign-on (ssoProvider, DTID-32) ---- */
213
+ .dt-auth-sso-btn {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ gap: 0.5rem;
218
+ width: 100%;
219
+ min-height: 2.75rem;
220
+ padding: 0.625rem 1rem;
221
+ border: 1px solid var(--dt-brand);
222
+ border-radius: 0.5rem;
223
+ background: var(--dt-brand);
224
+ color: #fff;
225
+ font: inherit;
226
+ font-size: 0.9375rem;
227
+ font-weight: 600;
228
+ text-decoration: none;
229
+ transition: background-color 120ms;
230
+ }
231
+ .dt-auth-sso-btn:hover { background: color-mix(in oklab, var(--dt-brand) 88%, black); }
232
+ .dt-auth-sso-btn:focus-visible { outline: 2px solid var(--dt-brand); outline-offset: 2px; }
233
+ .dt-auth-sso-icon { flex: none; }
234
+ .dt-auth-or {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 0.75rem;
238
+ margin: 1.25rem 0;
239
+ font-size: 0.75rem;
240
+ letter-spacing: 0.08em;
241
+ text-transform: uppercase;
242
+ color: var(--dt-auth-muted);
243
+ }
244
+ .dt-auth-or::before,
245
+ .dt-auth-or::after { content: ''; flex: 1; border-top: 1px solid var(--dt-auth-border); }
246
+ .dt-auth-sso-status {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 0.625rem;
250
+ margin: 0 0 1rem;
251
+ font-size: 0.875rem;
252
+ color: var(--dt-auth-muted);
253
+ }
254
+ .dt-auth-sso-spinner {
255
+ flex: none;
256
+ width: 1rem;
257
+ height: 1rem;
258
+ border: 2px solid var(--dt-brand-soft);
259
+ border-top-color: var(--dt-brand);
260
+ border-radius: 999px;
261
+ animation: dt-auth-sso-spin 0.8s linear infinite;
262
+ }
263
+ @keyframes dt-auth-sso-spin { to { transform: rotate(360deg); } }
264
+ @media (prefers-reduced-motion: reduce) {
265
+ .dt-auth-sso-spinner { animation: none; }
266
+ .dt-auth-sso-btn { transition: none; }
267
+ }
268
+
212
269
  /* ---- phones ---- */
213
270
  .dt-auth-column,
214
271
  .dt-auth-card { min-width: 0; }
package/auth.d.ts CHANGED
@@ -1,3 +1,3 @@
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';
1
+ export type { BrandId, Brand, LegalLink, AuthShellProps, SsoProvider, AuthDemoAccounts, DemoAccount, DemoAccountsResponse, DemoAccountsProps } from './index.js';
2
+ export { AuthShell, buildSsoUrl, brandStyle, brands, getBrand, toLandingBrand, DemoAccounts, useTapReveal, DEMO_ACCOUNTS_URL, fetchDemoAccounts, normalizeDemoAccounts, stashDemoFill, takeDemoFill } from './index.js';
3
3
  export type { BrandFile, BrandCopy } from './index.js';
package/dist/auth.cjs CHANGED
@@ -25,6 +25,7 @@ __export(auth_exports, {
25
25
  DemoAccounts: () => DemoAccounts,
26
26
  brandStyle: () => brandStyle,
27
27
  brands: () => brands,
28
+ buildSsoUrl: () => buildSsoUrl,
28
29
  fetchDemoAccounts: () => fetchDemoAccounts,
29
30
  getBrand: () => getBrand,
30
31
  normalizeDemoAccounts: () => normalizeDemoAccounts,
@@ -36,7 +37,7 @@ __export(auth_exports, {
36
37
  module.exports = __toCommonJS(auth_exports);
37
38
 
38
39
  // src/AuthShell.jsx
39
- var import_react3 = require("react");
40
+ var import_react5 = require("react");
40
41
 
41
42
  // src/DemoAccounts.jsx
42
43
  var import_react = require("react");
@@ -122,13 +123,116 @@ function DemoAccounts({ load, onSelect, className = "" }) {
122
123
  ] });
123
124
  }
124
125
 
125
- // src/useTapReveal.js
126
+ // src/DreamtreeSso.jsx
127
+ var import_react3 = require("react");
128
+
129
+ // src/sso.js
130
+ function decodeKey(part) {
131
+ const key = part.split("=")[0];
132
+ try {
133
+ return decodeURIComponent(key.replace(/\+/g, " "));
134
+ } catch {
135
+ return key;
136
+ }
137
+ }
138
+ function buildSsoUrl(startUrl, { returnTo, companySlug } = {}) {
139
+ const raw = String(startUrl != null ? startUrl : "");
140
+ const hashAt = raw.indexOf("#");
141
+ const hash = hashAt === -1 ? "" : raw.slice(hashAt);
142
+ const beforeHash = hashAt === -1 ? raw : raw.slice(0, hashAt);
143
+ const queryAt = beforeHash.indexOf("?");
144
+ const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
145
+ const query = queryAt === -1 ? "" : beforeHash.slice(queryAt + 1);
146
+ const add = [
147
+ ["return_to", returnTo],
148
+ ["dt_company", companySlug]
149
+ ].filter(([, v]) => v != null && v !== "");
150
+ const replaced = new Set(add.map(([k]) => k));
151
+ const params = query.split("&").filter((p) => p && !replaced.has(decodeKey(p))).concat(add.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`));
152
+ return `${path}${params.length ? `?${params.join("&")}` : ""}${hash}`;
153
+ }
154
+
155
+ // src/icons.jsx
126
156
  var import_react2 = require("react");
157
+ var import_jsx_runtime2 = require("react/jsx-runtime");
158
+ var base = {
159
+ viewBox: "0 0 24 24",
160
+ fill: "none",
161
+ stroke: "currentColor",
162
+ strokeWidth: 2,
163
+ strokeLinecap: "round",
164
+ strokeLinejoin: "round",
165
+ "aria-hidden": "true",
166
+ focusable: "false"
167
+ };
168
+ var make = (paths) => function Icon({ size = 16, className = "dt-lp-icon" }) {
169
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { ...base, width: size, height: size, className, children: paths });
170
+ };
171
+ var ArrowRight = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M5 12h14M13 6l6 6-6 6" }));
172
+ var ChevronRight = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m9 6 6 6-6 6" }));
173
+ var Check = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M20 6 9 17l-5-5" }));
174
+ var Help = make(
175
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
176
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
177
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01" })
178
+ ] })
179
+ );
180
+ var Alert = make(
181
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
182
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" }),
183
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 9v4M12 17h.01" })
184
+ ] })
185
+ );
186
+ var MenuIcon = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 6h16M4 12h16M4 18h16" }));
187
+ var Close = make(/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }));
188
+ var Calendar = make(
189
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 2v4M8 2v4M3 10h18m-12 6 2 2 4-4" })
192
+ ] })
193
+ );
194
+
195
+ // src/DreamtreeSso.jsx
196
+ var import_jsx_runtime3 = require("react/jsx-runtime");
197
+ var DEFAULT_SSO_LABEL = "Continue with Dreamtree";
198
+ function DreamtreeSso({ provider, children }) {
199
+ const { startUrl, mode, label = DEFAULT_SSO_LABEL, companySlug, returnTo } = provider;
200
+ const isSso = mode === "sso";
201
+ const autoRedirect = isSso && provider.autoRedirect !== false;
202
+ const href = buildSsoUrl(startUrl, { returnTo, companySlug });
203
+ const redirected = (0, import_react3.useRef)(false);
204
+ (0, import_react3.useEffect)(() => {
205
+ if (!autoRedirect || redirected.current) return;
206
+ redirected.current = true;
207
+ window.location.assign(href);
208
+ }, [autoRedirect, href]);
209
+ const button = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("a", { className: "dt-auth-sso-btn", href, children: [
210
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: label }),
211
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowRight, { size: 16, className: "dt-auth-sso-icon" })
212
+ ] });
213
+ if (isSso) {
214
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dt-auth-sso dt-auth-sso--only", children: [
215
+ autoRedirect ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "dt-auth-sso-status", role: "status", "aria-live": "polite", children: [
216
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dt-auth-sso-spinner", "aria-hidden": "true" }),
217
+ "Redirecting to Dreamtree ID\u2026"
218
+ ] }) : null,
219
+ button
220
+ ] });
221
+ }
222
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
223
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dt-auth-sso", children: button }),
224
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dt-auth-or", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "or" }) }),
225
+ children
226
+ ] });
227
+ }
228
+
229
+ // src/useTapReveal.js
230
+ var import_react4 = require("react");
127
231
  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)(() => {
232
+ const [revealed, setRevealed] = (0, import_react4.useState)(false);
233
+ const count = (0, import_react4.useRef)(0);
234
+ const last = (0, import_react4.useRef)(0);
235
+ const onClick = (0, import_react4.useCallback)(() => {
132
236
  if (revealed) return;
133
237
  const now = Date.now();
134
238
  count.current = now - last.current <= windowMs ? count.current + 1 : 1;
@@ -139,7 +243,7 @@ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
139
243
  }
140
244
 
141
245
  // src/AuthShell.jsx
142
- var import_jsx_runtime2 = require("react/jsx-runtime");
246
+ var import_jsx_runtime4 = require("react/jsx-runtime");
143
247
  var DEFAULT_LEGAL = [
144
248
  { label: "Privacy Policy", href: "/privacy" },
145
249
  { label: "Terms", href: "/terms" },
@@ -147,10 +251,10 @@ var DEFAULT_LEGAL = [
147
251
  ];
148
252
  function BrandMark({ brand, size }) {
149
253
  if (brand.logoSrc) {
150
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
254
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
151
255
  }
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) });
256
+ if (brand.mark) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-mark-node", children: brand.mark });
257
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
154
258
  }
155
259
  function brandStyle(brand) {
156
260
  return {
@@ -169,52 +273,53 @@ function AuthShell({
169
273
  legalLinks = DEFAULT_LEGAL,
170
274
  linkComponent: LinkComponent,
171
275
  className = "",
172
- demoAccounts
276
+ demoAccounts,
277
+ ssoProvider
173
278
  }) {
174
279
  var _a, _b;
175
- const headingId = (0, import_react3.useId)();
280
+ const headingId = (0, import_react5.useId)();
176
281
  const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
177
282
  const isPlatform = variant === "platform";
178
283
  const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
179
284
  const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
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)(
285
+ const renderLink = (l) => LinkComponent ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
286
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
182
287
  "div",
183
288
  {
184
289
  className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
185
290
  style: brandStyle(brand),
186
291
  "data-product": brand.id,
187
292
  children: [
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
293
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("aside", { className: "dt-auth-panel", children: [
294
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("a", { href: "/", className: "dt-auth-brand", children: [
295
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandMark, { brand, size: 28 }),
296
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
297
+ isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
193
298
  ] }),
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
299
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dt-auth-pitch", children: [
300
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { children: headline }),
301
+ pitch ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: pitch }) : null,
302
+ !isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { children: brand.points.map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("li", { children: p }, p)) }) : null
198
303
  ] }),
199
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
304
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
200
305
  ] }),
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
306
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("main", { className: "dt-auth-main", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dt-auth-column", children: [
307
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
308
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandMark, { brand, size: 24 }),
309
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-brand-name", children: brand.name }),
310
+ isPlatform ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dt-auth-badge", children: "Platform" }) : null
206
311
  ] }),
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
312
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("header", { className: "dt-auth-head", children: [
314
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
315
+ subtitle ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: subtitle }) : null
211
316
  ] }),
212
- children,
213
- demoAccounts && revealed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
317
+ ssoProvider ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DreamtreeSso, { provider: ssoProvider, children }) : children,
318
+ demoAccounts && revealed && (ssoProvider == null ? void 0 : ssoProvider.mode) !== "sso" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
214
319
  ] }),
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,
320
+ footer ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dt-auth-footer", children: footer }) : null,
321
+ (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dt-auth-legal-item", children: [
322
+ i > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-hidden": "true", children: "\xB7" }) : null,
218
323
  renderLink(l)
219
324
  ] }, l.href)) }) : null
220
325
  ] }) })
package/dist/auth.js CHANGED
@@ -87,12 +87,115 @@ function DemoAccounts({ load, onSelect, className = "" }) {
87
87
  ] });
88
88
  }
89
89
 
90
+ // src/DreamtreeSso.jsx
91
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
92
+
93
+ // src/sso.js
94
+ function decodeKey(part) {
95
+ const key = part.split("=")[0];
96
+ try {
97
+ return decodeURIComponent(key.replace(/\+/g, " "));
98
+ } catch {
99
+ return key;
100
+ }
101
+ }
102
+ function buildSsoUrl(startUrl, { returnTo, companySlug } = {}) {
103
+ const raw = String(startUrl != null ? startUrl : "");
104
+ const hashAt = raw.indexOf("#");
105
+ const hash = hashAt === -1 ? "" : raw.slice(hashAt);
106
+ const beforeHash = hashAt === -1 ? raw : raw.slice(0, hashAt);
107
+ const queryAt = beforeHash.indexOf("?");
108
+ const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
109
+ const query = queryAt === -1 ? "" : beforeHash.slice(queryAt + 1);
110
+ const add = [
111
+ ["return_to", returnTo],
112
+ ["dt_company", companySlug]
113
+ ].filter(([, v]) => v != null && v !== "");
114
+ const replaced = new Set(add.map(([k]) => k));
115
+ const params = query.split("&").filter((p) => p && !replaced.has(decodeKey(p))).concat(add.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`));
116
+ return `${path}${params.length ? `?${params.join("&")}` : ""}${hash}`;
117
+ }
118
+
119
+ // src/icons.jsx
120
+ import { createElement, isValidElement } from "react";
121
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
122
+ var base = {
123
+ viewBox: "0 0 24 24",
124
+ fill: "none",
125
+ stroke: "currentColor",
126
+ strokeWidth: 2,
127
+ strokeLinecap: "round",
128
+ strokeLinejoin: "round",
129
+ "aria-hidden": "true",
130
+ focusable: "false"
131
+ };
132
+ var make = (paths) => function Icon({ size = 16, className = "dt-lp-icon" }) {
133
+ return /* @__PURE__ */ jsx2("svg", { ...base, width: size, height: size, className, children: paths });
134
+ };
135
+ var ArrowRight = make(/* @__PURE__ */ jsx2("path", { d: "M5 12h14M13 6l6 6-6 6" }));
136
+ var ChevronRight = make(/* @__PURE__ */ jsx2("path", { d: "m9 6 6 6-6 6" }));
137
+ var Check = make(/* @__PURE__ */ jsx2("path", { d: "M20 6 9 17l-5-5" }));
138
+ var Help = make(
139
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
140
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
141
+ /* @__PURE__ */ jsx2("path", { d: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01" })
142
+ ] })
143
+ );
144
+ var Alert = make(
145
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
146
+ /* @__PURE__ */ jsx2("path", { d: "M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" }),
147
+ /* @__PURE__ */ jsx2("path", { d: "M12 9v4M12 17h.01" })
148
+ ] })
149
+ );
150
+ var MenuIcon = make(/* @__PURE__ */ jsx2("path", { d: "M4 6h16M4 12h16M4 18h16" }));
151
+ var Close = make(/* @__PURE__ */ jsx2("path", { d: "M18 6 6 18M6 6l12 12" }));
152
+ var Calendar = make(
153
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
154
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
155
+ /* @__PURE__ */ jsx2("path", { d: "M16 2v4M8 2v4M3 10h18m-12 6 2 2 4-4" })
156
+ ] })
157
+ );
158
+
159
+ // src/DreamtreeSso.jsx
160
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
161
+ var DEFAULT_SSO_LABEL = "Continue with Dreamtree";
162
+ function DreamtreeSso({ provider, children }) {
163
+ const { startUrl, mode, label = DEFAULT_SSO_LABEL, companySlug, returnTo } = provider;
164
+ const isSso = mode === "sso";
165
+ const autoRedirect = isSso && provider.autoRedirect !== false;
166
+ const href = buildSsoUrl(startUrl, { returnTo, companySlug });
167
+ const redirected = useRef2(false);
168
+ useEffect2(() => {
169
+ if (!autoRedirect || redirected.current) return;
170
+ redirected.current = true;
171
+ window.location.assign(href);
172
+ }, [autoRedirect, href]);
173
+ const button = /* @__PURE__ */ jsxs3("a", { className: "dt-auth-sso-btn", href, children: [
174
+ /* @__PURE__ */ jsx3("span", { children: label }),
175
+ /* @__PURE__ */ jsx3(ArrowRight, { size: 16, className: "dt-auth-sso-icon" })
176
+ ] });
177
+ if (isSso) {
178
+ return /* @__PURE__ */ jsxs3("div", { className: "dt-auth-sso dt-auth-sso--only", children: [
179
+ autoRedirect ? /* @__PURE__ */ jsxs3("p", { className: "dt-auth-sso-status", role: "status", "aria-live": "polite", children: [
180
+ /* @__PURE__ */ jsx3("span", { className: "dt-auth-sso-spinner", "aria-hidden": "true" }),
181
+ "Redirecting to Dreamtree ID\u2026"
182
+ ] }) : null,
183
+ button
184
+ ] });
185
+ }
186
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
187
+ /* @__PURE__ */ jsx3("div", { className: "dt-auth-sso", children: button }),
188
+ /* @__PURE__ */ jsx3("div", { className: "dt-auth-or", children: /* @__PURE__ */ jsx3("span", { children: "or" }) }),
189
+ children
190
+ ] });
191
+ }
192
+
90
193
  // src/useTapReveal.js
91
- import { useCallback, useRef as useRef2, useState as useState2 } from "react";
194
+ import { useCallback, useRef as useRef3, useState as useState2 } from "react";
92
195
  function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
93
196
  const [revealed, setRevealed] = useState2(false);
94
- const count = useRef2(0);
95
- const last = useRef2(0);
197
+ const count = useRef3(0);
198
+ const last = useRef3(0);
96
199
  const onClick = useCallback(() => {
97
200
  if (revealed) return;
98
201
  const now = Date.now();
@@ -104,7 +207,7 @@ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
104
207
  }
105
208
 
106
209
  // src/AuthShell.jsx
107
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
210
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
108
211
  var DEFAULT_LEGAL = [
109
212
  { label: "Privacy Policy", href: "/privacy" },
110
213
  { label: "Terms", href: "/terms" },
@@ -112,10 +215,10 @@ var DEFAULT_LEGAL = [
112
215
  ];
113
216
  function BrandMark({ brand, size }) {
114
217
  if (brand.logoSrc) {
115
- return /* @__PURE__ */ jsx2("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
218
+ return /* @__PURE__ */ jsx4("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
116
219
  }
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) });
220
+ if (brand.mark) return /* @__PURE__ */ jsx4("span", { className: "dt-auth-mark-node", children: brand.mark });
221
+ return /* @__PURE__ */ jsx4("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
119
222
  }
120
223
  function brandStyle(brand) {
121
224
  return {
@@ -134,7 +237,8 @@ function AuthShell({
134
237
  legalLinks = DEFAULT_LEGAL,
135
238
  linkComponent: LinkComponent,
136
239
  className = "",
137
- demoAccounts
240
+ demoAccounts,
241
+ ssoProvider
138
242
  }) {
139
243
  var _a, _b;
140
244
  const headingId = useId();
@@ -142,44 +246,44 @@ function AuthShell({
142
246
  const isPlatform = variant === "platform";
143
247
  const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
144
248
  const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
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(
249
+ const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx4(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx4("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
250
+ return /* @__PURE__ */ jsxs4(
147
251
  "div",
148
252
  {
149
253
  className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
150
254
  style: brandStyle(brand),
151
255
  "data-product": brand.id,
152
256
  children: [
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
257
+ /* @__PURE__ */ jsxs4("aside", { className: "dt-auth-panel", children: [
258
+ /* @__PURE__ */ jsxs4("a", { href: "/", className: "dt-auth-brand", children: [
259
+ /* @__PURE__ */ jsx4(BrandMark, { brand, size: 28 }),
260
+ /* @__PURE__ */ jsx4("span", { className: "dt-auth-brand-name", children: brand.name }),
261
+ isPlatform ? /* @__PURE__ */ jsx4("span", { className: "dt-auth-badge", children: "Platform" }) : null
158
262
  ] }),
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
263
+ /* @__PURE__ */ jsxs4("div", { className: "dt-auth-pitch", children: [
264
+ /* @__PURE__ */ jsx4("h2", { children: headline }),
265
+ pitch ? /* @__PURE__ */ jsx4("p", { children: pitch }) : null,
266
+ !isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx4("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx4("li", { children: p }, p)) }) : null
163
267
  ] }),
164
- /* @__PURE__ */ jsx2("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
268
+ /* @__PURE__ */ jsx4("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
165
269
  ] }),
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
270
+ /* @__PURE__ */ jsx4("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs4("div", { className: "dt-auth-column", children: [
271
+ /* @__PURE__ */ jsxs4("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
272
+ /* @__PURE__ */ jsx4(BrandMark, { brand, size: 24 }),
273
+ /* @__PURE__ */ jsx4("span", { className: "dt-auth-brand-name", children: brand.name }),
274
+ isPlatform ? /* @__PURE__ */ jsx4("span", { className: "dt-auth-badge", children: "Platform" }) : null
171
275
  ] }),
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
276
+ /* @__PURE__ */ jsxs4("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
277
+ /* @__PURE__ */ jsxs4("header", { className: "dt-auth-head", children: [
278
+ /* @__PURE__ */ jsx4("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
279
+ subtitle ? /* @__PURE__ */ jsx4("p", { children: subtitle }) : null
176
280
  ] }),
177
- children,
178
- demoAccounts && revealed ? /* @__PURE__ */ jsx2(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
281
+ ssoProvider ? /* @__PURE__ */ jsx4(DreamtreeSso, { provider: ssoProvider, children }) : children,
282
+ demoAccounts && revealed && (ssoProvider == null ? void 0 : ssoProvider.mode) !== "sso" ? /* @__PURE__ */ jsx4(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
179
283
  ] }),
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,
284
+ footer ? /* @__PURE__ */ jsx4("div", { className: "dt-auth-footer", children: footer }) : null,
285
+ (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx4("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs4("span", { className: "dt-auth-legal-item", children: [
286
+ i > 0 ? /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", children: "\xB7" }) : null,
183
287
  renderLink(l)
184
288
  ] }, l.href)) }) : null
185
289
  ] }) })
@@ -345,6 +449,7 @@ export {
345
449
  DemoAccounts,
346
450
  brandStyle,
347
451
  brands,
452
+ buildSsoUrl,
348
453
  fetchDemoAccounts,
349
454
  getBrand,
350
455
  normalizeDemoAccounts,