mancha 0.17.3 → 0.17.5

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 (70) hide show
  1. package/.github/workflows/ci.yml +8 -8
  2. package/.prettierrc +2 -2
  3. package/.vscode/extensions.json +1 -1
  4. package/.vscode/launch.json +33 -43
  5. package/README.md +94 -94
  6. package/dist/browser.js.map +1 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/css_gen_basic.js.map +1 -1
  9. package/dist/css_gen_utils.d.ts +786 -0
  10. package/dist/css_gen_utils.js +63 -23
  11. package/dist/css_gen_utils.js.map +1 -1
  12. package/dist/dome.js.map +1 -1
  13. package/dist/expressions/ast.d.ts +16 -16
  14. package/dist/expressions/ast.test.js +89 -64
  15. package/dist/expressions/ast.test.js.map +1 -1
  16. package/dist/expressions/ast_factory.d.ts +1 -1
  17. package/dist/expressions/ast_factory.js +17 -17
  18. package/dist/expressions/ast_factory.js.map +1 -1
  19. package/dist/expressions/ast_factory.test.js +42 -36
  20. package/dist/expressions/ast_factory.test.js.map +1 -1
  21. package/dist/expressions/constants.js +56 -56
  22. package/dist/expressions/constants.js.map +1 -1
  23. package/dist/expressions/constants.test.js +57 -57
  24. package/dist/expressions/constants.test.js.map +1 -1
  25. package/dist/expressions/eval.d.ts +17 -17
  26. package/dist/expressions/eval.js +58 -60
  27. package/dist/expressions/eval.js.map +1 -1
  28. package/dist/expressions/eval.test.js +11 -8
  29. package/dist/expressions/eval.test.js.map +1 -1
  30. package/dist/expressions/expressions.test.d.ts +6 -6
  31. package/dist/expressions/expressions.test.js +6 -6
  32. package/dist/expressions/index.d.ts +6 -6
  33. package/dist/expressions/index.js +6 -6
  34. package/dist/expressions/parser.d.ts +3 -3
  35. package/dist/expressions/parser.js +37 -42
  36. package/dist/expressions/parser.js.map +1 -1
  37. package/dist/expressions/parser.test.js +3 -6
  38. package/dist/expressions/parser.test.js.map +1 -1
  39. package/dist/expressions/tokenizer.js +22 -25
  40. package/dist/expressions/tokenizer.js.map +1 -1
  41. package/dist/expressions/tokenizer.test.js +40 -15
  42. package/dist/expressions/tokenizer.test.js.map +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/iterator.js.map +1 -1
  45. package/dist/mancha.js.map +1 -1
  46. package/dist/plugins.js +2 -2
  47. package/dist/plugins.js.map +1 -1
  48. package/dist/query.js.map +1 -1
  49. package/dist/renderer.js.map +1 -1
  50. package/dist/safe_browser.js.map +1 -1
  51. package/dist/store.js +1 -1
  52. package/dist/store.js.map +1 -1
  53. package/dist/test_utils.js.map +1 -1
  54. package/dist/trusted_attributes.js.map +1 -1
  55. package/dist/type_checker.js +11 -7
  56. package/dist/type_checker.js.map +1 -1
  57. package/dist/worker.js.map +1 -1
  58. package/docs/css.md +419 -0
  59. package/docs/quickstart.md +305 -296
  60. package/global.d.ts +2 -2
  61. package/gulpfile.ts +44 -0
  62. package/package.json +86 -84
  63. package/scripts/generate-css-docs.ts +374 -0
  64. package/tsconfig.json +42 -19
  65. package/tsec_exemptions.json +8 -3
  66. package/webpack.config.esmodule.ts +26 -0
  67. package/webpack.config.ts +21 -0
  68. package/gulpfile.js +0 -44
  69. package/webpack.config.esmodule.js +0 -23
  70. package/webpack.config.js +0 -18
@@ -12,30 +12,31 @@ To get started, simply add this to your HTML head attribute:
12
12
  ## Basic Form
13
13
 
14
14
  After importing `Mancha`, you can take advantage of reactivity and tailwind-compatible CSS styling.
15
+ For a full list of supported CSS utilities and basic styles, see the [CSS Documentation](./css.md).
15
16
  For example, a basic form might look like this
16
17
 
17
18
  ```html
18
19
  <body :data="{ name: null }">
19
- <form
20
- class="flex flex-col max-w-md p-4 bg-white rounded-lg"
21
- :on:submit="console.log('submitted')"
22
- >
23
- <label class="w-full mb-4">
24
- <span class="block text-sm font-medium text-gray-700">Name</span>
25
- <input
26
- required
27
- type="text"
28
- :bind="name"
29
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
30
- />
31
- </label>
32
- <button
33
- type="submit"
34
- class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
35
- >
36
- Submit
37
- </button>
38
- </form>
20
+ <form
21
+ class="flex flex-col max-w-md p-4 bg-white rounded-lg"
22
+ :on:submit="console.log('submitted')"
23
+ >
24
+ <label class="w-full mb-4">
25
+ <span class="block text-sm font-medium text-gray-700">Name</span>
26
+ <input
27
+ required
28
+ type="text"
29
+ :bind="name"
30
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
31
+ />
32
+ </label>
33
+ <button
34
+ type="submit"
35
+ class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
36
+ >
37
+ Submit
38
+ </button>
39
+ </form>
39
40
  </body>
40
41
  ```
41
42
 
@@ -46,42 +47,42 @@ modifier to the event attribute, for example `:on:click.prevent`. To provide mor
46
47
 
47
48
  ```html
48
49
  <body :data="{ name: null, message: null }">
49
- <!-- Form with handler referencing a user-defined function -->
50
- <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
51
- <label class="w-full mb-4">
52
- <span class="block text-sm font-medium text-gray-700">Name</span>
53
- <input
54
- required
55
- type="text"
56
- :bind="name"
57
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
58
- />
59
- </label>
60
- <button
61
- type="submit"
62
- class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
63
- >
64
- Submit
65
- </button>
66
-
67
- <!-- To be shown only once `message` is truthy -->
68
- <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
69
- </form>
50
+ <!-- Form with handler referencing a user-defined function -->
51
+ <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
52
+ <label class="w-full mb-4">
53
+ <span class="block text-sm font-medium text-gray-700">Name</span>
54
+ <input
55
+ required
56
+ type="text"
57
+ :bind="name"
58
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
59
+ />
60
+ </label>
61
+ <button
62
+ type="submit"
63
+ class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
64
+ >
65
+ Submit
66
+ </button>
67
+
68
+ <!-- To be shown only once `message` is truthy -->
69
+ <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
70
+ </form>
70
71
  </body>
71
72
 
72
73
  <script>
73
- // Mancha is a global variable and $ is a shorthand for the renderer context.
74
- const { $ } = Mancha;
75
-
76
- // We can use the $ shorthand to access the form data and define variables.
77
- $.handleForm = function (event) {
78
- console.log(event);
79
- this.message = `Hello, ${this.name}!`;
80
- };
81
-
82
- // The script tag already contains the `init` attribute. So we don't need
83
- // to call `$.mount()` explicitly.
84
- // $.mount(document.body);
74
+ // Mancha is a global variable and $ is a shorthand for the renderer context.
75
+ const { $ } = Mancha;
76
+
77
+ // We can use the $ shorthand to access the form data and define variables.
78
+ $.handleForm = function (event) {
79
+ console.log(event);
80
+ this.message = `Hello, ${this.name}!`;
81
+ };
82
+
83
+ // The script tag already contains the `init` attribute. So we don't need
84
+ // to call `$.mount()` explicitly.
85
+ // $.mount(document.body);
85
86
  </script>
86
87
  ```
87
88
 
@@ -92,43 +93,43 @@ the `<script>` tag that imports `Mancha`, and explicitly call the `mount()` func
92
93
  <script src="//unpkg.com/mancha" css="utils"></script>
93
94
 
94
95
  <body>
95
- <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
96
- <label class="w-full mb-4">
97
- <span class="block text-sm font-medium text-gray-700">Name</span>
98
- <input
99
- required
100
- type="text"
101
- :bind="name"
102
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
103
- />
104
- </label>
105
- <button
106
- type="submit"
107
- class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
108
- >
109
- Submit
110
- </button>
111
-
112
- <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
113
- </form>
96
+ <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
97
+ <label class="w-full mb-4">
98
+ <span class="block text-sm font-medium text-gray-700">Name</span>
99
+ <input
100
+ required
101
+ type="text"
102
+ :bind="name"
103
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
104
+ />
105
+ </label>
106
+ <button
107
+ type="submit"
108
+ class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
109
+ >
110
+ Submit
111
+ </button>
112
+
113
+ <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
114
+ </form>
114
115
  </body>
115
116
 
116
117
  <script type="module">
117
- // Mancha is a global variable and $ is a shorthand for the renderer context.
118
- const { $ } = Mancha;
118
+ // Mancha is a global variable and $ is a shorthand for the renderer context.
119
+ const { $ } = Mancha;
119
120
 
120
- // We can use the $ shorthand to access the form data and define variables.
121
- $.handleForm = function (event) {
122
- console.log(event);
123
- this.message = `Hello, ${this.name}!`;
124
- };
121
+ // We can use the $ shorthand to access the form data and define variables.
122
+ $.handleForm = function (event) {
123
+ console.log(event);
124
+ this.message = `Hello, ${this.name}!`;
125
+ };
125
126
 
126
- // Define the variables that will be used in the form.
127
- $.name = null;
128
- $.message = null;
127
+ // Define the variables that will be used in the form.
128
+ $.name = null;
129
+ $.message = null;
129
130
 
130
- // Mount the renderer context to the body element.
131
- await $.mount(document.body);
131
+ // Mount the renderer context to the body element.
132
+ await $.mount(document.body);
132
133
  </script>
133
134
  ```
134
135
 
@@ -139,9 +140,9 @@ the `<script>` tag that imports `Mancha`, and explicitly call the `mount()` func
139
140
  ```html
140
141
  <!-- Use <template is="my-component-name"> to register a component. -->
141
142
  <template is="my-red-button">
142
- <button style="background-color: red;">
143
- <slot></slot>
144
- </button>
143
+ <button style="background-color: red;">
144
+ <slot></slot>
145
+ </button>
145
146
  </template>
146
147
  ```
147
148
 
@@ -169,17 +170,18 @@ Then in `index.html`:
169
170
  ```html
170
171
  <!-- src/index.html -->
171
172
  <head>
172
- <!-- ... -->
173
+ <!-- ... -->
173
174
  </head>
174
175
  <body>
175
- <!-- Include the custom component definition before using any of the components -->
176
- <include src="components/registry.tpl.html" />
176
+ <!-- Include the custom component definition before using any of the components -->
177
+ <include src="components/registry.tpl.html" />
177
178
 
178
- <!-- Now you can use any of the custom components -->
179
- <my-red-button>Click Me!</my-red-button>
179
+ <!-- Now you can use any of the custom components -->
180
+ <my-red-button>Click Me!</my-red-button>
180
181
 
181
- <!-- Any other components can also use the custom components, and don't need to re-import them -->
182
- <include src="components/footer.tpl.html"/>
182
+ <!-- Any other components can also use the custom components, and don't need to re-import them -->
183
+ <include src="components/footer.tpl.html" />
184
+ </body>
183
185
  ```
184
186
 
185
187
  ## Element Initialization with `:render`
@@ -197,14 +199,14 @@ The module's default export is called with the element and renderer as arguments
197
199
  ```js
198
200
  // chart-init.js
199
201
  export default function (elem, renderer) {
200
- // Initialize a chart library on the canvas element
201
- const chart = new Chart(elem, {
202
- type: "bar",
203
- data: { labels: ["A", "B", "C"], datasets: [{ data: [1, 2, 3] }] },
204
- });
205
-
206
- // Optionally store a reference for later access
207
- elem._chart = chart;
202
+ // Initialize a chart library on the canvas element
203
+ const chart = new Chart(elem, {
204
+ type: "bar",
205
+ data: { labels: ["A", "B", "C"], datasets: [{ data: [1, 2, 3] }] },
206
+ });
207
+
208
+ // Optionally store a reference for later access
209
+ elem._chart = chart;
208
210
  }
209
211
  ```
210
212
 
@@ -224,26 +226,26 @@ src/
224
226
  ```html
225
227
  <!-- components/chart-widget.tpl.html -->
226
228
  <template is="chart-widget">
227
- <div class="chart-container">
228
- <canvas :render="./chart-widget.js"></canvas>
229
- <slot></slot>
230
- </div>
229
+ <div class="chart-container">
230
+ <canvas :render="./chart-widget.js"></canvas>
231
+ <slot></slot>
232
+ </div>
231
233
  </template>
232
234
  ```
233
235
 
234
236
  ```js
235
237
  // components/chart-widget.js
236
238
  export default function (elem, renderer) {
237
- // Access data passed via :data attribute on the component
238
- const { labels, values } = renderer.$;
239
-
240
- new Chart(elem, {
241
- type: "bar",
242
- data: {
243
- labels: labels || ["A", "B", "C"],
244
- datasets: [{ data: values || [1, 2, 3] }],
245
- },
246
- });
239
+ // Access data passed via :data attribute on the component
240
+ const { labels, values } = renderer.$;
241
+
242
+ new Chart(elem, {
243
+ type: "bar",
244
+ data: {
245
+ labels: labels || ["A", "B", "C"],
246
+ datasets: [{ data: values || [1, 2, 3] }],
247
+ },
248
+ });
247
249
  }
248
250
  ```
249
251
 
@@ -255,17 +257,17 @@ export default function (elem, renderer) {
255
257
  ```html
256
258
  <!-- index.html -->
257
259
  <body>
258
- <include src="components/registry.tpl.html" />
260
+ <include src="components/registry.tpl.html" />
259
261
 
260
- <!-- Use the component with custom data -->
261
- <chart-widget :data="{ labels: ['Jan', 'Feb', 'Mar'], values: [10, 20, 15] }">
262
- <p>Monthly Sales</p>
263
- </chart-widget>
262
+ <!-- Use the component with custom data -->
263
+ <chart-widget :data="{ labels: ['Jan', 'Feb', 'Mar'], values: [10, 20, 15] }">
264
+ <p>Monthly Sales</p>
265
+ </chart-widget>
264
266
 
265
- <!-- Use it again with different data -->
266
- <chart-widget :data="{ labels: ['Q1', 'Q2', 'Q3', 'Q4'], values: [100, 150, 120, 180] }">
267
- <p>Quarterly Revenue</p>
268
- </chart-widget>
267
+ <!-- Use it again with different data -->
268
+ <chart-widget :data="{ labels: ['Q1', 'Q2', 'Q3', 'Q4'], values: [100, 150, 120, 180] }">
269
+ <p>Quarterly Revenue</p>
270
+ </chart-widget>
269
271
  </body>
270
272
  ```
271
273
 
@@ -288,19 +290,19 @@ The init function receives the renderer instance, giving you access to reactive
288
290
  ```js
289
291
  // counter-canvas.js
290
292
  export default function (elem, renderer) {
291
- const ctx = elem.getContext("2d");
293
+ const ctx = elem.getContext("2d");
292
294
 
293
- // Access current state
294
- const count = renderer.$.count;
295
+ // Access current state
296
+ const count = renderer.$.count;
295
297
 
296
- // Draw based on state
297
- ctx.fillText(`Count: ${count}`, 10, 50);
298
+ // Draw based on state
299
+ ctx.fillText(`Count: ${count}`, 10, 50);
298
300
 
299
- // Watch for changes using the renderer's effect system
300
- renderer.effect(function () {
301
- ctx.clearRect(0, 0, elem.width, elem.height);
302
- ctx.fillText(`Count: ${this.$.count}`, 10, 50);
303
- });
301
+ // Watch for changes using the renderer's effect system
302
+ renderer.effect(function () {
303
+ ctx.clearRect(0, 0, elem.width, elem.height);
304
+ ctx.fillText(`Count: ${this.$.count}`, 10, 50);
305
+ });
304
306
  }
305
307
  ```
306
308
 
@@ -317,28 +319,28 @@ element's renderer, setting variables through it will trigger reactive updates i
317
319
 
318
320
  ```html
319
321
  <div :render="./data-loader.js">
320
- <h1>{{ pageTitle }}</h1>
321
- <ul :for="item in dataItems">
322
- <li>{{ item.name }}: {{ item.value }}</li>
323
- </ul>
324
- <p :show="loading">Loading...</p>
322
+ <h1>{{ pageTitle }}</h1>
323
+ <ul :for="item in dataItems">
324
+ <li>{{ item.name }}: {{ item.value }}</li>
325
+ </ul>
326
+ <p :show="loading">Loading...</p>
325
327
  </div>
326
328
  ```
327
329
 
328
330
  ```js
329
331
  // data-loader.js
330
332
  export default async function (elem, renderer) {
331
- // Set loading state.
332
- await renderer.set("loading", true);
333
+ // Set loading state.
334
+ await renderer.set("loading", true);
333
335
 
334
- // Fetch data from an API.
335
- const response = await fetch("/api/data");
336
- const data = await response.json();
336
+ // Fetch data from an API.
337
+ const response = await fetch("/api/data");
338
+ const data = await response.json();
337
339
 
338
- // Set the variables - the template will reactively update.
339
- await renderer.set("pageTitle", data.title);
340
- await renderer.set("dataItems", data.items);
341
- await renderer.set("loading", false);
340
+ // Set the variables - the template will reactively update.
341
+ await renderer.set("pageTitle", data.title);
342
+ await renderer.set("dataItems", data.items);
343
+ await renderer.set("loading", false);
342
344
  }
343
345
  ```
344
346
 
@@ -356,20 +358,20 @@ Here's how you can use it:
356
358
 
357
359
  ```html
358
360
  <body :data="{ $$search: '' }">
359
- <form class="flex flex-col max-w-md p-4 bg-white rounded-lg">
360
- <label class="w-full mb-4">
361
- <span class="block text-sm font-medium text-gray-700">Search</span>
362
- <input
363
- type="text"
364
- :bind="$$search"
365
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
366
- placeholder="Type to see the URL change..."
367
- />
368
- </label>
369
- <div :show="$$search" class="mt-2">
370
- <p>Current search query: <span class="font-mono">{{ $$search }}</span></p>
371
- </div>
372
- </form>
361
+ <form class="flex flex-col max-w-md p-4 bg-white rounded-lg">
362
+ <label class="w-full mb-4">
363
+ <span class="block text-sm font-medium text-gray-700">Search</span>
364
+ <input
365
+ type="text"
366
+ :bind="$$search"
367
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
368
+ placeholder="Type to see the URL change..."
369
+ />
370
+ </label>
371
+ <div :show="$$search" class="mt-2">
372
+ <p>Current search query: <span class="font-mono">{{ $$search }}</span></p>
373
+ </div>
374
+ </form>
373
375
  </body>
374
376
  ```
375
377
 
@@ -384,8 +386,8 @@ Here's an example of how you can test a simple component:
384
386
  ```html
385
387
  <!-- my-component.html -->
386
388
  <body>
387
- <button data-testid="login-button" :show="!user">Login</button>
388
- <button data-testid="logout-button" :show="user">Logout</button>
389
+ <button data-testid="login-button" :show="!user">Login</button>
390
+ <button data-testid="logout-button" :show="user">Logout</button>
389
391
  </body>
390
392
  ```
391
393
 
@@ -401,45 +403,44 @@ const componentPath = path.join(import.meta.dirname, "my-component.html");
401
403
  const findByTestId = (node, testId) => node.querySelector(`[data-testid="${testId}"]`);
402
404
 
403
405
  describe("My Component", () => {
406
+ test("renders correctly when logged out", async () => {
407
+ // 1. Initialize the renderer with the desired state for this test case.
408
+ const renderer = new Renderer({ user: null });
404
409
 
405
- test("renders correctly when logged out", async () => {
406
- // 1. Initialize the renderer with the desired state for this test case.
407
- const renderer = new Renderer({ user: null });
408
-
409
- // 2. Create a clean DOM fragment from the page content.
410
- const fragment = await renderer.preprocessLocal(componentPath);
410
+ // 2. Create a clean DOM fragment from the page content.
411
+ const fragment = await renderer.preprocessLocal(componentPath);
411
412
 
412
- // 3. Mount the renderer to the fragment to apply data bindings.
413
- await renderer.mount(fragment);
413
+ // 3. Mount the renderer to the fragment to apply data bindings.
414
+ await renderer.mount(fragment);
414
415
 
415
- // 4. Find elements and assert their state.
416
- const loginButton = findByTestId(fragment, "login-button");
417
- const logoutButton = findByTestId(fragment, "logout-button");
416
+ // 4. Find elements and assert their state.
417
+ const loginButton = findByTestId(fragment, "login-button");
418
+ const logoutButton = findByTestId(fragment, "logout-button");
418
419
 
419
- assert.ok(loginButton, "Login button should exist");
420
- assert.ok(logoutButton, "Logout button should exist");
420
+ assert.ok(loginButton, "Login button should exist");
421
+ assert.ok(logoutButton, "Logout button should exist");
421
422
 
422
- assert.strictEqual(loginButton.style.display, "", "Login button should be visible");
423
- assert.strictEqual(logoutButton.style.display, "none", "Logout button should be hidden");
424
- });
423
+ assert.strictEqual(loginButton.style.display, "", "Login button should be visible");
424
+ assert.strictEqual(logoutButton.style.display, "none", "Logout button should be hidden");
425
+ });
425
426
 
426
- test("renders correctly when logged in", async () => {
427
- // 1. Initialize the renderer with the desired state for this test case.
428
- const renderer = new Renderer({ user: { name: "John Doe" } });
427
+ test("renders correctly when logged in", async () => {
428
+ // 1. Initialize the renderer with the desired state for this test case.
429
+ const renderer = new Renderer({ user: { name: "John Doe" } });
429
430
 
430
- // 2. Create a clean DOM fragment from the page content.
431
- const fragment = await renderer.preprocessLocal(componentPath);
431
+ // 2. Create a clean DOM fragment from the page content.
432
+ const fragment = await renderer.preprocessLocal(componentPath);
432
433
 
433
- // 3. Mount the renderer to the fragment to apply data bindings.
434
- await renderer.mount(fragment);
434
+ // 3. Mount the renderer to the fragment to apply data bindings.
435
+ await renderer.mount(fragment);
435
436
 
436
- // 4. Find elements and assert their state.
437
- const loginButton = findByTestId(fragment, "login-button");
438
- const logoutButton = findByTestId(fragment, "logout-button");
437
+ // 4. Find elements and assert their state.
438
+ const loginButton = findByTestId(fragment, "login-button");
439
+ const logoutButton = findByTestId(fragment, "logout-button");
439
440
 
440
- assert.strictEqual(loginButton.style.display, "none", "Login button should be hidden");
441
- assert.strictEqual(logoutButton.style.display, "", "Logout button should be visible");
442
- });
441
+ assert.strictEqual(loginButton.style.display, "none", "Login button should be hidden");
442
+ assert.strictEqual(logoutButton.style.display, "", "Logout button should be visible");
443
+ });
443
444
  });
444
445
  ```
445
446
 
@@ -451,15 +452,15 @@ The `Renderer` and `SignalStore` classes support generic type parameters for typ
451
452
  import { Renderer } from "mancha";
452
453
 
453
454
  interface AppState {
454
- user: { name: string; email: string } | null;
455
- count: number;
456
- items: string[];
455
+ user: { name: string; email: string } | null;
456
+ count: number;
457
+ items: string[];
457
458
  }
458
459
 
459
460
  const renderer = new Renderer<AppState>({
460
- user: null,
461
- count: 0,
462
- items: ["a", "b"],
461
+ user: null,
462
+ count: 0,
463
+ items: ["a", "b"],
463
464
  });
464
465
 
465
466
  // Type-safe access via the $ proxy
@@ -491,12 +492,13 @@ Use the `:types` attribute to declare types for variables in your templates:
491
492
 
492
493
  ```html
493
494
  <div :types='{"name": "string", "age": "number"}'>
494
- <span>{{ name.toUpperCase() }}</span>
495
- <span>{{ age.toFixed(0) }}</span>
495
+ <span>{{ name.toUpperCase() }}</span>
496
+ <span>{{ age.toFixed(0) }}</span>
496
497
  </div>
497
498
  ```
498
499
 
499
500
  The type checker will validate that:
501
+
500
502
  - `name.toUpperCase()` is valid (string has toUpperCase method)
501
503
  - `age.toFixed(0)` is valid (number has toFixed method)
502
504
  - Using `name.toFixed()` would be an error (string doesn't have toFixed)
@@ -538,11 +540,11 @@ The type checker understands `:for` loops and infers the item type from the arra
538
540
 
539
541
  ```html
540
542
  <div :types='{"users": "{ name: string, age: number }[]"}'>
541
- <ul :for="user in users">
542
- <!-- 'user' is automatically typed as { name: string, age: number } -->
543
- <li>{{ user.name.toUpperCase() }}</li>
544
- <li>{{ user.age.toFixed(0) }}</li>
545
- </ul>
543
+ <ul :for="user in users">
544
+ <!-- 'user' is automatically typed as { name: string, age: number } -->
545
+ <li>{{ user.name.toUpperCase() }}</li>
546
+ <li>{{ user.age.toFixed(0) }}</li>
547
+ </ul>
546
548
  </div>
547
549
  ```
548
550
 
@@ -552,14 +554,14 @@ Child scopes inherit types from parent scopes:
552
554
 
553
555
  ```html
554
556
  <div :types='{"name": "string", "age": "number"}'>
555
- <span>{{ name.toUpperCase() }}</span>
556
-
557
- <div :types='{"city": "string"}'>
558
- <!-- This scope has access to: name, age, and city -->
559
- <span>{{ name.toLowerCase() }}</span>
560
- <span>{{ city.toUpperCase() }}</span>
561
- <span>{{ age.toFixed(0) }}</span>
562
- </div>
557
+ <span>{{ name.toUpperCase() }}</span>
558
+
559
+ <div :types='{"city": "string"}'>
560
+ <!-- This scope has access to: name, age, and city -->
561
+ <span>{{ name.toLowerCase() }}</span>
562
+ <span>{{ city.toUpperCase() }}</span>
563
+ <span>{{ age.toFixed(0) }}</span>
564
+ </div>
563
565
  </div>
564
566
  ```
565
567
 
@@ -567,15 +569,15 @@ Child scopes can override parent types:
567
569
 
568
570
  ```html
569
571
  <div :types='{"value": "string"}'>
570
- <span>{{ value.toUpperCase() }}</span>
572
+ <span>{{ value.toUpperCase() }}</span>
571
573
 
572
- <div :types='{"value": "number"}'>
573
- <!-- 'value' is now number, not string -->
574
- <span>{{ value.toFixed(2) }}</span>
575
- </div>
574
+ <div :types='{"value": "number"}'>
575
+ <!-- 'value' is now number, not string -->
576
+ <span>{{ value.toFixed(2) }}</span>
577
+ </div>
576
578
 
577
- <!-- Back to string scope -->
578
- <span>{{ value.toLowerCase() }}</span>
579
+ <!-- Back to string scope -->
580
+ <span>{{ value.toLowerCase() }}</span>
579
581
  </div>
580
582
  ```
581
583
 
@@ -588,25 +590,25 @@ You can import TypeScript types from external files using the `@import:` syntax:
588
590
  ```typescript
589
591
  // types/user.ts
590
592
  export interface User {
591
- id: number;
592
- name: string;
593
- email: string;
594
- isAdmin: boolean;
593
+ id: number;
594
+ name: string;
595
+ email: string;
596
+ isAdmin: boolean;
595
597
  }
596
598
 
597
599
  export interface Product {
598
- id: number;
599
- name: string;
600
- price: number;
600
+ id: number;
601
+ name: string;
602
+ price: number;
601
603
  }
602
604
  ```
603
605
 
604
606
  ```html
605
607
  <!-- Import a single type -->
606
608
  <div :types='{"user": "@import:./types/user.ts:User"}'>
607
- <span>{{ user.name.toUpperCase() }}</span>
608
- <span>{{ user.email.toLowerCase() }}</span>
609
- <span :show="user.isAdmin">Admin Badge</span>
609
+ <span>{{ user.name.toUpperCase() }}</span>
610
+ <span>{{ user.email.toLowerCase() }}</span>
611
+ <span :show="user.isAdmin">Admin Badge</span>
610
612
  </div>
611
613
  ```
612
614
 
@@ -620,20 +622,21 @@ The format is: `@import:MODULE_PATH:TYPE_NAME`
620
622
  - **TYPE_NAME**: The exported type/interface name
621
623
 
622
624
  **Examples of external package imports:**
625
+
623
626
  ```html
624
627
  <!-- Import from a standard package -->
625
628
  <div :types='{"program": "@import:typescript:Program"}'>
626
- <span>{{ program }}</span>
629
+ <span>{{ program }}</span>
627
630
  </div>
628
631
 
629
632
  <!-- Import from package subpath (with package.json exports) -->
630
633
  <div :types='{"parser": "@import:yargs/helpers:Parser.Arguments"}'>
631
- <span>{{ parser }}</span>
634
+ <span>{{ parser }}</span>
632
635
  </div>
633
636
 
634
637
  <!-- Import from your own packages in a monorepo -->
635
638
  <div :types='{"data": "@import:my-shared-lib/types:ApiResponse"}'>
636
- <span>{{ data.status }}</span>
639
+ <span>{{ data.status }}</span>
637
640
  </div>
638
641
  ```
639
642
 
@@ -641,23 +644,25 @@ The format is: `@import:MODULE_PATH:TYPE_NAME`
641
644
 
642
645
  ```html
643
646
  <div :types='{"users": "@import:./types/user.ts:User[]"}'>
644
- <ul :for="user in users">
645
- <li>{{ user.name }} - {{ user.email }}</li>
646
- </ul>
647
+ <ul :for="user in users">
648
+ <li>{{ user.name }} - {{ user.email }}</li>
649
+ </ul>
647
650
  </div>
648
651
  ```
649
652
 
650
653
  #### Multiple Imports
651
654
 
652
655
  ```html
653
- <div :types='{
656
+ <div
657
+ :types='{
654
658
  "user": "@import:./types/user.ts:User",
655
659
  "product": "@import:./types/user.ts:Product",
656
660
  "count": "number"
657
- }'>
658
- <span>{{ user.name }}</span>
659
- <span>{{ product.name }} - ${{ product.price.toFixed(2) }}</span>
660
- <span>Total: {{ count }}</span>
661
+ }'
662
+ >
663
+ <span>{{ user.name }}</span>
664
+ <span>{{ product.name }} - ${{ product.price.toFixed(2) }}</span>
665
+ <span>Total: {{ count }}</span>
661
666
  </div>
662
667
  ```
663
668
 
@@ -668,22 +673,22 @@ Use imports anywhere you'd use a type:
668
673
  ```html
669
674
  <!-- In object types -->
670
675
  <div :types='{"response": "{ data: @import:./types/user.ts:User[], total: number }"}'>
671
- <span>Total users: {{ response.total }}</span>
672
- <ul :for="user in response.data">
673
- <li>{{ user.name }}</li>
674
- </ul>
676
+ <span>Total users: {{ response.total }}</span>
677
+ <ul :for="user in response.data">
678
+ <li>{{ user.name }}</li>
679
+ </ul>
675
680
  </div>
676
681
 
677
682
  <!-- With generics -->
678
683
  <div :types='{"response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>"}'>
679
- <span>{{ response.data.name }}</span>
680
- <span>Status: {{ response.status }}</span>
684
+ <span>{{ response.data.name }}</span>
685
+ <span>Status: {{ response.status }}</span>
681
686
  </div>
682
687
 
683
688
  <!-- With unions -->
684
689
  <div :types='{"user": "@import:./types/user.ts:User | null"}'>
685
- <span :show="user !== null">{{ user.name }}</span>
686
- <span :show="user === null">Not logged in</span>
690
+ <span :show="user !== null">{{ user.name }}</span>
691
+ <span :show="user === null">Not logged in</span>
687
692
  </div>
688
693
  ```
689
694
 
@@ -693,12 +698,12 @@ Imports are inherited by nested scopes:
693
698
 
694
699
  ```html
695
700
  <div :types='{"user": "@import:./types/user.ts:User"}'>
696
- <span>{{ user.name }}</span>
701
+ <span>{{ user.name }}</span>
697
702
 
698
- <div :types='{"product": "@import:./types/user.ts:Product"}'>
699
- <!-- Has access to both User and Product types -->
700
- <span>{{ user.name }} bought {{ product.name }}</span>
701
- </div>
703
+ <div :types='{"product": "@import:./types/user.ts:Product"}'>
704
+ <!-- Has access to both User and Product types -->
705
+ <span>{{ user.name }} bought {{ product.name }}</span>
706
+ </div>
702
707
  </div>
703
708
  ```
704
709
 
@@ -706,26 +711,26 @@ Imports are inherited by nested scopes:
706
711
 
707
712
  ```html
708
713
  <div :types='{"orders": "@import:./types/orders.ts:Order[]"}'>
709
- <div :for="order in orders">
710
- <h2>Order #{{ order.id }}</h2>
711
- <p>Customer: {{ order.customer.name }}</p>
712
-
713
- <div :types='{"selectedProduct": "@import:./types/products.ts:Product"}'>
714
- <ul :for="item in order.items">
715
- <li>
716
- {{ item.product.name }} x {{ item.quantity }}
717
- = ${{ (item.product.price * item.quantity).toFixed(2) }}
718
- </li>
719
- </ul>
720
-
721
- <div :show="selectedProduct">
722
- <h3>Selected: {{ selectedProduct.name }}</h3>
723
- <p>${{ selectedProduct.price.toFixed(2) }}</p>
724
- </div>
725
- </div>
726
-
727
- <p>Total: ${{ order.total.toFixed(2) }}</p>
728
- </div>
714
+ <div :for="order in orders">
715
+ <h2>Order #{{ order.id }}</h2>
716
+ <p>Customer: {{ order.customer.name }}</p>
717
+
718
+ <div :types='{"selectedProduct": "@import:./types/products.ts:Product"}'>
719
+ <ul :for="item in order.items">
720
+ <li>
721
+ {{ item.product.name }} x {{ item.quantity }} = ${{ (item.product.price *
722
+ item.quantity).toFixed(2) }}
723
+ </li>
724
+ </ul>
725
+
726
+ <div :show="selectedProduct">
727
+ <h3>Selected: {{ selectedProduct.name }}</h3>
728
+ <p>${{ selectedProduct.price.toFixed(2) }}</p>
729
+ </div>
730
+ </div>
731
+
732
+ <p>Total: ${{ order.total.toFixed(2) }}</p>
733
+ </div>
729
734
  </div>
730
735
  ```
731
736
 
@@ -749,36 +754,40 @@ Imports are inherited by nested scopes:
749
754
  #### Form Validation
750
755
 
751
756
  ```html
752
- <div :types='{
757
+ <div
758
+ :types='{
753
759
  "formData": "{ name: string, email: string, age: number }",
754
760
  "errors": "{ name?: string, email?: string, age?: string }"
755
- }'>
756
- <form>
757
- <input type="text" :bind="formData.name" />
758
- <span :show="errors.name" class="error">{{ errors.name }}</span>
759
-
760
- <input type="email" :bind="formData.email" />
761
- <span :show="errors.email" class="error">{{ errors.email }}</span>
762
-
763
- <input type="number" :bind="formData.age" />
764
- <span :show="errors.age" class="error">{{ errors.age }}</span>
765
- </form>
761
+ }'
762
+ >
763
+ <form>
764
+ <input type="text" :bind="formData.name" />
765
+ <span :show="errors.name" class="error">{{ errors.name }}</span>
766
+
767
+ <input type="email" :bind="formData.email" />
768
+ <span :show="errors.email" class="error">{{ errors.email }}</span>
769
+
770
+ <input type="number" :bind="formData.age" />
771
+ <span :show="errors.age" class="error">{{ errors.age }}</span>
772
+ </form>
766
773
  </div>
767
774
  ```
768
775
 
769
776
  #### API Response Handling
770
777
 
771
778
  ```html
772
- <div :types='{
779
+ <div
780
+ :types='{
773
781
  "response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>",
774
782
  "loading": "boolean",
775
783
  "error": "string | null"
776
- }'>
777
- <div :show="loading">Loading...</div>
778
- <div :show="error">Error: {{ error }}</div>
779
- <div :show="!loading && !error && response">
780
- <h1>{{ response.data.name }}</h1>
781
- <p>{{ response.data.email }}</p>
782
- </div>
784
+ }'
785
+ >
786
+ <div :show="loading">Loading...</div>
787
+ <div :show="error">Error: {{ error }}</div>
788
+ <div :show="!loading && !error && response">
789
+ <h1>{{ response.data.name }}</h1>
790
+ <p>{{ response.data.email }}</p>
791
+ </div>
783
792
  </div>
784
793
  ```