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.
Files changed (303) hide show
  1. package/.editorconfig +15 -0
  2. package/.yarnrc.yml +6 -0
  3. package/CLI.md +82 -0
  4. package/LICENSE +9 -0
  5. package/PACKAGE_MANAGERS.md +35 -0
  6. package/README.md +232 -124
  7. package/app/404.tsx +28 -0
  8. package/app/README.md +92 -8
  9. package/app/assets/data/view/global.json +7 -47
  10. package/app/assets/data/view/index.json +9 -1
  11. package/app/assets/script/index.tsx +13 -0
  12. package/app/assets/script/tasks/TaskApplication.ts +77 -0
  13. package/app/assets/script/tasks/TaskMediator.ts +15 -0
  14. package/app/assets/script/tasks/TaskModel.ts +32 -0
  15. package/app/assets/script/tasks/TaskRouter.ts +30 -0
  16. package/app/assets/script/tasks/TaskView.tsx +24 -0
  17. package/app/assets/style/global.css +111 -0
  18. package/app/assets/style/print.css +8 -0
  19. package/app/assets/view/CodeBlock.tsx +17 -0
  20. package/app/assets/view/examples/tasks/TaskExample.tsx +63 -0
  21. package/app/assets/view/examples/tasks/task-state.ts +46 -0
  22. package/app/assets/view/layout/SiteFooter.tsx +11 -0
  23. package/app/assets/view/layout/SiteHeader.tsx +23 -0
  24. package/app/assets/view/page-data.ts +14 -0
  25. package/app/assets/view/sections/GettingStartedSection.tsx +27 -0
  26. package/app/assets/view/sections/HeroSection.tsx +21 -0
  27. package/app/assets/view/sections/LiveExampleSection.tsx +29 -0
  28. package/app/assets/view/sections/PackageDocsSection.tsx +69 -0
  29. package/app/assets/view/sections/SourceGuideSection.tsx +28 -0
  30. package/app/index.tsx +54 -0
  31. package/cli/index.ts +118 -0
  32. package/dist/app/404.d.ts +1 -0
  33. package/dist/app/404.js +6 -0
  34. package/dist/app/404.js.map +1 -0
  35. package/dist/app/assets/script/index.d.ts +8 -2
  36. package/dist/app/assets/script/index.js +11 -1
  37. package/dist/app/assets/script/index.js.map +1 -1
  38. package/dist/app/assets/script/tasks/TaskApplication.d.ts +20 -0
  39. package/dist/app/assets/script/tasks/TaskApplication.js +63 -0
  40. package/dist/app/assets/script/tasks/TaskApplication.js.map +1 -0
  41. package/dist/app/assets/script/tasks/TaskMediator.d.ts +10 -0
  42. package/dist/app/assets/script/tasks/TaskMediator.js +5 -0
  43. package/dist/app/assets/script/tasks/TaskMediator.js.map +1 -0
  44. package/dist/app/assets/script/tasks/TaskModel.d.ts +13 -0
  45. package/dist/app/assets/script/tasks/TaskModel.js +33 -0
  46. package/dist/app/assets/script/tasks/TaskModel.js.map +1 -0
  47. package/dist/app/assets/script/tasks/TaskRouter.d.ts +12 -0
  48. package/dist/app/assets/script/tasks/TaskRouter.js +28 -0
  49. package/dist/app/assets/script/tasks/TaskRouter.js.map +1 -0
  50. package/dist/app/assets/script/tasks/TaskView.d.ts +9 -0
  51. package/dist/app/assets/script/tasks/TaskView.js +24 -0
  52. package/dist/app/assets/script/tasks/TaskView.js.map +1 -0
  53. package/dist/app/assets/view/CodeBlock.d.ts +11 -0
  54. package/dist/app/assets/view/CodeBlock.js +12 -0
  55. package/dist/app/assets/view/CodeBlock.js.map +1 -0
  56. package/dist/app/assets/view/examples/tasks/TaskExample.d.ts +11 -0
  57. package/dist/app/assets/view/examples/tasks/TaskExample.js +16 -0
  58. package/dist/app/assets/view/examples/tasks/TaskExample.js.map +1 -0
  59. package/dist/app/assets/view/examples/tasks/task-state.d.ts +22 -0
  60. package/dist/app/assets/view/examples/tasks/task-state.js +30 -0
  61. package/dist/app/assets/view/examples/tasks/task-state.js.map +1 -0
  62. package/dist/app/assets/view/layout/SiteFooter.d.ts +2 -0
  63. package/dist/app/assets/view/layout/SiteFooter.js +6 -0
  64. package/dist/app/assets/view/layout/SiteFooter.js.map +1 -0
  65. package/dist/app/assets/view/layout/SiteHeader.d.ts +8 -0
  66. package/dist/app/assets/view/layout/SiteHeader.js +12 -0
  67. package/dist/app/assets/view/layout/SiteHeader.js.map +1 -0
  68. package/dist/app/assets/view/page-data.d.ts +16 -0
  69. package/dist/app/assets/view/page-data.js +2 -0
  70. package/dist/app/assets/view/page-data.js.map +1 -0
  71. package/dist/app/assets/view/sections/GettingStartedSection.d.ts +2 -0
  72. package/dist/app/assets/view/sections/GettingStartedSection.js +11 -0
  73. package/dist/app/assets/view/sections/GettingStartedSection.js.map +1 -0
  74. package/dist/app/assets/view/sections/HeroSection.d.ts +8 -0
  75. package/dist/app/assets/view/sections/HeroSection.js +12 -0
  76. package/dist/app/assets/view/sections/HeroSection.js.map +1 -0
  77. package/dist/app/assets/view/sections/LiveExampleSection.d.ts +6 -0
  78. package/dist/app/assets/view/sections/LiveExampleSection.js +13 -0
  79. package/dist/app/assets/view/sections/LiveExampleSection.js.map +1 -0
  80. package/dist/app/assets/view/sections/PackageDocsSection.d.ts +2 -0
  81. package/dist/app/assets/view/sections/PackageDocsSection.js +31 -0
  82. package/dist/app/assets/view/sections/PackageDocsSection.js.map +1 -0
  83. package/dist/app/assets/view/sections/SourceGuideSection.d.ts +7 -0
  84. package/dist/app/assets/view/sections/SourceGuideSection.js +11 -0
  85. package/dist/app/assets/view/sections/SourceGuideSection.js.map +1 -0
  86. package/dist/app/index.d.ts +8 -0
  87. package/dist/app/index.js +22 -0
  88. package/dist/app/index.js.map +1 -0
  89. package/dist/cli/index.d.ts +13 -0
  90. package/dist/cli/index.js +98 -0
  91. package/dist/cli/index.js.map +1 -0
  92. package/dist/scaffold/index.d.ts +64 -0
  93. package/dist/scaffold/index.js +125 -0
  94. package/dist/scaffold/index.js.map +1 -0
  95. package/dist/scripts/build.d.ts +5 -13
  96. package/dist/scripts/build.js +48 -79
  97. package/dist/scripts/build.js.map +1 -1
  98. package/dist/server/handler.d.ts +7 -0
  99. package/dist/server/handler.js +67 -0
  100. package/dist/server/handler.js.map +1 -0
  101. package/package.json +78 -40
  102. package/pnpm-workspace.yaml +4 -0
  103. package/scaffold/AGENTS.md +18 -0
  104. package/scaffold/index.ts +139 -0
  105. package/scaffold/no-jsx/README.md +73 -0
  106. package/scaffold/no-jsx/app/404.ts +10 -0
  107. package/scaffold/no-jsx/app/assets/script/index.ts +7 -0
  108. package/scaffold/no-jsx/app/assets/script/tasks/TaskView.ts +19 -0
  109. package/scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts +39 -0
  110. package/scaffold/no-jsx/app/index.ts +58 -0
  111. package/scripts/build.ts +55 -86
  112. package/server/handler.ts +75 -0
  113. package/template-test/site.test.js +235 -0
  114. package/tsconfig.site.json +6 -5
  115. package/tsconfig.site.no-jsx.json +24 -0
  116. package/app/404.hbs +0 -20
  117. package/app/assets/data/coutries.json +0 -992
  118. package/app/assets/data/states.json +0 -204
  119. package/app/assets/data/view/toolkit/colors.json +0 -17
  120. package/app/assets/data/view/toolkit/components.json +0 -39
  121. package/app/assets/data/view/toolkit/elements.json +0 -119
  122. package/app/assets/data/view/toolkit/grids.json +0 -17
  123. package/app/assets/data/view/toolkit/icons.json +0 -17
  124. package/app/assets/data/view/toolkit/index.json +0 -17
  125. package/app/assets/data/view/toolkit/type.json +0 -17
  126. package/app/assets/font/opensans/LICENSE.txt +0 -202
  127. package/app/assets/font/opensans/opensans-bold-webfont.woff2 +0 -0
  128. package/app/assets/font/opensans/opensans-bolditalic-webfont.woff2 +0 -0
  129. package/app/assets/font/opensans/opensans-extrabold-webfont.woff2 +0 -0
  130. package/app/assets/font/opensans/opensans-extrabolditalic-webfont.woff2 +0 -0
  131. package/app/assets/font/opensans/opensans-italic-webfont.woff2 +0 -0
  132. package/app/assets/font/opensans/opensans-light-webfont.woff2 +0 -0
  133. package/app/assets/font/opensans/opensans-lightitalic-webfont.woff2 +0 -0
  134. package/app/assets/font/opensans/opensans-regular-webfont.woff2 +0 -0
  135. package/app/assets/font/opensans/opensans-semibold-webfont.woff2 +0 -0
  136. package/app/assets/font/opensans/opensans-semibolditalic-webfont.woff2 +0 -0
  137. package/app/assets/font/opensans/stylesheet.css +0 -120
  138. package/app/assets/image/apple-touch-icon.png +0 -0
  139. package/app/assets/image/demo/fallback.avif +0 -0
  140. package/app/assets/image/demo/fallback.jpg +0 -0
  141. package/app/assets/image/demo/fallback.webp +0 -0
  142. package/app/assets/image/demo/large.avif +0 -0
  143. package/app/assets/image/demo/large.webp +0 -0
  144. package/app/assets/image/demo/small.avif +0 -0
  145. package/app/assets/image/demo/small.webp +0 -0
  146. package/app/assets/image/favicon.png +0 -0
  147. package/app/assets/image/graphic-css3.svg +0 -30
  148. package/app/assets/image/graphic-html5.svg +0 -29
  149. package/app/assets/markup/element/a.hbs +0 -29
  150. package/app/assets/markup/element/button.hbs +0 -30
  151. package/app/assets/markup/element/div.hbs +0 -11
  152. package/app/assets/markup/element/head.hbs +0 -78
  153. package/app/assets/markup/element/iframe.hbs +0 -20
  154. package/app/assets/markup/element/img.hbs +0 -30
  155. package/app/assets/markup/element/input.hbs +0 -43
  156. package/app/assets/markup/element/label.hbs +0 -14
  157. package/app/assets/markup/element/legend.hbs +0 -5
  158. package/app/assets/markup/element/meta.hbs +0 -11
  159. package/app/assets/markup/element/nav.hbs +0 -13
  160. package/app/assets/markup/element/p.hbs +0 -8
  161. package/app/assets/markup/element/picture.hbs +0 -58
  162. package/app/assets/markup/element/script.hbs +0 -3
  163. package/app/assets/markup/element/select.hbs +0 -35
  164. package/app/assets/markup/element/span.hbs +0 -8
  165. package/app/assets/markup/element/svg.hbs +0 -11
  166. package/app/assets/markup/element/table.hbs +0 -50
  167. package/app/assets/markup/element/textarea.hbs +0 -24
  168. package/app/assets/markup/element/ul.hbs +0 -13
  169. package/app/assets/markup/modal/modal-example-1.handlebars +0 -8
  170. package/app/assets/markup/toolkit/accessibility-skip-to-element-1.handlebars +0 -1
  171. package/app/assets/markup/toolkit/accessibility-speaker-1.handlebars +0 -1
  172. package/app/assets/markup/toolkit/anchor-1.handlebars +0 -1
  173. package/app/assets/markup/toolkit/button-1.handlebars +0 -1
  174. package/app/assets/markup/toolkit/footer-1.handlebars +0 -3
  175. package/app/assets/markup/toolkit/header-1.handlebars +0 -1
  176. package/app/assets/markup/toolkit/heading-1.handlebars +0 -1
  177. package/app/assets/markup/toolkit/heading-2.handlebars +0 -1
  178. package/app/assets/markup/toolkit/heading-3.handlebars +0 -1
  179. package/app/assets/markup/toolkit/heading-4.handlebars +0 -1
  180. package/app/assets/markup/toolkit/image-1.handlebars +0 -1
  181. package/app/assets/markup/toolkit/input-checkbox-1.handlebars +0 -1
  182. package/app/assets/markup/toolkit/input-error-1.handlebars +0 -8
  183. package/app/assets/markup/toolkit/input-radio-1.handlebars +0 -1
  184. package/app/assets/markup/toolkit/input-text-1.handlebars +0 -1
  185. package/app/assets/markup/toolkit/label-1.handlebars +0 -1
  186. package/app/assets/markup/toolkit/legend-1.handlebars +0 -1
  187. package/app/assets/markup/toolkit/list-1.handlebars +0 -1
  188. package/app/assets/markup/toolkit/modal-1.handlebars +0 -4
  189. package/app/assets/markup/toolkit/nav-1.handlebars +0 -1
  190. package/app/assets/markup/toolkit/picture-1.handlebars +0 -1
  191. package/app/assets/markup/toolkit/select-1.handlebars +0 -1
  192. package/app/assets/markup/toolkit/table-1.handlebars +0 -1
  193. package/app/assets/markup/toolkit/text-1.handlebars +0 -1
  194. package/app/assets/markup/toolkit/textarea-1.handlebars +0 -1
  195. package/app/assets/markup/toolkit/youtube-player-1.handlebars +0 -1
  196. package/app/assets/script/api/facebook.ts +0 -41
  197. package/app/assets/script/api/youtube.ts +0 -22
  198. package/app/assets/script/index.ts +0 -2
  199. package/app/assets/script/mediator/global.ts +0 -4
  200. package/app/assets/script/model/singletons/countries.ts +0 -5
  201. package/app/assets/script/model/singletons/global.ts +0 -5
  202. package/app/assets/script/model/singletons/states.ts +0 -5
  203. package/app/assets/script/model/user.ts +0 -4
  204. package/app/assets/script/toolkit.ts +0 -11
  205. package/app/assets/script/types.d.ts +0 -6
  206. package/app/assets/script/utility/ajax.ts +0 -15
  207. package/app/assets/script/utility/form.ts +0 -119
  208. package/app/assets/script/utility/regex.ts +0 -26
  209. package/app/assets/script/utility/share.ts +0 -77
  210. package/app/assets/script/utility/string.ts +0 -148
  211. package/app/assets/script/view/default.tsx +0 -14
  212. package/app/assets/script/view/toolkit/youtube-player-1.ts +0 -33
  213. package/app/assets/style/base/_toolkit.scss +0 -39
  214. package/app/assets/style/base/colors/_colors.scss +0 -18
  215. package/app/assets/style/base/components/_modal-1.scss +0 -80
  216. package/app/assets/style/base/components/_sticky-1.scss +0 -19
  217. package/app/assets/style/base/components/_tabpanel-1.scss +0 -115
  218. package/app/assets/style/base/components/_tabpanel-2.scss +0 -64
  219. package/app/assets/style/base/components/_tabpanel-base-1.scss +0 -13
  220. package/app/assets/style/base/components/_youtube-player-1.scss +0 -16
  221. package/app/assets/style/base/elements/_anchors.scss +0 -27
  222. package/app/assets/style/base/elements/_buttons.scss +0 -25
  223. package/app/assets/style/base/elements/_fieldsets.scss +0 -9
  224. package/app/assets/style/base/elements/_images.scss +0 -8
  225. package/app/assets/style/base/elements/_inputs.scss +0 -78
  226. package/app/assets/style/base/elements/_labels.scss +0 -16
  227. package/app/assets/style/base/elements/_landmarks.scss +0 -3
  228. package/app/assets/style/base/elements/_legends.scss +0 -11
  229. package/app/assets/style/base/elements/_lists.scss +0 -15
  230. package/app/assets/style/base/elements/_pictures.scss +0 -4
  231. package/app/assets/style/base/elements/_tables.scss +0 -39
  232. package/app/assets/style/base/layouts/_staggered-1.scss +0 -29
  233. package/app/assets/style/base/type/_fonts.scss +0 -125
  234. package/app/assets/style/base/type/_headings.scss +0 -36
  235. package/app/assets/style/base/type/_text.scss +0 -9
  236. package/app/assets/style/base/utilities/_accessibility.scss +0 -55
  237. package/app/assets/style/base/utilities/_functions.scss +0 -98
  238. package/app/assets/style/base/utilities/_scroll.scss +0 -11
  239. package/app/assets/style/base/utilities/_type.scss +0 -13
  240. package/app/assets/style/global.scss +0 -123
  241. package/app/assets/style/print.scss +0 -6
  242. package/app/assets/style/toolkit.scss +0 -387
  243. package/app/index.hbs +0 -21
  244. package/app/robots.txt +0 -1
  245. package/app/toolkit/colors.hbs +0 -102
  246. package/app/toolkit/components.hbs +0 -261
  247. package/app/toolkit/elements.hbs +0 -497
  248. package/app/toolkit/grids.hbs +0 -413
  249. package/app/toolkit/icons.hbs +0 -87
  250. package/app/toolkit/index.hbs +0 -144
  251. package/app/toolkit/type.hbs +0 -120
  252. package/dist/app/assets/data/coutries.json +0 -992
  253. package/dist/app/assets/data/states.json +0 -204
  254. package/dist/app/assets/script/api/facebook.d.ts +0 -8
  255. package/dist/app/assets/script/api/facebook.js +0 -42
  256. package/dist/app/assets/script/api/facebook.js.map +0 -1
  257. package/dist/app/assets/script/api/youtube.d.ts +0 -8
  258. package/dist/app/assets/script/api/youtube.js +0 -22
  259. package/dist/app/assets/script/api/youtube.js.map +0 -1
  260. package/dist/app/assets/script/mediator/global.d.ts +0 -4
  261. package/dist/app/assets/script/mediator/global.js +0 -4
  262. package/dist/app/assets/script/mediator/global.js.map +0 -1
  263. package/dist/app/assets/script/model/singletons/countries.d.ts +0 -4
  264. package/dist/app/assets/script/model/singletons/countries.js +0 -5
  265. package/dist/app/assets/script/model/singletons/countries.js.map +0 -1
  266. package/dist/app/assets/script/model/singletons/global.d.ts +0 -4
  267. package/dist/app/assets/script/model/singletons/global.js +0 -5
  268. package/dist/app/assets/script/model/singletons/global.js.map +0 -1
  269. package/dist/app/assets/script/model/singletons/states.d.ts +0 -4
  270. package/dist/app/assets/script/model/singletons/states.js +0 -5
  271. package/dist/app/assets/script/model/singletons/states.js.map +0 -1
  272. package/dist/app/assets/script/model/user.d.ts +0 -4
  273. package/dist/app/assets/script/model/user.js +0 -6
  274. package/dist/app/assets/script/model/user.js.map +0 -1
  275. package/dist/app/assets/script/toolkit.d.ts +0 -1
  276. package/dist/app/assets/script/toolkit.js +0 -7
  277. package/dist/app/assets/script/toolkit.js.map +0 -1
  278. package/dist/app/assets/script/utility/ajax.d.ts +0 -8
  279. package/dist/app/assets/script/utility/ajax.js +0 -17
  280. package/dist/app/assets/script/utility/ajax.js.map +0 -1
  281. package/dist/app/assets/script/utility/form.d.ts +0 -64
  282. package/dist/app/assets/script/utility/form.js +0 -111
  283. package/dist/app/assets/script/utility/form.js.map +0 -1
  284. package/dist/app/assets/script/utility/regex.d.ts +0 -14
  285. package/dist/app/assets/script/utility/regex.js +0 -27
  286. package/dist/app/assets/script/utility/regex.js.map +0 -1
  287. package/dist/app/assets/script/utility/share.d.ts +0 -68
  288. package/dist/app/assets/script/utility/share.js +0 -81
  289. package/dist/app/assets/script/utility/share.js.map +0 -1
  290. package/dist/app/assets/script/utility/string.d.ts +0 -64
  291. package/dist/app/assets/script/utility/string.js +0 -135
  292. package/dist/app/assets/script/utility/string.js.map +0 -1
  293. package/dist/app/assets/script/view/default.d.ts +0 -11
  294. package/dist/app/assets/script/view/default.js +0 -15
  295. package/dist/app/assets/script/view/default.js.map +0 -1
  296. package/dist/app/assets/script/view/toolkit/youtube-player-1.d.ts +0 -23
  297. package/dist/app/assets/script/view/toolkit/youtube-player-1.js +0 -34
  298. package/dist/app/assets/script/view/toolkit/youtube-player-1.js.map +0 -1
  299. package/dist/generators/app/index.d.ts +0 -18
  300. package/dist/generators/app/index.js +0 -59
  301. package/dist/generators/app/index.js.map +0 -1
  302. package/generators/app/index.js +0 -2
  303. package/generators/app/index.ts +0 -61
package/app/README.md CHANGED
@@ -1,19 +1,103 @@
1
- # Your white-label site
1
+ # White Label starter application
2
2
 
3
- This site uses TypeScript for application code, Handlebars for pages, and Sass plus full Bootstrap CSS for styling.
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 24. Run `npm install`, then `npm run typecheck` and `npm run build`. The build output is `_deploy`; serve that directory with your preferred static server. `npm test` runs the generated site's build check. The generator repository has the broader unit and coverage suites; add application-specific tests as your site grows.
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
- Edit `.ts` files under `app/assets/script`, and use `.tsx` for React components. Keep `.js` extensions in relative imports, for example `import User from './model/user.js'`. TypeScript resolves the source and emits valid JavaScript imports. Do not edit `dist`, which is generated by the compiler.
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
- Pages live in `app/*.hbs`, partials in `app/assets/markup`, and page data in `app/assets/data/view`. Add shared styles in `app/assets/style`. The full Bootstrap stylesheet is included before the project styles.
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
- Optional flags are `--www`, `--cdn`, and `--service`. Their defaults are `/`, `/`, and `/service-endpoint`. The version selects `_deploy/release/<version>/assets`; without a version, the current timestamp is used. Each build replaces `_deploy`.
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
- Document new functions with JSDoc comments explaining behavior, parameters, return values, and meaningful errors. Keep strict type checking enabled. Templates accept trusted HTML, so validate or escape external content before rendering it. Optional SDK adapters need real provider configuration before use.
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
- "title":"page title",
3
- "meta":{
4
- "description":"description here",
5
- "keywords":"keywords, here",
6
- "apple-mobile-web-app-title":"app title here"
7
- },
8
- "link":{
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": "Home",
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
+ }