ripple 0.2.208 → 0.2.210

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 (108) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/package.json +2 -6
  4. package/shims/rollup-estree-types.d.ts +1 -1
  5. package/src/compiler/index.d.ts +1 -0
  6. package/src/compiler/index.js +7 -1
  7. package/src/compiler/phases/1-parse/index.js +15 -6
  8. package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
  9. package/src/compiler/phases/2-analyze/index.js +215 -2
  10. package/src/compiler/phases/3-transform/client/index.js +388 -50
  11. package/src/compiler/phases/3-transform/segments.js +123 -39
  12. package/src/compiler/phases/3-transform/server/index.js +266 -13
  13. package/src/compiler/types/index.d.ts +16 -3
  14. package/src/compiler/utils.js +1 -15
  15. package/src/constants.js +0 -2
  16. package/src/helpers.d.ts +4 -0
  17. package/src/html-tree-validation.js +211 -0
  18. package/src/jsx-runtime.d.ts +260 -259
  19. package/src/jsx-runtime.js +12 -12
  20. package/src/runtime/array.js +17 -17
  21. package/src/runtime/create-subscriber.js +1 -1
  22. package/src/runtime/index-client.js +1 -5
  23. package/src/runtime/index-server.js +15 -0
  24. package/src/runtime/internal/client/compat.js +3 -3
  25. package/src/runtime/internal/client/composite.js +6 -1
  26. package/src/runtime/internal/client/head.js +50 -4
  27. package/src/runtime/internal/client/html.js +73 -12
  28. package/src/runtime/internal/client/hydration.js +12 -0
  29. package/src/runtime/internal/client/index.js +1 -1
  30. package/src/runtime/internal/client/portal.js +54 -29
  31. package/src/runtime/internal/client/rpc.js +3 -1
  32. package/src/runtime/internal/client/switch.js +5 -0
  33. package/src/runtime/internal/client/template.js +117 -11
  34. package/src/runtime/internal/client/try.js +1 -0
  35. package/src/runtime/internal/server/index.js +113 -1
  36. package/src/runtime/internal/server/rpc.js +4 -4
  37. package/src/runtime/map.js +2 -2
  38. package/src/runtime/object.js +6 -6
  39. package/src/runtime/proxy.js +12 -11
  40. package/src/runtime/reactive-value.js +9 -1
  41. package/src/runtime/set.js +12 -7
  42. package/src/runtime/url-search-params.js +0 -1
  43. package/src/server/index.js +4 -0
  44. package/src/utils/hashing.js +15 -0
  45. package/src/utils/normalize_css_property_name.js +1 -1
  46. package/tests/client/array/array.mutations.test.ripple +8 -8
  47. package/tests/client/basic/basic.errors.test.ripple +28 -0
  48. package/tests/client/basic/basic.events.test.ripple +6 -3
  49. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  50. package/tests/client/compiler/compiler.regex.test.ripple +10 -8
  51. package/tests/client/composite/composite.generics.test.ripple +5 -2
  52. package/tests/client/dynamic-elements.test.ripple +30 -1
  53. package/tests/client/function-overload-import.ripple +6 -7
  54. package/tests/client/html.test.ripple +0 -1
  55. package/tests/client/object.test.ripple +2 -2
  56. package/tests/client/portal.test.ripple +3 -3
  57. package/tests/client/return.test.ripple +2500 -0
  58. package/tests/client/try.test.ripple +69 -0
  59. package/tests/client/typescript-generics.test.ripple +1 -1
  60. package/tests/client/url/url.derived.test.ripple +1 -1
  61. package/tests/client/url/url.parsing.test.ripple +3 -3
  62. package/tests/client/url/url.partial-removal.test.ripple +7 -7
  63. package/tests/client/url/url.reactivity.test.ripple +15 -15
  64. package/tests/client/url/url.serialization.test.ripple +2 -2
  65. package/tests/hydration/basic.test.js +23 -0
  66. package/tests/hydration/build-components.js +10 -4
  67. package/tests/hydration/compiled/client/basic.js +165 -3
  68. package/tests/hydration/compiled/client/for.js +1140 -23
  69. package/tests/hydration/compiled/client/head.js +234 -0
  70. package/tests/hydration/compiled/client/html.js +135 -0
  71. package/tests/hydration/compiled/client/portal.js +172 -0
  72. package/tests/hydration/compiled/client/reactivity.js +3 -1
  73. package/tests/hydration/compiled/client/return.js +1976 -0
  74. package/tests/hydration/compiled/client/switch.js +162 -0
  75. package/tests/hydration/compiled/server/basic.js +249 -0
  76. package/tests/hydration/compiled/server/events.js +1 -1
  77. package/tests/hydration/compiled/server/for.js +891 -1
  78. package/tests/hydration/compiled/server/head.js +291 -0
  79. package/tests/hydration/compiled/server/html.js +133 -0
  80. package/tests/hydration/compiled/server/if.js +1 -1
  81. package/tests/hydration/compiled/server/portal.js +250 -0
  82. package/tests/hydration/compiled/server/reactivity.js +1 -1
  83. package/tests/hydration/compiled/server/return.js +1969 -0
  84. package/tests/hydration/compiled/server/switch.js +130 -0
  85. package/tests/hydration/components/basic.ripple +55 -0
  86. package/tests/hydration/components/for.ripple +403 -0
  87. package/tests/hydration/components/head.ripple +111 -0
  88. package/tests/hydration/components/html.ripple +38 -0
  89. package/tests/hydration/components/portal.ripple +49 -0
  90. package/tests/hydration/components/return.ripple +564 -0
  91. package/tests/hydration/components/switch.ripple +51 -0
  92. package/tests/hydration/for.test.js +363 -0
  93. package/tests/hydration/head.test.js +105 -0
  94. package/tests/hydration/html.test.js +46 -0
  95. package/tests/hydration/portal.test.js +71 -0
  96. package/tests/hydration/return.test.js +544 -0
  97. package/tests/hydration/switch.test.js +42 -0
  98. package/tests/server/basic.attributes.test.ripple +1 -1
  99. package/tests/server/compiler.test.ripple +22 -0
  100. package/tests/server/composite.test.ripple +5 -2
  101. package/tests/server/html-nesting-validation.test.ripple +237 -0
  102. package/tests/server/return.test.ripple +1379 -0
  103. package/tests/setup-hydration.js +6 -1
  104. package/tests/utils/escaping.test.js +3 -1
  105. package/tests/utils/normalize_css_property_name.test.js +0 -1
  106. package/tests/utils/patterns.test.js +6 -2
  107. package/tests/utils/sanitize_template_string.test.js +3 -2
  108. package/types/server.d.ts +16 -0
@@ -0,0 +1,130 @@
1
+ import * as _$_ from 'ripple/internal/server';
2
+
3
+ import { track } from 'ripple/server';
4
+
5
+ export function SwitchStatic(__output) {
6
+ _$_.push_component();
7
+
8
+ const status = 'success';
9
+
10
+ __output.push('<!--[-->');
11
+
12
+ switch (status) {
13
+ case 'success':
14
+ __output.push('<div');
15
+ __output.push(' class="status-success"');
16
+ __output.push('>');
17
+ {
18
+ __output.push('Success');
19
+ }
20
+ __output.push('</div>');
21
+ break;
22
+
23
+ case 'error':
24
+ __output.push('<div');
25
+ __output.push(' class="status-error"');
26
+ __output.push('>');
27
+ {
28
+ __output.push('Error');
29
+ }
30
+ __output.push('</div>');
31
+ break;
32
+
33
+ default:
34
+ __output.push('<div');
35
+ __output.push(' class="status-unknown"');
36
+ __output.push('>');
37
+ {
38
+ __output.push('Unknown');
39
+ }
40
+ __output.push('</div>');
41
+ }
42
+
43
+ __output.push('<!--]-->');
44
+ _$_.pop_component();
45
+ }
46
+
47
+ export function SwitchReactive(__output) {
48
+ _$_.push_component();
49
+
50
+ let status = track('a');
51
+
52
+ __output.push('<button');
53
+ __output.push(' class="toggle"');
54
+ __output.push('>');
55
+
56
+ {
57
+ __output.push('Toggle');
58
+ }
59
+
60
+ __output.push('</button>');
61
+ __output.push('<!--[-->');
62
+
63
+ switch (_$_.get(status)) {
64
+ case 'a':
65
+ __output.push('<div');
66
+ __output.push(' class="case-a"');
67
+ __output.push('>');
68
+ {
69
+ __output.push('Case A');
70
+ }
71
+ __output.push('</div>');
72
+ break;
73
+
74
+ case 'b':
75
+ __output.push('<div');
76
+ __output.push(' class="case-b"');
77
+ __output.push('>');
78
+ {
79
+ __output.push('Case B');
80
+ }
81
+ __output.push('</div>');
82
+ break;
83
+
84
+ default:
85
+ __output.push('<div');
86
+ __output.push(' class="case-c"');
87
+ __output.push('>');
88
+ {
89
+ __output.push('Case C');
90
+ }
91
+ __output.push('</div>');
92
+ }
93
+
94
+ __output.push('<!--]-->');
95
+ _$_.pop_component();
96
+ }
97
+
98
+ export function SwitchFallthrough(__output) {
99
+ _$_.push_component();
100
+
101
+ const val = 1;
102
+
103
+ __output.push('<!--[-->');
104
+
105
+ switch (val) {
106
+ case 1:
107
+
108
+ case 2:
109
+ __output.push('<div');
110
+ __output.push(' class="case-1-2"');
111
+ __output.push('>');
112
+ {
113
+ __output.push('1 or 2');
114
+ }
115
+ __output.push('</div>');
116
+ break;
117
+
118
+ default:
119
+ __output.push('<div');
120
+ __output.push(' class="case-other"');
121
+ __output.push('>');
122
+ {
123
+ __output.push('Other');
124
+ }
125
+ __output.push('</div>');
126
+ }
127
+
128
+ __output.push('<!--]-->');
129
+ _$_.pop_component();
130
+ }
@@ -63,3 +63,58 @@ export component ExpressionContent() {
63
63
  <div>{value}</div>
64
64
  <span>{text.toUpperCase()}</span>
65
65
  }
66
+
67
+ // Test for static content in child component followed by sibling content
68
+ component StaticHeader() {
69
+ <h1 class="sr-only">{'heading'}</h1>
70
+ <p class="subtitle">{'first paragraph'}</p>
71
+ <p class="subtitle">{'second paragraph'}</p>
72
+ }
73
+
74
+ export component StaticChildWithSiblings() {
75
+ const foo = 'bar';
76
+ <StaticHeader />
77
+ <span class="sibling1">{foo}</span>
78
+ <span class="sibling2">{foo}</span>
79
+ }
80
+
81
+ // Website-like components for testing complex hydration scenarios
82
+
83
+ component Header() {
84
+ <h1 class="sr-only">{'Ripple'}</h1>
85
+ <img src="/images/logo.png" alt="Logo" class="logo" />
86
+ <p class="subtitle">{'the elegant TypeScript UI framework'}</p>
87
+ }
88
+
89
+ component Actions({ playgroundVisible = false }: { playgroundVisible: boolean }) {
90
+ <div class="social-links">
91
+ <a href="https://github.com" class="github-link">{'GitHub'}</a>
92
+ <a href="https://discord.com" class="discord-link">{'Discord'}</a>
93
+ if (playgroundVisible) {
94
+ <a href="/playground" class="playground-link">{'Playground'}</a>
95
+ }
96
+ </div>
97
+ }
98
+
99
+ component Layout({ children }) {
100
+ <main>
101
+ <div class="container">
102
+ <children />
103
+ </div>
104
+ </main>
105
+ }
106
+
107
+ component Content() {
108
+ <div class="content">
109
+ <p>{'Some content here'}</p>
110
+ </div>
111
+ }
112
+
113
+ export component WebsiteIndex() {
114
+ <Layout>
115
+ <Header />
116
+ <Actions playgroundVisible={true} />
117
+ <Content />
118
+ <Actions playgroundVisible={false} />
119
+ </Layout>
120
+ }
@@ -126,3 +126,406 @@ export component ForLoopComplexObjects() {
126
126
  }
127
127
  </div>
128
128
  }
129
+
130
+ // Test reordering items in a keyed for loop
131
+ export component KeyedForLoopReorder() {
132
+ let items = track([
133
+ { id: 1, name: 'First' },
134
+ { id: 2, name: 'Second' },
135
+ { id: 3, name: 'Third' },
136
+ ]);
137
+ <button
138
+ class="reorder"
139
+ onClick={() => {
140
+ @items = [@items[2], @items[0], @items[1]];
141
+ }}
142
+ >
143
+ {'Reorder'}
144
+ </button>
145
+ <ul>
146
+ for (const item of @items; key item.id) {
147
+ <li class={`item-${item.id}`}>{item.name}</li>
148
+ }
149
+ </ul>
150
+ }
151
+
152
+ // Test for loop with item property updates (keyed)
153
+ export component KeyedForLoopUpdate() {
154
+ let items = track([
155
+ { id: 1, name: 'Item 1' },
156
+ { id: 2, name: 'Item 2' },
157
+ ]);
158
+ <button
159
+ class="update"
160
+ onClick={() => {
161
+ @items = @items.map((item) => (item.id === 1 ? { ...item, name: 'Updated' } : item));
162
+ }}
163
+ >
164
+ {'Update'}
165
+ </button>
166
+ <ul>
167
+ for (const item of @items; key item.id) {
168
+ <li class={`item-${item.id}`}>{item.name}</li>
169
+ }
170
+ </ul>
171
+ }
172
+
173
+ // Test for loop with combined add/remove/reorder
174
+ export component ForLoopMixedOperations() {
175
+ let items = track(['A', 'B', 'C', 'D']);
176
+ <button
177
+ class="shuffle"
178
+ onClick={() => {
179
+ // Remove B, add E, reorder to D, C, A, E
180
+ @items = ['D', 'C', 'A', 'E'];
181
+ }}
182
+ >
183
+ {'Shuffle'}
184
+ </button>
185
+ <ul>
186
+ for (const item of @items) {
187
+ <li class={`item-${item}`}>{item}</li>
188
+ }
189
+ </ul>
190
+ }
191
+
192
+ // Test for loop inside if block (combined control flow)
193
+ export component ForLoopInsideIf() {
194
+ let showList = track(true);
195
+ let items = track(['X', 'Y', 'Z']);
196
+ <button
197
+ class="toggle"
198
+ onClick={() => {
199
+ @showList = !@showList;
200
+ }}
201
+ >
202
+ {'Toggle List'}
203
+ </button>
204
+ <button
205
+ class="add"
206
+ onClick={() => {
207
+ @items = [...@items, 'W'];
208
+ }}
209
+ >
210
+ {'Add Item'}
211
+ </button>
212
+ if (@showList) {
213
+ <ul class="list">
214
+ for (const item of @items) {
215
+ <li>{item}</li>
216
+ }
217
+ </ul>
218
+ }
219
+ }
220
+
221
+ // Test for loop that transitions from empty to populated
222
+ export component ForLoopEmptyToPopulated() {
223
+ let items = track<string[]>([]);
224
+ <button
225
+ class="populate"
226
+ onClick={() => {
227
+ @items = ['One', 'Two', 'Three'];
228
+ }}
229
+ >
230
+ {'Populate'}
231
+ </button>
232
+ <ul class="list">
233
+ for (const item of @items) {
234
+ <li>{item}</li>
235
+ }
236
+ </ul>
237
+ }
238
+
239
+ // Test for loop that transitions from populated to empty
240
+ export component ForLoopPopulatedToEmpty() {
241
+ let items = track(['One', 'Two', 'Three']);
242
+ <button
243
+ class="clear"
244
+ onClick={() => {
245
+ @items = [];
246
+ }}
247
+ >
248
+ {'Clear'}
249
+ </button>
250
+ <ul class="list">
251
+ for (const item of @items) {
252
+ <li>{item}</li>
253
+ }
254
+ </ul>
255
+ }
256
+
257
+ // Test nested for loops with reactivity
258
+ export component NestedForLoopReactive() {
259
+ let grid = track([
260
+ [1, 2],
261
+ [3, 4],
262
+ ]);
263
+ <button
264
+ class="add-row"
265
+ onClick={() => {
266
+ @grid = [...@grid, [5, 6]];
267
+ }}
268
+ >
269
+ {'Add Row'}
270
+ </button>
271
+ <button
272
+ class="update-cell"
273
+ onClick={() => {
274
+ const newGrid = @grid.map((row) => [...row]);
275
+ newGrid[0][0] = 99;
276
+ @grid = newGrid;
277
+ }}
278
+ >
279
+ {'Update Cell'}
280
+ </button>
281
+ <div class="grid">
282
+ for (const row of @grid; index rowIndex) {
283
+ <div class={`row-${rowIndex}`}>
284
+ for (const cell of row; index colIndex) {
285
+ <span class={`cell-${rowIndex}-${colIndex}`}>{cell}</span>
286
+ }
287
+ </div>
288
+ }
289
+ </div>
290
+ }
291
+
292
+ // Test for loop with deeply nested data
293
+ export component ForLoopDeeplyNested() {
294
+ const departments = [
295
+ {
296
+ id: 'd1',
297
+ name: 'Engineering',
298
+ teams: [
299
+ { id: 't1', name: 'Frontend', members: ['Alice', 'Bob'] },
300
+ { id: 't2', name: 'Backend', members: ['Charlie'] },
301
+ ],
302
+ },
303
+ {
304
+ id: 'd2',
305
+ name: 'Design',
306
+ teams: [
307
+ { id: 't3', name: 'UX', members: ['Diana', 'Eve', 'Frank'] },
308
+ ],
309
+ },
310
+ ];
311
+ <div class="org">
312
+ for (const dept of departments; key dept.id) {
313
+ <div class={`dept-${dept.id}`}>
314
+ <h2 class="dept-name">{dept.name}</h2>
315
+ for (const team of dept.teams; key team.id) {
316
+ <div class={`team-${team.id}`}>
317
+ <h3 class="team-name">{team.name}</h3>
318
+ <ul>
319
+ for (const member of team.members) {
320
+ <li class="member">{member}</li>
321
+ }
322
+ </ul>
323
+ </div>
324
+ }
325
+ </div>
326
+ }
327
+ </div>
328
+ }
329
+
330
+ // Test for loop with index that gets updated
331
+ export component ForLoopIndexUpdate() {
332
+ let items = track(['First', 'Second', 'Third']);
333
+ <button
334
+ class="prepend"
335
+ onClick={() => {
336
+ @items = ['Zeroth', ...@items];
337
+ }}
338
+ >
339
+ {'Prepend'}
340
+ </button>
341
+ <ul>
342
+ for (const item of @items; index i) {
343
+ <li class={`item-${i}`}>{`[${i}] ${item}`}</li>
344
+ }
345
+ </ul>
346
+ }
347
+
348
+ // Test keyed for loop with index
349
+ export component KeyedForLoopWithIndex() {
350
+ let items = track([
351
+ { id: 'a', value: 'Alpha' },
352
+ { id: 'b', value: 'Beta' },
353
+ { id: 'c', value: 'Gamma' },
354
+ ]);
355
+ <button
356
+ class="reorder"
357
+ onClick={() => {
358
+ @items = [@items[1], @items[2], @items[0]];
359
+ }}
360
+ >
361
+ {'Rotate'}
362
+ </button>
363
+ <ul>
364
+ for (const item of @items; index i; key item.id) {
365
+ <li class={`item-${item.id}`} data-index={i}>{`[${i}] ${item.id}: ${item.value}`}</li>
366
+ }
367
+ </ul>
368
+ }
369
+
370
+ // Test for loop with sibling elements
371
+ export component ForLoopWithSiblings() {
372
+ let items = track(['A', 'B']);
373
+ <div class="wrapper">
374
+ <header class="before">{'Before'}</header>
375
+ for (const item of @items) {
376
+ <div class={`item-${item}`}>{item}</div>
377
+ }
378
+ <footer class="after">{'After'}</footer>
379
+ </div>
380
+ <button
381
+ class="add"
382
+ onClick={() => {
383
+ @items = [...@items, 'C'];
384
+ }}
385
+ >
386
+ {'Add'}
387
+ </button>
388
+ }
389
+
390
+ // Test for loop items with their own reactive state
391
+ export component ForLoopItemState() {
392
+ const initialItems = [
393
+ { id: 1, text: 'Todo 1' },
394
+ { id: 2, text: 'Todo 2' },
395
+ { id: 3, text: 'Todo 3' },
396
+ ];
397
+ <div>
398
+ for (const item of initialItems; key item.id) {
399
+ <TodoItem id={item.id} text={item.text} />
400
+ }
401
+ </div>
402
+ }
403
+
404
+ component TodoItem(props: { id: number; text: string }) {
405
+ let done = track(false);
406
+ <div class={`todo-${props.id}`}>
407
+ <input
408
+ type="checkbox"
409
+ class="checkbox"
410
+ checked={@done}
411
+ onChange={(e) => {
412
+ @done = (e.target as HTMLInputElement).checked;
413
+ }}
414
+ />
415
+ <span class={@done ? 'completed' : 'pending'}>{props.text}</span>
416
+ </div>
417
+ }
418
+
419
+ // Test for loop with single item
420
+ export component ForLoopSingleItem() {
421
+ const items = ['Only'];
422
+ <ul>
423
+ for (const item of items) {
424
+ <li class="single">{item}</li>
425
+ }
426
+ </ul>
427
+ }
428
+
429
+ // Test for loop adding at beginning
430
+ export component ForLoopAddAtBeginning() {
431
+ let items = track(['B', 'C']);
432
+ <button
433
+ class="prepend"
434
+ onClick={() => {
435
+ @items = ['A', ...@items];
436
+ }}
437
+ >
438
+ {'Prepend A'}
439
+ </button>
440
+ <ul>
441
+ for (const item of @items) {
442
+ <li class={`item-${item}`}>{item}</li>
443
+ }
444
+ </ul>
445
+ }
446
+
447
+ // Test for loop adding in the middle
448
+ export component ForLoopAddInMiddle() {
449
+ let items = track(['A', 'C']);
450
+ <button
451
+ class="insert"
452
+ onClick={() => {
453
+ const copy = [...@items];
454
+ copy.splice(1, 0, 'B');
455
+ @items = copy;
456
+ }}
457
+ >
458
+ {'Insert B'}
459
+ </button>
460
+ <ul>
461
+ for (const item of @items) {
462
+ <li class={`item-${item}`}>{item}</li>
463
+ }
464
+ </ul>
465
+ }
466
+
467
+ // Test for loop removing from the middle
468
+ export component ForLoopRemoveFromMiddle() {
469
+ let items = track(['A', 'B', 'C']);
470
+ <button
471
+ class="remove-middle"
472
+ onClick={() => {
473
+ @items = @items.filter((item) => item !== 'B');
474
+ }}
475
+ >
476
+ {'Remove B'}
477
+ </button>
478
+ <ul>
479
+ for (const item of @items) {
480
+ <li class={`item-${item}`}>{item}</li>
481
+ }
482
+ </ul>
483
+ }
484
+
485
+ // Test for loop with large list
486
+ export component ForLoopLargeList() {
487
+ const items = Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`);
488
+ <ul class="large-list">
489
+ for (const item of items; index i) {
490
+ <li class={`item-${i}`}>{item}</li>
491
+ }
492
+ </ul>
493
+ }
494
+
495
+ // Test for loop with swap operation
496
+ export component ForLoopSwap() {
497
+ let items = track(['A', 'B', 'C', 'D']);
498
+ <button
499
+ class="swap"
500
+ onClick={() => {
501
+ const copy = [...@items];
502
+ [copy[0], copy[3]] = [copy[3], copy[0]];
503
+ @items = copy;
504
+ }}
505
+ >
506
+ {'Swap First and Last'}
507
+ </button>
508
+ <ul>
509
+ for (const item of @items) {
510
+ <li class={`item-${item}`}>{item}</li>
511
+ }
512
+ </ul>
513
+ }
514
+
515
+ // Test for loop with reverse operation
516
+ export component ForLoopReverse() {
517
+ let items = track(['A', 'B', 'C', 'D']);
518
+ <button
519
+ class="reverse"
520
+ onClick={() => {
521
+ @items = [...@items].reverse();
522
+ }}
523
+ >
524
+ {'Reverse'}
525
+ </button>
526
+ <ul>
527
+ for (const item of @items) {
528
+ <li class={`item-${item}`}>{item}</li>
529
+ }
530
+ </ul>
531
+ }
@@ -0,0 +1,111 @@
1
+ import { track } from 'ripple';
2
+
3
+ // Static title
4
+ export component StaticTitle() {
5
+ <head>
6
+ <title>{'Static Test Title'}</title>
7
+ </head>
8
+
9
+ <div>{'Content'}</div>
10
+ }
11
+
12
+ // Reactive title
13
+ export component ReactiveTitle() {
14
+ let title = track('Initial Title');
15
+
16
+ <head>
17
+ <title>{@title}</title>
18
+ </head>
19
+ <div>
20
+ <span>{@title}</span>
21
+ </div>
22
+ }
23
+
24
+ // Multiple head elements
25
+ export component MultipleHeadElements() {
26
+ <head>
27
+ <title>{'Page Title'}</title>
28
+ <meta name="description" content="Page description" />
29
+ <link rel="stylesheet" href="/styles.css" />
30
+ </head>
31
+
32
+ <div>{'Page content'}</div>
33
+ }
34
+
35
+ // Head with reactive meta tags
36
+ export component ReactiveMetaTags() {
37
+ let description = track('Initial description');
38
+
39
+ <head>
40
+ <title>{'My Page'}</title>
41
+ <meta name="description" content={@description} />
42
+ </head>
43
+
44
+ <div>{@description}</div>
45
+ }
46
+
47
+ // Title with template literal
48
+ export component TitleWithTemplate() {
49
+ let name = track('World');
50
+
51
+ <head>
52
+ <title>{`Hello ${@name}!`}</title>
53
+ </head>
54
+
55
+ <div>{@name}</div>
56
+ }
57
+
58
+ // Empty title
59
+ export component EmptyTitle() {
60
+ <head>
61
+ <title>{''}</title>
62
+ </head>
63
+ <div>{'Empty title test'}</div>
64
+ }
65
+
66
+ // Title with conditional content
67
+ export component ConditionalTitle() {
68
+ let showPrefix = track(true);
69
+ let title = track('Main Page');
70
+
71
+ <head>
72
+ <title>{@showPrefix ? 'App - ' + @title : @title}</title>
73
+ </head>
74
+
75
+ <div>{@title}</div>
76
+ }
77
+
78
+ // Title with computed value
79
+ export component ComputedTitle() {
80
+ let count = track(0);
81
+ let prefix = 'Count: ';
82
+
83
+ <head>
84
+ <title>{prefix + @count}</title>
85
+ </head>
86
+ <div>
87
+ <span>{@count}</span>
88
+ </div>
89
+ }
90
+
91
+ // Multiple head blocks (edge case)
92
+ export component MultipleHeadBlocks() {
93
+ <head>
94
+ <title>{'First Head'}</title>
95
+ </head>
96
+
97
+ <div>{'Content'}</div>
98
+
99
+ <head>
100
+ <meta name="author" content="Test Author" />
101
+ </head>
102
+ }
103
+
104
+ // Head with style tag
105
+ export component HeadWithStyle() {
106
+ <head>
107
+ <title>{'Styled Page'}</title>
108
+ </head>
109
+
110
+ <div>{'Styled content'}</div>
111
+ }