@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/index.d.ts ADDED
@@ -0,0 +1,224 @@
1
+ import type { ComponentType, CSSProperties, ReactElement, ReactNode } from 'react';
2
+
3
+ /** Products with a built-in brand entry. */
4
+ export type BrandId =
5
+ | 'billpro'
6
+ | 'taskpro'
7
+ | 'venturepro'
8
+ | 'campaignpro'
9
+ | 'firepro'
10
+ | 'procurementpro'
11
+ | 'fitweavepro'
12
+ | 'aiagentbuilder'
13
+ | 'runloompro'
14
+ | 'appforge';
15
+
16
+ export interface Brand {
17
+ id: string;
18
+ name: string;
19
+ /** Drives every derived shade: panel, links, focus. Any CSS colour. */
20
+ primary: string;
21
+ /** Mark tile, bullet dots. Defaults to primary. */
22
+ accent?: string | undefined;
23
+ /** Override the derived brand-panel colour. */
24
+ panel?: string | undefined;
25
+ /** Square logo mark URL. Falls back to `mark`, then a monogram tile. */
26
+ logoSrc?: string | undefined;
27
+ mark?: ReactNode;
28
+ headline: string;
29
+ pitch?: string | undefined;
30
+ points?: string[] | undefined;
31
+ /** Small line at the bottom of the brand panel. */
32
+ category?: string | undefined;
33
+ platformHeadline?: string | undefined;
34
+ platformPitch?: string | undefined;
35
+ }
36
+
37
+ export interface LegalLink {
38
+ label: string;
39
+ href: string;
40
+ }
41
+
42
+ export interface AuthShellProps {
43
+ brand: Brand;
44
+ title: ReactNode;
45
+ subtitle?: ReactNode;
46
+ children?: ReactNode;
47
+ footer?: ReactNode;
48
+ /** 'platform' = super-admin door: darker panel + "Platform" badge. */
49
+ variant?: 'tenant' | 'platform' | undefined;
50
+ legalLinks?: LegalLink[] | undefined;
51
+ /** react-router `Link` or next/link; receives both `to` and `href`. */
52
+ linkComponent?: ComponentType<any> | undefined;
53
+ className?: string | undefined;
54
+ }
55
+
56
+ export declare function AuthShell(props: AuthShellProps): ReactElement;
57
+ export declare function brandStyle(brand: Brand): CSSProperties;
58
+ export declare const brands: Record<string, Brand>;
59
+ export declare function getBrand(id: BrandId | (string & {})): Brand;
60
+
61
+ /** A lucide-react style component (`Receipt`) or an element (`<Receipt />`). */
62
+ export type Icon = ComponentType<any> | ReactElement;
63
+ export interface NavLink { label: string; href: string; reload?: boolean }
64
+ export interface Item { title: string; body?: string; icon?: Icon; href?: string; /** small kicker above the title (cards) */ label?: string }
65
+ export interface QA { q: string; a: string }
66
+ export interface Step { name: string; text: string }
67
+ /** `{ label, href }` is a link; `{ label, demo: true }` (or a string) opens "Book a demo". */
68
+ export type CtaSpec = string | { label: string; href?: string; demo?: boolean };
69
+
70
+ export interface HeroProps {
71
+ eyebrow?: ReactNode;
72
+ /** A string, or `[lead, highlight]` to underline the second half. */
73
+ title: ReactNode | [ReactNode, ReactNode];
74
+ subtitle?: ReactNode;
75
+ lead?: ReactNode;
76
+ chips?: string[];
77
+ chipsLabel?: string;
78
+ /** Button label; `false`/'' hides the button. Default 'Book a demo'. */
79
+ cta?: ReactNode;
80
+ secondary?: NavLink;
81
+ overlap?: boolean;
82
+ id?: string;
83
+ /** A decorative node (illustration) laid behind the hero copy, aria-hidden. */
84
+ backdrop?: ReactNode;
85
+ }
86
+
87
+ export type FeatureVariant = 'cards' | 'split' | 'list' | 'intro' | 'highlight' | 'band' | 'compare' | 'links' | 'detail';
88
+ export type Tone = 'plain' | 'tinted' | 'muted' | 'dark';
89
+
90
+ export interface FeatureSectionProps {
91
+ id?: string;
92
+ variant?: FeatureVariant;
93
+ tone?: Tone;
94
+ eyebrow?: ReactNode;
95
+ title: ReactNode;
96
+ body?: ReactNode;
97
+ items?: Array<Item | string>;
98
+ /** One call to action or several side by side. */
99
+ cta?: CtaSpec | CtaSpec[];
100
+ footnote?: ReactNode;
101
+ /** split · band · highlight: your own node on the visual side (items then show as an icon row under the text). */
102
+ media?: ReactNode;
103
+ /** Visual side first. */
104
+ reverse?: boolean;
105
+ /** intro */ subtitle?: ReactNode; detail?: ReactNode; note?: ReactNode; glance?: string[]; glanceTitle?: string; glanceNote?: ReactNode; link?: NavLink;
106
+ /** highlight */ noteLabel?: string;
107
+ /** band */ examples?: string[]; examplesLabel?: string;
108
+ /** compare */ columns?: Array<{ title: string; items: string[]; tone?: 'good' | 'check' }>;
109
+ /** split, detail: a checklist under the text */ bullets?: string[];
110
+ }
111
+
112
+ export interface AudiencesProps {
113
+ id?: string; eyebrow?: ReactNode; title: ReactNode; body?: ReactNode; tone?: Tone; linkLabel?: string;
114
+ items: Array<{
115
+ id?: string; keyword?: string; title: string; body: string; href?: string; linkLabel?: string; icon?: Icon;
116
+ /** decorative illustration beside the heading */ media?: ReactNode;
117
+ /** a short icon list under the text */ features?: Array<string | { title: string; icon?: Icon }>;
118
+ }>;
119
+ }
120
+ export interface HowItWorksProps { id?: string; eyebrow?: ReactNode; title: ReactNode; body?: ReactNode; steps?: Step[]; items?: Step[]; tone?: Tone; cta?: CtaSpec | CtaSpec[]; /** seo only */ howToName?: string }
121
+ export interface FaqProps { id?: string; eyebrow?: ReactNode; title?: ReactNode; intro?: ReactNode; items: QA[]; openFirst?: boolean; tone?: Tone }
122
+ export interface DemoCtaProps {
123
+ id?: string; eyebrow?: ReactNode; title: ReactNode; body?: ReactNode; label?: ReactNode; chips?: string[]; chipsLabel?: string;
124
+ /** a "what to tell us" box under the button */ note?: { title?: ReactNode; body?: ReactNode };
125
+ /** a quiet link under it, e.g. { label: 'Already have a workspace? Log in', href: '/login', reload: true } */ link?: NavLink;
126
+ }
127
+ export interface FooterProps {
128
+ blurb?: ReactNode;
129
+ columns?: Array<{ title: string; label?: string; links: NavLink[]; compact?: boolean }>;
130
+ org?: string; tagline?: string; legal?: NavLink[]; year?: number; brand?: Brand;
131
+ }
132
+
133
+ export interface LandingContent {
134
+ product?: { name?: string; definition?: string; description?: string; features?: string[]; [k: string]: unknown };
135
+ nav?: NavLink[];
136
+ /** `null` hides "Log in". Default { label: 'Log in', href: '/login' }. */
137
+ login?: NavLink | null;
138
+ header?: { cta?: ReactNode };
139
+ hero: HeroProps;
140
+ /** FeatureSection props, `{ use: 'audiences' | 'steps' | 'faq' | 'cta' | 'preview' | <slot name> }`, or an element. */
141
+ sections?: Array<FeatureSectionProps | { use: string } | ReactElement>;
142
+ audiences?: AudiencesProps;
143
+ steps?: HowItWorksProps;
144
+ faq?: FaqProps;
145
+ cta?: DemoCtaProps;
146
+ /** `false` hides the footer. */
147
+ footer?: FooterProps | false;
148
+ }
149
+
150
+ export interface LayoutProps {
151
+ brand: Brand;
152
+ /** react-router `Link` or next/link; receives both `to` and `href`. */
153
+ linkComponent?: ComponentType<any> | null;
154
+ /** Called with the clicked button when the page is hydrated. Un-hydrated pages use enhanceLanding(). */
155
+ onBookDemo?: ((trigger: HTMLElement) => void) | null;
156
+ /** false on sub-pages: header `#section` links become `/#section`. */
157
+ home?: boolean;
158
+ className?: string;
159
+ }
160
+
161
+ export interface LandingPageProps extends LayoutProps {
162
+ content: LandingContent;
163
+ preview?: ReactNode;
164
+ /** A product's own sections, placed by `{ use: '<name>' }` in content.sections (content stays data). */
165
+ slots?: Record<string, ReactNode>;
166
+ }
167
+ export interface MarketingLayoutProps extends LayoutProps {
168
+ nav?: NavLink[]; login?: NavLink | null; cta?: ReactNode; footer?: FooterProps | false; children?: ReactNode;
169
+ }
170
+
171
+ export interface DemoField {
172
+ name: string; label: string;
173
+ type?: 'text' | 'tel' | 'email' | 'number' | 'select' | 'textarea';
174
+ required?: boolean; options?: Array<{ value: string; label: string }>;
175
+ placeholder?: string; maxLength?: number; min?: number; rows?: number;
176
+ autoComplete?: string; inputMode?: string; wide?: boolean;
177
+ }
178
+ export interface DemoDialogProps {
179
+ brand?: Brand;
180
+ open?: boolean;
181
+ onClose?: () => void;
182
+ /** POST the request; throw an Error whose message is shown to the visitor. */
183
+ onSubmit: (data: Record<string, string> & { consent: true; website: string; source_path: string }) => Promise<unknown>;
184
+ fields?: DemoField[];
185
+ title?: ReactNode; intro?: ReactNode;
186
+ fiduciary?: string; consentText?: ReactNode; privacyHref?: string | null; policyVersion?: string;
187
+ submitLabel?: ReactNode; successMessage?: ReactNode | ((form: Record<string, string>) => ReactNode);
188
+ /** Inline errors before submit: { field: message }, empty when valid. With it, the button only waits for consent.
189
+ * An Error thrown by onSubmit with `fields` shows those messages under their fields too. */
190
+ validate?: (form: Record<string, string>) => Record<string, string> | null | undefined;
191
+ }
192
+
193
+ export declare function LandingPage(props: LandingPageProps): JSX.Element;
194
+ export declare function MarketingLayout(props: MarketingLayoutProps): JSX.Element;
195
+ export declare function Header(props: { nav?: NavLink[]; login?: NavLink | null; cta?: ReactNode; brand?: Brand }): JSX.Element;
196
+ export declare function Hero(props: HeroProps): JSX.Element;
197
+ export declare function PreviewFrame(props: { id?: string; heading?: string; caption?: string; meta?: ReactNode; badge?: ReactNode; brand?: Brand; children?: ReactNode }): JSX.Element;
198
+ export declare function FeatureSection(props: FeatureSectionProps): JSX.Element;
199
+ export declare function Audiences(props: AudiencesProps): JSX.Element;
200
+ export declare function HowItWorks(props: HowItWorksProps): JSX.Element;
201
+ export declare function Faq(props: FaqProps): JSX.Element;
202
+ export declare function DemoCta(props: DemoCtaProps): JSX.Element;
203
+ export declare function DemoDialog(props: DemoDialogProps): JSX.Element | null;
204
+ export declare const DEFAULT_DEMO_FIELDS: DemoField[];
205
+ export declare function DreamtreeFooter(props: FooterProps): JSX.Element;
206
+ export declare function DreamtreeMark(props: { size?: number }): JSX.Element;
207
+ export declare function PageHero(props: { breadcrumbs?: Array<{ label: string; href?: string }>; eyebrow?: ReactNode; title: ReactNode; lead?: ReactNode; cta?: ReactNode; secondary?: NavLink }): JSX.Element;
208
+ export declare function Breadcrumbs(props: { items: Array<{ label: string; href?: string }> }): JSX.Element | null;
209
+ export declare function BookDemoButton(props: { size?: 'sm' | 'md' | 'lg'; variant?: 'primary' | 'inverse' | 'outline'; className?: string; children?: ReactNode; onClick?: (e: any) => void }): JSX.Element;
210
+ export declare function BrandLockup(props: { brand?: Brand; byline?: string; href?: string }): JSX.Element | null;
211
+ export declare function BrandMark(props: { brand: Brand; size?: number }): JSX.Element;
212
+ export declare function Wordmark(props: { name: string }): JSX.Element;
213
+ export declare function Section(props: { id?: string; labelledBy?: string; tone?: Tone; className?: string; narrow?: boolean; children?: ReactNode }): JSX.Element;
214
+ export declare function SectionIntro(props: { id: string; eyebrow?: ReactNode; title: ReactNode; body?: ReactNode; align?: 'start' | 'center'; children?: ReactNode }): JSX.Element;
215
+ export declare function Wrap(props: { className?: string; narrow?: boolean; children?: ReactNode }): JSX.Element;
216
+ export declare function Eyebrow(props: { className?: string; children?: ReactNode }): JSX.Element | null;
217
+ export declare function TextLink(props: { href: string; className?: string; children?: ReactNode; [k: string]: unknown }): JSX.Element;
218
+ export declare function Cta(props: { cta?: CtaSpec; className?: string }): JSX.Element | null;
219
+ export declare function SmartLink(props: { href: string; reload?: boolean; className?: string; children?: ReactNode; [k: string]: unknown }): JSX.Element;
220
+ export declare function resolveHref(href: string, home: boolean): string;
221
+ export declare const LandingProvider: import('react').Provider<{ brand: Brand | null; linkComponent?: ComponentType<any> | null; onBookDemo?: ((t: HTMLElement) => void) | null; home?: boolean }>;
222
+ export declare function useLanding(): { brand: Brand | null; linkComponent: ComponentType<any> | null; onBookDemo: ((t: HTMLElement) => void) | null; home: boolean };
223
+ /** The "Book a demo" island for an un-hydrated page: React mounts on first open. */
224
+ export declare function createDemoIsland(props: Omit<DemoDialogProps, 'onClose'> | (() => Omit<DemoDialogProps, 'onClose'>)): { openDemo(trigger?: HTMLElement | null): void; close(): void };
package/package.json CHANGED
@@ -1,6 +1,96 @@
1
1
  {
2
2
  "name": "@dreamtree-org/landing-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "The shared marketing site and auth frame (AuthShell + brands) for every Dreamtree product: content-driven landing pages, login/signup shell, Express routes, prerender and SEO files.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dreamtree-org/dreamtree-org.landing-kit.git"
10
+ },
11
+ "homepage": "https://github.com/dreamtree-org/dreamtree-org.landing-kit#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/dreamtree-org/dreamtree-org.landing-kit/issues"
14
+ },
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./auth": {
25
+ "types": "./auth.d.ts",
26
+ "import": "./dist/auth.js",
27
+ "require": "./dist/auth.cjs"
28
+ },
29
+ "./server": {
30
+ "types": "./server.d.ts",
31
+ "import": "./dist/server.js",
32
+ "require": "./dist/server.cjs"
33
+ },
34
+ "./seo": {
35
+ "types": "./seo.d.ts",
36
+ "import": "./dist/seo.js",
37
+ "require": "./dist/seo.cjs"
38
+ },
39
+ "./enhance": {
40
+ "types": "./enhance.d.ts",
41
+ "import": "./dist/enhance.js",
42
+ "require": "./dist/enhance.cjs"
43
+ },
44
+ "./styles.css": "./styles.css",
45
+ "./auth.css": "./auth.css"
46
+ },
47
+ "sideEffects": [
48
+ "*.css"
49
+ ],
50
+ "files": [
51
+ "dist",
52
+ "styles.css",
53
+ "auth.css",
54
+ "index.d.ts",
55
+ "auth.d.ts",
56
+ "server.d.ts",
57
+ "seo.d.ts",
58
+ "enhance.d.ts",
59
+ "README.md",
60
+ "MIGRATION.md",
61
+ "LICENSE"
62
+ ],
63
+ "scripts": {
64
+ "build": "node build.mjs",
65
+ "test": "node build.mjs && node --test test/*.test.mjs",
66
+ "prepublishOnly": "npm test",
67
+ "test:viewports": "node build.mjs && node test/viewports.mjs"
68
+ },
69
+ "peerDependencies": {
70
+ "react": ">=18",
71
+ "react-dom": ">=18"
72
+ },
73
+ "devDependencies": {
74
+ "esbuild": "^0.25.0",
75
+ "express": "^4.22.3",
76
+ "react": "^18.3.1",
77
+ "react-dom": "^18.3.1"
78
+ },
79
+ "engines": {
80
+ "node": ">=18"
81
+ },
82
+ "keywords": [
83
+ "react",
84
+ "landing-page",
85
+ "marketing",
86
+ "seo",
87
+ "json-ld",
88
+ "llms.txt",
89
+ "express",
90
+ "prerender",
91
+ "dreamtree"
92
+ ],
93
+ "publishConfig": {
94
+ "access": "public"
95
+ }
96
+ }
package/seo.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ export declare const SITE_PLACEHOLDER: '__SITE_URL__';
2
+ export declare const AI_CRAWLERS: string[];
3
+ export declare const DEFAULT_PRIVATE_PATHS: string[];
4
+ export declare const PRIVATE_ROBOTS: string;
5
+ export interface Locale { lang: string; og: string; country?: string }
6
+ export declare const DEFAULT_LOCALE: Locale;
7
+
8
+ export interface SeoProduct {
9
+ name: string;
10
+ /** The one-sentence definition answer engines quote. */
11
+ definition: string;
12
+ description?: string;
13
+ logo?: string;
14
+ image?: string | { path: string; width?: number; height?: number; alt?: string; type?: string };
15
+ /** SoftwareApplication.screenshot (a path or URL) */
16
+ screenshot?: string;
17
+ category?: string;
18
+ subCategory?: string;
19
+ operatingSystem?: string;
20
+ audience?: string;
21
+ features?: string[];
22
+ /** Adds an Offer with no price ("Contact us for pricing"). Off by default. */
23
+ offers?: { description?: string; url?: string };
24
+ }
25
+ export interface SeoOrg { name: string; logo?: string; email?: string; sameAs?: string[] }
26
+ export interface SeoPage { path: string; title?: string; description?: string; name?: string; summary?: string; priority?: string; changefreq?: string; faq?: Array<{ q: string; a: string }> }
27
+ /** The origin; SITE_PLACEHOLDER (default) when the server fills it in, null for a graph without URLs. */
28
+ export type Site = string | null;
29
+
30
+ export declare function escapeHtml(value: unknown): string;
31
+ export declare function jsonLdScript(graph: unknown): string;
32
+ export declare function pageUrl(path?: string, site?: Site): string | undefined;
33
+ export declare function buildJsonLd(args: {
34
+ site?: Site; product: SeoProduct; org?: SeoOrg; page?: SeoPage;
35
+ faq?: Array<{ q: string; a: string }>;
36
+ howTo?: { name?: string; description?: string; steps: Array<{ name: string; text: string }>; anchor?: string } | null;
37
+ breadcrumbs?: Array<[string, string]>; parts?: Array<{ path: string; name?: string }>;
38
+ keywords?: string[]; lastmod?: string; speakable?: string[]; locale?: Locale;
39
+ }): { '@context': 'https://schema.org'; '@graph': Array<Record<string, unknown>> };
40
+ export declare function contentFacts(content: unknown): {
41
+ faq: Array<{ q: string; a: string }>;
42
+ howTo: { name?: string; description?: string; steps: Array<{ name: string; text: string }>; anchor: string } | null;
43
+ features: string[];
44
+ sections: Array<[string, string]>;
45
+ };
46
+ export declare function buildHead(args: { site?: Site; page: SeoPage; product: SeoProduct; twitterHandle?: string; jsonLd?: unknown; locale?: Locale; robots?: string }): string;
47
+ export declare function buildRobots(args?: { site?: Site; privatePaths?: string[]; crawlers?: string[] }): string;
48
+ export declare function buildSitemap(args: { site?: Site; pages: SeoPage[]; lastmod?: string; changefreq?: string }): string;
49
+ export declare function buildLlmsTxt(args: { site?: Site; product: SeoProduct; org?: SeoOrg; pages?: SeoPage[]; about?: string; facts?: string[]; optional?: Array<{ label: string; path: string }> }): string;
50
+ export declare function buildLlmsFullTxt(args: { site?: Site; product: SeoProduct; org?: SeoOrg; pages?: SeoPage[]; sections?: (page: SeoPage) => Array<[string, string]> }): string;
package/server.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ export * from './seo';
2
+
3
+ type Req = { headers: Record<string, string | string[] | undefined>; protocol?: string; [k: string]: any };
4
+
5
+ export declare const CRAWLER_FILES: Record<string, string>;
6
+ export declare function siteUrl(req: Req, opts?: { appUrl?: string }): string;
7
+ export declare function readCookie(req: Req, name: string): string | null;
8
+ export declare function sessionCheck(opts: { cookie?: string | null; bearer?: boolean; verify: (token: string) => boolean }): (req: Req) => boolean;
9
+ export declare function verificationTags(env?: Record<string, string | undefined>): string;
10
+ export declare function fillSite(body: string, origin: string, opts?: { html?: boolean; env?: Record<string, string | undefined> }): string;
11
+
12
+ export interface LandingRoutesOptions {
13
+ /** client/dist */
14
+ dist: string;
15
+ /** Default 'landing-pages.json'. Its paths are relative to its own directory. */
16
+ manifest?: string;
17
+ /** May return a Promise (e.g. a cached registry lookup). */
18
+ isPublicHost?: (req: any) => boolean | Promise<boolean>;
19
+ hasSession?: (req: any) => boolean;
20
+ /** 'next' (default) falls through to the SPA; 'app-shell' sends dist/index.html. */
21
+ onSession?: 'next' | 'app-shell';
22
+ /** What `/` does on a host that is not public. Default 'next'. */
23
+ onPrivateHost?: 'next' | 'app-shell';
24
+ /** 'redirect' (default): /features/ → 301 /features. 'serve': 200 at both. */
25
+ trailingSlash?: 'redirect' | 'serve';
26
+ /** The prerendered files' own URLs: 'not-found' (default, 404) or 'redirect' (301 to the page's path). */
27
+ rawFiles?: 'not-found' | 'redirect';
28
+ redirects?: Record<string, string>;
29
+ cacheControl?: string;
30
+ crawlerCache?: string;
31
+ privateRobots?: string;
32
+ appUrl?: string;
33
+ env?: Record<string, string | undefined>;
34
+ }
35
+ export declare function landingRoutes(app: any, options: LandingRoutesOptions | string): string[];
36
+ export declare function pageFile(path: string, opts?: { homeFile?: string; pagesDir?: string }): string;
37
+ export declare function prerenderPages(args: {
38
+ dist: string;
39
+ template?: string;
40
+ pages: Array<{ path: string; html: string; title?: string; description?: string; jsonLd?: unknown; head?: string }>;
41
+ files?: Record<string, string | null | undefined>;
42
+ homeFile?: string; pagesDir?: string; manifest?: string; slot?: string;
43
+ }): { manifest: Record<string, string>; bytes: number };