ripple 0.3.128 → 0.4.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 (192) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/package.json +10 -4
  3. package/src/runtime/element.js +23 -6
  4. package/src/runtime/index-client.js +81 -41
  5. package/src/runtime/index-server.js +1 -0
  6. package/src/runtime/internal/client/blocks.js +173 -91
  7. package/src/runtime/internal/client/component.js +4 -30
  8. package/src/runtime/internal/client/composite.js +12 -13
  9. package/src/runtime/internal/client/constants.js +9 -0
  10. package/src/runtime/internal/client/context.js +12 -19
  11. package/src/runtime/internal/client/css.js +7 -9
  12. package/src/runtime/internal/client/events.js +178 -90
  13. package/src/runtime/internal/client/expression.js +45 -25
  14. package/src/runtime/internal/client/for.js +409 -129
  15. package/src/runtime/internal/client/head.js +10 -5
  16. package/src/runtime/internal/client/html.js +1 -0
  17. package/src/runtime/internal/client/hydration.js +152 -10
  18. package/src/runtime/internal/client/if.js +239 -50
  19. package/src/runtime/internal/client/index.js +4 -12
  20. package/src/runtime/internal/client/operations.js +40 -61
  21. package/src/runtime/internal/client/portal.js +163 -85
  22. package/src/runtime/internal/client/render.js +39 -39
  23. package/src/runtime/internal/client/rpc.js +3 -3
  24. package/src/runtime/internal/client/runtime.js +592 -433
  25. package/src/runtime/internal/client/selector.js +5 -2
  26. package/src/runtime/internal/client/template.js +133 -158
  27. package/src/runtime/internal/client/transport.js +38 -0
  28. package/src/runtime/internal/client/try.js +465 -426
  29. package/src/runtime/internal/client/types.d.ts +56 -23
  30. package/src/runtime/internal/client/utils.js +1 -3
  31. package/src/runtime/internal/server/dynamic.js +20 -21
  32. package/src/runtime/internal/server/index.js +325 -275
  33. package/src/runtime/internal/server/rpc.js +3 -2
  34. package/src/runtime/internal/server/transport.js +16 -0
  35. package/src/runtime/transport.js +21 -0
  36. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
  37. package/tests/client/array/array.copy-within.test.tsrx +12 -12
  38. package/tests/client/array/array.derived.test.tsrx +46 -46
  39. package/tests/client/array/array.iteration.test.tsrx +10 -10
  40. package/tests/client/array/array.mutations.test.tsrx +20 -20
  41. package/tests/client/array/array.static.test.tsrx +11 -11
  42. package/tests/client/array/array.to-methods.test.tsrx +6 -6
  43. package/tests/client/async-suspend.test.tsrx +74 -75
  44. package/tests/client/basic/basic.attributes.test.tsrx +86 -83
  45. package/tests/client/basic/basic.collections.test.tsrx +12 -12
  46. package/tests/client/basic/basic.components.test.tsrx +99 -25
  47. package/tests/client/basic/basic.errors.test.tsrx +18 -18
  48. package/tests/client/basic/basic.events.test.tsrx +42 -42
  49. package/tests/client/basic/basic.get-set.test.tsrx +8 -8
  50. package/tests/client/basic/basic.reactivity.test.tsrx +73 -38
  51. package/tests/client/basic/basic.rendering.test.tsrx +25 -25
  52. package/tests/client/basic/basic.utilities.test.tsrx +3 -3
  53. package/tests/client/boundaries.test.tsrx +8 -8
  54. package/tests/client/boxed-locals.test.tsrx +452 -0
  55. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
  56. package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
  57. package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
  58. package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
  59. package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
  60. package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
  61. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
  62. package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
  63. package/tests/client/composite/composite.props.test.tsrx +149 -12
  64. package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
  65. package/tests/client/composite/composite.render.test.tsrx +26 -9
  66. package/tests/client/css/scoped-styles.test.tsrx +8 -8
  67. package/tests/client/date.test.tsrx +42 -42
  68. package/tests/client/derived-release.test.tsrx +370 -0
  69. package/tests/client/dynamic-elements.test.tsrx +45 -45
  70. package/tests/client/events.test.tsrx +97 -71
  71. package/tests/client/for-selector.test.tsrx +14 -14
  72. package/tests/client/for-single-root-items.test.tsrx +118 -3
  73. package/tests/client/for.test.tsrx +332 -31
  74. package/tests/client/head.test.tsrx +19 -19
  75. package/tests/client/html.test.tsrx +11 -9
  76. package/tests/client/if-block.test.tsrx +80 -0
  77. package/tests/client/map.test.tsrx +16 -16
  78. package/tests/client/media-query.test.tsrx +7 -7
  79. package/tests/client/portal.test.tsrx +224 -26
  80. package/tests/client/primitive-text.test.tsrx +50 -0
  81. package/tests/client/prop-kind-switch.test.tsrx +141 -0
  82. package/tests/client/ref.test.tsrx +17 -17
  83. package/tests/client/return.test.tsrx +26 -3
  84. package/tests/client/root-boundary.test.tsrx +42 -0
  85. package/tests/client/scoped-flush.test.tsrx +8 -8
  86. package/tests/client/set.test.tsrx +6 -6
  87. package/tests/client/svg.test.tsrx +11 -11
  88. package/tests/client/switch.test.tsrx +44 -44
  89. package/tests/client/track-async-hydration.test.tsrx +4 -4
  90. package/tests/client/try.test.tsrx +151 -83
  91. package/tests/client/tsx.test.tsrx +43 -40
  92. package/tests/client/url/url.derived.test.tsrx +6 -6
  93. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
  95. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
  96. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
  97. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
  98. package/tests/hydration/basic.test.js +74 -1
  99. package/tests/hydration/build-components.js +22 -1
  100. package/tests/hydration/compiled/client/basic.js +1142 -713
  101. package/tests/hydration/compiled/client/composite.js +151 -100
  102. package/tests/hydration/compiled/client/events.js +184 -171
  103. package/tests/hydration/compiled/client/for.js +1472 -1132
  104. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  105. package/tests/hydration/compiled/client/head.js +254 -201
  106. package/tests/hydration/compiled/client/hmr.js +54 -41
  107. package/tests/hydration/compiled/client/html-in-template.js +54 -39
  108. package/tests/hydration/compiled/client/html.js +1633 -1263
  109. package/tests/hydration/compiled/client/if-children.js +420 -270
  110. package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
  111. package/tests/hydration/compiled/client/if.js +229 -219
  112. package/tests/hydration/compiled/client/mixed-control-flow.js +420 -404
  113. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
  114. package/tests/hydration/compiled/client/portal.js +87 -95
  115. package/tests/hydration/compiled/client/reactivity.js +135 -101
  116. package/tests/hydration/compiled/client/return.js +28 -19
  117. package/tests/hydration/compiled/client/streaming.js +390 -306
  118. package/tests/hydration/compiled/client/switch.js +258 -266
  119. package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
  120. package/tests/hydration/compiled/client/try.js +173 -129
  121. package/tests/hydration/compiled/client/typed-text.js +71 -0
  122. package/tests/hydration/compiled/fixtures/money.js +10 -0
  123. package/tests/hydration/compiled/server/basic.js +123 -6
  124. package/tests/hydration/compiled/server/composite.js +16 -30
  125. package/tests/hydration/compiled/server/events.js +66 -18
  126. package/tests/hydration/compiled/server/for.js +117 -41
  127. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  128. package/tests/hydration/compiled/server/head.js +20 -20
  129. package/tests/hydration/compiled/server/hmr.js +2 -2
  130. package/tests/hydration/compiled/server/html.js +70 -238
  131. package/tests/hydration/compiled/server/if-children.js +107 -15
  132. package/tests/hydration/compiled/server/if.js +11 -15
  133. package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
  134. package/tests/hydration/compiled/server/portal.js +14 -22
  135. package/tests/hydration/compiled/server/reactivity.js +14 -14
  136. package/tests/hydration/compiled/server/streaming.js +19 -19
  137. package/tests/hydration/compiled/server/switch.js +8 -8
  138. package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
  139. package/tests/hydration/compiled/server/try.js +8 -8
  140. package/tests/hydration/compiled/server/typed-text.js +17 -0
  141. package/tests/hydration/components/basic.tsrx +44 -8
  142. package/tests/hydration/components/composite.tsrx +2 -2
  143. package/tests/hydration/components/events.tsrx +25 -25
  144. package/tests/hydration/components/for.tsrx +115 -67
  145. package/tests/hydration/components/fragment-cursor.tsrx +650 -0
  146. package/tests/hydration/components/head.tsrx +16 -16
  147. package/tests/hydration/components/hmr.tsrx +2 -2
  148. package/tests/hydration/components/html.tsrx +7 -7
  149. package/tests/hydration/components/if-children.tsrx +72 -25
  150. package/tests/hydration/components/if.tsrx +18 -18
  151. package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
  152. package/tests/hydration/components/portal.tsrx +3 -3
  153. package/tests/hydration/components/reactivity.tsrx +16 -16
  154. package/tests/hydration/components/streaming.tsrx +20 -20
  155. package/tests/hydration/components/switch.tsrx +20 -20
  156. package/tests/hydration/components/text-types.ts +4 -0
  157. package/tests/hydration/components/track-async-serialization.tsrx +80 -26
  158. package/tests/hydration/components/try.tsrx +8 -8
  159. package/tests/hydration/components/typed-text.tsrx +22 -0
  160. package/tests/hydration/fixtures/money.js +10 -0
  161. package/tests/hydration/for.test.js +36 -0
  162. package/tests/hydration/fragment-cursor.test.js +111 -0
  163. package/tests/hydration/if-children.test.js +24 -0
  164. package/tests/hydration/streaming.test.js +19 -0
  165. package/tests/hydration/track-async-serialization.test.js +87 -2
  166. package/tests/hydration/tsconfig.text-types.json +10 -0
  167. package/tests/hydration/typed-text.test.js +34 -0
  168. package/tests/server/await.test.tsrx +3 -3
  169. package/tests/server/basic.attributes.test.tsrx +42 -42
  170. package/tests/server/basic.components.test.tsrx +64 -8
  171. package/tests/server/basic.test.tsrx +34 -32
  172. package/tests/server/compiler.test.tsrx +1 -70
  173. package/tests/server/composite.props.test.tsrx +6 -6
  174. package/tests/server/dynamic-elements.test.tsrx +31 -8
  175. package/tests/server/for.test.tsrx +94 -0
  176. package/tests/server/head.test.tsrx +11 -11
  177. package/tests/server/scoped-styles.test.tsrx +2 -2
  178. package/tests/server/streaming-ssr.test.tsrx +48 -43
  179. package/tests/server/track-async-serialization.test.tsrx +204 -21
  180. package/tests/server/try.test.tsrx +48 -48
  181. package/tests/types/transport.ts +40 -0
  182. package/tests/utils/vite-plugin-config.test.js +23 -0
  183. package/types/index.d.ts +48 -7
  184. package/types/server.d.ts +3 -0
  185. package/types/transport.d.ts +15 -0
  186. package/src/runtime/internal/client/switch.js +0 -113
  187. package/src/utils/errors.js +0 -13
  188. package/tests/client/lazy-array.test.tsrx +0 -31
  189. package/tests/client/lazy-destructuring.test.tsrx +0 -438
  190. package/tests/client/tracked-index-access.test.tsrx +0 -113
  191. package/tests/server/lazy-destructuring.test.tsrx +0 -595
  192. package/tests/server/tracked-index-access.test.tsrx +0 -76
@@ -12,13 +12,13 @@ export function StaticTitle() @{
12
12
 
13
13
  // Reactive title
14
14
  export function ReactiveTitle() @{
15
- let &[title] = track('Initial Title');
15
+ const title = track('Initial Title');
16
16
  <>
17
17
  <head>
18
- <title>{title}</title>
18
+ <title>{title.value}</title>
19
19
  </head>
20
20
  <div>
21
- <span>{title}</span>
21
+ <span>{title.value}</span>
22
22
  </div>
23
23
  </>
24
24
  }
@@ -37,24 +37,24 @@ export function MultipleHeadElements() @{
37
37
 
38
38
  // Head with reactive meta tags
39
39
  export function ReactiveMetaTags() @{
40
- let &[description] = track('Initial description');
40
+ const description = track('Initial description');
41
41
  <>
42
42
  <head>
43
43
  <title>{'My Page'}</title>
44
- <meta name="description" content={description} />
44
+ <meta name="description" content={description.value} />
45
45
  </head>
46
- <div>{description}</div>
46
+ <div>{description.value}</div>
47
47
  </>
48
48
  }
49
49
 
50
50
  // Title with template literal
51
51
  export function TitleWithTemplate() @{
52
- let &[name] = track('World');
52
+ const name = track('World');
53
53
  <>
54
54
  <head>
55
- <title>{`Hello ${name}!`}</title>
55
+ <title>{`Hello ${name.value}!`}</title>
56
56
  </head>
57
- <div>{name}</div>
57
+ <div>{name.value}</div>
58
58
  </>
59
59
  }
60
60
 
@@ -70,28 +70,28 @@ export function EmptyTitle() @{
70
70
 
71
71
  // Title with conditional content
72
72
  export function ConditionalTitle() @{
73
- let &[showPrefix] = track(true);
74
- let &[title] = track('Main Page');
73
+ const showPrefix = track(true);
74
+ const title = track('Main Page');
75
75
  <>
76
76
  <head>
77
77
  <title>
78
- {showPrefix ? 'App - ' + title : title}
78
+ {showPrefix.value ? 'App - ' + title.value : title.value}
79
79
  </title>
80
80
  </head>
81
- <div>{title}</div>
81
+ <div>{title.value}</div>
82
82
  </>
83
83
  }
84
84
 
85
85
  // Title with computed value
86
86
  export function ComputedTitle() @{
87
- let &[count] = track(0);
87
+ const count = track(0);
88
88
  let prefix = 'Count: ';
89
89
  <>
90
90
  <head>
91
- <title>{prefix + count}</title>
91
+ <title>{prefix + count.value}</title>
92
92
  </head>
93
93
  <div>
94
- <span>{count}</span>
94
+ <span>{count.value}</span>
95
95
  </div>
96
96
  </>
97
97
  }
@@ -17,9 +17,9 @@ export function Layout({ children }: { children: any }) @{
17
17
 
18
18
  // A child component with an if block (the key source of deep hydrate_node)
19
19
  export function Content() @{
20
- let &[visible] = track(true);
20
+ const visible = track(true);
21
21
  <div class="content">
22
- @if (visible) {
22
+ @if (visible.value) {
23
23
  <p class="text">{'Hello world'}</p>
24
24
  }
25
25
  </div>
@@ -89,7 +89,7 @@ function DocFooter() @{
89
89
  <footer class="doc-footer">{'Footer content'}</footer>
90
90
  }
91
91
 
92
- export function DocLayout(&{
92
+ export function DocLayout({
93
93
  children,
94
94
  editPath = '',
95
95
  nextLink = null,
@@ -274,7 +274,7 @@ export function HtmlAfterComponentInChildren() @{
274
274
  </ContentWrapper>
275
275
  }
276
276
 
277
- function NavItem(&{
277
+ function NavItem({
278
278
  href,
279
279
  text: label,
280
280
  active = false,
@@ -294,13 +294,13 @@ function NavItem(&{
294
294
  }
295
295
 
296
296
  function SidebarSection({ title, children }: { title: string; children: any }) @{
297
- let &[expanded] = track(true);
297
+ const expanded = track(true);
298
298
  <section class="sidebar-section">
299
299
  <div class="section-header">
300
300
  <h2>{title}</h2>
301
- <button onClick={() => (expanded = !expanded)}>{'Toggle'}</button>
301
+ <button onClick={() => (expanded.value = !expanded.value)}>{'Toggle'}</button>
302
302
  </div>
303
- @if (expanded) {
303
+ @if (expanded.value) {
304
304
  <div class="section-items">{children}</div>
305
305
  }
306
306
  </section>
@@ -432,7 +432,7 @@ function FooterStub() @{
432
432
  <footer class="footer">{'Footer'}</footer>
433
433
  }
434
434
 
435
- function DocsLayoutInner(&{
435
+ function DocsLayoutInner({
436
436
  children,
437
437
  editPath = '',
438
438
  nextLink = null,
@@ -485,7 +485,7 @@ export function DocsLayoutWithoutData() @{
485
485
  </DocsLayoutInner>
486
486
  }
487
487
 
488
- function DocsLayoutExact(&{
488
+ function DocsLayoutExact({
489
489
  children,
490
490
  editPath = '',
491
491
  prevLink = null,
@@ -1,13 +1,17 @@
1
- import { track } from 'ripple';
1
+ import { track, type Tracked } from 'ripple';
2
2
 
3
3
  // Minimal repro for hydration issue with if block containing children
4
4
  // Based on SidebarGroup pattern from website-new
5
5
 
6
6
  export function IfWithChildren({ children }: { children: any }) @{
7
- let &[expanded] = track(true);
7
+ const expanded = track(true);
8
8
  <div class="container">
9
- <div class="header" role="button" onClick={() => (expanded = !expanded)}>{'Toggle'}</div>
10
- @if (expanded) {
9
+ <div
10
+ class="header"
11
+ role="button"
12
+ onClick={() => (expanded.value = !expanded.value)}
13
+ >{'Toggle'}</div>
14
+ @if (expanded.value) {
11
15
  <div class="content">{children}</div>
12
16
  }
13
17
  </div>
@@ -26,10 +30,14 @@ export function TestIfWithChildren() @{
26
30
 
27
31
  // Simpler variant - if block with static children
28
32
  export function IfWithStaticChildren() @{
29
- let &[expanded] = track(true);
33
+ const expanded = track(true);
30
34
  <div class="container">
31
- <div class="header" role="button" onClick={() => (expanded = !expanded)}>{'Toggle'}</div>
32
- @if (expanded) {
35
+ <div
36
+ class="header"
37
+ role="button"
38
+ onClick={() => (expanded.value = !expanded.value)}
39
+ >{'Toggle'}</div>
40
+ @if (expanded.value) {
33
41
  <div class="content">
34
42
  <span>{'Static child 1'}</span>
35
43
  <span>{'Static child 2'}</span>
@@ -40,9 +48,9 @@ export function IfWithStaticChildren() @{
40
48
 
41
49
  // Variant with sibling elements before the if block (like SidebarGroup)
42
50
  export function IfWithSiblingsAndChildren({ children }: { children: any }) @{
43
- let &[expanded] = track(true);
51
+ const expanded = track(true);
44
52
  <section class="group">
45
- <div class="item" role="button" onClick={() => (expanded = !expanded)}>
53
+ <div class="item" role="button" onClick={() => (expanded.value = !expanded.value)}>
46
54
  <div class="indicator" />
47
55
  <h2 class="text">{'Title'}</h2>
48
56
  <div class="caret">
@@ -51,7 +59,7 @@ export function IfWithSiblingsAndChildren({ children }: { children: any }) @{
51
59
  </svg>
52
60
  </div>
53
61
  </div>
54
- @if (expanded) {
62
+ @if (expanded.value) {
55
63
  <div class="items">{children}</div>
56
64
  }
57
65
  </section>
@@ -68,7 +76,7 @@ export function TestIfWithSiblingsAndChildren() @{
68
76
  // This tests that hydrate_node is properly restored after processing an element's children
69
77
  // before navigating to a dynamic sibling (if/for/switch)
70
78
  export function ElementWithChildrenThenIf() @{
71
- let &[show] = track(true);
79
+ const show = track(true);
72
80
  <>
73
81
  <div class="wrapper">
74
82
  <div class="nested-parent">
@@ -76,17 +84,17 @@ export function ElementWithChildrenThenIf() @{
76
84
  <span class="deep">{'Deep content'}</span>
77
85
  </div>
78
86
  </div>
79
- @if (show) {
87
+ @if (show.value) {
80
88
  <div class="conditional">{'Conditional content'}</div>
81
89
  }
82
90
  </div>
83
- <button class="toggle" onClick={() => (show = !show)}>{'Toggle'}</button>
91
+ <button class="toggle" onClick={() => (show.value = !show.value)}>{'Toggle'}</button>
84
92
  </>
85
93
  }
86
94
 
87
95
  // More complex: multiple levels of nesting before if sibling
88
96
  export function DeepNestingThenIf() @{
89
- let &[visible] = track(true);
97
+ const visible = track(true);
90
98
  <>
91
99
  <section class="outer">
92
100
  <article class="middle">
@@ -97,11 +105,11 @@ export function DeepNestingThenIf() @{
97
105
  </p>
98
106
  </div>
99
107
  </article>
100
- @if (visible) {
108
+ @if (visible.value) {
101
109
  <footer class="footer">{'Footer'}</footer>
102
110
  }
103
111
  </section>
104
- <button class="btn" onClick={() => (visible = !visible)}>{'Toggle'}</button>
112
+ <button class="btn" onClick={() => (visible.value = !visible.value)}>{'Toggle'}</button>
105
113
  </>
106
114
  }
107
115
 
@@ -109,22 +117,22 @@ export function DeepNestingThenIf() @{
109
117
  // followed by another sibling element. This requires pop() to restore hydrate_node
110
118
  // because we descend into the first element to get the button children.
111
119
  export function DomElementChildrenThenSibling() @{
112
- let &[activeTab] = track('code');
120
+ const activeTab = track('code');
113
121
  <div class="tabs">
114
122
  <div class="tab-list">
115
123
  <button
116
124
  class="tab"
117
- aria-selected={activeTab === 'code' ? 'true' : 'false'}
118
- onClick={() => (activeTab = 'code')}
125
+ aria-selected={activeTab.value === 'code' ? 'true' : 'false'}
126
+ onClick={() => (activeTab.value = 'code')}
119
127
  >{'Code'}</button>
120
128
  <button
121
129
  class="tab"
122
- aria-selected={activeTab === 'preview' ? 'true' : 'false'}
123
- onClick={() => (activeTab = 'preview')}
130
+ aria-selected={activeTab.value === 'preview' ? 'true' : 'false'}
131
+ onClick={() => (activeTab.value = 'preview')}
124
132
  >{'Preview'}</button>
125
133
  </div>
126
134
  <div class="panel">
127
- @if (activeTab === 'code') {
135
+ @if (activeTab.value === 'code') {
128
136
  <pre class="code">{'const x = 1;'}</pre>
129
137
  } @else {
130
138
  <div class="preview">{'Preview content'}</div>
@@ -137,20 +145,20 @@ export function DomElementChildrenThenSibling() @{
137
145
  // generate sibling() calls. This was causing incorrect pop() generation before next().
138
146
  // Pattern: <ul> with dynamic <li> children -> static <h2> -> static <p> -> next()
139
147
  export function DomChildrenThenStaticSiblings() @{
140
- let &[count] = track(0);
148
+ const count = track(0);
141
149
  <>
142
150
  <div class="container">
143
151
  <ul class="list">
144
152
  <li class="item">
145
153
  {'Item count: '}
146
- {count}
154
+ {count.value}
147
155
  </li>
148
156
  <li class="item">{'Another item'}</li>
149
157
  </ul>
150
158
  <h2 class="heading">{'Static Heading'}</h2>
151
159
  <p class="para">{'Static paragraph'}</p>
152
160
  </div>
153
- <button class="inc" onClick={() => count++}>{'Increment'}</button>
161
+ <button class="inc" onClick={() => count.value++}>{'Increment'}</button>
154
162
  </>
155
163
  }
156
164
 
@@ -183,3 +191,42 @@ export function StaticListThenStaticSiblings() @{
183
191
  </p>
184
192
  </div>
185
193
  }
194
+
195
+ // All-component children render into their parent through an append-into
196
+ // sentinel instead of a `<!>` placeholder per child. A child whose root is
197
+ // control flow keeps re-rendering relative to its anchor after hydration, so it
198
+ // must resolve the sentinel to its own position rather than the parent's end.
199
+ function RootIfChild(props: { on: Tracked<boolean> }) @{
200
+ @if (props.on.value) {
201
+ <span class="root-if">{'on'}</span>
202
+ } @else {
203
+ <span class="root-if">{'off'}</span>
204
+ }
205
+ }
206
+
207
+ function RootForChild(props: { items: Tracked<number[]> }) @{
208
+ @for (const item of props.items.value; key item) {
209
+ <span class="root-for">{item}</span>
210
+ }
211
+ }
212
+
213
+ function TrailingChild() @{
214
+ <span class="trailing">{'end'}</span>
215
+ }
216
+
217
+ export function ComponentChildrenWithControlFlowRoots() @{
218
+ const on = track(true);
219
+ const items = track([1, 2, 3]);
220
+ <div class="wrapper">
221
+ <div class="host">
222
+ <RootIfChild {on} />
223
+ <RootForChild {items} />
224
+ <TrailingChild />
225
+ </div>
226
+ <button class="toggle" onClick={() => (on.value = !on.value)}>{'Toggle'}</button>
227
+ <button
228
+ class="rotate"
229
+ onClick={() => (items.value = [items.value[1], items.value[2], items.value[0]])}
230
+ >{'Rotate'}</button>
231
+ </div>
232
+ }
@@ -26,30 +26,30 @@ export function IfElse() @{
26
26
  }
27
27
 
28
28
  export function ReactiveIf() @{
29
- let &[show] = track(true);
29
+ const show = track(true);
30
30
  <>
31
31
  <button
32
32
  class="toggle"
33
33
  onClick={() => {
34
- show = !show;
34
+ show.value = !show.value;
35
35
  }}
36
36
  >{'Toggle'}</button>
37
- @if (show) {
37
+ @if (show.value) {
38
38
  <div class="content">{'Content visible'}</div>
39
39
  }
40
40
  </>
41
41
  }
42
42
 
43
43
  export function ReactiveIfElse() @{
44
- let &[isOn] = track(false);
44
+ const isOn = track(false);
45
45
  <>
46
46
  <button
47
47
  class="toggle"
48
48
  onClick={() => {
49
- isOn = !isOn;
49
+ isOn.value = !isOn.value;
50
50
  }}
51
51
  >{'Toggle'}</button>
52
- @if (isOn) {
52
+ @if (isOn.value) {
53
53
  <div class="on">{'ON'}</div>
54
54
  } @else {
55
55
  <div class="off">{'OFF'}</div>
@@ -58,25 +58,25 @@ export function ReactiveIfElse() @{
58
58
  }
59
59
 
60
60
  export function NestedIf() @{
61
- let &[outer] = track(true);
62
- let &[inner] = track(true);
61
+ const outer = track(true);
62
+ const inner = track(true);
63
63
  <>
64
64
  <button
65
65
  class="outer-toggle"
66
66
  onClick={() => {
67
- outer = !outer;
67
+ outer.value = !outer.value;
68
68
  }}
69
69
  >{'Outer'}</button>
70
70
  <button
71
71
  class="inner-toggle"
72
72
  onClick={() => {
73
- inner = !inner;
73
+ inner.value = !inner.value;
74
74
  }}
75
75
  >{'Inner'}</button>
76
- @if (outer) {
76
+ @if (outer.value) {
77
77
  <div class="outer-content">
78
78
  {'Outer'}
79
- @if (inner) {
79
+ @if (inner.value) {
80
80
  <span class="inner-content">{'Inner'}</span>
81
81
  }
82
82
  </div>
@@ -85,29 +85,29 @@ export function NestedIf() @{
85
85
  }
86
86
 
87
87
  export function IfElseIfChain() @{
88
- let &[status] = track<'loading' | 'success' | 'error'>('loading');
88
+ const status = track<'loading' | 'success' | 'error'>('loading');
89
89
  <div>
90
90
  <button
91
91
  class="success"
92
92
  onClick={() => {
93
- status = 'success';
93
+ status.value = 'success';
94
94
  }}
95
95
  >{'Success'}</button>
96
96
  <button
97
97
  class="error"
98
98
  onClick={() => {
99
- status = 'error';
99
+ status.value = 'error';
100
100
  }}
101
101
  >{'Error'}</button>
102
102
  <button
103
103
  class="loading"
104
104
  onClick={() => {
105
- status = 'loading';
105
+ status.value = 'loading';
106
106
  }}
107
107
  >{'Loading'}</button>
108
- @if (status === 'loading') {
108
+ @if (status.value === 'loading') {
109
109
  <div class="state">{'Loading...'}</div>
110
- } @else if (status === 'success') {
110
+ } @else if (status.value === 'success') {
111
111
  <div class="state">{'Success!'}</div>
112
112
  } @else {
113
113
  <div class="state">{'Error occurred'}</div>
@@ -31,9 +31,9 @@ export function MixedControlFlowStatic() @{
31
31
  }
32
32
 
33
33
  export function MixedControlFlowReactive() @{
34
- let &[show] = track(true);
35
- let &[mode] = track<'a' | 'b'>('a');
36
- let &[items] = track([
34
+ const show = track(true);
35
+ const mode = track<'a' | 'b'>('a');
36
+ const items = track([
37
37
  { id: 1, label: 'One' },
38
38
  { id: 2, label: 'Two' },
39
39
  ]);
@@ -41,25 +41,25 @@ export function MixedControlFlowReactive() @{
41
41
  <button
42
42
  class="toggle-show"
43
43
  onClick={() => {
44
- show = !show;
44
+ show.value = !show.value;
45
45
  }}
46
46
  >{'Toggle Show'}</button>
47
47
  <button
48
48
  class="toggle-mode"
49
49
  onClick={() => {
50
- mode = mode === 'a' ? 'b' : 'a';
50
+ mode.value = mode.value === 'a' ? 'b' : 'a';
51
51
  }}
52
52
  >{'Toggle Mode'}</button>
53
53
  <button
54
54
  class="add-item"
55
55
  onClick={() => {
56
- items = [...items, { id: 3, label: 'Three' }];
56
+ items.value = [...items.value, { id: 3, label: 'Three' }];
57
57
  }}
58
58
  >{'Add Item'}</button>
59
- @if (show) {
59
+ @if (show.value) {
60
60
  <div class="mixed-reactive-list">
61
- @for (const item of items; key item.id) {
62
- @switch (mode) {
61
+ @for (const item of items.value; key item.id) {
62
+ @switch (mode.value) {
63
63
  @case 'a': {
64
64
  @try {
65
65
  <p class={`item item-${item.id}`}>{`A:${item.label}`}</p>
@@ -106,6 +106,6 @@ export function MixedControlFlowAsyncPending() @{
106
106
  }
107
107
 
108
108
  function AsyncRow({ label }: { label: string }) @{
109
- let &[value] = trackAsync(() => Promise.resolve(label));
110
- <div class="resolved-row">{value}</div>
109
+ const value = trackAsync(() => Promise.resolve(label));
110
+ <div class="resolved-row">{value.value}</div>
111
111
  }
@@ -13,10 +13,10 @@ export function SimplePortal() @{
13
13
 
14
14
  // Portal with conditional rendering
15
15
  export function ConditionalPortal() @{
16
- let &[show] = track(true);
16
+ const show = track(true);
17
17
  <div class="container">
18
- <button class="toggle" onClick={() => (show = !show)}>{'Toggle'}</button>
19
- @if (show) {
18
+ <button class="toggle" onClick={() => (show.value = !show.value)}>{'Toggle'}</button>
19
+ @if (show.value) {
20
20
  <Portal target={typeof document !== 'undefined' ? document.body : null}>
21
21
  <div class="portal-content">{'Portal is visible'}</div>
22
22
  </Portal>
@@ -3,14 +3,14 @@ import { track } from 'ripple';
3
3
  // Reactive components for hydration testing
4
4
 
5
5
  export function TrackedState() @{
6
- let &[count] = track(0);
7
- <div class="count">{count}</div>
6
+ const count = track(0);
7
+ <div class="count">{count.value}</div>
8
8
  }
9
9
 
10
10
  export function CounterWithInitial(props: { initial: number }) @{
11
- let &[count] = track(props.initial);
11
+ const count = track(props.initial);
12
12
  <div>
13
- <span class="count">{count}</span>
13
+ <span class="count">{count.value}</span>
14
14
  </div>
15
15
  }
16
16
 
@@ -19,26 +19,26 @@ export function CounterWrapper() @{
19
19
  }
20
20
 
21
21
  export function ComputedValues() @{
22
- let &[a] = track(2);
23
- let &[b] = track(3);
24
- const sum = () => a + b;
22
+ const a = track(2);
23
+ const b = track(3);
24
+ const sum = () => a.value + b.value;
25
25
  <div class="sum">{sum()}</div>
26
26
  }
27
27
 
28
28
  export function MultipleTracked() @{
29
- let &[x] = track(10);
30
- let &[y] = track(20);
31
- let &[z] = track(30);
29
+ const x = track(10);
30
+ const y = track(20);
31
+ const z = track(30);
32
32
  <div class="multiple-tracked">
33
- <div class="x">{x}</div>
34
- <div class="y">{y}</div>
35
- <div class="z">{z}</div>
33
+ <div class="x">{x.value}</div>
34
+ <div class="y">{y.value}</div>
35
+ <div class="z">{z.value}</div>
36
36
  </div>
37
37
  }
38
38
 
39
39
  export function DerivedState() @{
40
- let &[firstName] = track('John');
41
- let &[lastName] = track('Doe');
42
- const fullName = () => `${firstName} ${lastName}`;
40
+ const firstName = track('John');
41
+ const lastName = track('Doe');
42
+ const fullName = () => `${firstName.value} ${lastName.value}`;
43
43
  <div class="name">{fullName()}</div>
44
44
  }
@@ -32,11 +32,11 @@ export function resetControls() {
32
32
  resetControls();
33
33
 
34
34
  function BasicContent() @{
35
- let &[data] = trackAsync(() => controls.basic.promise);
36
- let &[count] = track(0);
35
+ const data = trackAsync(() => controls.basic.promise);
36
+ const count = track(0);
37
37
  <div class="resolved">
38
- <span class="value">{data + ':' + count}</span>
39
- <button class="inc" onClick={() => count++}>{'inc'}</button>
38
+ <span class="value">{data.value + ':' + count.value}</span>
39
+ <button class="inc" onClick={() => count.value++}>{'inc'}</button>
40
40
  </div>
41
41
  }
42
42
 
@@ -56,8 +56,8 @@ export function StreamPending() @{
56
56
  }
57
57
 
58
58
  function CatchOnlyContent() @{
59
- let &[data] = trackAsync(() => controls.catchOnly.promise);
60
- <p class="resolved">{data}</p>
59
+ const data = trackAsync(() => controls.catchOnly.promise);
60
+ <p class="resolved">{data.value}</p>
61
61
  }
62
62
 
63
63
  export function StreamCatchOnly() @{
@@ -72,8 +72,8 @@ export function StreamCatchOnly() @{
72
72
  }
73
73
 
74
74
  function RejectContent() @{
75
- let &[data] = trackAsync(() => controls.rejects.promise);
76
- <p class="resolved">{data}</p>
75
+ const data = trackAsync(() => controls.rejects.promise);
76
+ <p class="resolved">{data.value}</p>
77
77
  }
78
78
 
79
79
  export function StreamRejects() @{
@@ -87,8 +87,8 @@ export function StreamRejects() @{
87
87
  }
88
88
 
89
89
  function NoCatchContent() @{
90
- let &[data] = trackAsync(() => controls.noCatch.promise);
91
- <p class="resolved">{data}</p>
90
+ const data = trackAsync(() => controls.noCatch.promise);
91
+ <p class="resolved">{data.value}</p>
92
92
  }
93
93
 
94
94
  export function StreamNoCatch() @{
@@ -108,14 +108,14 @@ export function RootPending() @{
108
108
  }
109
109
 
110
110
  function HeadContent() @{
111
- let &[data] = trackAsync(() => controls.head.promise);
111
+ const data = trackAsync(() => controls.head.promise);
112
112
  <>
113
- @if (data) {
113
+ @if (data.value) {
114
114
  <>
115
115
  <head>
116
- <title>{'title:' + data}</title>
116
+ <title>{'title:' + data.value}</title>
117
117
  </head>
118
- <p class="head-content">{data}</p>
118
+ <p class="head-content">{data.value}</p>
119
119
  </>
120
120
  }
121
121
  </>
@@ -130,18 +130,18 @@ export function StreamHead() @{
130
130
  }
131
131
 
132
132
  export function StreamRootDirect() @{
133
- let &[data] = trackAsync(() => controls.rootDirect.promise);
134
- <p class="root-async">{data}</p>
133
+ const data = trackAsync(() => controls.rootDirect.promise);
134
+ <p class="root-async">{data.value}</p>
135
135
  }
136
136
 
137
137
  function OuterContent() @{
138
- let &[data] = trackAsync(() => controls.outer.promise);
139
- <p class="outer">{data}</p>
138
+ const data = trackAsync(() => controls.outer.promise);
139
+ <p class="outer">{data.value}</p>
140
140
  }
141
141
 
142
142
  function InnerContent() @{
143
- let &[data] = trackAsync(() => controls.inner.promise);
144
- <p class="inner">{data}</p>
143
+ const data = trackAsync(() => controls.inner.promise);
144
+ <p class="inner">{data.value}</p>
145
145
  }
146
146
 
147
147
  export function StreamNested() @{