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.
- package/CLI.md +16 -20
- package/EXISTING_APPLICATIONS.md +155 -0
- package/PACKAGE_MANAGERS.md +6 -4
- package/README.md +136 -126
- package/app/404.ts +28 -0
- package/app/README.md +45 -7
- package/{scaffold/no-jsx/app → app}/assets/script/index.ts +1 -0
- package/app/assets/script/tasks/TaskApplication.ts +11 -11
- package/app/assets/script/tasks/TaskMediator.ts +6 -11
- package/app/assets/script/tasks/TaskRouter.ts +1 -1
- package/{scaffold/no-jsx/app → app}/assets/script/tasks/TaskView.ts +1 -1
- package/app/assets/view/CodeBlock.ts +15 -0
- package/app/assets/view/examples/tasks/TaskExample.ts +50 -0
- package/app/assets/view/examples/tasks/task-state.ts +1 -1
- package/app/assets/view/layout/SiteFooter.ts +11 -0
- package/app/assets/view/layout/SiteHeader.ts +17 -0
- package/app/assets/view/page-data.ts +2 -2
- package/app/assets/view/sections/GettingStartedSection.ts +26 -0
- package/app/assets/view/sections/HeroSection.ts +15 -0
- package/app/assets/view/sections/LiveExampleSection.ts +24 -0
- package/app/assets/view/sections/PackageDocsSection.ts +69 -0
- package/app/assets/view/sections/{SourceGuideSection.tsx → SourceGuideSection.ts} +11 -16
- package/app/index.ts +50 -0
- package/cli/index.ts +6 -42
- package/dist/app/404.d.ts +1 -1
- package/dist/app/404.js +18 -2
- package/dist/app/404.js.map +1 -1
- package/dist/app/assets/script/index.d.ts +1 -6
- package/dist/app/assets/script/index.js +1 -6
- package/dist/app/assets/script/index.js.map +1 -1
- package/dist/app/assets/script/tasks/TaskApplication.js +11 -11
- package/dist/app/assets/script/tasks/TaskApplication.js.map +1 -1
- package/dist/app/assets/script/tasks/TaskMediator.d.ts +5 -10
- package/dist/app/assets/script/tasks/TaskMediator.js.map +1 -1
- package/dist/app/assets/script/tasks/TaskRouter.js +1 -1
- package/dist/app/assets/script/tasks/TaskRouter.js.map +1 -1
- package/dist/app/assets/script/tasks/TaskView.d.ts +1 -6
- package/dist/app/assets/script/tasks/TaskView.js +2 -8
- package/dist/app/assets/script/tasks/TaskView.js.map +1 -1
- package/dist/app/assets/view/CodeBlock.d.ts +3 -3
- package/dist/app/assets/view/CodeBlock.js +4 -2
- package/dist/app/assets/view/CodeBlock.js.map +1 -1
- package/dist/app/assets/view/examples/tasks/TaskExample.d.ts +2 -8
- package/dist/app/assets/view/examples/tasks/TaskExample.js +41 -9
- package/dist/app/assets/view/examples/tasks/TaskExample.js.map +1 -1
- package/dist/app/assets/view/examples/tasks/task-state.d.ts +1 -1
- package/dist/app/assets/view/examples/tasks/task-state.js.map +1 -1
- package/dist/app/assets/view/layout/SiteFooter.d.ts +2 -2
- package/dist/app/assets/view/layout/SiteFooter.js +8 -3
- package/dist/app/assets/view/layout/SiteFooter.js.map +1 -1
- package/dist/app/assets/view/layout/SiteHeader.d.ts +2 -8
- package/dist/app/assets/view/layout/SiteHeader.js +14 -9
- package/dist/app/assets/view/layout/SiteHeader.js.map +1 -1
- package/dist/app/assets/view/page-data.d.ts +2 -2
- package/dist/app/assets/view/page-data.js.map +1 -1
- package/dist/app/assets/view/sections/GettingStartedSection.d.ts +1 -1
- package/dist/app/assets/view/sections/GettingStartedSection.js +21 -6
- package/dist/app/assets/view/sections/GettingStartedSection.js.map +1 -1
- package/dist/app/assets/view/sections/HeroSection.d.ts +2 -8
- package/dist/app/assets/view/sections/HeroSection.js +12 -9
- package/dist/app/assets/view/sections/HeroSection.js.map +1 -1
- package/dist/app/assets/view/sections/LiveExampleSection.d.ts +2 -6
- package/dist/app/assets/view/sections/LiveExampleSection.js +17 -7
- package/dist/app/assets/view/sections/LiveExampleSection.js.map +1 -1
- package/dist/app/assets/view/sections/PackageDocsSection.d.ts +1 -1
- package/dist/app/assets/view/sections/PackageDocsSection.js +57 -20
- package/dist/app/assets/view/sections/PackageDocsSection.js.map +1 -1
- package/dist/app/assets/view/sections/SourceGuideSection.d.ts +2 -7
- package/dist/app/assets/view/sections/SourceGuideSection.js +20 -8
- package/dist/app/assets/view/sections/SourceGuideSection.js.map +1 -1
- package/dist/app/index.d.ts +2 -8
- package/dist/app/index.js +36 -11
- package/dist/app/index.js.map +1 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.js +6 -37
- package/dist/cli/index.js.map +1 -1
- package/dist/scaffold/index.d.ts +2 -13
- package/dist/scaffold/index.js +36 -56
- package/dist/scaffold/index.js.map +1 -1
- package/dist/scripts/build.js +97 -28
- package/dist/scripts/build.js.map +1 -1
- package/dist/server/handler.d.ts +7 -0
- package/dist/server/handler.js +60 -0
- package/dist/server/handler.js.map +1 -0
- package/package.json +25 -24
- package/pnpm-workspace.yaml +0 -1
- package/scaffold/AGENTS.md +23 -0
- package/scaffold/index.ts +32 -60
- package/scripts/build.ts +90 -30
- package/server/handler.ts +2 -10
- package/template-test/site.test.js +3 -1
- package/tsconfig.site.json +1 -4
- package/app/404.tsx +0 -28
- package/app/assets/script/index.tsx +0 -13
- package/app/assets/script/tasks/TaskView.tsx +0 -24
- package/app/assets/view/CodeBlock.tsx +0 -17
- package/app/assets/view/examples/tasks/TaskExample.tsx +0 -63
- package/app/assets/view/layout/SiteFooter.tsx +0 -11
- package/app/assets/view/layout/SiteHeader.tsx +0 -23
- package/app/assets/view/sections/GettingStartedSection.tsx +0 -27
- package/app/assets/view/sections/HeroSection.tsx +0 -21
- package/app/assets/view/sections/LiveExampleSection.tsx +0 -29
- package/app/assets/view/sections/PackageDocsSection.tsx +0 -69
- package/app/index.tsx +0 -54
- package/scaffold/no-jsx/README.md +0 -47
- package/scaffold/no-jsx/app/404.ts +0 -10
- package/scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts +0 -39
- package/scaffold/no-jsx/app/index.ts +0 -58
- 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
|
|
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
|
-
|
|
5
|
+
The rendering path is intentionally small and explicit:
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = (
|
|
36
|
-
const toggleTask = (
|
|
37
|
-
const setFilter = (
|
|
38
|
-
mediator.
|
|
39
|
-
mediator.
|
|
40
|
-
mediator.
|
|
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.
|
|
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.
|
|
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.
|
|
70
|
-
mediator.
|
|
71
|
-
mediator.
|
|
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':
|
|
12
|
-
'task:filter':
|
|
13
|
-
'task:toggle':
|
|
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.
|
|
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
|
-
/**
|
|
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
|
+
}
|
|
@@ -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
|
|
5
|
-
*
|
|
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 => 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 => 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) => {`,
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
12
|
+
<div class="source-paths">
|
|
17
13
|
<dl>
|
|
18
|
-
<div><dt>Page composition</dt><dd><code>app/index.
|
|
19
|
-
<div><dt>
|
|
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
|
+
}
|