@dreamtree-org/landing-kit 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dreamtree-Org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MIGRATION.md ADDED
@@ -0,0 +1,87 @@
1
+ # Moving a product onto @dreamtree-org/landing-kit
2
+
3
+ ## Where each piece came from
4
+
5
+ | Package piece | Extracted from | Notes |
6
+ |---|---|---|
7
+ | `LandingPage` content model, multi-page `MarketingLayout`, `Breadcrumbs`, `PageHero` | taskpro `features/landing/{pages.js, content.js, LandingChrome.jsx}` | the best architecture: one content file, one page list |
8
+ | `Header`, `Hero`, `FeatureSection` variants, `Audiences`, `HowItWorks`, `Faq`, `DemoCta`, `DreamtreeFooter`, `PreviewFrame` | billpro / venturepro `pages/landing/Landing.jsx` (billpro is the newest copy) | Tailwind classes turned into plain CSS on auth-kit's `--dt-*` variables |
9
+ | `DemoDialog` | venturepro `DemoDialog.jsx` (fields, consent + privacy line) + billpro `DemoDialog.jsx` (native `<dialog>`, no UI library) | fields and submit are props |
10
+ | `createDemoIsland` | billpro / taskpro / venturepro `landing/demoIsland.jsx` | |
11
+ | `enhanceLanding` (`/enhance`) | taskpro `landing/wire.js` + billpro `landing/main.jsx` (`#book-demo`) | adds Escape-to-close |
12
+ | `/seo` builders | billpro `landing/seo.js`, taskpro `lib/seo.js` (head, AI crawler list), procurementpro `structured-data.tsx` (origin-less graph) | framework-neutral; `site` is a parameter |
13
+ | `landingRoutes`, `sessionCheck`, `siteUrl`, `verificationTags` (`/server`) | billpro `utils/landing.js` (host rule, verification tags, raw-file 404s, `/solutions` redirect) + taskpro (manifest-only routing, trailing-slash handling, `app-shell` session mode) | host and session checks are hooks |
14
+ | `prerenderPages` | billpro `client/scripts/prerender-landing.mjs` steps 3–4 | validates every page before writing any |
15
+
16
+ ## Files it replaces (per product)
17
+
18
+ | Before | After |
19
+ |---|---|
20
+ | `pages/landing/Landing.jsx` (≈680 lines: Header, Hero, sections, footer, WRAP/EYEBROW/BookDemoButton) | `<LandingPage brand content preview />` (≈15 lines) |
21
+ | `pages/landing/SolutionPage.jsx` / taskpro `MarketingPage.jsx`, `LandingChrome.jsx` | `MarketingLayout` + `PageHero` + `FeatureSection variant="detail"` + `Faq` + `DemoCta` |
22
+ | `DemoDialog.jsx` / `DemoRequestDialog.jsx` | `DemoDialog` with the product's `fields` + `onSubmit` |
23
+ | `landing/main.jsx`, `landing/wire.js`, `landing/demoIsland.jsx` | `enhanceLanding()` + `createDemoIsland()` (3–5 lines each) |
24
+ | `landing/seo.js` | `/seo` builders called with the product's data |
25
+ | `utils/landing.js` | `/server` `landingRoutes(app, { dist, isPublicHost, hasSession })` |
26
+ | `scripts/prerender-landing.mjs` steps 3–4 | `prerenderPages()` |
27
+ | `tailwind.landing.config.*`, `landing/landing.css` | not needed for the page; keep only if your product preview uses Tailwind |
28
+
29
+ Kept per product: `content.js` (the words), solution/sub-page data, the product preview, the Vite build of
30
+ `landing.html` + SSR entry, the demo request model, and the server's host/session rules.
31
+
32
+ ## Before / after (billpro pilot, branch `adopt/landing-kit`)
33
+
34
+ ```jsx
35
+ // before: client/src/pages/landing/Landing.jsx, 678 lines of JSX + Tailwind
36
+ export default function Landing() {
37
+ return (<><a href="#main" …>Skip to content</a><Header /><main id="main"><Hero /><ProductPreview /><Intro />
38
+ <Billing /><Inventory /><Gst /><Accounts /><Audiences /><HowItWorks /><AiBand /><RightFit /><SolutionsGrid />
39
+ <Faq /><DemoCta /></main><Footer /></>);
40
+ }
41
+
42
+ // after: 16 lines; the words live in content.js's `content` object
43
+ export const brand = getBrand('billpro');
44
+ export default function Landing() {
45
+ return <LandingPage brand={brand} content={content} preview={<ProductPreview />} />;
46
+ }
47
+ ```
48
+
49
+ ```js
50
+ // before: utils/landing.js, 152 lines
51
+ // after:
52
+ landingRoutes(app, { dist: clientDist, isPublicHost: isPlatformHost, hasSession, redirects: { '/solutions': '/#solutions' } });
53
+ ```
54
+
55
+ Result: 15 source files, +391 / −1375 lines. BillPro's own tests (landing, solution pages, server routes, the
56
+ full client suite) pass unchanged except two footer assertions that named Tailwind classes. Client build and prerender produce the same 7 pages.
57
+
58
+ ## Steps
59
+
60
+ 1. `npm install @dreamtree-org/landing-kit` in the server and client packages.
61
+ 2. Add a `content` object to your `content.js` (see README). Keep the existing named exports if tests or SEO read them.
62
+ 3. Replace `Landing.jsx` with `<LandingPage>`. Move the product preview into `<PreviewFrame>`.
63
+ 4. Rebuild sub-pages with `MarketingLayout home={false}` + blocks.
64
+ 5. `main.jsx`: import `@dreamtree-org/landing-kit/styles.css` and call `enhanceLanding`. Make `demoIsland.jsx` call `createDemoIsland`.
65
+ 6. Rewrite `seo.js` with `/seo` builders (pass `faq`, `howTo` from the same content).
66
+ 7. Use `prerenderPages()` in the prerender script and `landingRoutes()` in the server.
67
+ 8. Update the `speakable` selectors: section headings are `#<section id>-title` (e.g. `#what-is-billpro-title`).
68
+
69
+ ## Visible changes to expect
70
+
71
+ - Brand colour is auth-kit's (BillPro `#15803d` instead of `#19a249`), the same as the login screen.
72
+ - Headings, cards, spacing and the closing CTA panel follow one shared scale. Section `h2` ids become `<id>-title`.
73
+ - The phone menu now collapses below 1024px (it was 768px), so tablets get the menu instead of a crowded bar.
74
+
75
+ ## taskpro and venturepro (0.1.1)
76
+
77
+ Both moved over on branch `adopt/landing-kit`. What they needed that 0.1.0 lacked became options, not forks:
78
+
79
+ - **taskpro** (4 pages: `/`, `/features`, `/use-cases`, `/ai-connections`): `slots` for "What is TaskPro?" and
80
+ "About", `media` for the My Work / workflow / MCP illustrations, card `label`s, `cta` arrays, `DemoDialog validate`
81
+ + server `fields` (its inline-error demo form), and `landingRoutes` `manifest: 'landing/pages.json'` (paths relative
82
+ to the manifest), `trailingSlash: 'serve'`, `rawFiles: 'redirect'`, `cacheControl: 'no-cache'` so its server tests
83
+ pass unchanged. Its `lib/seo.js` keeps its API as an adapter over `/seo`.
84
+ - **venturepro** (1 page): `hero.backdrop` (the skyline), `Audiences` `media` + `features` (crane / bedroom
85
+ illustrations, icon lists), `media` + `reverse` for Floor Plan Studio and the collections table, `HowItWorks cta`,
86
+ `DemoCta note`, `intro` `cta` / `glanceNote`, an async `isPublicHost` (registry lookup) and `onPrivateHost: 'app-shell'`.
87
+ - `/seo` gained `product.screenshot`, FAQPage `isPartOf` and `og:image:type`, so both JSON-LD graphs match what they had.
package/README.md CHANGED
@@ -1,3 +1,192 @@
1
- # Temporary Holding Version
1
+ # @dreamtree-org/landing-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Replaces the marketing landing setup copied across billpro, taskpro and venturepro (page chrome,
4
+ sections, "Book a demo" dialog, SEO files, Express routes and prerender) with one content-driven
5
+ package that looks like the same family as the `@dreamtree-org/auth-kit` login screens.
6
+
7
+ - One facade: `<LandingPage brand={getBrand('billpro')} content={content} />`. A product's landing page is a content file.
8
+ - Same brand objects and `--dt-*` colours as auth-kit (`brands` / `getBrand` are re-exported from it).
9
+ - Plain CSS. It looks the same under Tailwind 3, Tailwind 4 (preflight or not) or no Tailwind. Dark mode via `[data-theme="dark"]` or `.dark`.
10
+ - Built for prerendered, **un-hydrated** pages (fast and crawlable): `enhanceLanding()` wires the menu and the demo button in a few hundred bytes. It also works hydrated in an SPA or Next.js (`'use client'`, `onBookDemo`, `linkComponent`).
11
+ - Responsive from 320px up. The nav folds into a menu below 1024px. Checked with Playwright at 320/375/768/1024/1440, light and dark.
12
+
13
+ ## 60-second start
14
+
15
+ ```bash
16
+ npm install @dreamtree-org/landing-kit
17
+ ```
18
+
19
+ ```jsx
20
+ // Landing.jsx
21
+ import { LandingPage, getBrand } from '@dreamtree-org/landing-kit';
22
+ import '@dreamtree-org/landing-kit/styles.css';
23
+ import { content } from './content';
24
+
25
+ export default function Landing() {
26
+ return <LandingPage brand={getBrand('billpro')} content={content} />;
27
+ }
28
+ ```
29
+
30
+ ```js
31
+ // content.js: plain data (icons are any component, e.g. lucide-react)
32
+ import { FileText, Barcode } from 'lucide-react';
33
+
34
+ export const content = {
35
+ nav: [{ label: 'Billing', href: '#billing' }, { label: 'FAQ', href: '#faq' }],
36
+ hero: {
37
+ eyebrow: 'GST billing software · Inventory',
38
+ title: ['GST Billing & Inventory Software', 'for Indian Businesses'], // 2nd half is underlined
39
+ lead: 'Create GST invoices, manage stock and prepare GST summaries in one app.',
40
+ chips: ['Retail shops', 'Wholesalers'],
41
+ secondary: { label: 'See it in action', href: '#product' },
42
+ },
43
+ sections: [
44
+ { id: 'billing', eyebrow: 'Billing', title: 'GST Billing Software', body: '…',
45
+ items: [{ title: 'GST tax invoices', body: '…', icon: FileText }, { title: 'Barcode billing', body: '…', icon: Barcode }],
46
+ cta: { label: 'Book a billing demo', demo: true } },
47
+ { id: 'gst', variant: 'highlight', title: 'GSTR-1 & GSTR-3B reports', note: 'Summaries, not portal filing.', items: ['GSTR-1', 'GSTR-3B'] },
48
+ { use: 'audiences' }, // place a tail block here instead of at the end
49
+ ],
50
+ audiences: { id: 'industries', title: 'Who it is for', items: [{ id: 'retail', title: 'Retail shops', body: '…', href: '/solutions/retail' }] },
51
+ steps: { title: 'How it works', steps: [{ name: 'Set up', text: '…' }, { name: 'Bill', text: '…' }, { name: 'Review', text: '…' }] },
52
+ faq: { title: 'Frequently asked questions', items: [{ q: 'What is BillPro?', a: '…' }] },
53
+ cta: { title: 'See BillPro with your business', body: '…', chips: ['Retail', 'Wholesale'] },
54
+ footer: { blurb: '…', tagline: 'Bill fast.', columns: [{ title: 'Product', links: [{ label: 'Log in', href: '/login', reload: true }] }] },
55
+ };
56
+ ```
57
+
58
+ Then prerender it and serve it (Express recipe below), or render it in your SPA / Next page.
59
+
60
+ ## Reference
61
+
62
+ ### `<LandingPage>`
63
+
64
+ | Prop | Type | Default | Meaning |
65
+ |---|---|---|---|
66
+ | `brand` | `Brand` | required | `getBrand(id)` (auth-kit's brand objects) or your own `{ id, name, primary, accent?, logoSrc? }` |
67
+ | `content` | `LandingContent` | required | `{ nav, login, hero, sections[], audiences, steps, faq, cta, footer }`; only `hero` is required |
68
+ | `preview` | node | | e.g. `<PreviewFrame>…your app screen in HTML…</PreviewFrame>`, which rides over the hero |
69
+ | `slots` | `{ [name]: node }` | | a product's own sections, placed by `{ use: '<name>' }` in `content.sections` |
70
+ | `linkComponent` | component | `<a>` | react-router `Link` / next/link for in-app paths; gets `to` and `href` |
71
+ | `onBookDemo` | `(button) => void` | | when hydrated; un-hydrated pages use `enhanceLanding` |
72
+ | `home` | boolean | `true` | `false` on sub-pages: `#faq` links become `/#faq` |
73
+
74
+ `content.sections[]` render in order after the hero. Each one is `FeatureSection` props, or
75
+ `{ use: 'audiences' | 'steps' | 'faq' | 'cta' | 'preview' | <slot name> }`, or a React element. `audiences`, `steps`, `faq` and
76
+ `cta` that are not placed with `use` follow the sections in that order. `login: null` hides "Log in".
77
+
78
+ ### Blocks (all exported, all usable on their own)
79
+
80
+ | Block | Purpose |
81
+ |---|---|
82
+ | `MarketingLayout` | skip link + sticky `Header` + `<main>` + `DreamtreeFooter`; sets brand colours and context. Use it for sub-pages |
83
+ | `Header` | brand lockup, nav, "Log in", "Book a demo", phone menu (`data-menu-toggle`, `#mobile-nav`) |
84
+ | `Hero` / `PageHero` + `Breadcrumbs` | the page's one `h1` (`#hero-title`); `PageHero` adds breadcrumbs and `#answer` for sub-pages |
85
+ | `PreviewFrame` | window chrome + "Demo data" badge + hidden heading/caption around your product screen |
86
+ | `FeatureSection` | `variant`: `cards` (default) · `split` · `list` · `intro` · `highlight` (dark band + warning note) · `band` · `compare` · `links` · `detail`; `tone`: `plain` · `tinted` · `muted` · `dark` |
87
+ | `Audiences` | who-it-is-for cards with anchors and links |
88
+ | `HowItWorks` | numbered steps (feed the same steps to the HowTo JSON-LD) |
89
+ | `Faq` | native `<details>`, every answer in the HTML |
90
+ | `DemoCta` | the closing brand panel |
91
+ | `DreamtreeFooter` | Dreamtree-Org mark, blurb, link columns, copyright bar |
92
+ | `DemoDialog` | native `<dialog>` demo form: your `fields`, honeypot, consent + privacy line, `onSubmit(data)` |
93
+ | `createDemoIsland(props)` | mounts `DemoDialog` on demand for un-hydrated pages; returns `{ openDemo }` |
94
+ | `BookDemoButton`, `BrandLockup`, `Section`, `SectionIntro`, `TextLink`, `SmartLink` | building blocks |
95
+
96
+ ### `@dreamtree-org/landing-kit/enhance`
97
+
98
+ `enhanceLanding(root = document, { openDemo | loadDemo, warmDemo, hash = '#book-demo' })`: wires
99
+ `[data-menu-toggle]`, `[data-scroll-to]` and `[data-book-demo]` on a prerendered page. Returns `unwire()`.
100
+
101
+ ### `@dreamtree-org/landing-kit/seo` (pure: browser, Node, Next)
102
+
103
+ | Function | Returns |
104
+ |---|---|
105
+ | `buildJsonLd({ site, product, org, page, faq, howTo, breadcrumbs, parts, keywords, speakable, lastmod })` | schema.org graph: Organization, WebSite, SoftwareApplication, WebPage, BreadcrumbList, FAQPage, HowTo |
106
+ | `contentFacts(content)` | `{ faq, howTo, features, sections }` from a LandingPage content object, so page and JSON-LD cannot drift |
107
+ | `buildHead({ site, page, product, jsonLd })` | `<title>`, description, canonical, OG/Twitter, llms alternate, JSON-LD script |
108
+ | `buildRobots({ privatePaths, crawlers })` · `buildSitemap({ pages, lastmod })` · `buildLlmsTxt(…)` · `buildLlmsFullTxt(…)` | crawler files |
109
+ | `jsonLdScript(graph)` · `pageUrl(path, site)` · `escapeHtml` | helpers |
110
+
111
+ `site` defaults to `'__SITE_URL__'` (filled in at serve time). Pass a real origin when you have one, or `null`
112
+ to emit a graph without URLs. No ratings, reviews or prices are ever generated; an `Offer` (with no price) only appears with `product.offers`.
113
+
114
+ ### `@dreamtree-org/landing-kit/server` (Express, CommonJS-safe, includes everything in `/seo`)
115
+
116
+ | Function | Meaning |
117
+ |---|---|
118
+ | `landingRoutes(app, { dist, isPublicHost, hasSession, redirects, onSession, onPrivateHost, trailingSlash, rawFiles, manifest, cacheControl })` | serves `/` (visitors only, `Vary: Cookie`), every page in the manifest (default `landing-pages.json`; its paths are relative to its own directory), robots/sitemap/llms with the origin filled in, verification meta tags from env; 404s the raw files (`rawFiles: 'redirect'` 301s them to their page instead); `/x/` → 301 `/x` (`trailingSlash: 'serve'` answers both); `isPublicHost` may be async; a non-public host gets `Disallow: /` and, at `/`, `next()` or the app shell (`onPrivateHost: 'app-shell'`) |
119
+ | `prerenderPages({ dist, pages, files, homeFile, pagesDir, manifest })` | build step: fills `%TITLE% %DESCRIPTION% %PATH% %JSONLD% %HEAD% <!--landing-->` per page, writes `landing.html`, `landing-pages/…`, the manifest and crawler files; throws on an unfilled slot before writing anything |
120
+ | `sessionCheck({ cookie: 'JWT_TOKEN', bearer, verify })` | `hasSession(req)` from your own JWT check |
121
+ | `siteUrl(req)` · `verificationTags(env)` · `readCookie` · `fillSite` | helpers |
122
+
123
+ ### CSS variables
124
+
125
+ Set by `brand`: `--dt-brand`, `--dt-brand-accent`, `--dt-brand-panel`. Override on `.dt-landing`:
126
+ `--dt-lp-bg`, `--dt-lp-surface`, `--dt-lp-muted-bg`, `--dt-lp-border`, `--dt-lp-text`, `--dt-lp-body`, `--dt-lp-muted`,
127
+ `--dt-lp-radius`, `--dt-lp-max`, `--dt-lp-font`, `--dt-lp-header-h`.
128
+
129
+ ### Product extension points (0.1.1)
130
+
131
+ Keep a product's own pictures and sections without forking markup:
132
+
133
+ | Where | Prop | What it takes |
134
+ |---|---|---|
135
+ | `FeatureSection` `split` · `band` · `highlight` | `media`, `reverse` | your node (preview, figure, illustration) on the visual side; items become an icon row under the text; `reverse` puts the visual first |
136
+ | `FeatureSection` | `cta` | one CtaSpec or an array (side by side) |
137
+ | `FeatureSection` `split` / `detail` | `bullets` | a checklist under the text |
138
+ | `FeatureSection` `intro` | `cta`, `glanceNote` | a CTA under the text; a note inside the "at a glance" card |
139
+ | card items | `label` | a small kicker above the title ("01 · Visibility") |
140
+ | `Hero` | `backdrop` | a decorative node behind the copy (aria-hidden) |
141
+ | `Audiences` items | `media`, `features` | an illustration beside the heading; a short icon list |
142
+ | `HowItWorks` | `cta` | a CTA under the steps |
143
+ | `DemoCta` | `note`, `link` | a "what to tell us" box; a quiet link (e.g. Log in) |
144
+ | `DemoDialog` | `validate` | `(form) => ({ field: message })`, inline errors before submit; an Error thrown by `onSubmit` with `fields` shows them too |
145
+ | `landingRoutes` | `isPublicHost` async, `onPrivateHost`, `trailingSlash`, `rawFiles` | see `/server` above |
146
+
147
+ ## Recipes
148
+
149
+ **Express + Vite, prerendered and un-hydrated (billpro / taskpro / venturepro).**
150
+
151
+ ```js
152
+ // client/src/landing/main.jsx (landing.html's script)
153
+ import '@dreamtree-org/landing-kit/styles.css';
154
+ import { enhanceLanding } from '@dreamtree-org/landing-kit/enhance';
155
+ enhanceLanding(document, { loadDemo: () => import('./demoIsland.jsx') });
156
+
157
+ // client/src/landing/demoIsland.jsx
158
+ import { createDemoIsland, getBrand } from '@dreamtree-org/landing-kit';
159
+ export const { openDemo } = createDemoIsland({ brand: getBrand('billpro'), fields, onSubmit: postDemoRequest });
160
+
161
+ // client/scripts/prerender-landing.mjs, after vite builds landing.html + the SSR entry
162
+ const { prerenderPages } = await import('@dreamtree-org/landing-kit/server');
163
+ prerenderPages({ dist, pages: mod.renderPages(today), files: { 'robots.txt': robots, 'sitemap.xml': map, 'llms.txt': llms } });
164
+
165
+ // server.js (before express.static)
166
+ const { landingRoutes, sessionCheck } = require('@dreamtree-org/landing-kit/server');
167
+ landingRoutes(app, {
168
+ dist: path.join(__dirname, 'client/dist'),
169
+ isPublicHost: (req) => req.context?.domainKind === 'shared' && req.context?.domainCompanyId == null,
170
+ hasSession: sessionCheck({ verify: (t) => tokenService.verify(t).success === true }),
171
+ redirects: { '/solutions': '/#solutions' },
172
+ });
173
+ ```
174
+
175
+ **SPA, hydrated.** `<LandingPage brand={brand} content={content} linkComponent={Link} onBookDemo={() => setOpen(true)} />`
176
+ and render `<DemoDialog open={open} onClose={…} onSubmit={…} brand={brand} />`.
177
+
178
+ **Next.js (App Router).** The components are `'use client'`. For SEO, build in server code:
179
+ `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdScript(buildJsonLd({ site: origin, … })) }} />`,
180
+ and in `app/robots.ts` / `app/sitemap.ts` use `buildRobots` / `buildSitemap` with `site: origin`, or return Next's metadata objects from the same data.
181
+
182
+ ## What it deliberately does NOT do
183
+
184
+ - No product copy, prices, ratings or customer counts. Every word comes from your content file.
185
+ - No product screenshots. `PreviewFrame` frames the HTML preview you write (and may style with Tailwind).
186
+ - No HTTP for the demo form. `onSubmit` is yours (korm `submitDemoRequest`, fetch, anything). It does not store consent either.
187
+ - No Vite/Next build plugin. You keep your own landing.html, SSR entry and build; the package fills and writes the files.
188
+ - No icon library. Pass lucide (or any) components.
189
+
190
+ ## License
191
+
192
+ MIT
package/auth.css ADDED
@@ -0,0 +1,195 @@
1
+ /*
2
+ * @dreamtree-org/auth-kit — the shared auth frame.
3
+ * A product sets --dt-brand (and optionally --dt-brand-accent / --dt-brand-panel)
4
+ * through the `brand` prop; every other colour is derived here so all products
5
+ * share the same contrast, spacing and shape.
6
+ */
7
+ .dt-auth {
8
+ --dt-brand: #1e3a6e;
9
+ --dt-brand-accent: var(--dt-brand);
10
+ --dt-brand-panel: color-mix(in oklab, var(--dt-brand) 82%, black);
11
+ --dt-brand-soft: color-mix(in oklab, var(--dt-brand) 12%, white);
12
+ --dt-auth-bg: #f6f7f9;
13
+ --dt-auth-card: #ffffff;
14
+ --dt-auth-border: #e4e7ec;
15
+ --dt-auth-text: #101828;
16
+ --dt-auth-muted: #667085;
17
+ --dt-auth-radius: 0.75rem;
18
+ --dt-auth-font: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
19
+
20
+ display: flex;
21
+ flex-direction: column;
22
+ min-height: 100vh;
23
+ min-height: 100dvh;
24
+ background: var(--dt-auth-bg);
25
+ color: var(--dt-auth-text);
26
+ font-family: var(--dt-auth-font);
27
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
28
+ env(safe-area-inset-left);
29
+ overflow-x: hidden;
30
+ -webkit-text-size-adjust: 100%;
31
+ }
32
+ .dt-auth *,
33
+ .dt-auth *::before,
34
+ .dt-auth *::after { box-sizing: border-box; }
35
+
36
+ :root[data-theme='dark'] .dt-auth,
37
+ .dark .dt-auth {
38
+ --dt-brand-panel: color-mix(in oklab, var(--dt-brand) 55%, #0b0f17);
39
+ --dt-auth-bg: #0b0f17;
40
+ --dt-auth-card: #131a26;
41
+ --dt-auth-border: #243042;
42
+ --dt-auth-text: #e6eaf2;
43
+ --dt-auth-muted: #98a2b3;
44
+ }
45
+
46
+ /* Platform doors get a darker, unmistakable panel so an admin never confuses them */
47
+ .dt-auth--platform { --dt-brand-panel: color-mix(in oklab, var(--dt-brand) 30%, #0b0f17); }
48
+
49
+ /* ---- brand panel (desktop only) ---- */
50
+ .dt-auth-panel { display: none; }
51
+ @media (min-width: 1024px) {
52
+ .dt-auth { flex-direction: row; }
53
+ .dt-auth-panel {
54
+ display: flex;
55
+ flex-direction: column;
56
+ justify-content: space-between;
57
+ width: 40%;
58
+ max-width: 640px;
59
+ padding: 2.5rem;
60
+ background: var(--dt-brand-panel);
61
+ color: #fff;
62
+ }
63
+ }
64
+ .dt-auth-brand {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 0.5rem;
68
+ color: inherit;
69
+ text-decoration: none;
70
+ }
71
+ .dt-auth-brand-name { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.01em; }
72
+ .dt-auth-brand--mobile {
73
+ display: flex;
74
+ flex-wrap: wrap;
75
+ justify-content: center;
76
+ text-align: center;
77
+ margin-bottom: 1.5rem;
78
+ color: var(--dt-brand);
79
+ }
80
+ .dt-auth-brand--mobile .dt-auth-brand-name { font-size: 1.125rem; }
81
+ @media (min-width: 1024px) { .dt-auth-brand--mobile { display: none; } }
82
+
83
+ .dt-auth-monogram {
84
+ display: inline-grid;
85
+ place-items: center;
86
+ border-radius: 0.5rem;
87
+ background: var(--dt-brand-accent);
88
+ color: #fff;
89
+ font-weight: 800;
90
+ font-size: 0.95em;
91
+ }
92
+ .dt-auth-panel .dt-auth-monogram { background: var(--dt-brand-accent); box-shadow: 0 0 0 1px rgb(255 255 255 / 0.2); }
93
+ .dt-auth-mark-img { display: block; object-fit: contain; }
94
+ /* A dark logo must stay visible on the dark brand panel */
95
+ .dt-auth-panel .dt-auth-mark-img { background: #fff; border-radius: 0.375rem; padding: 0.1875rem; }
96
+ .dt-auth-mark-node { display: inline-flex; color: var(--dt-brand-accent); }
97
+ .dt-auth-panel .dt-auth-mark-node { color: var(--dt-brand-accent); }
98
+
99
+ .dt-auth-badge {
100
+ margin-left: 0.25rem;
101
+ padding: 0.125rem 0.5rem;
102
+ border-radius: 999px;
103
+ font-size: 0.6875rem;
104
+ font-weight: 600;
105
+ letter-spacing: 0.04em;
106
+ text-transform: uppercase;
107
+ background: rgb(255 255 255 / 0.14);
108
+ border: 1px solid rgb(255 255 255 / 0.24);
109
+ }
110
+ .dt-auth-brand--mobile .dt-auth-badge { background: var(--dt-brand-soft); border-color: transparent; }
111
+
112
+ .dt-auth-pitch h2 { margin: 0; color: #fff; font-size: 1.875rem; line-height: 1.2; font-weight: 700; letter-spacing: -0.02em; }
113
+ .dt-auth-pitch p { margin: 1rem 0 0; max-width: 28rem; line-height: 1.6; color: rgb(255 255 255 / 0.8); }
114
+ .dt-auth-pitch ul { margin: 1.5rem 0 0; padding: 0; list-style: none; display: grid; gap: 0.625rem; }
115
+ .dt-auth-pitch li { position: relative; padding-left: 1.5rem; color: rgb(255 255 255 / 0.88); font-size: 0.9375rem; }
116
+ .dt-auth-pitch li::before {
117
+ content: '';
118
+ position: absolute;
119
+ left: 0;
120
+ top: 0.45em;
121
+ width: 0.625rem;
122
+ height: 0.625rem;
123
+ border-radius: 999px;
124
+ background: var(--dt-brand-accent);
125
+ box-shadow: 0 0 0 3px rgb(255 255 255 / 0.15);
126
+ }
127
+ .dt-auth-panel-foot { margin: 0; font-size: 0.75rem; color: rgb(255 255 255 / 0.6); }
128
+
129
+ /* ---- form side ---- */
130
+ .dt-auth-main {
131
+ display: flex;
132
+ flex: 1;
133
+ align-items: center;
134
+ justify-content: center;
135
+ padding: 1rem;
136
+ }
137
+ @media (min-width: 640px) { .dt-auth-main { padding: 2rem; } }
138
+ .dt-auth-column { width: 100%; max-width: 28rem; }
139
+ .dt-auth-card {
140
+ background: var(--dt-auth-card);
141
+ border: 1px solid var(--dt-auth-border);
142
+ border-radius: var(--dt-auth-radius);
143
+ box-shadow: 0 1px 2px rgb(16 24 40 / 0.05);
144
+ padding: 1.5rem;
145
+ }
146
+ @media (min-width: 640px) { .dt-auth-card { padding: 2rem; } }
147
+ .dt-auth-head { margin-bottom: 1.5rem; }
148
+ .dt-auth-head h1 { margin: 0; font-size: 1.25rem; font-weight: 700; color: var(--dt-auth-text); }
149
+ .dt-auth-head p { margin: 0.25rem 0 0; font-size: 0.875rem; color: var(--dt-auth-muted); }
150
+
151
+ .dt-auth-footer { margin-top: 1rem; text-align: center; font-size: 0.875rem; color: var(--dt-auth-muted); }
152
+ /* Host apps often colour bare h2/p globally (Tailwind @layer base); keep the frame's own colours */
153
+ .dt-auth-footer p { color: inherit; }
154
+ /* Only real links take the brand colour; an inert <a> (no href, or aria-disabled) keeps its own style */
155
+ .dt-auth-footer a[href]:not([aria-disabled='true']) { color: var(--dt-brand); font-weight: 500; text-decoration: none; }
156
+ .dt-auth-footer a[href]:not([aria-disabled='true']):hover { text-decoration: underline; }
157
+ .dt-auth-legal {
158
+ display: flex;
159
+ flex-wrap: wrap;
160
+ justify-content: center;
161
+ gap: 0.75rem;
162
+ margin-top: 0.75rem;
163
+ font-size: 0.75rem;
164
+ color: var(--dt-auth-muted);
165
+ }
166
+ .dt-auth-legal-item { display: inline-flex; align-items: center; gap: 0.75rem; }
167
+ .dt-auth-legal-link { color: inherit; text-decoration: none; }
168
+ .dt-auth-legal-link:hover { color: var(--dt-brand); text-decoration: underline; }
169
+
170
+ /* ---- phones ---- */
171
+ .dt-auth-column,
172
+ .dt-auth-card { min-width: 0; }
173
+ .dt-auth-card { overflow-wrap: anywhere; }
174
+ .dt-auth-legal-link,
175
+ .dt-auth-footer a { display: inline-block; padding: 0.375rem 0.125rem; } /* ≥24px tap target */
176
+ @media (max-width: 639px) {
177
+ /* iOS Safari zooms into any focused field under 16px — keep the page still */
178
+ .dt-auth input,
179
+ .dt-auth select,
180
+ .dt-auth textarea { font-size: 16px; }
181
+ .dt-auth-main { align-items: flex-start; padding-top: 2rem; }
182
+ }
183
+ @media (max-width: 359px) {
184
+ .dt-auth-main { padding: 1.25rem 0.75rem; }
185
+ .dt-auth-card { padding: 1.125rem; border-radius: 0.625rem; }
186
+ .dt-auth-head h1 { font-size: 1.125rem; }
187
+ }
188
+ /* Short landscape phones: don't vertically centre a form taller than the screen */
189
+ @media (max-height: 560px) {
190
+ .dt-auth-main { align-items: flex-start; }
191
+ }
192
+
193
+ /* Shared helpers for form content inside the card */
194
+ .dt-auth-link { color: var(--dt-brand); font-weight: 500; font-size: 0.875rem; text-decoration: none; }
195
+ .dt-auth-link:hover { text-decoration: underline; }
package/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type { BrandId, Brand, LegalLink, AuthShellProps } from './index.js';
2
+ export { AuthShell, brandStyle, brands, getBrand } from './index.js';