generator-white-label 5.0.1 → 9.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/.editorconfig +15 -0
- package/.yarnrc.yml +6 -0
- package/CLI.md +82 -0
- package/LICENSE +9 -0
- package/PACKAGE_MANAGERS.md +35 -0
- package/README.md +232 -124
- package/app/404.tsx +28 -0
- package/app/README.md +92 -8
- package/app/assets/data/view/global.json +7 -47
- package/app/assets/data/view/index.json +9 -1
- package/app/assets/script/index.tsx +13 -0
- package/app/assets/script/tasks/TaskApplication.ts +77 -0
- package/app/assets/script/tasks/TaskMediator.ts +15 -0
- package/app/assets/script/tasks/TaskModel.ts +32 -0
- package/app/assets/script/tasks/TaskRouter.ts +30 -0
- package/app/assets/script/tasks/TaskView.tsx +24 -0
- package/app/assets/style/global.css +111 -0
- package/app/assets/style/print.css +8 -0
- package/app/assets/view/CodeBlock.tsx +17 -0
- package/app/assets/view/examples/tasks/TaskExample.tsx +63 -0
- package/app/assets/view/examples/tasks/task-state.ts +46 -0
- package/app/assets/view/layout/SiteFooter.tsx +11 -0
- package/app/assets/view/layout/SiteHeader.tsx +23 -0
- package/app/assets/view/page-data.ts +14 -0
- package/app/assets/view/sections/GettingStartedSection.tsx +27 -0
- package/app/assets/view/sections/HeroSection.tsx +21 -0
- package/app/assets/view/sections/LiveExampleSection.tsx +29 -0
- package/app/assets/view/sections/PackageDocsSection.tsx +69 -0
- package/app/assets/view/sections/SourceGuideSection.tsx +28 -0
- package/app/index.tsx +54 -0
- package/cli/index.ts +118 -0
- package/dist/app/404.d.ts +1 -0
- package/dist/app/404.js +6 -0
- package/dist/app/404.js.map +1 -0
- package/dist/app/assets/script/index.d.ts +8 -2
- package/dist/app/assets/script/index.js +11 -1
- package/dist/app/assets/script/index.js.map +1 -1
- package/dist/app/assets/script/tasks/TaskApplication.d.ts +20 -0
- package/dist/app/assets/script/tasks/TaskApplication.js +63 -0
- package/dist/app/assets/script/tasks/TaskApplication.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskMediator.d.ts +10 -0
- package/dist/app/assets/script/tasks/TaskMediator.js +5 -0
- package/dist/app/assets/script/tasks/TaskMediator.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskModel.d.ts +13 -0
- package/dist/app/assets/script/tasks/TaskModel.js +33 -0
- package/dist/app/assets/script/tasks/TaskModel.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskRouter.d.ts +12 -0
- package/dist/app/assets/script/tasks/TaskRouter.js +28 -0
- package/dist/app/assets/script/tasks/TaskRouter.js.map +1 -0
- package/dist/app/assets/script/tasks/TaskView.d.ts +9 -0
- package/dist/app/assets/script/tasks/TaskView.js +24 -0
- package/dist/app/assets/script/tasks/TaskView.js.map +1 -0
- package/dist/app/assets/view/CodeBlock.d.ts +11 -0
- package/dist/app/assets/view/CodeBlock.js +12 -0
- package/dist/app/assets/view/CodeBlock.js.map +1 -0
- package/dist/app/assets/view/examples/tasks/TaskExample.d.ts +11 -0
- package/dist/app/assets/view/examples/tasks/TaskExample.js +16 -0
- package/dist/app/assets/view/examples/tasks/TaskExample.js.map +1 -0
- package/dist/app/assets/view/examples/tasks/task-state.d.ts +22 -0
- package/dist/app/assets/view/examples/tasks/task-state.js +30 -0
- package/dist/app/assets/view/examples/tasks/task-state.js.map +1 -0
- package/dist/app/assets/view/layout/SiteFooter.d.ts +2 -0
- package/dist/app/assets/view/layout/SiteFooter.js +6 -0
- package/dist/app/assets/view/layout/SiteFooter.js.map +1 -0
- package/dist/app/assets/view/layout/SiteHeader.d.ts +8 -0
- package/dist/app/assets/view/layout/SiteHeader.js +12 -0
- package/dist/app/assets/view/layout/SiteHeader.js.map +1 -0
- package/dist/app/assets/view/page-data.d.ts +16 -0
- package/dist/app/assets/view/page-data.js +2 -0
- package/dist/app/assets/view/page-data.js.map +1 -0
- package/dist/app/assets/view/sections/GettingStartedSection.d.ts +2 -0
- package/dist/app/assets/view/sections/GettingStartedSection.js +11 -0
- package/dist/app/assets/view/sections/GettingStartedSection.js.map +1 -0
- package/dist/app/assets/view/sections/HeroSection.d.ts +8 -0
- package/dist/app/assets/view/sections/HeroSection.js +12 -0
- package/dist/app/assets/view/sections/HeroSection.js.map +1 -0
- package/dist/app/assets/view/sections/LiveExampleSection.d.ts +6 -0
- package/dist/app/assets/view/sections/LiveExampleSection.js +13 -0
- package/dist/app/assets/view/sections/LiveExampleSection.js.map +1 -0
- package/dist/app/assets/view/sections/PackageDocsSection.d.ts +2 -0
- package/dist/app/assets/view/sections/PackageDocsSection.js +31 -0
- package/dist/app/assets/view/sections/PackageDocsSection.js.map +1 -0
- package/dist/app/assets/view/sections/SourceGuideSection.d.ts +7 -0
- package/dist/app/assets/view/sections/SourceGuideSection.js +11 -0
- package/dist/app/assets/view/sections/SourceGuideSection.js.map +1 -0
- package/dist/app/index.d.ts +8 -0
- package/dist/app/index.js +22 -0
- package/dist/app/index.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.js +98 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/scaffold/index.d.ts +64 -0
- package/dist/scaffold/index.js +125 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scripts/build.d.ts +5 -13
- package/dist/scripts/build.js +48 -79
- package/dist/scripts/build.js.map +1 -1
- package/dist/server/handler.d.ts +7 -0
- package/dist/server/handler.js +67 -0
- package/dist/server/handler.js.map +1 -0
- package/package.json +78 -40
- package/pnpm-workspace.yaml +4 -0
- package/scaffold/AGENTS.md +18 -0
- package/scaffold/index.ts +139 -0
- package/scaffold/no-jsx/README.md +73 -0
- package/scaffold/no-jsx/app/404.ts +10 -0
- package/scaffold/no-jsx/app/assets/script/index.ts +7 -0
- package/scaffold/no-jsx/app/assets/script/tasks/TaskView.ts +19 -0
- package/scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts +39 -0
- package/scaffold/no-jsx/app/index.ts +58 -0
- package/scripts/build.ts +55 -86
- package/server/handler.ts +75 -0
- package/template-test/site.test.js +235 -0
- package/tsconfig.site.json +6 -5
- package/tsconfig.site.no-jsx.json +24 -0
- package/app/404.hbs +0 -20
- package/app/assets/data/coutries.json +0 -992
- package/app/assets/data/states.json +0 -204
- package/app/assets/data/view/toolkit/colors.json +0 -17
- package/app/assets/data/view/toolkit/components.json +0 -39
- package/app/assets/data/view/toolkit/elements.json +0 -119
- package/app/assets/data/view/toolkit/grids.json +0 -17
- package/app/assets/data/view/toolkit/icons.json +0 -17
- package/app/assets/data/view/toolkit/index.json +0 -17
- package/app/assets/data/view/toolkit/type.json +0 -17
- package/app/assets/font/opensans/LICENSE.txt +0 -202
- package/app/assets/font/opensans/opensans-bold-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-bolditalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-extrabold-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-extrabolditalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-italic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-light-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-lightitalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-regular-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-semibold-webfont.woff2 +0 -0
- package/app/assets/font/opensans/opensans-semibolditalic-webfont.woff2 +0 -0
- package/app/assets/font/opensans/stylesheet.css +0 -120
- package/app/assets/image/apple-touch-icon.png +0 -0
- package/app/assets/image/demo/fallback.avif +0 -0
- package/app/assets/image/demo/fallback.jpg +0 -0
- package/app/assets/image/demo/fallback.webp +0 -0
- package/app/assets/image/demo/large.avif +0 -0
- package/app/assets/image/demo/large.webp +0 -0
- package/app/assets/image/demo/small.avif +0 -0
- package/app/assets/image/demo/small.webp +0 -0
- package/app/assets/image/favicon.png +0 -0
- package/app/assets/image/graphic-css3.svg +0 -30
- package/app/assets/image/graphic-html5.svg +0 -29
- package/app/assets/markup/element/a.hbs +0 -29
- package/app/assets/markup/element/button.hbs +0 -30
- package/app/assets/markup/element/div.hbs +0 -11
- package/app/assets/markup/element/head.hbs +0 -78
- package/app/assets/markup/element/iframe.hbs +0 -20
- package/app/assets/markup/element/img.hbs +0 -30
- package/app/assets/markup/element/input.hbs +0 -43
- package/app/assets/markup/element/label.hbs +0 -14
- package/app/assets/markup/element/legend.hbs +0 -5
- package/app/assets/markup/element/meta.hbs +0 -11
- package/app/assets/markup/element/nav.hbs +0 -13
- package/app/assets/markup/element/p.hbs +0 -8
- package/app/assets/markup/element/picture.hbs +0 -58
- package/app/assets/markup/element/script.hbs +0 -3
- package/app/assets/markup/element/select.hbs +0 -35
- package/app/assets/markup/element/span.hbs +0 -8
- package/app/assets/markup/element/svg.hbs +0 -11
- package/app/assets/markup/element/table.hbs +0 -50
- package/app/assets/markup/element/textarea.hbs +0 -24
- package/app/assets/markup/element/ul.hbs +0 -13
- package/app/assets/markup/modal/modal-example-1.handlebars +0 -8
- package/app/assets/markup/toolkit/accessibility-skip-to-element-1.handlebars +0 -1
- package/app/assets/markup/toolkit/accessibility-speaker-1.handlebars +0 -1
- package/app/assets/markup/toolkit/anchor-1.handlebars +0 -1
- package/app/assets/markup/toolkit/button-1.handlebars +0 -1
- package/app/assets/markup/toolkit/footer-1.handlebars +0 -3
- package/app/assets/markup/toolkit/header-1.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-1.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-2.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-3.handlebars +0 -1
- package/app/assets/markup/toolkit/heading-4.handlebars +0 -1
- package/app/assets/markup/toolkit/image-1.handlebars +0 -1
- package/app/assets/markup/toolkit/input-checkbox-1.handlebars +0 -1
- package/app/assets/markup/toolkit/input-error-1.handlebars +0 -8
- package/app/assets/markup/toolkit/input-radio-1.handlebars +0 -1
- package/app/assets/markup/toolkit/input-text-1.handlebars +0 -1
- package/app/assets/markup/toolkit/label-1.handlebars +0 -1
- package/app/assets/markup/toolkit/legend-1.handlebars +0 -1
- package/app/assets/markup/toolkit/list-1.handlebars +0 -1
- package/app/assets/markup/toolkit/modal-1.handlebars +0 -4
- package/app/assets/markup/toolkit/nav-1.handlebars +0 -1
- package/app/assets/markup/toolkit/picture-1.handlebars +0 -1
- package/app/assets/markup/toolkit/select-1.handlebars +0 -1
- package/app/assets/markup/toolkit/table-1.handlebars +0 -1
- package/app/assets/markup/toolkit/text-1.handlebars +0 -1
- package/app/assets/markup/toolkit/textarea-1.handlebars +0 -1
- package/app/assets/markup/toolkit/youtube-player-1.handlebars +0 -1
- package/app/assets/script/api/facebook.ts +0 -41
- package/app/assets/script/api/youtube.ts +0 -22
- package/app/assets/script/index.ts +0 -2
- package/app/assets/script/mediator/global.ts +0 -4
- package/app/assets/script/model/singletons/countries.ts +0 -5
- package/app/assets/script/model/singletons/global.ts +0 -5
- package/app/assets/script/model/singletons/states.ts +0 -5
- package/app/assets/script/model/user.ts +0 -4
- package/app/assets/script/toolkit.ts +0 -11
- package/app/assets/script/types.d.ts +0 -6
- package/app/assets/script/utility/ajax.ts +0 -15
- package/app/assets/script/utility/form.ts +0 -119
- package/app/assets/script/utility/regex.ts +0 -26
- package/app/assets/script/utility/share.ts +0 -77
- package/app/assets/script/utility/string.ts +0 -148
- package/app/assets/script/view/default.tsx +0 -14
- package/app/assets/script/view/toolkit/youtube-player-1.ts +0 -33
- package/app/assets/style/base/_toolkit.scss +0 -39
- package/app/assets/style/base/colors/_colors.scss +0 -18
- package/app/assets/style/base/components/_modal-1.scss +0 -80
- package/app/assets/style/base/components/_sticky-1.scss +0 -19
- package/app/assets/style/base/components/_tabpanel-1.scss +0 -115
- package/app/assets/style/base/components/_tabpanel-2.scss +0 -64
- package/app/assets/style/base/components/_tabpanel-base-1.scss +0 -13
- package/app/assets/style/base/components/_youtube-player-1.scss +0 -16
- package/app/assets/style/base/elements/_anchors.scss +0 -27
- package/app/assets/style/base/elements/_buttons.scss +0 -25
- package/app/assets/style/base/elements/_fieldsets.scss +0 -9
- package/app/assets/style/base/elements/_images.scss +0 -8
- package/app/assets/style/base/elements/_inputs.scss +0 -78
- package/app/assets/style/base/elements/_labels.scss +0 -16
- package/app/assets/style/base/elements/_landmarks.scss +0 -3
- package/app/assets/style/base/elements/_legends.scss +0 -11
- package/app/assets/style/base/elements/_lists.scss +0 -15
- package/app/assets/style/base/elements/_pictures.scss +0 -4
- package/app/assets/style/base/elements/_tables.scss +0 -39
- package/app/assets/style/base/layouts/_staggered-1.scss +0 -29
- package/app/assets/style/base/type/_fonts.scss +0 -125
- package/app/assets/style/base/type/_headings.scss +0 -36
- package/app/assets/style/base/type/_text.scss +0 -9
- package/app/assets/style/base/utilities/_accessibility.scss +0 -55
- package/app/assets/style/base/utilities/_functions.scss +0 -98
- package/app/assets/style/base/utilities/_scroll.scss +0 -11
- package/app/assets/style/base/utilities/_type.scss +0 -13
- package/app/assets/style/global.scss +0 -123
- package/app/assets/style/print.scss +0 -6
- package/app/assets/style/toolkit.scss +0 -387
- package/app/index.hbs +0 -21
- package/app/robots.txt +0 -1
- package/app/toolkit/colors.hbs +0 -102
- package/app/toolkit/components.hbs +0 -261
- package/app/toolkit/elements.hbs +0 -497
- package/app/toolkit/grids.hbs +0 -413
- package/app/toolkit/icons.hbs +0 -87
- package/app/toolkit/index.hbs +0 -144
- package/app/toolkit/type.hbs +0 -120
- package/dist/app/assets/data/coutries.json +0 -992
- package/dist/app/assets/data/states.json +0 -204
- package/dist/app/assets/script/api/facebook.d.ts +0 -8
- package/dist/app/assets/script/api/facebook.js +0 -42
- package/dist/app/assets/script/api/facebook.js.map +0 -1
- package/dist/app/assets/script/api/youtube.d.ts +0 -8
- package/dist/app/assets/script/api/youtube.js +0 -22
- package/dist/app/assets/script/api/youtube.js.map +0 -1
- package/dist/app/assets/script/mediator/global.d.ts +0 -4
- package/dist/app/assets/script/mediator/global.js +0 -4
- package/dist/app/assets/script/mediator/global.js.map +0 -1
- package/dist/app/assets/script/model/singletons/countries.d.ts +0 -4
- package/dist/app/assets/script/model/singletons/countries.js +0 -5
- package/dist/app/assets/script/model/singletons/countries.js.map +0 -1
- package/dist/app/assets/script/model/singletons/global.d.ts +0 -4
- package/dist/app/assets/script/model/singletons/global.js +0 -5
- package/dist/app/assets/script/model/singletons/global.js.map +0 -1
- package/dist/app/assets/script/model/singletons/states.d.ts +0 -4
- package/dist/app/assets/script/model/singletons/states.js +0 -5
- package/dist/app/assets/script/model/singletons/states.js.map +0 -1
- package/dist/app/assets/script/model/user.d.ts +0 -4
- package/dist/app/assets/script/model/user.js +0 -6
- package/dist/app/assets/script/model/user.js.map +0 -1
- package/dist/app/assets/script/toolkit.d.ts +0 -1
- package/dist/app/assets/script/toolkit.js +0 -7
- package/dist/app/assets/script/toolkit.js.map +0 -1
- package/dist/app/assets/script/utility/ajax.d.ts +0 -8
- package/dist/app/assets/script/utility/ajax.js +0 -17
- package/dist/app/assets/script/utility/ajax.js.map +0 -1
- package/dist/app/assets/script/utility/form.d.ts +0 -64
- package/dist/app/assets/script/utility/form.js +0 -111
- package/dist/app/assets/script/utility/form.js.map +0 -1
- package/dist/app/assets/script/utility/regex.d.ts +0 -14
- package/dist/app/assets/script/utility/regex.js +0 -27
- package/dist/app/assets/script/utility/regex.js.map +0 -1
- package/dist/app/assets/script/utility/share.d.ts +0 -68
- package/dist/app/assets/script/utility/share.js +0 -81
- package/dist/app/assets/script/utility/share.js.map +0 -1
- package/dist/app/assets/script/utility/string.d.ts +0 -64
- package/dist/app/assets/script/utility/string.js +0 -135
- package/dist/app/assets/script/utility/string.js.map +0 -1
- package/dist/app/assets/script/view/default.d.ts +0 -11
- package/dist/app/assets/script/view/default.js +0 -15
- package/dist/app/assets/script/view/default.js.map +0 -1
- package/dist/app/assets/script/view/toolkit/youtube-player-1.d.ts +0 -23
- package/dist/app/assets/script/view/toolkit/youtube-player-1.js +0 -34
- package/dist/app/assets/script/view/toolkit/youtube-player-1.js.map +0 -1
- package/dist/generators/app/index.d.ts +0 -18
- package/dist/generators/app/index.js +0 -59
- package/dist/generators/app/index.js.map +0 -1
- package/generators/app/index.js +0 -2
- package/generators/app/index.ts +0 -61
package/app/README.md
CHANGED
|
@@ -1,19 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# White Label starter application
|
|
2
2
|
|
|
3
|
-
This
|
|
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.
|
|
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.
|
|
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.
|
|
4
8
|
|
|
5
9
|
## Start developing
|
|
6
10
|
|
|
7
|
-
Use Node.js
|
|
11
|
+
Use Node.js `^22.18.0` or `>=24.11.0`, matching the generated `package.json`. npm, Yarn, and pnpm are supported.
|
|
12
|
+
|
|
13
|
+
With npm:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install
|
|
17
|
+
npm test
|
|
18
|
+
npm run build -- --version=local
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With Yarn:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
yarn install
|
|
25
|
+
yarn test
|
|
26
|
+
yarn build --version=local
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
With pnpm:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm test
|
|
34
|
+
pnpm build -- --version=local
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then preview the generated output:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
python3 -m http.server 8080 --directory _deploy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Open `http://localhost:8080/`.
|
|
44
|
+
|
|
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.
|
|
46
|
+
|
|
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.
|
|
48
|
+
|
|
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.
|
|
50
|
+
|
|
51
|
+
## Simple View click event
|
|
52
|
+
|
|
53
|
+
View lifecycle hooks are the simplest place to own a browser listener:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import View from 'white-label-view';
|
|
57
|
+
|
|
58
|
+
class ButtonView extends View {
|
|
59
|
+
handleClick = () => {
|
|
60
|
+
console.log('Clicked');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
addListeners() {
|
|
64
|
+
this.element.addEventListener('click', this.handleClick);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeListeners() {
|
|
69
|
+
this.element.removeEventListener('click', this.handleClick);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The same callback reference is used for both registration and cleanup. View calls `removeListeners()` before replacement or destruction.
|
|
76
|
+
|
|
77
|
+
## Serverless / function runtimes
|
|
78
|
+
|
|
79
|
+
`server/handler.ts` demonstrates a cloud-agnostic server function using the Web `Request` and `Response` APIs. It composes a request-scoped Mediator, Model, Router, and server View, then destroys those mutable instances before the invocation completes.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import {handleRequest} from './dist/server/handler.js';
|
|
83
|
+
|
|
84
|
+
const response = await handleRequest(new Request('https://example.com/hello?name=Ada'));
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Keep immutable configuration at module scope when useful, but create mutable Model/View/Router/Mediator instances per request unless their shared lifetime is intentional. Serverless platforms may reuse one process for many warm invocations, so module-level mutable application state can leak data between requests.
|
|
88
|
+
|
|
89
|
+
Cloud adapters should stay thin: translate the provider's event/request into a Web `Request`, call `handleRequest`, then translate the resulting `Response` if the platform requires it. The starter does not depend on AWS, Vercel, Netlify, Cloudflare, Azure, or another provider SDK.
|
|
90
|
+
|
|
91
|
+
The portability test bundles the server-side composition with a browser/Web target to catch unresolved Node built-ins. That is an edge-portability smoke test, not a claim that every edge provider is supported; verify the actual provider runtime before deployment.
|
|
8
92
|
|
|
9
|
-
|
|
93
|
+
`app/assets/style/global.css` imports Tailwind and contains the starter's custom theme styles. `print.css` remains plain CSS. Tailwind scans the project source during the build and emits static CSS; there is no browser-side Tailwind runtime.
|
|
10
94
|
|
|
11
|
-
|
|
95
|
+
For production, pass a version and HTTPS site URL through your package manager's normal script-argument syntax. For example with npm:
|
|
12
96
|
|
|
13
97
|
```sh
|
|
14
|
-
npm run build -- --version=release-1 --production=true
|
|
98
|
+
npm run build -- --version=release-1 --production=true --site-url=https://www.example.com
|
|
15
99
|
```
|
|
16
100
|
|
|
17
|
-
|
|
101
|
+
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`.
|
|
18
102
|
|
|
19
|
-
|
|
103
|
+
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.
|
|
@@ -1,49 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"apple-touch-icon-precomposed":"apple-touch-icon.png",
|
|
10
|
-
"author":"https://plus.google.com/",
|
|
11
|
-
"favicon":"favicon.png"
|
|
12
|
-
},
|
|
13
|
-
"twitter":{
|
|
14
|
-
"card":"summary",
|
|
15
|
-
"url":"url here",
|
|
16
|
-
"title":"title here",
|
|
17
|
-
"description":"description here",
|
|
18
|
-
"image":"apple-touch-icon.png"
|
|
19
|
-
},
|
|
20
|
-
"og":{
|
|
21
|
-
"title":"title here",
|
|
22
|
-
"type":"website",
|
|
23
|
-
"url":"url here",
|
|
24
|
-
"image":"apple-touch-icon.png",
|
|
25
|
-
"site_name":"site here",
|
|
26
|
-
"description":"description here",
|
|
27
|
-
"locale":"en_US"
|
|
28
|
-
},
|
|
29
|
-
"fb":{
|
|
30
|
-
"admins":"admins here",
|
|
31
|
-
"app_id":"app id here"
|
|
32
|
-
},
|
|
33
|
-
"metrics":{
|
|
34
|
-
"google-analytics":{
|
|
35
|
-
"id":"UA-XXXXXX-X"
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"styles":{
|
|
39
|
-
"all":{
|
|
40
|
-
"global":"assets/style/global.css"
|
|
41
|
-
},
|
|
42
|
-
"print":{
|
|
43
|
-
"global":"assets/style/print.css"
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"scripts":{
|
|
47
|
-
"global":"assets/script/global.compiled.js"
|
|
48
|
-
}
|
|
2
|
+
"title": "White Label | TypeScript Web Starter",
|
|
3
|
+
"meta": {"description": "A production-minded TypeScript web starter demonstrating static rendering, progressive enhancement, accessible UI, routing, state, and reusable White Label packages.", "apple-mobile-web-app-title": "White Label"},
|
|
4
|
+
"link": {},
|
|
5
|
+
"twitter": {"card": "summary", "title": "White Label", "description": "A modular TypeScript web stack and starter generator."},
|
|
6
|
+
"og": {"title": "White Label", "type": "website", "site_name": "White Label", "description": "A modular TypeScript web stack and starter generator.", "locale": "en_US"},
|
|
7
|
+
"styles": {"all": {"global": "assets/style/global.css"}, "print": {"global": "assets/style/print.css"}},
|
|
8
|
+
"scripts": {"global": "assets/script/global.compiled.js"}
|
|
49
9
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"title": "
|
|
2
|
+
"title": "White Label | TypeScript Web Starter",
|
|
3
3
|
"scripts":{
|
|
4
4
|
"global":"assets/script/global.compiled.js",
|
|
5
5
|
"section":"assets/script/index.compiled.js"
|
|
6
|
+
},
|
|
7
|
+
"structuredData": {
|
|
8
|
+
"@context": "https://schema.org",
|
|
9
|
+
"@type": "WebPage",
|
|
10
|
+
"name": "White Label TypeScript Web Starter",
|
|
11
|
+
"description": "A production-minded starter demonstrating the White Label TypeScript web stack.",
|
|
12
|
+
"author": {"@type": "Organization", "name": "White Label"},
|
|
13
|
+
"inLanguage": "en-US"
|
|
6
14
|
}
|
|
7
15
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @module app/assets/script/index */
|
|
2
|
+
import {initializeTaskApplication} from './tasks/TaskApplication.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser entry points should stay small.
|
|
6
|
+
*
|
|
7
|
+
* The task application owns its own composition and lifecycle, so the entry
|
|
8
|
+
* module has one obvious responsibility: start the feature for this document.
|
|
9
|
+
*/
|
|
10
|
+
export {initializeTaskApplication} from './tasks/TaskApplication.js';
|
|
11
|
+
export {normalizeTaskFilter} from './tasks/TaskRouter.js';
|
|
12
|
+
|
|
13
|
+
initializeTaskApplication(document);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type Router from 'white-label-router';
|
|
2
|
+
import type View from 'white-label-view';
|
|
3
|
+
import {createInitialTaskState, describeTaskStatus} from '../../view/examples/tasks/task-state.js';
|
|
4
|
+
import {createTaskMediator, type TaskMediator} from './TaskMediator.js';
|
|
5
|
+
import TaskModel from './TaskModel.js';
|
|
6
|
+
import {createTaskRouter} from './TaskRouter.js';
|
|
7
|
+
import {createTaskView} from './TaskView.js';
|
|
8
|
+
|
|
9
|
+
/** Public handles make the example straightforward to test and tear down. */
|
|
10
|
+
export interface TaskApplication {
|
|
11
|
+
mediator: TaskMediator;
|
|
12
|
+
model: TaskModel;
|
|
13
|
+
router: Router;
|
|
14
|
+
view: View;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compose the application at one explicit boundary.
|
|
20
|
+
*
|
|
21
|
+
* This is the only module that knows every moving part. Model, View, Router,
|
|
22
|
+
* and Mediator stay independently understandable, while this file documents
|
|
23
|
+
* the small amount of wiring needed to make them cooperate in a real feature.
|
|
24
|
+
*/
|
|
25
|
+
export function initializeTaskApplication(documentRoot: Document): TaskApplication {
|
|
26
|
+
const parentElement = documentRoot.querySelector<HTMLElement>('[data-task-example]');
|
|
27
|
+
if (!parentElement) {throw new TypeError('White Label task example requires a data-task-example container');}
|
|
28
|
+
|
|
29
|
+
const mediator = createTaskMediator();
|
|
30
|
+
const model = new TaskModel(createInitialTaskState());
|
|
31
|
+
const view = createTaskView(parentElement, model);
|
|
32
|
+
const status = parentElement.querySelector<HTMLElement>('[data-task-status]')!;
|
|
33
|
+
const updateStatus = (): void => {status.textContent = describeTaskStatus(model.get());};
|
|
34
|
+
|
|
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
|
+
|
|
42
|
+
const delegated = view.delegate(parentElement);
|
|
43
|
+
delegated.on('submit', '[data-task-form]', (event: Event) => {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
const form = event.target as HTMLFormElement;
|
|
46
|
+
const input = form.querySelector<HTMLInputElement>('[name="task"]')!;
|
|
47
|
+
mediator.dispatchEvent(new CustomEvent('task:add', {detail: input.value}));
|
|
48
|
+
parentElement.querySelector<HTMLInputElement>('[name="task"]')!.focus();
|
|
49
|
+
});
|
|
50
|
+
delegated.on('change', '[data-task-toggle]', (event: Event) => {
|
|
51
|
+
const input = event.target as HTMLInputElement;
|
|
52
|
+
const id = Number(input.dataset.taskId);
|
|
53
|
+
mediator.dispatchEvent(new CustomEvent('task:toggle', {detail: id}));
|
|
54
|
+
const focusTarget = parentElement.querySelector<HTMLInputElement>(`[data-task-id="${id}"]`)
|
|
55
|
+
?? parentElement.querySelector<HTMLAnchorElement>('[data-task-filter][aria-current="page"]')!;
|
|
56
|
+
focusTarget.focus();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const router = createTaskRouter(documentRoot, mediator);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
mediator,
|
|
63
|
+
model,
|
|
64
|
+
router,
|
|
65
|
+
view,
|
|
66
|
+
destroy() {
|
|
67
|
+
delegated.clear();
|
|
68
|
+
router.destroy();
|
|
69
|
+
mediator.removeEventListener('task:add', addTask);
|
|
70
|
+
mediator.removeEventListener('task:toggle', toggleTask);
|
|
71
|
+
mediator.removeEventListener('task:filter', setFilter);
|
|
72
|
+
view.destroy();
|
|
73
|
+
model.destroy();
|
|
74
|
+
mediator.destroy();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Mediator from 'white-label-mediator';
|
|
2
|
+
import type {TaskFilter} from '../../view/examples/tasks/task-state.js';
|
|
3
|
+
|
|
4
|
+
/** Application event vocabulary shared by the generated task modules. */
|
|
5
|
+
export type TaskEvents = {
|
|
6
|
+
'task:add': string;
|
|
7
|
+
'task:filter': TaskFilter;
|
|
8
|
+
'task:toggle': number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type TaskMediator = Mediator<TaskEvents>;
|
|
12
|
+
|
|
13
|
+
export function createTaskMediator(): TaskMediator {
|
|
14
|
+
return new Mediator<TaskEvents>().initialize();
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Model} from 'white-label-model';
|
|
2
|
+
import type {TaskFilter, TaskState} from '../../view/examples/tasks/task-state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Domain state belongs in Model, not in the DOM or router.
|
|
6
|
+
*
|
|
7
|
+
* Extending Model with small domain methods gives application code meaningful
|
|
8
|
+
* operations while preserving White Label's observable state contract.
|
|
9
|
+
*/
|
|
10
|
+
export default class TaskModel extends Model<TaskState> {
|
|
11
|
+
add(title: string): boolean {
|
|
12
|
+
const trimmed = title.trim();
|
|
13
|
+
if (!trimmed) {return false;}
|
|
14
|
+
const state = this.get();
|
|
15
|
+
const nextId = Math.max(0, ...state.tasks.map(task => task.id)) + 1;
|
|
16
|
+
return this.update({tasks: [...state.tasks, {complete: false, id: nextId, title: trimmed}]});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toggle(id: number): boolean {
|
|
20
|
+
const state = this.get();
|
|
21
|
+
const index = state.tasks.findIndex(task => task.id === id);
|
|
22
|
+
if (index < 0) {return false;}
|
|
23
|
+
const tasks = [...state.tasks];
|
|
24
|
+
const task = tasks[index]!;
|
|
25
|
+
tasks[index] = {...task, complete: !task.complete};
|
|
26
|
+
return this.update({tasks});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setFilter(filter: TaskFilter): boolean {
|
|
30
|
+
return this.update({filter});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Router from 'white-label-router';
|
|
2
|
+
import type {TaskFilter} from '../../view/examples/tasks/task-state.js';
|
|
3
|
+
import type {TaskMediator} from './TaskMediator.js';
|
|
4
|
+
|
|
5
|
+
const taskFilters = new Set<TaskFilter>(['all', 'active', 'completed']);
|
|
6
|
+
|
|
7
|
+
/** Normalize URL input at the routing boundary before it reaches domain state. */
|
|
8
|
+
export function normalizeTaskFilter(value: string | undefined): TaskFilter {
|
|
9
|
+
return value && taskFilters.has(value as TaskFilter) ? value as TaskFilter : 'all';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Router translates URL state into an application event.
|
|
14
|
+
*
|
|
15
|
+
* It does not update the Model or DOM directly. That separation keeps routing
|
|
16
|
+
* usable in different applications and makes browser navigation easy to test.
|
|
17
|
+
*/
|
|
18
|
+
export function createTaskRouter(documentRoot: Document, mediator: TaskMediator): Router {
|
|
19
|
+
const router = new Router();
|
|
20
|
+
router.scope = documentRoot.querySelector('main');
|
|
21
|
+
router.mediator = mediator;
|
|
22
|
+
const route = (_scope: Element | null, location: {data: {query: Record<string, string>}}): void => {
|
|
23
|
+
const filter = normalizeTaskFilter(location.data.query.tasks);
|
|
24
|
+
const restoreFocus = documentRoot.activeElement?.hasAttribute('data-task-filter') === true;
|
|
25
|
+
mediator.dispatchEvent(new CustomEvent('task:filter', {detail: filter}));
|
|
26
|
+
if (restoreFocus) {documentRoot.querySelector<HTMLAnchorElement>(`[data-task-filter="${filter}"]`)!.focus();}
|
|
27
|
+
};
|
|
28
|
+
router.routes = {'/': route, defaultRoute: route};
|
|
29
|
+
return router.initialize();
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import View from 'white-label-view';
|
|
2
|
+
import TaskExample from '../../view/examples/tasks/TaskExample.js';
|
|
3
|
+
import type {TaskState} from '../../view/examples/tasks/task-state.js';
|
|
4
|
+
import type TaskModel from './TaskModel.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* View owns rendering, but not application decisions.
|
|
8
|
+
*
|
|
9
|
+
* Passing the already-rendered task root lets View adopt useful static HTML
|
|
10
|
+
* instead of replacing the document merely because JavaScript has started.
|
|
11
|
+
*/
|
|
12
|
+
export function createTaskView(parentElement: HTMLElement, model: TaskModel): View {
|
|
13
|
+
const element = parentElement.querySelector<HTMLElement>('[data-task-app]');
|
|
14
|
+
if (!element) {throw new TypeError('Task example requires its static task-app root');}
|
|
15
|
+
|
|
16
|
+
return new View({
|
|
17
|
+
element,
|
|
18
|
+
model,
|
|
19
|
+
parentElement,
|
|
20
|
+
template(data) {
|
|
21
|
+
return <TaskExample state={data as TaskState} />;
|
|
22
|
+
}
|
|
23
|
+
}).initialize();
|
|
24
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
@import "tailwindcss" source("../../..");
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-white-label-white: #ffffff;
|
|
5
|
+
--color-white-label-black: #000000;
|
|
6
|
+
--color-white-label-grey: #767676;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
--white: #ffffff;
|
|
11
|
+
--black: #000000;
|
|
12
|
+
--grey: #767676;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html { scroll-behavior: smooth; }
|
|
16
|
+
body { margin: 0; background: var(--white); color: var(--black); font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; line-height: 1.65; }
|
|
17
|
+
a { color: var(--grey); text-decoration-thickness: 1px; text-underline-offset: .22em; }
|
|
18
|
+
a:hover { color: var(--black); }
|
|
19
|
+
button, input { font: inherit; }
|
|
20
|
+
button { cursor: pointer; }
|
|
21
|
+
.container { width: min(100% - 2rem, 72rem); margin-inline: auto; }
|
|
22
|
+
.skip-link { position: fixed; top: .75rem; left: .75rem; z-index: 100; padding: .75rem 1rem; border: 1px solid var(--black); background: var(--white); color: var(--black); transform: translateY(-180%); }
|
|
23
|
+
.skip-link:focus { transform: none; }
|
|
24
|
+
.site-header { position: sticky; top: 0; z-index: 80; border-bottom: 1px solid var(--grey); background: var(--white); }
|
|
25
|
+
.site-header__inner { display: flex; align-items: center; justify-content: space-between; min-height: 4.5rem; gap: 2rem; }
|
|
26
|
+
.wordmark { color: var(--black); font-size: .9rem; font-weight: 800; letter-spacing: .14em; text-decoration: none; text-transform: uppercase; }
|
|
27
|
+
.site-header nav { display: flex; flex-wrap: wrap; gap: 1.25rem; font-size: .9rem; }
|
|
28
|
+
.site-header nav a { display: inline-flex; align-items: center; min-height: 44px; color: var(--grey); }
|
|
29
|
+
.site-header nav a:hover, .site-header nav a:focus-visible { color: var(--black); }
|
|
30
|
+
.hero { padding: clamp(4rem, 8vw, 7rem) 0 clamp(4.5rem, 10vw, 8rem); border-bottom: 1px solid var(--grey); }
|
|
31
|
+
.hero__inner { max-width: 64rem; }
|
|
32
|
+
.hero h1 { margin: 0 0 1.75rem; font-size: clamp(3.6rem, 10vw, 8.75rem); font-weight: 800; line-height: .97; letter-spacing: -.04em; }
|
|
33
|
+
.hero__lede { max-width: 45rem; margin: 0 0 2rem; font-size: clamp(1.15rem, 2.2vw, 1.55rem); line-height: 1.5; }
|
|
34
|
+
.hero__links { display: flex; flex-wrap: wrap; gap: .75rem 1.5rem; align-items: center; }
|
|
35
|
+
.primary-link { display: inline-flex; align-items: center; min-height: 44px; padding: .45rem .85rem; border: 1px solid var(--black); background: var(--black); color: var(--white); text-decoration: none; }
|
|
36
|
+
.primary-link:hover, .primary-link:focus-visible { background: var(--white); color: var(--black); }
|
|
37
|
+
.eyebrow, .code-label { margin: 0 0 .7rem; color: var(--grey); font-size: .75rem; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; }
|
|
38
|
+
.section { padding: clamp(4.5rem, 9vw, 8rem) 0; border-bottom: 1px solid var(--grey); }
|
|
39
|
+
.section-intro { max-width: 50rem; margin-bottom: clamp(3rem, 7vw, 5rem); }
|
|
40
|
+
.section h2 { max-width: 13ch; margin: 0 0 1.35rem; font-size: clamp(2.6rem, 6vw, 5.5rem); line-height: .98; letter-spacing: -.055em; }
|
|
41
|
+
.section-intro > p:last-child { max-width: 44rem; font-size: 1.12rem; }
|
|
42
|
+
.demo { border-top: 1px solid var(--black); border-bottom: 1px solid var(--black); }
|
|
43
|
+
.task-app { padding: clamp(1.5rem, 4vw, 2.5rem) 0; }
|
|
44
|
+
.task-form { display: grid; grid-template-columns: minmax(8rem, .35fr) minmax(0, 1.65fr); gap: clamp(1rem, 4vw, 4rem); align-items: end; padding-bottom: 1.5rem; border-bottom: 1px solid var(--grey); }
|
|
45
|
+
.task-form > label { color: var(--grey); font-size: .75rem; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; }
|
|
46
|
+
.task-form__controls { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
|
|
47
|
+
.task-form input { min-width: 0; min-height: 44px; padding: .6rem .75rem; border: 1px solid var(--grey); border-right: 0; border-radius: 0; background: var(--white); color: var(--black); }
|
|
48
|
+
.task-form button { min-height: 44px; padding: .6rem 1rem; border: 1px solid var(--black); border-radius: 0; background: var(--black); color: var(--white); }
|
|
49
|
+
.task-form button:hover, .task-form button:focus-visible { background: var(--white); color: var(--black); }
|
|
50
|
+
.task-filters { display: flex; flex-wrap: wrap; gap: .5rem; padding: 1.5rem 0; border-bottom: 1px solid var(--grey); }
|
|
51
|
+
.task-filters a { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; padding-inline: .8rem; border: 1px solid var(--grey); color: var(--grey); text-decoration: none; }
|
|
52
|
+
.task-filters a:hover, .task-filters a:focus-visible, .task-filters a[aria-current='page'] { border-color: var(--black); color: var(--black); }
|
|
53
|
+
.task-filters a[aria-current='page'] { text-decoration: underline; text-underline-offset: .3em; }
|
|
54
|
+
.task-list { margin: 0; padding: 0; list-style: none; }
|
|
55
|
+
.task-list li { border-bottom: 1px solid var(--grey); }
|
|
56
|
+
.task-list label { display: flex; gap: .85rem; align-items: center; min-height: 54px; padding: .55rem 0; cursor: pointer; }
|
|
57
|
+
.task-list input { width: 1.2rem; height: 1.2rem; margin: 0; accent-color: var(--black); }
|
|
58
|
+
.task-list input:checked + span { color: var(--grey); text-decoration: line-through; }
|
|
59
|
+
.demo__trace { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.5rem; margin: 0; padding: 1.5rem 0 0; }
|
|
60
|
+
.demo__trace > div { min-width: 0; }
|
|
61
|
+
.demo__trace dt { margin-bottom: .3rem; color: var(--grey); font-size: .72rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
|
62
|
+
.demo__trace dd { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: .9rem; overflow-wrap: anywhere; }
|
|
63
|
+
.architecture { margin-bottom: clamp(3rem, 8vw, 6rem); }
|
|
64
|
+
.architecture ol { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--black); }
|
|
65
|
+
.architecture li { display: grid; grid-template-columns: minmax(9rem, .4fr) minmax(0, 1.6fr); gap: 2rem; padding: 1.2rem 0; border-bottom: 1px solid var(--grey); }
|
|
66
|
+
.architecture strong { font-size: 1.15rem; }
|
|
67
|
+
.architecture span { color: var(--grey); }
|
|
68
|
+
.docs-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--grey); border-left: 1px solid var(--grey); }
|
|
69
|
+
.docs-grid article { min-width: 0; padding: clamp(1.5rem, 4vw, 2.5rem); border-right: 1px solid var(--grey); border-bottom: 1px solid var(--grey); }
|
|
70
|
+
.docs-grid h3 { margin: 0 0 1.5rem; font-size: 1.35rem; }
|
|
71
|
+
.docs-grid article > p:last-child { margin-bottom: 0; }
|
|
72
|
+
pre { max-width: 100%; margin: 0; padding: 1.1rem; border: 1px solid var(--grey); background: var(--white); color: var(--black); overflow-x: auto; font-size: .84rem; line-height: 1.55; }
|
|
73
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
74
|
+
.code-block { padding: .9rem 0; border-color: #cecece; background: #f4f4f4; color: #242424; }
|
|
75
|
+
.code-block__line { display: grid; grid-template-columns: 2.75rem minmax(max-content, 1fr); padding-right: 1rem; }
|
|
76
|
+
.code-block__number { padding-right: .85rem; color: #666666; text-align: right; user-select: none; }
|
|
77
|
+
.code-syntax-keyword { color: #5f5f5f; }
|
|
78
|
+
.code-syntax-type { color: #626262; }
|
|
79
|
+
.code-syntax-value { color: #666666; }
|
|
80
|
+
.code-syntax-muted { color: #6b6b6b; }
|
|
81
|
+
.start-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18rem, .8fr); gap: clamp(3rem, 8vw, 8rem); align-items: start; }
|
|
82
|
+
.start-grid .section-intro { margin-bottom: 0; }
|
|
83
|
+
.start-grid pre { margin-bottom: 1.5rem; }
|
|
84
|
+
.source-paths dl { margin: 0; border-top: 1px solid var(--black); }
|
|
85
|
+
.source-paths dl > div { padding: 1rem 0; border-bottom: 1px solid var(--grey); }
|
|
86
|
+
.source-paths dt { color: var(--grey); font-size: .75rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
|
87
|
+
.source-paths dd { margin: .25rem 0 0; overflow-wrap: anywhere; }
|
|
88
|
+
.site-footer { padding: 2rem 0; }
|
|
89
|
+
.site-footer__inner { display: flex; justify-content: space-between; gap: 2rem; }
|
|
90
|
+
.site-footer p { margin: 0; }
|
|
91
|
+
.visually-hidden { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; }
|
|
92
|
+
section[id] { scroll-margin-top: 5.5rem; }
|
|
93
|
+
*:focus-visible { outline: 3px solid var(--black); outline-offset: 3px; }
|
|
94
|
+
|
|
95
|
+
@media (max-width: 767px) {
|
|
96
|
+
.site-header { position: static; }
|
|
97
|
+
.site-header__inner { align-items: flex-start; flex-direction: column; padding-block: 1rem; gap: .35rem; }
|
|
98
|
+
.site-header nav { gap: .35rem 1rem; }
|
|
99
|
+
.hero h1 { font-size: clamp(3.4rem, 18vw, 6rem); }
|
|
100
|
+
.task-form, .architecture li, .start-grid { grid-template-columns: 1fr; }
|
|
101
|
+
.task-form { align-items: stretch; gap: .5rem; }
|
|
102
|
+
.demo__trace, .docs-grid { grid-template-columns: 1fr; }
|
|
103
|
+
.demo__trace { gap: 1rem; }
|
|
104
|
+
.docs-grid { border-right: 1px solid var(--grey); }
|
|
105
|
+
.docs-grid article { border-right: 0; }
|
|
106
|
+
.site-footer__inner { flex-direction: column; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (prefers-reduced-motion: reduce) {
|
|
110
|
+
html { scroll-behavior: auto; }
|
|
111
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
@media print {
|
|
2
|
+
.site-header, .task-form, .task-filters, .primary-link { display: none !important; }
|
|
3
|
+
body { color: #000000; background: #ffffff; font: 11pt/1.5 Georgia, serif; }
|
|
4
|
+
a { color: #000000; }
|
|
5
|
+
a[href^='http']::after { content: ' (' attr(href) ')'; overflow-wrap: anywhere; }
|
|
6
|
+
h1, h2, h3 { break-after: avoid; }
|
|
7
|
+
article, .task-app, pre { break-inside: avoid; }
|
|
8
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {JSX} from 'white-label-view/jsx-runtime';
|
|
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: JSX.Element[]}) {
|
|
12
|
+
return (
|
|
13
|
+
<pre className="code-block"><code>{lines.map((line, index) => (
|
|
14
|
+
<span className="code-block__line"><span aria-hidden="true" className="code-block__number">{index + 1}</span><span>{line}</span></span>
|
|
15
|
+
))}</code></pre>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {getVisibleTasks, type TaskFilter, type TaskState} from './task-state.js';
|
|
2
|
+
|
|
3
|
+
const filters: TaskFilter[] = ['all', 'active', 'completed'];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render the task example from state only.
|
|
7
|
+
*
|
|
8
|
+
* The same JSX function is used twice: once while generating the initial HTML
|
|
9
|
+
* and again by white-label-view in the browser. Sharing one renderer keeps the
|
|
10
|
+
* static and interactive versions of the example from drifting apart.
|
|
11
|
+
*/
|
|
12
|
+
export default function TaskExample({state}: {state: TaskState}) {
|
|
13
|
+
const visibleTasks = getVisibleTasks(state);
|
|
14
|
+
const completed = state.tasks.filter(task => task.complete).length;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<section className="task-app" data-task-app aria-label="Interactive task example">
|
|
18
|
+
<form className="task-form" data-task-form action="#example" method="get">
|
|
19
|
+
<label htmlFor="task-title">New task</label>
|
|
20
|
+
<div className="task-form__controls">
|
|
21
|
+
<input id="task-title" name="task" type="text" autocomplete="off" required />
|
|
22
|
+
<button type="submit">Add task</button>
|
|
23
|
+
</div>
|
|
24
|
+
</form>
|
|
25
|
+
|
|
26
|
+
<nav className="task-filters" aria-label="Filter tasks">
|
|
27
|
+
{filters.map(filter => (
|
|
28
|
+
<a
|
|
29
|
+
href={`/?tasks=${filter}#example`}
|
|
30
|
+
data-task-filter={filter}
|
|
31
|
+
data-pushstate
|
|
32
|
+
aria-current={state.filter === filter ? 'page' : 'false'}
|
|
33
|
+
>
|
|
34
|
+
{filter.charAt(0).toUpperCase()}{filter.slice(1)}
|
|
35
|
+
</a>
|
|
36
|
+
))}
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<ul className="task-list">
|
|
40
|
+
{visibleTasks.map(task => (
|
|
41
|
+
<li>
|
|
42
|
+
<label>
|
|
43
|
+
<input
|
|
44
|
+
type="checkbox"
|
|
45
|
+
checked={task.complete}
|
|
46
|
+
data-task-toggle
|
|
47
|
+
data-task-id={task.id}
|
|
48
|
+
/>
|
|
49
|
+
<span>{task.title}</span>
|
|
50
|
+
</label>
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
|
|
55
|
+
<dl className="demo__trace">
|
|
56
|
+
<div><dt>Router</dt><dd>/?tasks={state.filter}</dd></div>
|
|
57
|
+
<div><dt>Mediator</dt><dd>task:* application events</dd></div>
|
|
58
|
+
<div><dt>Model</dt><dd>{state.tasks.length} tasks · {completed} complete</dd></div>
|
|
59
|
+
<div><dt>View</dt><dd>{visibleTasks.length} tasks rendered</dd></div>
|
|
60
|
+
</dl>
|
|
61
|
+
</section>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Shared state shape used by build-time JSX and the browser application. */
|
|
2
|
+
export type TaskFilter = 'all' | 'active' | 'completed';
|
|
3
|
+
|
|
4
|
+
export interface TaskItem {
|
|
5
|
+
complete: boolean;
|
|
6
|
+
id: number;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TaskState extends Record<PropertyKey, unknown> {
|
|
11
|
+
filter: TaskFilter;
|
|
12
|
+
tasks: TaskItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const taskPredicates: Record<TaskFilter, (task: TaskItem) => boolean> = {
|
|
16
|
+
active: task => !task.complete,
|
|
17
|
+
all: () => true,
|
|
18
|
+
completed: task => task.complete
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Return the tasks visible for the current filter. */
|
|
22
|
+
export function getVisibleTasks(state: TaskState): TaskItem[] {
|
|
23
|
+
return state.tasks.filter(taskPredicates[state.filter]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Describe the currently rendered task result for assistive technology. */
|
|
27
|
+
export function describeTaskStatus(state: TaskState): string {
|
|
28
|
+
const visible = getVisibleTasks(state).length;
|
|
29
|
+
return `Showing ${visible} ${visible === 1 ? 'task' : 'tasks'} for the ${state.filter} filter.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return fresh demo data for each application instance.
|
|
34
|
+
*
|
|
35
|
+
* A factory avoids sharing mutable state between tests, pages, or future
|
|
36
|
+
* server-side renders while keeping the initial HTML deterministic.
|
|
37
|
+
*/
|
|
38
|
+
export function createInitialTaskState(): TaskState {
|
|
39
|
+
return {
|
|
40
|
+
filter: 'all',
|
|
41
|
+
tasks: [
|
|
42
|
+
{complete: true, id: 1, title: 'Read the architecture'},
|
|
43
|
+
{complete: false, id: 2, title: 'Build something with White Label'}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|