@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/dist/platform.js CHANGED
@@ -36,7 +36,7 @@ function readPlatformCsrf(cookieString, name) {
36
36
  return null;
37
37
  }
38
38
  function joinUrl(basePath, path, query) {
39
- const base2 = basePath.replace(/\/+$/, "");
39
+ const base3 = basePath.replace(/\/+$/, "");
40
40
  const rel = path ? `/${String(path).replace(/^\/+/, "")}` : "";
41
41
  const params = new URLSearchParams();
42
42
  for (const [k, v] of Object.entries(query != null ? query : {})) {
@@ -44,7 +44,7 @@ function joinUrl(basePath, path, query) {
44
44
  params.append(k, String(v));
45
45
  }
46
46
  const qs = params.toString();
47
- return `${base2}${rel}${qs ? `?${qs}` : ""}`;
47
+ return `${base3}${rel}${qs ? `?${qs}` : ""}`;
48
48
  }
49
49
  function createPlatformClient(opts = {}) {
50
50
  var _a, _b, _c, _d, _e;
@@ -2780,10 +2780,10 @@ function PlatformConsole({
2780
2780
  const browser = useBrowserLocation(!routed);
2781
2781
  const pathname = routed ? givenPath : browser.pathname;
2782
2782
  const navigate = givenNavigate != null ? givenNavigate : browser.navigate;
2783
- const base2 = `/${trim(basePath)}`.replace(/\/$/, "") || "";
2783
+ const base3 = `/${trim(basePath)}`.replace(/\/$/, "") || "";
2784
2784
  const client = useMemo3(
2785
- () => given != null ? given : createPlatformClient({ basePath: apiBasePath, loginPath: loginPath != null ? loginPath : `${base2}/login` }),
2786
- [given, apiBasePath, loginPath, base2]
2785
+ () => given != null ? given : createPlatformClient({ basePath: apiBasePath, loginPath: loginPath != null ? loginPath : `${base3}/login` }),
2786
+ [given, apiBasePath, loginPath, base3]
2787
2787
  );
2788
2788
  const Link = useMemo3(() => makeLink(linkAs, navigate), [linkAs, navigate]);
2789
2789
  const [me, setMe] = useState14(initialMe);
@@ -2797,7 +2797,7 @@ function PlatformConsole({
2797
2797
  useEffect7(() => {
2798
2798
  void reloadMe();
2799
2799
  }, [reloadMe]);
2800
- const href = useCallback4((rel2 = "") => `${base2}/${trim(rel2)}`.replace(/\/$/, "") || "/", [base2]);
2800
+ const href = useCallback4((rel2 = "") => `${base3}/${trim(rel2)}`.replace(/\/$/, "") || "/", [base3]);
2801
2801
  const companyLoginPath = useCallback4(
2802
2802
  (slug) => {
2803
2803
  var _a2, _b2;
@@ -2815,10 +2815,10 @@ function PlatformConsole({
2815
2815
  await client.post("auth/logout", {}, { skipAuthRedirect: true });
2816
2816
  } finally {
2817
2817
  if (onSignOut) onSignOut();
2818
- else if (typeof window !== "undefined") window.location.assign(loginPath != null ? loginPath : `${base2}/login`);
2818
+ else if (typeof window !== "undefined") window.location.assign(loginPath != null ? loginPath : `${base3}/login`);
2819
2819
  }
2820
2820
  };
2821
- const rel = pathname.startsWith(base2) ? pathname.slice(base2.length) : pathname;
2821
+ const rel = pathname.startsWith(base3) ? pathname.slice(base3.length) : pathname;
2822
2822
  const route = matchConsoleRoute(rel, visiblePages);
2823
2823
  const gate = me && me.access && me.access.console === false;
2824
2824
  const active = (prefix) => prefix === "" ? route.view === "overview" : trim(rel) === prefix || trim(rel).startsWith(`${prefix}/`);
@@ -2844,7 +2844,7 @@ function PlatformConsole({
2844
2844
  features: enabled,
2845
2845
  companyLoginPath,
2846
2846
  companyLoginUrl: (slug) => `${origin}${companyLoginPath(slug)}`,
2847
- loginUrl: `${origin}${loginPath != null ? loginPath : `${base2}/login`}`,
2847
+ loginUrl: `${origin}${loginPath != null ? loginPath : `${base3}/login`}`,
2848
2848
  brand,
2849
2849
  appName: appName != null ? appName : (_a = me == null ? void 0 : me.app) == null ? void 0 : _a.name
2850
2850
  };
@@ -2867,7 +2867,7 @@ function PlatformConsole({
2867
2867
 
2868
2868
  // src/platform/PlatformLogin.jsx
2869
2869
  import React17, { useMemo as useMemo4, useState as useState17 } from "react";
2870
- import { Alert as Alert9, Button as Button13, Input as Input11 } from "@dreamtree-org/twreact-ui";
2870
+ import { Alert as Alert10, Button as Button13, Input as Input11 } from "@dreamtree-org/twreact-ui";
2871
2871
 
2872
2872
  // src/AuthShell.jsx
2873
2873
  import { useId } from "react";
@@ -2928,12 +2928,115 @@ function DemoAccounts({ load, onSelect, className = "" }) {
2928
2928
  ] });
2929
2929
  }
2930
2930
 
2931
+ // src/DreamtreeSso.jsx
2932
+ import { useEffect as useEffect9, useRef as useRef4 } from "react";
2933
+
2934
+ // src/sso.js
2935
+ function decodeKey(part) {
2936
+ const key = part.split("=")[0];
2937
+ try {
2938
+ return decodeURIComponent(key.replace(/\+/g, " "));
2939
+ } catch {
2940
+ return key;
2941
+ }
2942
+ }
2943
+ function buildSsoUrl(startUrl, { returnTo, companySlug } = {}) {
2944
+ const raw = String(startUrl != null ? startUrl : "");
2945
+ const hashAt = raw.indexOf("#");
2946
+ const hash = hashAt === -1 ? "" : raw.slice(hashAt);
2947
+ const beforeHash = hashAt === -1 ? raw : raw.slice(0, hashAt);
2948
+ const queryAt = beforeHash.indexOf("?");
2949
+ const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
2950
+ const query = queryAt === -1 ? "" : beforeHash.slice(queryAt + 1);
2951
+ const add = [
2952
+ ["return_to", returnTo],
2953
+ ["dt_company", companySlug]
2954
+ ].filter(([, v]) => v != null && v !== "");
2955
+ const replaced = new Set(add.map(([k]) => k));
2956
+ const params = query.split("&").filter((p) => p && !replaced.has(decodeKey(p))).concat(add.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`));
2957
+ return `${path}${params.length ? `?${params.join("&")}` : ""}${hash}`;
2958
+ }
2959
+
2960
+ // src/icons.jsx
2961
+ import { createElement, isValidElement } from "react";
2962
+ import { Fragment as Fragment7, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
2963
+ var base2 = {
2964
+ viewBox: "0 0 24 24",
2965
+ fill: "none",
2966
+ stroke: "currentColor",
2967
+ strokeWidth: 2,
2968
+ strokeLinecap: "round",
2969
+ strokeLinejoin: "round",
2970
+ "aria-hidden": "true",
2971
+ focusable: "false"
2972
+ };
2973
+ var make2 = (paths) => function Icon({ size = 16, className = "dt-lp-icon" }) {
2974
+ return /* @__PURE__ */ jsx19("svg", { ...base2, width: size, height: size, className, children: paths });
2975
+ };
2976
+ var ArrowRight = make2(/* @__PURE__ */ jsx19("path", { d: "M5 12h14M13 6l6 6-6 6" }));
2977
+ var ChevronRight = make2(/* @__PURE__ */ jsx19("path", { d: "m9 6 6 6-6 6" }));
2978
+ var Check = make2(/* @__PURE__ */ jsx19("path", { d: "M20 6 9 17l-5-5" }));
2979
+ var Help = make2(
2980
+ /* @__PURE__ */ jsxs17(Fragment7, { children: [
2981
+ /* @__PURE__ */ jsx19("circle", { cx: "12", cy: "12", r: "10" }),
2982
+ /* @__PURE__ */ jsx19("path", { d: "M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01" })
2983
+ ] })
2984
+ );
2985
+ var Alert9 = make2(
2986
+ /* @__PURE__ */ jsxs17(Fragment7, { children: [
2987
+ /* @__PURE__ */ jsx19("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" }),
2988
+ /* @__PURE__ */ jsx19("path", { d: "M12 9v4M12 17h.01" })
2989
+ ] })
2990
+ );
2991
+ var MenuIcon = make2(/* @__PURE__ */ jsx19("path", { d: "M4 6h16M4 12h16M4 18h16" }));
2992
+ var Close = make2(/* @__PURE__ */ jsx19("path", { d: "M18 6 6 18M6 6l12 12" }));
2993
+ var Calendar = make2(
2994
+ /* @__PURE__ */ jsxs17(Fragment7, { children: [
2995
+ /* @__PURE__ */ jsx19("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
2996
+ /* @__PURE__ */ jsx19("path", { d: "M16 2v4M8 2v4M3 10h18m-12 6 2 2 4-4" })
2997
+ ] })
2998
+ );
2999
+
3000
+ // src/DreamtreeSso.jsx
3001
+ import { Fragment as Fragment8, jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3002
+ var DEFAULT_SSO_LABEL = "Continue with Dreamtree";
3003
+ function DreamtreeSso({ provider, children }) {
3004
+ const { startUrl, mode, label = DEFAULT_SSO_LABEL, companySlug, returnTo } = provider;
3005
+ const isSso = mode === "sso";
3006
+ const autoRedirect = isSso && provider.autoRedirect !== false;
3007
+ const href = buildSsoUrl(startUrl, { returnTo, companySlug });
3008
+ const redirected = useRef4(false);
3009
+ useEffect9(() => {
3010
+ if (!autoRedirect || redirected.current) return;
3011
+ redirected.current = true;
3012
+ window.location.assign(href);
3013
+ }, [autoRedirect, href]);
3014
+ const button = /* @__PURE__ */ jsxs18("a", { className: "dt-auth-sso-btn", href, children: [
3015
+ /* @__PURE__ */ jsx20("span", { children: label }),
3016
+ /* @__PURE__ */ jsx20(ArrowRight, { size: 16, className: "dt-auth-sso-icon" })
3017
+ ] });
3018
+ if (isSso) {
3019
+ return /* @__PURE__ */ jsxs18("div", { className: "dt-auth-sso dt-auth-sso--only", children: [
3020
+ autoRedirect ? /* @__PURE__ */ jsxs18("p", { className: "dt-auth-sso-status", role: "status", "aria-live": "polite", children: [
3021
+ /* @__PURE__ */ jsx20("span", { className: "dt-auth-sso-spinner", "aria-hidden": "true" }),
3022
+ "Redirecting to Dreamtree ID\u2026"
3023
+ ] }) : null,
3024
+ button
3025
+ ] });
3026
+ }
3027
+ return /* @__PURE__ */ jsxs18(Fragment8, { children: [
3028
+ /* @__PURE__ */ jsx20("div", { className: "dt-auth-sso", children: button }),
3029
+ /* @__PURE__ */ jsx20("div", { className: "dt-auth-or", children: /* @__PURE__ */ jsx20("span", { children: "or" }) }),
3030
+ children
3031
+ ] });
3032
+ }
3033
+
2931
3034
  // src/useTapReveal.js
2932
- import { useCallback as useCallback5, useRef as useRef4, useState as useState16 } from "react";
3035
+ import { useCallback as useCallback5, useRef as useRef5, useState as useState16 } from "react";
2933
3036
  function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
2934
3037
  const [revealed, setRevealed] = useState16(false);
2935
- const count = useRef4(0);
2936
- const last = useRef4(0);
3038
+ const count = useRef5(0);
3039
+ const last = useRef5(0);
2937
3040
  const onClick = useCallback5(() => {
2938
3041
  if (revealed) return;
2939
3042
  const now = Date.now();
@@ -2945,7 +3048,7 @@ function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
2945
3048
  }
2946
3049
 
2947
3050
  // src/AuthShell.jsx
2948
- import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3051
+ import { jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
2949
3052
  var DEFAULT_LEGAL = [
2950
3053
  { label: "Privacy Policy", href: "/privacy" },
2951
3054
  { label: "Terms", href: "/terms" },
@@ -2953,10 +3056,10 @@ var DEFAULT_LEGAL = [
2953
3056
  ];
2954
3057
  function BrandMark({ brand, size }) {
2955
3058
  if (brand.logoSrc) {
2956
- return /* @__PURE__ */ jsx19("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
3059
+ return /* @__PURE__ */ jsx21("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
2957
3060
  }
2958
- if (brand.mark) return /* @__PURE__ */ jsx19("span", { className: "dt-auth-mark-node", children: brand.mark });
2959
- return /* @__PURE__ */ jsx19("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
3061
+ if (brand.mark) return /* @__PURE__ */ jsx21("span", { className: "dt-auth-mark-node", children: brand.mark });
3062
+ return /* @__PURE__ */ jsx21("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
2960
3063
  }
2961
3064
  function brandStyle(brand) {
2962
3065
  return {
@@ -2975,7 +3078,8 @@ function AuthShell({
2975
3078
  legalLinks = DEFAULT_LEGAL,
2976
3079
  linkComponent: LinkComponent,
2977
3080
  className = "",
2978
- demoAccounts
3081
+ demoAccounts,
3082
+ ssoProvider
2979
3083
  }) {
2980
3084
  var _a, _b;
2981
3085
  const headingId = useId();
@@ -2983,44 +3087,44 @@ function AuthShell({
2983
3087
  const isPlatform = variant === "platform";
2984
3088
  const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
2985
3089
  const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
2986
- const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx19(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx19("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
2987
- return /* @__PURE__ */ jsxs17(
3090
+ const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx21(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx21("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
3091
+ return /* @__PURE__ */ jsxs19(
2988
3092
  "div",
2989
3093
  {
2990
3094
  className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
2991
3095
  style: brandStyle(brand),
2992
3096
  "data-product": brand.id,
2993
3097
  children: [
2994
- /* @__PURE__ */ jsxs17("aside", { className: "dt-auth-panel", children: [
2995
- /* @__PURE__ */ jsxs17("a", { href: "/", className: "dt-auth-brand", children: [
2996
- /* @__PURE__ */ jsx19(BrandMark, { brand, size: 28 }),
2997
- /* @__PURE__ */ jsx19("span", { className: "dt-auth-brand-name", children: brand.name }),
2998
- isPlatform ? /* @__PURE__ */ jsx19("span", { className: "dt-auth-badge", children: "Platform" }) : null
3098
+ /* @__PURE__ */ jsxs19("aside", { className: "dt-auth-panel", children: [
3099
+ /* @__PURE__ */ jsxs19("a", { href: "/", className: "dt-auth-brand", children: [
3100
+ /* @__PURE__ */ jsx21(BrandMark, { brand, size: 28 }),
3101
+ /* @__PURE__ */ jsx21("span", { className: "dt-auth-brand-name", children: brand.name }),
3102
+ isPlatform ? /* @__PURE__ */ jsx21("span", { className: "dt-auth-badge", children: "Platform" }) : null
2999
3103
  ] }),
3000
- /* @__PURE__ */ jsxs17("div", { className: "dt-auth-pitch", children: [
3001
- /* @__PURE__ */ jsx19("h2", { children: headline }),
3002
- pitch ? /* @__PURE__ */ jsx19("p", { children: pitch }) : null,
3003
- !isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx19("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx19("li", { children: p }, p)) }) : null
3104
+ /* @__PURE__ */ jsxs19("div", { className: "dt-auth-pitch", children: [
3105
+ /* @__PURE__ */ jsx21("h2", { children: headline }),
3106
+ pitch ? /* @__PURE__ */ jsx21("p", { children: pitch }) : null,
3107
+ !isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx21("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx21("li", { children: p }, p)) }) : null
3004
3108
  ] }),
3005
- /* @__PURE__ */ jsx19("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
3109
+ /* @__PURE__ */ jsx21("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
3006
3110
  ] }),
3007
- /* @__PURE__ */ jsx19("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs17("div", { className: "dt-auth-column", children: [
3008
- /* @__PURE__ */ jsxs17("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
3009
- /* @__PURE__ */ jsx19(BrandMark, { brand, size: 24 }),
3010
- /* @__PURE__ */ jsx19("span", { className: "dt-auth-brand-name", children: brand.name }),
3011
- isPlatform ? /* @__PURE__ */ jsx19("span", { className: "dt-auth-badge", children: "Platform" }) : null
3111
+ /* @__PURE__ */ jsx21("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs19("div", { className: "dt-auth-column", children: [
3112
+ /* @__PURE__ */ jsxs19("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
3113
+ /* @__PURE__ */ jsx21(BrandMark, { brand, size: 24 }),
3114
+ /* @__PURE__ */ jsx21("span", { className: "dt-auth-brand-name", children: brand.name }),
3115
+ isPlatform ? /* @__PURE__ */ jsx21("span", { className: "dt-auth-badge", children: "Platform" }) : null
3012
3116
  ] }),
3013
- /* @__PURE__ */ jsxs17("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
3014
- /* @__PURE__ */ jsxs17("header", { className: "dt-auth-head", children: [
3015
- /* @__PURE__ */ jsx19("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
3016
- subtitle ? /* @__PURE__ */ jsx19("p", { children: subtitle }) : null
3117
+ /* @__PURE__ */ jsxs19("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
3118
+ /* @__PURE__ */ jsxs19("header", { className: "dt-auth-head", children: [
3119
+ /* @__PURE__ */ jsx21("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
3120
+ subtitle ? /* @__PURE__ */ jsx21("p", { children: subtitle }) : null
3017
3121
  ] }),
3018
- children,
3019
- demoAccounts && revealed ? /* @__PURE__ */ jsx19(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
3122
+ ssoProvider ? /* @__PURE__ */ jsx21(DreamtreeSso, { provider: ssoProvider, children }) : children,
3123
+ demoAccounts && revealed && (ssoProvider == null ? void 0 : ssoProvider.mode) !== "sso" ? /* @__PURE__ */ jsx21(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
3020
3124
  ] }),
3021
- footer ? /* @__PURE__ */ jsx19("div", { className: "dt-auth-footer", children: footer }) : null,
3022
- (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx19("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs17("span", { className: "dt-auth-legal-item", children: [
3023
- i > 0 ? /* @__PURE__ */ jsx19("span", { "aria-hidden": "true", children: "\xB7" }) : null,
3125
+ footer ? /* @__PURE__ */ jsx21("div", { className: "dt-auth-footer", children: footer }) : null,
3126
+ (legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx21("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs19("span", { className: "dt-auth-legal-item", children: [
3127
+ i > 0 ? /* @__PURE__ */ jsx21("span", { "aria-hidden": "true", children: "\xB7" }) : null,
3024
3128
  renderLink(l)
3025
3129
  ] }, l.href)) }) : null
3026
3130
  ] }) })
@@ -3030,7 +3134,7 @@ function AuthShell({
3030
3134
  }
3031
3135
 
3032
3136
  // src/platform/PlatformLogin.jsx
3033
- import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3137
+ import { jsx as jsx22, jsxs as jsxs20 } from "react/jsx-runtime";
3034
3138
  function PlatformLogin({ brand, basePath, client: given, onSignedIn, redirectTo = "/platform", title = "Platform administration", subtitle = "Administrator sign-in", linkComponent, legalLinks }) {
3035
3139
  const client = useMemo4(() => given != null ? given : createPlatformClient({ basePath, onUnauthorized: () => {
3036
3140
  } }), [given, basePath]);
@@ -3080,17 +3184,17 @@ function PlatformLogin({ brand, basePath, client: given, onSignedIn, redirectTo
3080
3184
  setBusy(false);
3081
3185
  }
3082
3186
  };
3083
- return /* @__PURE__ */ jsx20(AuthShell, { brand, variant: "platform", title, subtitle, linkComponent, legalLinks, children: step === "password" ? /* @__PURE__ */ jsxs18("form", { onSubmit: submitPassword, className: "flex flex-col gap-4", children: [
3084
- /* @__PURE__ */ jsx20(Input11, { label: "Admin email", type: "email", autoComplete: "username", autoFocus: true, value: email, onChange: (e) => setEmail(e.target.value) }),
3085
- /* @__PURE__ */ jsx20(Input11, { label: "Password", type: "password", autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value) }),
3086
- error && /* @__PURE__ */ jsx20(Alert9, { variant: "error", children: error }),
3087
- /* @__PURE__ */ jsx20(Button13, { type: "submit", loading: busy, disabled: !email || !password, fullWidth: true, children: "Sign in" })
3088
- ] }) : /* @__PURE__ */ jsxs18("form", { onSubmit: submitCode, className: "flex flex-col gap-4", children: [
3089
- /* @__PURE__ */ jsx20("p", { className: "text-sm", children: "Enter the 6-digit code from your authenticator app." }),
3090
- /* @__PURE__ */ jsx20(Input11, { label: "Authenticator code", otp: true, autoFocus: true, value: code, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }),
3091
- error && /* @__PURE__ */ jsx20(Alert9, { variant: "error", children: error }),
3092
- /* @__PURE__ */ jsx20(Button13, { type: "submit", loading: busy, disabled: code.length < 6, fullWidth: true, children: "Verify" }),
3093
- /* @__PURE__ */ jsx20(
3187
+ return /* @__PURE__ */ jsx22(AuthShell, { brand, variant: "platform", title, subtitle, linkComponent, legalLinks, children: step === "password" ? /* @__PURE__ */ jsxs20("form", { onSubmit: submitPassword, className: "flex flex-col gap-4", children: [
3188
+ /* @__PURE__ */ jsx22(Input11, { label: "Admin email", type: "email", autoComplete: "username", autoFocus: true, value: email, onChange: (e) => setEmail(e.target.value) }),
3189
+ /* @__PURE__ */ jsx22(Input11, { label: "Password", type: "password", autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value) }),
3190
+ error && /* @__PURE__ */ jsx22(Alert10, { variant: "error", children: error }),
3191
+ /* @__PURE__ */ jsx22(Button13, { type: "submit", loading: busy, disabled: !email || !password, fullWidth: true, children: "Sign in" })
3192
+ ] }) : /* @__PURE__ */ jsxs20("form", { onSubmit: submitCode, className: "flex flex-col gap-4", children: [
3193
+ /* @__PURE__ */ jsx22("p", { className: "text-sm", children: "Enter the 6-digit code from your authenticator app." }),
3194
+ /* @__PURE__ */ jsx22(Input11, { label: "Authenticator code", otp: true, autoFocus: true, value: code, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }),
3195
+ error && /* @__PURE__ */ jsx22(Alert10, { variant: "error", children: error }),
3196
+ /* @__PURE__ */ jsx22(Button13, { type: "submit", loading: busy, disabled: code.length < 6, fullWidth: true, children: "Verify" }),
3197
+ /* @__PURE__ */ jsx22(
3094
3198
  Button13,
3095
3199
  {
3096
3200
  variant: "ghost",
package/index.d.ts CHANGED
@@ -57,6 +57,26 @@ export interface AuthShellProps {
57
57
  * the form. Nothing renders when `load` returns no accounts (demo mode off).
58
58
  */
59
59
  demoAccounts?: AuthDemoAccounts | undefined;
60
+ /**
61
+ * Dreamtree ID single sign-on (DTID-32). Absent = today's local form, unchanged.
62
+ * 'hybrid' adds a "Continue with Dreamtree" button and an "or" divider above `children`;
63
+ * 'sso' drops `children` (and the demo-accounts panel) and redirects to the start URL on mount.
64
+ */
65
+ ssoProvider?: SsoProvider | undefined;
66
+ }
67
+
68
+ export interface SsoProvider {
69
+ /** The app's SSO entry point, e.g. '/auth/sso/start'. May already carry a query or #hash. */
70
+ startUrl: string;
71
+ mode: 'hybrid' | 'sso';
72
+ /** Button text. Default "Continue with Dreamtree". */
73
+ label?: string | undefined;
74
+ /** Sent as `dt_company` (the /c/[slug]/login company). */
75
+ companySlug?: string | undefined;
76
+ /** Sent as `return_to`: where the app lands after sign-in. */
77
+ returnTo?: string | undefined;
78
+ /** 'sso' only: redirect with window.location.assign after mount. Default true; the button always stays as a fallback. */
79
+ autoRedirect?: boolean | undefined;
60
80
  }
61
81
 
62
82
  export interface DemoAccount {
@@ -89,6 +109,11 @@ export interface DemoAccountsProps {
89
109
  }
90
110
 
91
111
  export declare function AuthShell(props: AuthShellProps): ReactElement;
112
+ /**
113
+ * `startUrl` plus `return_to` / `dt_company` (URI-encoded, replacing same-named params already
114
+ * there; other params and the #hash are kept). Pure: safe during SSR.
115
+ */
116
+ export declare function buildSsoUrl(startUrl: string, opts?: Pick<SsoProvider, 'returnTo' | 'companySlug'>): string;
92
117
  /** The chip row AuthShell reveals; render it yourself for a login outside AuthShell. */
93
118
  export declare function DemoAccounts(props: DemoAccountsProps): ReactElement | null;
94
119
  /** `taps` clicks, each within `windowMs` of the last, set `revealed` for the page's life. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamtree-org/landing-kit",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "The shared marketing site, auth frame (AuthShell + brands) and Platform admin console for every Dreamtree product: content-driven landing pages, login/signup shell, platform console client, Express routes, prerender and SEO files.",
5
5
  "author": {
6
6
  "name": "Partha Preetham Krishna",
@@ -106,7 +106,8 @@
106
106
  "esbuild": "^0.25.0",
107
107
  "express": "^4.22.3",
108
108
  "react": "^18.3.1",
109
- "react-dom": "^18.3.1"
109
+ "react-dom": "^18.3.1",
110
+ "react-test-renderer": "^18.3.1"
110
111
  },
111
112
  "engines": {
112
113
  "node": ">=18"
package/styles.css CHANGED
@@ -850,6 +850,63 @@ html:has(.dt-landing) { scroll-behavior: smooth; scroll-padding-top: calc(var(--
850
850
  .dt-auth-demo-note { margin: 0.625rem 0 0; font-size: 0.75rem; color: var(--dt-auth-muted); }
851
851
  .dt-auth-demo-note code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.75rem; color: inherit; }
852
852
 
853
+ /* ---- Dreamtree ID single sign-on (ssoProvider, DTID-32) ---- */
854
+ .dt-auth-sso-btn {
855
+ display: flex;
856
+ align-items: center;
857
+ justify-content: center;
858
+ gap: 0.5rem;
859
+ width: 100%;
860
+ min-height: 2.75rem;
861
+ padding: 0.625rem 1rem;
862
+ border: 1px solid var(--dt-brand);
863
+ border-radius: 0.5rem;
864
+ background: var(--dt-brand);
865
+ color: #fff;
866
+ font: inherit;
867
+ font-size: 0.9375rem;
868
+ font-weight: 600;
869
+ text-decoration: none;
870
+ transition: background-color 120ms;
871
+ }
872
+ .dt-auth-sso-btn:hover { background: color-mix(in oklab, var(--dt-brand) 88%, black); }
873
+ .dt-auth-sso-btn:focus-visible { outline: 2px solid var(--dt-brand); outline-offset: 2px; }
874
+ .dt-auth-sso-icon { flex: none; }
875
+ .dt-auth-or {
876
+ display: flex;
877
+ align-items: center;
878
+ gap: 0.75rem;
879
+ margin: 1.25rem 0;
880
+ font-size: 0.75rem;
881
+ letter-spacing: 0.08em;
882
+ text-transform: uppercase;
883
+ color: var(--dt-auth-muted);
884
+ }
885
+ .dt-auth-or::before,
886
+ .dt-auth-or::after { content: ''; flex: 1; border-top: 1px solid var(--dt-auth-border); }
887
+ .dt-auth-sso-status {
888
+ display: flex;
889
+ align-items: center;
890
+ gap: 0.625rem;
891
+ margin: 0 0 1rem;
892
+ font-size: 0.875rem;
893
+ color: var(--dt-auth-muted);
894
+ }
895
+ .dt-auth-sso-spinner {
896
+ flex: none;
897
+ width: 1rem;
898
+ height: 1rem;
899
+ border: 2px solid var(--dt-brand-soft);
900
+ border-top-color: var(--dt-brand);
901
+ border-radius: 999px;
902
+ animation: dt-auth-sso-spin 0.8s linear infinite;
903
+ }
904
+ @keyframes dt-auth-sso-spin { to { transform: rotate(360deg); } }
905
+ @media (prefers-reduced-motion: reduce) {
906
+ .dt-auth-sso-spinner { animation: none; }
907
+ .dt-auth-sso-btn { transition: none; }
908
+ }
909
+
853
910
  /* ---- phones ---- */
854
911
  .dt-auth-column,
855
912
  .dt-auth-card { min-width: 0; }