generator-white-label 8.0.0 → 10.0.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.
Files changed (109) hide show
  1. package/CLI.md +16 -20
  2. package/EXISTING_APPLICATIONS.md +155 -0
  3. package/PACKAGE_MANAGERS.md +6 -4
  4. package/README.md +136 -126
  5. package/app/404.ts +28 -0
  6. package/app/README.md +45 -7
  7. package/{scaffold/no-jsx/app → app}/assets/script/index.ts +1 -0
  8. package/app/assets/script/tasks/TaskApplication.ts +11 -11
  9. package/app/assets/script/tasks/TaskMediator.ts +6 -11
  10. package/app/assets/script/tasks/TaskRouter.ts +1 -1
  11. package/{scaffold/no-jsx/app → app}/assets/script/tasks/TaskView.ts +1 -1
  12. package/app/assets/view/CodeBlock.ts +15 -0
  13. package/app/assets/view/examples/tasks/TaskExample.ts +50 -0
  14. package/app/assets/view/examples/tasks/task-state.ts +1 -1
  15. package/app/assets/view/layout/SiteFooter.ts +11 -0
  16. package/app/assets/view/layout/SiteHeader.ts +17 -0
  17. package/app/assets/view/page-data.ts +2 -2
  18. package/app/assets/view/sections/GettingStartedSection.ts +26 -0
  19. package/app/assets/view/sections/HeroSection.ts +15 -0
  20. package/app/assets/view/sections/LiveExampleSection.ts +24 -0
  21. package/app/assets/view/sections/PackageDocsSection.ts +69 -0
  22. package/app/assets/view/sections/{SourceGuideSection.tsx → SourceGuideSection.ts} +11 -16
  23. package/app/index.ts +50 -0
  24. package/cli/index.ts +6 -42
  25. package/dist/app/404.d.ts +1 -1
  26. package/dist/app/404.js +18 -2
  27. package/dist/app/404.js.map +1 -1
  28. package/dist/app/assets/script/index.d.ts +1 -6
  29. package/dist/app/assets/script/index.js +1 -6
  30. package/dist/app/assets/script/index.js.map +1 -1
  31. package/dist/app/assets/script/tasks/TaskApplication.js +11 -11
  32. package/dist/app/assets/script/tasks/TaskApplication.js.map +1 -1
  33. package/dist/app/assets/script/tasks/TaskMediator.d.ts +5 -10
  34. package/dist/app/assets/script/tasks/TaskMediator.js.map +1 -1
  35. package/dist/app/assets/script/tasks/TaskRouter.js +1 -1
  36. package/dist/app/assets/script/tasks/TaskRouter.js.map +1 -1
  37. package/dist/app/assets/script/tasks/TaskView.d.ts +1 -6
  38. package/dist/app/assets/script/tasks/TaskView.js +2 -8
  39. package/dist/app/assets/script/tasks/TaskView.js.map +1 -1
  40. package/dist/app/assets/view/CodeBlock.d.ts +3 -3
  41. package/dist/app/assets/view/CodeBlock.js +4 -2
  42. package/dist/app/assets/view/CodeBlock.js.map +1 -1
  43. package/dist/app/assets/view/examples/tasks/TaskExample.d.ts +2 -8
  44. package/dist/app/assets/view/examples/tasks/TaskExample.js +41 -9
  45. package/dist/app/assets/view/examples/tasks/TaskExample.js.map +1 -1
  46. package/dist/app/assets/view/examples/tasks/task-state.d.ts +1 -1
  47. package/dist/app/assets/view/examples/tasks/task-state.js.map +1 -1
  48. package/dist/app/assets/view/layout/SiteFooter.d.ts +2 -2
  49. package/dist/app/assets/view/layout/SiteFooter.js +8 -3
  50. package/dist/app/assets/view/layout/SiteFooter.js.map +1 -1
  51. package/dist/app/assets/view/layout/SiteHeader.d.ts +2 -8
  52. package/dist/app/assets/view/layout/SiteHeader.js +14 -9
  53. package/dist/app/assets/view/layout/SiteHeader.js.map +1 -1
  54. package/dist/app/assets/view/page-data.d.ts +2 -2
  55. package/dist/app/assets/view/page-data.js.map +1 -1
  56. package/dist/app/assets/view/sections/GettingStartedSection.d.ts +1 -1
  57. package/dist/app/assets/view/sections/GettingStartedSection.js +21 -6
  58. package/dist/app/assets/view/sections/GettingStartedSection.js.map +1 -1
  59. package/dist/app/assets/view/sections/HeroSection.d.ts +2 -8
  60. package/dist/app/assets/view/sections/HeroSection.js +12 -9
  61. package/dist/app/assets/view/sections/HeroSection.js.map +1 -1
  62. package/dist/app/assets/view/sections/LiveExampleSection.d.ts +2 -6
  63. package/dist/app/assets/view/sections/LiveExampleSection.js +17 -7
  64. package/dist/app/assets/view/sections/LiveExampleSection.js.map +1 -1
  65. package/dist/app/assets/view/sections/PackageDocsSection.d.ts +1 -1
  66. package/dist/app/assets/view/sections/PackageDocsSection.js +57 -20
  67. package/dist/app/assets/view/sections/PackageDocsSection.js.map +1 -1
  68. package/dist/app/assets/view/sections/SourceGuideSection.d.ts +2 -7
  69. package/dist/app/assets/view/sections/SourceGuideSection.js +20 -8
  70. package/dist/app/assets/view/sections/SourceGuideSection.js.map +1 -1
  71. package/dist/app/index.d.ts +2 -8
  72. package/dist/app/index.js +36 -11
  73. package/dist/app/index.js.map +1 -1
  74. package/dist/cli/index.d.ts +0 -3
  75. package/dist/cli/index.js +6 -37
  76. package/dist/cli/index.js.map +1 -1
  77. package/dist/scaffold/index.d.ts +2 -13
  78. package/dist/scaffold/index.js +36 -56
  79. package/dist/scaffold/index.js.map +1 -1
  80. package/dist/scripts/build.js +97 -28
  81. package/dist/scripts/build.js.map +1 -1
  82. package/dist/server/handler.d.ts +7 -0
  83. package/dist/server/handler.js +60 -0
  84. package/dist/server/handler.js.map +1 -0
  85. package/package.json +25 -24
  86. package/pnpm-workspace.yaml +0 -1
  87. package/scaffold/AGENTS.md +23 -0
  88. package/scaffold/index.ts +32 -60
  89. package/scripts/build.ts +90 -30
  90. package/server/handler.ts +2 -10
  91. package/template-test/site.test.js +3 -1
  92. package/tsconfig.site.json +1 -4
  93. package/app/404.tsx +0 -28
  94. package/app/assets/script/index.tsx +0 -13
  95. package/app/assets/script/tasks/TaskView.tsx +0 -24
  96. package/app/assets/view/CodeBlock.tsx +0 -17
  97. package/app/assets/view/examples/tasks/TaskExample.tsx +0 -63
  98. package/app/assets/view/layout/SiteFooter.tsx +0 -11
  99. package/app/assets/view/layout/SiteHeader.tsx +0 -23
  100. package/app/assets/view/sections/GettingStartedSection.tsx +0 -27
  101. package/app/assets/view/sections/HeroSection.tsx +0 -21
  102. package/app/assets/view/sections/LiveExampleSection.tsx +0 -29
  103. package/app/assets/view/sections/PackageDocsSection.tsx +0 -69
  104. package/app/index.tsx +0 -54
  105. package/scaffold/no-jsx/README.md +0 -47
  106. package/scaffold/no-jsx/app/404.ts +0 -10
  107. package/scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts +0 -39
  108. package/scaffold/no-jsx/app/index.ts +0 -58
  109. package/tsconfig.site.no-jsx.json +0 -24
package/app/404.ts ADDED
@@ -0,0 +1,28 @@
1
+ import {html} from 'white-label-view/html';
2
+
3
+ interface PageData {
4
+ cdn: string;
5
+ version: string;
6
+ www: string;
7
+ }
8
+
9
+ export default function NotFoundPage(data: Record<string, unknown>) {
10
+ const page = data as unknown as PageData;
11
+ return html`<html lang="en-US" dir="ltr">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <meta name="robots" content="noindex,follow">
16
+ <title>Page not found | White Label</title>
17
+ <link rel="stylesheet" href="${page.cdn}release/${page.version}/assets/style/global.css">
18
+ </head>
19
+ <body>
20
+ <a class="skip-link" href="#main">Skip to the message</a>
21
+ <main id="main" tabindex="-1" class="mx-auto max-w-5xl px-4 py-24 sm:px-6 lg:px-8">
22
+ <h1 class="text-5xl font-extrabold tracking-tight">Page not found</h1>
23
+ <p class="mt-6 text-lg">The requested page does not exist.</p>
24
+ <p class="mt-6"><a class="font-bold underline" href="${page.www}">Return to the White Label starter</a></p>
25
+ </main>
26
+ </body>
27
+ </html>`;
28
+ }
package/app/README.md CHANGED
@@ -1,10 +1,22 @@
1
1
  # White Label starter application
2
2
 
3
- This static starter was generated with the **JSX/TSX** template option. It uses TypeScript and the White Label JSX runtime for page and client-side View rendering, with Tailwind CSS 4 for styling. It makes no API or service calls.
3
+ This static starter uses ordinary TypeScript and White Label View's first-party tagged HTML templates, with Tailwind CSS 4 for styling. It makes no API or service calls.
4
4
 
5
- JSX is optional in White Label. If you prefer plain TypeScript or want a third-party template engine to own rendering, generate with `--no-jsx` (or answer **No** to the generator's JSX question), then install and call that renderer from the View `template` function. The Model, View, Router, Mediator, progressive-enhancement behavior, and generated feature set remain equivalent.
5
+ The rendering path is intentionally small and explicit:
6
6
 
7
- See [Template engines and JSX options](https://whitelabeljs.org/docs/view/#template-engines) for the tested third-party engines, rendering contract, and no-JSX setup.
7
+ ```ts
8
+ import {html} from 'white-label-view/html';
9
+
10
+ export default function Page(data: {title: string}) {
11
+ return html`<main><h1>${data.title}</h1></main>`;
12
+ }
13
+ ```
14
+
15
+ Dynamic text and quoted-attribute values are HTML-escaped by default. Use `attributes()` for conditional/opening-tag attributes. `unsafeHTML()` is an explicit trust boundary for application-owned content that is already trusted or sanitized; it is not a sanitizer.
16
+
17
+ White Label View remains template-engine agnostic. If the application prefers JSX, Handlebars, Eta, EJS, Mustache, Nunjucks, Pug, or another renderer, add it to the application and return its rendered output from View's `template` function. JSX is a tested third-party option rather than a White Label runtime requirement.
18
+
19
+ See [Template engines](https://whitelabeljs.org/docs/view/#template-engines) for tested integrations and rendering/security boundaries.
8
20
 
9
21
  ## Start developing
10
22
 
@@ -42,11 +54,37 @@ python3 -m http.server 8080 --directory _deploy
42
54
 
43
55
  Open `http://localhost:8080/`.
44
56
 
45
- Serve `_deploy` as the web server's document root. Do **not** browse to `_deploy/index.html` through a server rooted at the project directory (for example, `/white-label-site/_deploy/index.html`), because generated asset URLs such as `/release/local/assets/style/global.css` are intentionally rooted at the deployed site's origin and will otherwise return 404 responses.
57
+ Serve `_deploy` as the web server's document root. Do **not** browse to `_deploy/index.html` through a server rooted at the project directory, because generated asset URLs such as `/release/local/assets/style/global.css` are intentionally rooted at the deployed site's origin.
46
58
 
47
- The build output is `_deploy`. The test suite checks the starter content, progressive interaction, production build, accessibility/indexability signals, serverless request isolation, Web-standard bundling, and serverless size/startup budgets.
59
+ The build output is `_deploy`. The test suite checks starter content, progressive interaction, production build behavior, accessibility/indexability signals, serverless request isolation, Web-standard bundling, and serverless size/startup budgets.
60
+
61
+ Pages live in `app/*.ts`, page data lives in `app/assets/data/view`, browser code lives in `app/assets/script`, the provider-neutral serverless example lives in `server/handler.ts`, and shared styles live in `app/assets/style`.
62
+
63
+ ## Simple View click event
64
+
65
+ View lifecycle hooks are the simplest place to own a browser listener:
66
+
67
+ ```ts
68
+ import View from 'white-label-view';
69
+
70
+ class ButtonView extends View {
71
+ handleClick = () => {
72
+ console.log('Clicked');
73
+ };
74
+
75
+ addListeners() {
76
+ this.element.addEventListener('click', this.handleClick);
77
+ return this;
78
+ }
79
+
80
+ removeListeners() {
81
+ this.element.removeEventListener('click', this.handleClick);
82
+ return this;
83
+ }
84
+ }
85
+ ```
48
86
 
49
- Pages live in `app/*.tsx`, page data lives in `app/assets/data/view`, browser code lives in `app/assets/script`, the provider-neutral serverless example lives in `server/handler.ts`, and shared styles live in `app/assets/style`. TypeScript is configured with `jsx: react-jsx` and `jsxImportSource: white-label-view`, so this scaffold's JSX does not require React.
87
+ The same callback reference is used for both registration and cleanup. View calls `removeListeners()` before replacement or destruction.
50
88
 
51
89
  ## Serverless / function runtimes
52
90
 
@@ -74,4 +112,4 @@ npm run build -- --version=release-1 --production=true --site-url=https://www.ex
74
112
 
75
113
  Build flags use `--key=value`. `--www` and `--cdn` default to `/`; `--version` selects `_deploy/release/<version>/assets`; without a version, the current timestamp is used. Production builds require a real HTTPS `--site-url` for canonical, Open Graph, robots, and sitemap output. Each build replaces `_deploy`.
76
114
 
77
- Edit TypeScript/TSX sources, not `dist`. JSX expressions are escaped by the White Label runtime; reserve `raw()` for trusted application-authored markup. Replace the starter copy with application content while retaining the tested accessibility and progressive-enhancement patterns.
115
+ Edit TypeScript sources, not `dist`. Keep untrusted values in normal `html` interpolations; use `unsafeHTML()` only when the application has deliberately established trust or sanitization. Replace the starter copy with application content while retaining the tested accessibility and progressive-enhancement patterns.
@@ -1,6 +1,7 @@
1
1
  /** @module app/assets/script/index */
2
2
  import {initializeTaskApplication} from './tasks/TaskApplication.js';
3
3
 
4
+ /** Start the feature composition for this document. */
4
5
  export {initializeTaskApplication} from './tasks/TaskApplication.js';
5
6
  export {normalizeTaskFilter} from './tasks/TaskRouter.js';
6
7
 
@@ -32,25 +32,25 @@ export function initializeTaskApplication(documentRoot: Document): TaskApplicati
32
32
  const status = parentElement.querySelector<HTMLElement>('[data-task-status]')!;
33
33
  const updateStatus = (): void => {status.textContent = describeTaskStatus(model.get());};
34
34
 
35
- const addTask = (title: string): void => {model.add(title); updateStatus();};
36
- const toggleTask = (id: number): void => {model.toggle(id); updateStatus();};
37
- const setFilter = (filter: 'all' | 'active' | 'completed'): void => {model.setFilter(filter); updateStatus();};
38
- mediator.on('task:add', addTask);
39
- mediator.on('task:toggle', toggleTask);
40
- mediator.on('task:filter', setFilter);
35
+ const addTask = (event: CustomEvent<string>): void => {model.add(event.detail); updateStatus();};
36
+ const toggleTask = (event: CustomEvent<number>): void => {model.toggle(event.detail); updateStatus();};
37
+ const setFilter = (event: CustomEvent<'all' | 'active' | 'completed'>): void => {model.setFilter(event.detail); updateStatus();};
38
+ mediator.addEventListener('task:add', addTask);
39
+ mediator.addEventListener('task:toggle', toggleTask);
40
+ mediator.addEventListener('task:filter', setFilter);
41
41
 
42
42
  const delegated = view.delegate(parentElement);
43
43
  delegated.on('submit', '[data-task-form]', (event: Event) => {
44
44
  event.preventDefault();
45
45
  const form = event.target as HTMLFormElement;
46
46
  const input = form.querySelector<HTMLInputElement>('[name="task"]')!;
47
- mediator.emit('task:add', input.value);
47
+ mediator.dispatchEvent(new CustomEvent('task:add', {detail: input.value}));
48
48
  parentElement.querySelector<HTMLInputElement>('[name="task"]')!.focus();
49
49
  });
50
50
  delegated.on('change', '[data-task-toggle]', (event: Event) => {
51
51
  const input = event.target as HTMLInputElement;
52
52
  const id = Number(input.dataset.taskId);
53
- mediator.emit('task:toggle', id);
53
+ mediator.dispatchEvent(new CustomEvent('task:toggle', {detail: id}));
54
54
  const focusTarget = parentElement.querySelector<HTMLInputElement>(`[data-task-id="${id}"]`)
55
55
  ?? parentElement.querySelector<HTMLAnchorElement>('[data-task-filter][aria-current="page"]')!;
56
56
  focusTarget.focus();
@@ -66,9 +66,9 @@ export function initializeTaskApplication(documentRoot: Document): TaskApplicati
66
66
  destroy() {
67
67
  delegated.clear();
68
68
  router.destroy();
69
- mediator.removeListener('task:add', addTask);
70
- mediator.removeListener('task:toggle', toggleTask);
71
- mediator.removeListener('task:filter', setFilter);
69
+ mediator.removeEventListener('task:add', addTask);
70
+ mediator.removeEventListener('task:toggle', toggleTask);
71
+ mediator.removeEventListener('task:filter', setFilter);
72
72
  view.destroy();
73
73
  model.destroy();
74
74
  mediator.destroy();
@@ -1,20 +1,15 @@
1
1
  import Mediator from 'white-label-mediator';
2
2
  import type {TaskFilter} from '../../view/examples/tasks/task-state.js';
3
3
 
4
- /**
5
- * Document the event protocol in one place even though the currently installed
6
- * Mediator exposes the EventEmitter-compatible runtime without a generic map.
7
- * Keeping names and payloads explicit still gives learners one clear contract
8
- * to follow when tracing the example.
9
- */
4
+ /** Application event vocabulary shared by the generated task modules. */
10
5
  export type TaskEvents = {
11
- 'task:add': [title: string];
12
- 'task:filter': [filter: TaskFilter];
13
- 'task:toggle': [id: number];
6
+ 'task:add': string;
7
+ 'task:filter': TaskFilter;
8
+ 'task:toggle': number;
14
9
  };
15
10
 
16
- export type TaskMediator = Mediator;
11
+ export type TaskMediator = Mediator<TaskEvents>;
17
12
 
18
13
  export function createTaskMediator(): TaskMediator {
19
- return new Mediator().initialize();
14
+ return new Mediator<TaskEvents>().initialize();
20
15
  }
@@ -22,7 +22,7 @@ export function createTaskRouter(documentRoot: Document, mediator: TaskMediator)
22
22
  const route = (_scope: Element | null, location: {data: {query: Record<string, string>}}): void => {
23
23
  const filter = normalizeTaskFilter(location.data.query.tasks);
24
24
  const restoreFocus = documentRoot.activeElement?.hasAttribute('data-task-filter') === true;
25
- mediator.emit('task:filter', filter);
25
+ mediator.dispatchEvent(new CustomEvent('task:filter', {detail: filter}));
26
26
  if (restoreFocus) {documentRoot.querySelector<HTMLAnchorElement>(`[data-task-filter="${filter}"]`)!.focus();}
27
27
  };
28
28
  router.routes = {'/': route, defaultRoute: route};
@@ -3,7 +3,7 @@ import TaskExample from '../../view/examples/tasks/TaskExample.js';
3
3
  import type {TaskState} from '../../view/examples/tasks/task-state.js';
4
4
  import type TaskModel from './TaskModel.js';
5
5
 
6
- /** Create the task view without JSX/TSX syntax. */
6
+ /** Adopt the build-time task root and reuse the same tagged template for model updates. */
7
7
  export function createTaskView(parentElement: HTMLElement, model: TaskModel): View {
8
8
  const element = parentElement.querySelector<HTMLElement>('[data-task-app]');
9
9
  if (!element) {throw new TypeError('Task example requires its static task-app root');}
@@ -0,0 +1,15 @@
1
+ import {html, type HTMLMarkup} from 'white-label-view/html';
2
+
3
+ export const syntax = {
4
+ keyword: 'code-syntax-keyword',
5
+ type: 'code-syntax-type',
6
+ value: 'code-syntax-value',
7
+ muted: 'code-syntax-muted'
8
+ };
9
+
10
+ /** Render a readable code example with stable line numbers and caller-supplied syntax spans. */
11
+ export default function CodeBlock({lines}: {lines: HTMLMarkup[]}) {
12
+ return html`<pre class="code-block"><code>${lines.map((line, index) => html`<span class="code-block__line">
13
+ <span aria-hidden="true" class="code-block__number">${index + 1}</span><span>${line}</span>
14
+ </span>`)}</code></pre>`;
15
+ }
@@ -0,0 +1,50 @@
1
+ import {attributes, html} from 'white-label-view/html';
2
+ import {getVisibleTasks, type TaskFilter, type TaskState} from './task-state.js';
3
+
4
+ const filters: TaskFilter[] = ['all', 'active', 'completed'];
5
+
6
+ /** Render the task example from state for both build-time HTML and browser updates. */
7
+ export default function TaskExample({state}: {state: TaskState}) {
8
+ const visibleTasks = getVisibleTasks(state);
9
+ const completed = state.tasks.filter(task => task.complete).length;
10
+
11
+ return html`<section class="task-app" data-task-app aria-label="Interactive task example">
12
+ <form class="task-form" data-task-form action="#example" method="get">
13
+ <label for="task-title">New task</label>
14
+ <div class="task-form__controls">
15
+ <input id="task-title" name="task" type="text" autocomplete="off" required>
16
+ <button type="submit">Add task</button>
17
+ </div>
18
+ </form>
19
+
20
+ <nav class="task-filters" aria-label="Filter tasks">
21
+ ${filters.map(filter => html`<a ${attributes({
22
+ href: `/?tasks=${filter}#example`,
23
+ 'data-task-filter': filter,
24
+ 'data-pushstate': true,
25
+ 'aria-current': state.filter === filter ? 'page' : 'false'
26
+ })}>${filter.charAt(0).toUpperCase()}${filter.slice(1)}</a>`)}
27
+ </nav>
28
+
29
+ <ul class="task-list">
30
+ ${visibleTasks.map(task => html`<li>
31
+ <label>
32
+ <input ${attributes({
33
+ type: 'checkbox',
34
+ checked: task.complete,
35
+ 'data-task-toggle': true,
36
+ 'data-task-id': task.id
37
+ })}>
38
+ <span>${task.title}</span>
39
+ </label>
40
+ </li>`)}
41
+ </ul>
42
+
43
+ <dl class="demo__trace">
44
+ <div><dt>Router</dt><dd>/?tasks=${state.filter}</dd></div>
45
+ <div><dt>Mediator</dt><dd>task:* application events</dd></div>
46
+ <div><dt>Model</dt><dd>${state.tasks.length} tasks · ${completed} complete</dd></div>
47
+ <div><dt>View</dt><dd>${visibleTasks.length} tasks rendered</dd></div>
48
+ </dl>
49
+ </section>`;
50
+ }
@@ -1,4 +1,4 @@
1
- /** Shared state shape used by build-time JSX and the browser application. */
1
+ /** Shared state shape used by build-time tagged HTML and the browser application. */
2
2
  export type TaskFilter = 'all' | 'active' | 'completed';
3
3
 
4
4
  export interface TaskItem {
@@ -0,0 +1,11 @@
1
+ import {html} from 'white-label-view/html';
2
+
3
+ /** Keep reusable static page chrome as a small tagged-template function. */
4
+ export default function SiteFooter() {
5
+ return html`<footer class="site-footer">
6
+ <div class="container site-footer__inner">
7
+ <p>White Label. Framework-independent TypeScript building blocks.</p>
8
+ <a href="#top">Back to top</a>
9
+ </div>
10
+ </footer>`;
11
+ }
@@ -0,0 +1,17 @@
1
+ import {html} from 'white-label-view/html';
2
+
3
+ /** Static navigation remains ordinary crawlable HTML; JavaScript enhances only opted-in links. */
4
+ export default function SiteHeader() {
5
+ return html`<header class="site-header">
6
+ <div class="container site-header__inner">
7
+ <a class="wordmark" href="#top">White Label</a>
8
+ <nav aria-label="Primary navigation">
9
+ <a href="#example">Example</a>
10
+ <a href="#packages">Packages</a>
11
+ <a href="#source">Source</a>
12
+ <a href="#start">Get started</a>
13
+ <a href="https://github.com/bshack/white-label">GitHub</a>
14
+ </nav>
15
+ </div>
16
+ </header>`;
17
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Build-time data available to every page template.
3
3
  *
4
- * Keeping this contract separate from the page component makes the top-level
5
- * JSX read like application composition instead of deployment plumbing.
4
+ * Keeping this contract separate from the renderer keeps top-level tagged
5
+ * HTML focused on application composition instead of deployment plumbing.
6
6
  */
7
7
  export interface PageData {
8
8
  cdn: string;
@@ -0,0 +1,26 @@
1
+ import {html} from 'white-label-view/html';
2
+ import CodeBlock, {syntax} from '../CodeBlock.js';
3
+
4
+ /** Keep the shortest path from generator install to a verified local build visible. */
5
+ export default function GettingStartedSection() {
6
+ return html`<section class="section" id="start" aria-labelledby="start-title">
7
+ <div class="container start-grid">
8
+ <div class="section-intro">
9
+ <p class="eyebrow">Get started</p>
10
+ <h2 id="start-title">Create a project with the generator.</h2>
11
+ <p>The generated project is static-first, TypeScript-first, accessible by default, and ready for application-specific code.</p>
12
+ </div>
13
+ <div>
14
+ <p class="code-label">Create a project</p>
15
+ ${CodeBlock({lines: [html`<span class="${syntax.type}">npx</span> generator-white-label`]})}
16
+ <p class="code-label">Then develop</p>
17
+ ${CodeBlock({lines: [
18
+ html`<span class="${syntax.type}">npm</span> ci`,
19
+ html`<span class="${syntax.type}">npm</span> test`,
20
+ html`<span class="${syntax.type}">npm</span> run build -- --version=<span class="${syntax.value}">local</span>`
21
+ ]})}
22
+ <p><a href="https://github.com/bshack/white-label#readme">Read the complete generator documentation</a></p>
23
+ </div>
24
+ </div>
25
+ </section>`;
26
+ }
@@ -0,0 +1,15 @@
1
+ import {html} from 'white-label-view/html';
2
+
3
+ /** Static presentation stays a plain tagged-template function; View is only needed for lifecycle/state. */
4
+ export default function HeroSection() {
5
+ return html`<section class="hero" id="top" aria-labelledby="hero-title">
6
+ <div class="container hero__inner">
7
+ <h1 id="hero-title">Small pieces.<br>Complete applications.</h1>
8
+ <p class="hero__lede">Framework-independent TypeScript packages for state, rendering, coordination, routing, and project generation.</p>
9
+ <div class="hero__links">
10
+ <a class="primary-link" href="#example">See the example</a>
11
+ <a href="#start">Install the generator</a>
12
+ </div>
13
+ </div>
14
+ </section>`;
15
+ }
@@ -0,0 +1,24 @@
1
+ import {html} from 'white-label-view/html';
2
+ import TaskExample from '../examples/tasks/TaskExample.js';
3
+ import {createInitialTaskState, describeTaskStatus} from '../examples/tasks/task-state.js';
4
+
5
+ /** Render useful initial HTML with the same tagged template reused by Browser View. */
6
+ export default function LiveExampleSection() {
7
+ const state = createInitialTaskState();
8
+
9
+ return html`<section class="section" id="example" aria-labelledby="example-title">
10
+ <div class="container">
11
+ <div class="section-intro">
12
+ <p class="eyebrow">Live example</p>
13
+ <h2 id="example-title">Four packages. One small application.</h2>
14
+ <p>Add a task, toggle its state, or change the filter. Router translates the URL into intent, Mediator coordinates actions, Model owns state, and View keeps the interface in sync.</p>
15
+ </div>
16
+ <div class="demo" data-task-example>
17
+ ${TaskExample({state})}
18
+ <p class="visually-hidden" role="status" aria-live="polite" aria-atomic="true" data-task-status>
19
+ ${describeTaskStatus(state)}
20
+ </p>
21
+ </div>
22
+ </div>
23
+ </section>`;
24
+ }
@@ -0,0 +1,69 @@
1
+ import {html} from 'white-label-view/html';
2
+ import CodeBlock, {syntax} from '../CodeBlock.js';
3
+
4
+ const repositories = {
5
+ mediator: 'https://github.com/bshack/white-label-mediator',
6
+ model: 'https://github.com/bshack/white-label-model',
7
+ router: 'https://github.com/bshack/white-label-router',
8
+ view: 'https://github.com/bshack/white-label-view'
9
+ };
10
+
11
+ /** Package documentation stays close to the example it describes. */
12
+ export default function PackageDocsSection() {
13
+ return html`<section class="section" id="packages" aria-labelledby="packages-title">
14
+ <div class="container">
15
+ <div class="section-intro">
16
+ <p class="eyebrow">Documentation</p>
17
+ <h2 id="packages-title">The core flow stays explicit.</h2>
18
+ <p>Each package has one job, can be used independently, and stays easy to replace or test because application concerns are not hidden behind a framework.</p>
19
+ </div>
20
+ <div class="architecture"><ol>
21
+ <li><strong>Router</strong><span>turns a URL into application intent</span></li>
22
+ <li><strong>Mediator</strong><span>coordinates that intent between modules</span></li>
23
+ <li><strong>Model</strong><span>stores and publishes application state</span></li>
24
+ <li><strong>View</strong><span>renders the resulting interface</span></li>
25
+ </ol></div>
26
+ <div class="docs-grid">
27
+ <article>
28
+ <p class="eyebrow">Model</p><h3>State is observable, not magical.</h3>
29
+ ${CodeBlock({lines: [
30
+ html`<span class="${syntax.keyword}">const</span> model = <span class="${syntax.keyword}">new</span> <span class="${syntax.type}">Model</span>({count: 0});`,
31
+ html`model.addEventListener(<span class="${syntax.value}">'change'</span>, event =&gt; render(event.detail));`,
32
+ html`model.update({count: 1});`
33
+ ]})}
34
+ <p><a href="${repositories.model}">Model documentation</a></p>
35
+ </article>
36
+ <article>
37
+ <p class="eyebrow">View</p><h3>Tagged HTML is the first-party template syntax.</h3>
38
+ ${CodeBlock({lines: [
39
+ html`<span class="${syntax.keyword}">import</span> {html} from <span class="${syntax.value}">'white-label-view/html'</span>;`,
40
+ html`<span class="${syntax.keyword}">const</span> view = <span class="${syntax.keyword}">new</span> <span class="${syntax.type}">View</span>({`,
41
+ html` model,`,
42
+ html` template: state =&gt; html\`${'<p>'}\${state.count}${'</p>'}\``,
43
+ html`}).initialize();`
44
+ ]})}
45
+ <p><a href="${repositories.view}">View documentation</a></p>
46
+ </article>
47
+ <article>
48
+ <p class="eyebrow">Mediator</p><h3>Modules communicate through events.</h3>
49
+ ${CodeBlock({lines: [
50
+ html`mediator.addEventListener(<span class="${syntax.value}">'counter:increment'</span>, increment);`,
51
+ html`mediator.dispatchEvent(<span class="${syntax.keyword}">new</span> <span class="${syntax.type}">CustomEvent</span>(<span class="${syntax.value}">'counter:increment'</span>));`
52
+ ]})}
53
+ <p><a href="${repositories.mediator}">Mediator documentation</a></p>
54
+ </article>
55
+ <article>
56
+ <p class="eyebrow">Router</p><h3>Routes describe intent.</h3>
57
+ ${CodeBlock({lines: [
58
+ html`router.routes = {`,
59
+ html` <span class="${syntax.value}">'/'</span>: (_scope, location) =&gt; {`,
60
+ html` mediator.dispatchEvent(<span class="${syntax.keyword}">new</span> <span class="${syntax.type}">CustomEvent</span>(<span class="${syntax.value}">'filter:set'</span>, {detail: location.data.query.filter}));`,
61
+ html` }`,
62
+ html`};`
63
+ ]})}
64
+ <p><a href="${repositories.router}">Router documentation</a></p>
65
+ </article>
66
+ </div>
67
+ </div>
68
+ </section>`;
69
+ }
@@ -1,28 +1,23 @@
1
- /**
2
- * Point learners at the implementation itself.
3
- *
4
- * The landing page is intentionally organized so each path answers a different
5
- * question: composition, rendering, state, coordination, routing, or testing.
6
- */
1
+ import {html} from 'white-label-view/html';
2
+
3
+ /** Point learners at the implementation paths that demonstrate each responsibility. */
7
4
  export default function SourceGuideSection() {
8
- return (
9
- <section className="section" id="source" aria-labelledby="source-title">
10
- <div className="container start-grid">
11
- <div className="section-intro">
12
- <p className="eyebrow">Read the source</p>
5
+ return html`<section class="section" id="source" aria-labelledby="source-title">
6
+ <div class="container start-grid">
7
+ <div class="section-intro">
8
+ <p class="eyebrow">Read the source</p>
13
9
  <h2 id="source-title">This site is part of the documentation.</h2>
14
10
  <p>The landing page is built with the same White Label packages it teaches. Start at the page composition, then follow the imports into the interactive example.</p>
15
11
  </div>
16
- <div className="source-paths">
12
+ <div class="source-paths">
17
13
  <dl>
18
- <div><dt>Page composition</dt><dd><code>app/index.tsx</code></dd></div>
19
- <div><dt>Static JSX views</dt><dd><code>app/assets/view/</code></dd></div>
14
+ <div><dt>Page composition</dt><dd><code>app/index.ts</code></dd></div>
15
+ <div><dt>Tagged HTML views</dt><dd><code>app/assets/view/</code></dd></div>
20
16
  <div><dt>Task application</dt><dd><code>app/assets/script/tasks/</code></dd></div>
21
17
  <div><dt>Integration tests</dt><dd><code>test/app.test.js</code></dd></div>
22
18
  </dl>
23
19
  <p><a href="https://github.com/bshack/white-label/tree/master/app">Browse the application source</a></p>
24
20
  </div>
25
21
  </div>
26
- </section>
27
- );
22
+ </section>`;
28
23
  }
package/app/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import {html, unsafeHTML} from 'white-label-view/html';
2
+ import SiteFooter from './assets/view/layout/SiteFooter.js';
3
+ import SiteHeader from './assets/view/layout/SiteHeader.js';
4
+ import type {PageData} from './assets/view/page-data.js';
5
+ import GettingStartedSection from './assets/view/sections/GettingStartedSection.js';
6
+ import HeroSection from './assets/view/sections/HeroSection.js';
7
+ import LiveExampleSection from './assets/view/sections/LiveExampleSection.js';
8
+ import PackageDocsSection from './assets/view/sections/PackageDocsSection.js';
9
+ import SourceGuideSection from './assets/view/sections/SourceGuideSection.js';
10
+
11
+ function structuredDataScript(value: unknown) {
12
+ const json = JSON.stringify(value).replaceAll('<', '\\u003c');
13
+ return unsafeHTML(`<script type="application/ld+json">${json}</script>`);
14
+ }
15
+
16
+ /** Compose the static page from small tagged-template functions. */
17
+ export default function IndexPage(data: Record<string, unknown>) {
18
+ const page = data as unknown as PageData;
19
+
20
+ return html`<html id="index" lang="en-US" dir="ltr">
21
+ <head>
22
+ <meta charset="utf-8">
23
+ <meta name="viewport" content="width=device-width, initial-scale=1">
24
+ <title>${page.title}</title>
25
+ <meta name="description" content="${page.meta.description}">
26
+ <meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1">
27
+ <link rel="canonical" href="${page.siteUrl}/">
28
+ <meta property="og:title" content="${page.title}">
29
+ <meta property="og:description" content="${page.meta.description}">
30
+ <meta property="og:type" content="website">
31
+ <meta property="og:url" content="${page.siteUrl}/">
32
+ <link rel="stylesheet" href="${page.cdn}release/${page.version}/assets/style/global.css" media="all">
33
+ <link rel="stylesheet" href="${page.cdn}release/${page.version}/assets/style/print.css" media="print">
34
+ ${structuredDataScript(page.structuredData)}
35
+ </head>
36
+ <body itemscope itemtype="https://schema.org/WebPage">
37
+ <a class="skip-link" href="#main">Skip to content</a>
38
+ ${SiteHeader()}
39
+ <main id="main" tabindex="-1">
40
+ ${HeroSection()}
41
+ ${LiveExampleSection()}
42
+ ${PackageDocsSection()}
43
+ ${SourceGuideSection()}
44
+ ${GettingStartedSection()}
45
+ </main>
46
+ ${SiteFooter()}
47
+ <script src="${page.cdn}release/${page.version}/assets/script/index.compiled.js" defer></script>
48
+ </body>
49
+ </html>`;
50
+ }