slexkit 0.3.0 → 0.3.2

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 (48) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -3
  3. package/README.zh-CN.md +4 -3
  4. package/dist/ai/llms-components.txt +2 -1
  5. package/dist/ai/llms-full.txt +85 -42
  6. package/dist/ai/llms-runtime.txt +11 -12
  7. package/dist/ai/llms.txt +1 -1
  8. package/dist/ai/slexkit-ai-manifest.json +77 -73
  9. package/dist/base.css +0 -46
  10. package/dist/components/checkbox.js +1 -0
  11. package/dist/components/choice.css +2 -2
  12. package/dist/components/display.css +1 -1
  13. package/dist/components/index.js +56 -153
  14. package/dist/components/input.css +53 -119
  15. package/dist/components/input.js +33 -143
  16. package/dist/components/radio-group.js +1 -0
  17. package/dist/components/select.css +0 -6
  18. package/dist/components/slider.css +27 -18
  19. package/dist/components/specs.js +2 -1
  20. package/dist/components/switch.css +3 -3
  21. package/dist/components/switch.js +1 -0
  22. package/dist/components/text-input.css +21 -90
  23. package/dist/components/text.js +12 -0
  24. package/dist/components/tooling.css +0 -1
  25. package/dist/runtime.cjs +67 -28
  26. package/dist/runtime.js +67 -28
  27. package/dist/slexkit.cjs +123 -181
  28. package/dist/slexkit.css +54 -167
  29. package/dist/slexkit.js +123 -181
  30. package/dist/types/engine/types.d.ts +1 -1
  31. package/dist/types/version.d.ts +2 -2
  32. package/dist/umd/slexkit.tooling.umd.js +122 -181
  33. package/dist/umd/slexkit.umd.js +123 -181
  34. package/package.json +3 -2
  35. package/skills/slexkit-host-integration/SKILL.md +1 -1
  36. package/src/components/svelte/display/Text.svelte +14 -1
  37. package/src/components/svelte/input/Checkbox.svelte +1 -1
  38. package/src/components/svelte/input/Input.svelte +0 -110
  39. package/src/components/svelte/input/RadioGroup.svelte +1 -1
  40. package/src/components/svelte/input/Switch.svelte +1 -1
  41. package/src/styles/components/choice.css +2 -2
  42. package/src/styles/components/select.css +0 -6
  43. package/src/styles/components/slider.css +27 -18
  44. package/src/styles/components/switch.css +3 -3
  45. package/src/styles/components/text-input.css +21 -90
  46. package/src/styles/display.css +1 -1
  47. package/src/styles/layout.css +0 -46
  48. package/src/styles/tooling.css +0 -1
@@ -104,11 +104,11 @@
104
104
  opacity: 0.55;
105
105
  }
106
106
 
107
- .slex-switch:has(.slex-switch-input:disabled) {
107
+ .slex-switch[data-disabled="true"] {
108
108
  cursor: not-allowed;
109
109
  }
110
110
 
111
- .slex-switch:has(.slex-switch-input:disabled):hover .slex-switch-control,
112
- .slex-switch:has(.slex-switch-input:disabled):hover .slex-switch-control::after {
111
+ .slex-switch[data-disabled="true"]:hover .slex-switch-control,
112
+ .slex-switch[data-disabled="true"]:hover .slex-switch-control::after {
113
113
  box-shadow: none;
114
114
  }
@@ -89,6 +89,7 @@ function Switch($$anchor, $$props) {
89
89
  reset(span);
90
90
  template_effect(($0, $1) => {
91
91
  set_attribute(label, "data-state", get(enabled) ? "on" : "off");
92
+ set_attribute(label, "data-disabled", get(p).disabled ? "true" : undefined);
92
93
  input.disabled = !!get(p).disabled;
93
94
  set_attribute(input, "aria-label", $0);
94
95
  set_text(text_1, $1);
@@ -45,16 +45,18 @@
45
45
  border: 1px solid var(--input);
46
46
  border-radius: var(--radius);
47
47
  background: var(--background);
48
+ background-clip: padding-box;
48
49
  color: var(--foreground);
49
50
  font-family: inherit;
50
51
  font-size: 0.875rem;
51
52
  line-height: 1.5;
52
53
  outline: none;
54
+ -webkit-appearance: none;
55
+ appearance: none;
53
56
  transition: border-color 150ms ease, box-shadow 150ms ease;
54
57
  }
55
58
 
56
- .slex-input-control[data-has-unit="true"] .slex-input,
57
- .slex-input-control[data-has-controls="true"] .slex-input {
59
+ .slex-input-control[data-has-unit="true"] .slex-input {
58
60
  border-top-right-radius: 0;
59
61
  border-bottom-right-radius: 0;
60
62
  }
@@ -78,79 +80,16 @@
78
80
  transition: border-color 150ms ease, box-shadow 150ms ease;
79
81
  }
80
82
 
81
- .slex-input-control[data-has-controls="true"] .slex-input-unit {
82
- border-radius: 0;
83
- }
84
-
85
- .slex-input-controls {
86
- box-sizing: border-box;
87
- display: inline-grid;
88
- grid-template-rows: repeat(2, minmax(0, 1fr));
89
- align-items: stretch;
90
- flex: 0 0 auto;
91
- width: 1.875rem;
92
- min-width: 1.875rem;
93
- min-height: 2.5625rem;
94
- overflow: hidden;
95
- border: 1px solid var(--input);
96
- border-left: 0;
97
- border-radius: 0 var(--radius) var(--radius) 0;
98
- background: var(--background);
99
- transition: border-color 150ms ease, box-shadow 150ms ease;
100
- }
101
-
102
- .slex-input-step {
103
- box-sizing: border-box;
104
- display: inline-flex;
105
- align-items: center;
106
- justify-content: center;
107
- width: 100%;
108
- min-width: 0;
109
- min-height: 0;
110
- padding: 0;
111
- border: 0;
112
- border-top: 1px solid var(--input);
113
- border-radius: 0;
114
- background: transparent;
115
- color: var(--foreground);
116
- font: inherit;
117
- font-size: 0.75rem;
118
- font-weight: 600;
119
- line-height: 1;
120
- cursor: pointer;
121
- transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease;
122
- }
123
-
124
- .slex-input-step:first-child {
125
- border-top: 0;
126
- }
127
-
128
- .slex-input-step:last-child {
129
- border-radius: 0;
130
- }
131
-
132
- .slex-input-step:hover:not(:disabled) {
133
- background: color-mix(in oklab, var(--muted) 52%, var(--background));
134
- }
135
-
136
- .slex-input-step:focus-visible {
137
- z-index: 1;
138
- outline: none;
139
- background: color-mix(in oklab, var(--muted) 58%, var(--background));
140
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ring) 34%, transparent);
141
- }
142
-
143
- .slex-input-step:disabled {
144
- opacity: 0.45;
145
- cursor: not-allowed;
146
- }
147
-
148
83
  .slex-input::placeholder {
149
84
  color: var(--muted-foreground);
150
85
  }
151
86
 
152
87
  .slex-input:focus {
153
88
  border-color: var(--ring);
89
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--ring) 18%, transparent);
90
+ }
91
+
92
+ .slex-input-control[data-has-unit="true"] .slex-input:focus {
154
93
  box-shadow: none;
155
94
  }
156
95
 
@@ -175,6 +114,10 @@
175
114
 
176
115
  .slex-input-field[data-invalid="true"] .slex-input:focus {
177
116
  border-color: var(--destructive);
117
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--destructive) 18%, transparent);
118
+ }
119
+
120
+ .slex-input-field[data-invalid="true"] .slex-input-control[data-has-unit="true"] .slex-input:focus {
178
121
  box-shadow: none;
179
122
  }
180
123
 
@@ -182,13 +125,12 @@
182
125
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--ring) 16%, transparent);
183
126
  }
184
127
 
185
- .slex-input-control:focus-within .slex-input,
186
- .slex-input-control:focus-within .slex-input-unit,
187
- .slex-input-control:focus-within .slex-input-controls {
188
- border-color: var(--ring);
128
+ .slex-input-control:not([data-has-unit]):focus-within {
129
+ box-shadow: none;
189
130
  }
190
131
 
191
- .slex-input-control:focus-within .slex-input-step {
132
+ .slex-input-control:focus-within .slex-input,
133
+ .slex-input-control:focus-within .slex-input-unit {
192
134
  border-color: var(--ring);
193
135
  }
194
136
 
@@ -197,22 +139,16 @@
197
139
  color: color-mix(in oklab, var(--destructive) 84%, var(--muted-foreground));
198
140
  }
199
141
 
200
- .slex-input-field[data-invalid="true"] .slex-input-step {
201
- border-color: color-mix(in oklab, var(--destructive) 72%, var(--input));
202
- }
203
-
204
- .slex-input-field[data-invalid="true"] .slex-input-controls {
205
- border-color: color-mix(in oklab, var(--destructive) 72%, var(--input));
206
- }
207
-
208
142
  .slex-input-field[data-invalid="true"] .slex-input-control:focus-within {
209
143
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--destructive) 18%, transparent);
210
144
  }
211
145
 
146
+ .slex-input-field[data-invalid="true"] .slex-input-control:not([data-has-unit]):focus-within {
147
+ box-shadow: none;
148
+ }
149
+
212
150
  .slex-input-field[data-invalid="true"] .slex-input-control:focus-within .slex-input,
213
- .slex-input-field[data-invalid="true"] .slex-input-control:focus-within .slex-input-unit,
214
- .slex-input-field[data-invalid="true"] .slex-input-control:focus-within .slex-input-controls,
215
- .slex-input-field[data-invalid="true"] .slex-input-control:focus-within .slex-input-step {
151
+ .slex-input-field[data-invalid="true"] .slex-input-control:focus-within .slex-input-unit {
216
152
  border-color: var(--destructive);
217
153
  }
218
154
 
@@ -226,11 +162,6 @@
226
162
  cursor: not-allowed;
227
163
  }
228
164
 
229
- .slex-input[disabled] ~ .slex-input-controls,
230
- .slex-input[readonly] ~ .slex-input-controls {
231
- opacity: 0.6;
232
- }
233
-
234
165
  .slex-input-description {
235
166
  color: var(--muted-foreground);
236
167
  font-size: 0.75rem;
@@ -11,10 +11,12 @@ import {
11
11
  reset,
12
12
  set,
13
13
  set_class,
14
+ set_style,
14
15
  set_text,
15
16
  state,
16
17
  template_effect,
17
18
  text1 as text,
19
+ user_derived,
18
20
  user_effect
19
21
  } from "../chunks/button-cr1fhsa7.js";
20
22
 
@@ -27,11 +29,21 @@ function Text($$anchor, $$props) {
27
29
  push($$props, true);
28
30
  let p = state(proxy({}));
29
31
  user_effect(() => bindPropStore($$props.props, (next) => set(p, next, true)));
32
+ const color = user_derived(() => get(p).color == null || get(p).color === "" ? undefined : text(get(p).color));
33
+ const size = user_derived(() => cssLength(get(p).size));
34
+ function cssLength(value) {
35
+ if (value == null || value === "")
36
+ return;
37
+ const rendered = text(value);
38
+ return /^-?\d+(\.\d+)?$/.test(rendered) ? `${rendered}px` : rendered;
39
+ }
30
40
  var div = root();
41
+ let styles;
31
42
  var text_1 = child(div, true);
32
43
  reset(div);
33
44
  template_effect(($0, $1) => {
34
45
  set_class(div, 1, $0);
46
+ styles = set_style(div, "", styles, { color: get(color), "font-size": get(size) });
35
47
  set_text(text_1, $1);
36
48
  }, [
37
49
  () => `slex-text${get(p).variant ? ` slex-text--${text(get(p).variant)}` : ""}${get(p).class ? ` ${text(get(p).class)}` : ""}`,
@@ -572,7 +572,6 @@
572
572
  overflow: hidden;
573
573
  clip: rect(0 0 0 0);
574
574
  white-space: nowrap;
575
- clip-path: inset(50%);
576
575
  }
577
576
 
578
577
  .slex-playground-error {
package/dist/runtime.cjs CHANGED
@@ -468,19 +468,31 @@ function configureComponentScope(options) {
468
468
  function createComponentAccessor(read) {
469
469
  const subscribers = new Set;
470
470
  let current = read();
471
- const accessor = () => current;
472
- accessor.subscribe = (run) => {
473
- subscribers.add(run);
474
- run(current);
475
- const stop = createEffect(() => {
471
+ let stopEffect;
472
+ const start = () => {
473
+ if (stopEffect)
474
+ return;
475
+ stopEffect = createEffect(() => {
476
476
  current = read();
477
- for (const subscriber of subscribers)
477
+ for (const subscriber of Array.from(subscribers))
478
478
  subscriber(current);
479
479
  flushDom?.();
480
480
  });
481
+ };
482
+ const accessor = () => current;
483
+ accessor.subscribe = (run) => {
484
+ const wasIdle = subscribers.size === 0;
485
+ subscribers.add(run);
486
+ if (wasIdle)
487
+ start();
488
+ else
489
+ run(current);
481
490
  return () => {
482
491
  subscribers.delete(run);
483
- stop();
492
+ if (subscribers.size === 0) {
493
+ stopEffect?.();
494
+ stopEffect = undefined;
495
+ }
484
496
  };
485
497
  };
486
498
  return accessor;
@@ -761,6 +773,9 @@ function isWritableComponent(type) {
761
773
  const mode = getComponentStateMode(type);
762
774
  return mode === "value" || mode === "checked" || mode === "enabled";
763
775
  }
776
+ function isReadableComponent(type) {
777
+ return getComponentStateMode(type) === "readable";
778
+ }
764
779
  function isStatefulComponent(type) {
765
780
  return getComponentStateMode(type) !== "none";
766
781
  }
@@ -877,7 +892,10 @@ function createGProxy(g, components, componentTypes) {
877
892
  function ensureComponentState(name, type, components, componentTypes) {
878
893
  if (!components[name])
879
894
  components[name] = {};
880
- componentTypes[name] = type;
895
+ const previousType = componentTypes[name] ?? "";
896
+ if (!(isWritableComponent(previousType) && isReadableComponent(type))) {
897
+ componentTypes[name] = type;
898
+ }
881
899
  return components[name];
882
900
  }
883
901
  function syncReadableComponentProp(type, state, propName, value) {
@@ -905,6 +923,9 @@ function syncReadableComponentProp(type, state, propName, value) {
905
923
  function syncComponentProps(type, name, props, components, componentTypes) {
906
924
  if (!name || !isStatefulComponent(type))
907
925
  return;
926
+ const previousType = componentTypes[name] ?? "";
927
+ if (isWritableComponent(previousType) && isReadableComponent(type))
928
+ return;
908
929
  const state = ensureComponentState(name, type, components, componentTypes);
909
930
  if (type === "input" && typeof props.type === "string") {
910
931
  assignInputType(state, props.type);
@@ -969,7 +990,7 @@ function seedStaticComponentState(type, state, props) {
969
990
  function warnDuplicateState(ns, name, currentType, currentPath, previous) {
970
991
  console.warn(`[SlexKit][${ns}] Component state '${name}' is declared more than once at ${previous.path} and ${currentPath}; state is shared by namespace and component name.`);
971
992
  if (previous.type !== currentType) {
972
- console.warn(`[SlexKit][${ns}] Component state '${name}' is used by multiple component types (${previous.type}, ${currentType}); the latest rendered type controls write behavior.`);
993
+ console.warn(`[SlexKit][${ns}] Component state '${name}' is used by multiple component types (${previous.type}, ${currentType}); use distinct names when components should not share state.`);
973
994
  }
974
995
  }
975
996
  function dynamicStateBinding(type, props) {
@@ -981,6 +1002,9 @@ function isMirroredValueControlPair(previousType, currentType) {
981
1002
  return previousType === "input" && currentType === "slider" || previousType === "slider" && currentType === "input";
982
1003
  }
983
1004
  function shouldWarnDuplicateState(currentType, currentBinding, previous) {
1005
+ if (isReadableComponent(previous.type) && isReadableComponent(currentType)) {
1006
+ return false;
1007
+ }
984
1008
  if (currentBinding && previous.stateBinding === currentBinding && isMirroredValueControlPair(previous.type, currentType)) {
985
1009
  return false;
986
1010
  }
@@ -1319,9 +1343,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1319
1343
  const renderer = getRenderer(type);
1320
1344
  if (!renderer)
1321
1345
  return;
1322
- const forWrapper = (container.ownerDocument || document).createElement("div");
1323
- forWrapper.className = "slexkit-for-wrapper";
1324
- container.appendChild(forWrapper);
1346
+ const doc = container.ownerDocument || document;
1347
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1348
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1349
+ container.append(startAnchor, endAnchor);
1325
1350
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1326
1351
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1327
1352
  const $keyProp = props.$key;
@@ -1334,8 +1359,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1334
1359
  disposedSlots.add(slot);
1335
1360
  leavingSlots.delete(slot);
1336
1361
  callHook(g, name, "onUnmount");
1337
- disposeComponent(slot.el);
1338
- slot.el.remove();
1362
+ if (slot.el) {
1363
+ disposeComponent(slot.el);
1364
+ slot.el.remove();
1365
+ }
1339
1366
  if (slot.dispose)
1340
1367
  slot.dispose();
1341
1368
  };
@@ -1360,12 +1387,16 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1360
1387
  }
1361
1388
  }
1362
1389
  for (const slot of deletedSlots) {
1363
- container.appendChild(slot.el);
1364
1390
  leavingSlots.add(slot);
1391
+ if (!slot.el) {
1392
+ disposeSlot(slot);
1393
+ continue;
1394
+ }
1365
1395
  applyLeaveAnimation(slot.el, slot.props, () => {
1366
1396
  disposeSlot(slot);
1367
1397
  });
1368
1398
  }
1399
+ let cursor = startAnchor;
1369
1400
  arr.forEach((item, index) => {
1370
1401
  item = asReactiveValue(item, g);
1371
1402
  const keyVal = newKeys[index];
@@ -1393,20 +1424,22 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1393
1424
  const indexSignal = createSignal(index);
1394
1425
  const revisionSignal = createSignal(0);
1395
1426
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1396
- if (slot.el) {
1397
- applyEnterAnimation(slot.el, slot.props);
1398
- callHook(g, name, "onMount");
1427
+ if (!slot.el) {
1428
+ disposeSlot(slot);
1429
+ return;
1399
1430
  }
1431
+ applyEnterAnimation(slot.el, slot.props);
1432
+ callHook(g, name, "onMount");
1400
1433
  slotMap.set(keyVal, slot);
1401
1434
  }
1402
- const refChild = forWrapper.children[index];
1403
- if (slot.el && refChild !== slot.el) {
1404
- forWrapper.insertBefore(slot.el, refChild ?? null);
1435
+ const nextChild = cursor.nextSibling;
1436
+ if (slot.el && nextChild !== slot.el) {
1437
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1438
+ }
1439
+ if (slot.el) {
1440
+ cursor = slot.el;
1405
1441
  }
1406
1442
  });
1407
- while (forWrapper.children.length > arr.length) {
1408
- forWrapper.lastChild.remove();
1409
- }
1410
1443
  });
1411
1444
  onCleanup(() => {
1412
1445
  for (const slot of Array.from(slotMap.values()))
@@ -1414,7 +1447,8 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1414
1447
  slotMap.clear();
1415
1448
  for (const slot of Array.from(leavingSlots))
1416
1449
  disposeSlot(slot);
1417
- forWrapper.remove();
1450
+ startAnchor.remove();
1451
+ endAnchor.remove();
1418
1452
  });
1419
1453
  }
1420
1454
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1791,7 +1825,7 @@ ${parseSource}
1791
1825
  var parseSlexKitDsl = parseSlexSource;
1792
1826
 
1793
1827
  // src/version.ts
1794
- var SLEXKIT_VERSION = "0.3.0";
1828
+ var SLEXKIT_VERSION = "0.3.2";
1795
1829
  var SLEX_PROTOCOL_VERSION = "0.1";
1796
1830
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1797
1831
  function getSlexKitInfo() {
@@ -2919,7 +2953,6 @@ var inputSpec = component({
2919
2953
  min: { type: "string | number", dynamic: true, description: "Minimum value used by numeric input controls." },
2920
2954
  max: { type: "string | number", dynamic: true, description: "Maximum value used by numeric input controls." },
2921
2955
  step: { type: "string | number", dynamic: true, description: "Step size used by numeric input controls." },
2922
- controls: { type: "boolean", default: true, dynamic: true, description: "Show decrement and increment buttons for numeric inputs." },
2923
2956
  onchange: { type: "write-expression", description: "Write expression invoked when the value changes." }
2924
2957
  },
2925
2958
  children: noChildren,
@@ -3427,6 +3460,8 @@ var textSpec = component({
3427
3460
  content: { type: "string", dynamic: true, description: "Alias for text." },
3428
3461
  label: { type: "string", dynamic: true, description: "Alias for text." },
3429
3462
  variant: { type: "string", values: ["default", "muted"], default: "default", description: "Text visual variant." },
3463
+ color: { type: "string", dynamic: true, description: "Optional CSS color for controlled previews." },
3464
+ size: { type: "string | number", dynamic: true, description: "Optional font size. Numbers are treated as px." },
3430
3465
  class: { type: "string", description: "Additional host-controlled CSS class." }
3431
3466
  },
3432
3467
  children: noChildren,
@@ -3727,7 +3762,11 @@ function isRenderableSource(value) {
3727
3762
  return Object.keys(value).some((key) => key.includes(":"));
3728
3763
  }
3729
3764
  function bareLayoutFromSource(value) {
3730
- const { slex: _slex, namespace: _namespace, g: _g, layout: _layout, ...layout } = value;
3765
+ const layout = { ...value };
3766
+ delete layout.slex;
3767
+ delete layout.namespace;
3768
+ delete layout.g;
3769
+ delete layout.layout;
3731
3770
  return layout;
3732
3771
  }
3733
3772
  function isStateOnlySource(value) {
package/dist/runtime.js CHANGED
@@ -396,19 +396,31 @@ function configureComponentScope(options) {
396
396
  function createComponentAccessor(read) {
397
397
  const subscribers = new Set;
398
398
  let current = read();
399
- const accessor = () => current;
400
- accessor.subscribe = (run) => {
401
- subscribers.add(run);
402
- run(current);
403
- const stop = createEffect(() => {
399
+ let stopEffect;
400
+ const start = () => {
401
+ if (stopEffect)
402
+ return;
403
+ stopEffect = createEffect(() => {
404
404
  current = read();
405
- for (const subscriber of subscribers)
405
+ for (const subscriber of Array.from(subscribers))
406
406
  subscriber(current);
407
407
  flushDom?.();
408
408
  });
409
+ };
410
+ const accessor = () => current;
411
+ accessor.subscribe = (run) => {
412
+ const wasIdle = subscribers.size === 0;
413
+ subscribers.add(run);
414
+ if (wasIdle)
415
+ start();
416
+ else
417
+ run(current);
409
418
  return () => {
410
419
  subscribers.delete(run);
411
- stop();
420
+ if (subscribers.size === 0) {
421
+ stopEffect?.();
422
+ stopEffect = undefined;
423
+ }
412
424
  };
413
425
  };
414
426
  return accessor;
@@ -689,6 +701,9 @@ function isWritableComponent(type) {
689
701
  const mode = getComponentStateMode(type);
690
702
  return mode === "value" || mode === "checked" || mode === "enabled";
691
703
  }
704
+ function isReadableComponent(type) {
705
+ return getComponentStateMode(type) === "readable";
706
+ }
692
707
  function isStatefulComponent(type) {
693
708
  return getComponentStateMode(type) !== "none";
694
709
  }
@@ -805,7 +820,10 @@ function createGProxy(g, components, componentTypes) {
805
820
  function ensureComponentState(name, type, components, componentTypes) {
806
821
  if (!components[name])
807
822
  components[name] = {};
808
- componentTypes[name] = type;
823
+ const previousType = componentTypes[name] ?? "";
824
+ if (!(isWritableComponent(previousType) && isReadableComponent(type))) {
825
+ componentTypes[name] = type;
826
+ }
809
827
  return components[name];
810
828
  }
811
829
  function syncReadableComponentProp(type, state, propName, value) {
@@ -833,6 +851,9 @@ function syncReadableComponentProp(type, state, propName, value) {
833
851
  function syncComponentProps(type, name, props, components, componentTypes) {
834
852
  if (!name || !isStatefulComponent(type))
835
853
  return;
854
+ const previousType = componentTypes[name] ?? "";
855
+ if (isWritableComponent(previousType) && isReadableComponent(type))
856
+ return;
836
857
  const state = ensureComponentState(name, type, components, componentTypes);
837
858
  if (type === "input" && typeof props.type === "string") {
838
859
  assignInputType(state, props.type);
@@ -897,7 +918,7 @@ function seedStaticComponentState(type, state, props) {
897
918
  function warnDuplicateState(ns, name, currentType, currentPath, previous) {
898
919
  console.warn(`[SlexKit][${ns}] Component state '${name}' is declared more than once at ${previous.path} and ${currentPath}; state is shared by namespace and component name.`);
899
920
  if (previous.type !== currentType) {
900
- console.warn(`[SlexKit][${ns}] Component state '${name}' is used by multiple component types (${previous.type}, ${currentType}); the latest rendered type controls write behavior.`);
921
+ console.warn(`[SlexKit][${ns}] Component state '${name}' is used by multiple component types (${previous.type}, ${currentType}); use distinct names when components should not share state.`);
901
922
  }
902
923
  }
903
924
  function dynamicStateBinding(type, props) {
@@ -909,6 +930,9 @@ function isMirroredValueControlPair(previousType, currentType) {
909
930
  return previousType === "input" && currentType === "slider" || previousType === "slider" && currentType === "input";
910
931
  }
911
932
  function shouldWarnDuplicateState(currentType, currentBinding, previous) {
933
+ if (isReadableComponent(previous.type) && isReadableComponent(currentType)) {
934
+ return false;
935
+ }
912
936
  if (currentBinding && previous.stateBinding === currentBinding && isMirroredValueControlPair(previous.type, currentType)) {
913
937
  return false;
914
938
  }
@@ -1247,9 +1271,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1247
1271
  const renderer = getRenderer(type);
1248
1272
  if (!renderer)
1249
1273
  return;
1250
- const forWrapper = (container.ownerDocument || document).createElement("div");
1251
- forWrapper.className = "slexkit-for-wrapper";
1252
- container.appendChild(forWrapper);
1274
+ const doc = container.ownerDocument || document;
1275
+ const startAnchor = doc.createComment(`slexkit-for:${fullKey}:start`);
1276
+ const endAnchor = doc.createComment(`slexkit-for:${fullKey}:end`);
1277
+ container.append(startAnchor, endAnchor);
1253
1278
  const evalCtx = buildComponentEvalContext(g, components, componentTypes, api, forCtx);
1254
1279
  const items = createMemo(() => trackForCollection(evalRead(props.$for, evalCtx, ns, `${fullKey}:$for`)));
1255
1280
  const $keyProp = props.$key;
@@ -1262,8 +1287,10 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1262
1287
  disposedSlots.add(slot);
1263
1288
  leavingSlots.delete(slot);
1264
1289
  callHook(g, name, "onUnmount");
1265
- disposeComponent(slot.el);
1266
- slot.el.remove();
1290
+ if (slot.el) {
1291
+ disposeComponent(slot.el);
1292
+ slot.el.remove();
1293
+ }
1267
1294
  if (slot.dispose)
1268
1295
  slot.dispose();
1269
1296
  };
@@ -1288,12 +1315,16 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1288
1315
  }
1289
1316
  }
1290
1317
  for (const slot of deletedSlots) {
1291
- container.appendChild(slot.el);
1292
1318
  leavingSlots.add(slot);
1319
+ if (!slot.el) {
1320
+ disposeSlot(slot);
1321
+ continue;
1322
+ }
1293
1323
  applyLeaveAnimation(slot.el, slot.props, () => {
1294
1324
  disposeSlot(slot);
1295
1325
  });
1296
1326
  }
1327
+ let cursor = startAnchor;
1297
1328
  arr.forEach((item, index) => {
1298
1329
  item = asReactiveValue(item, g);
1299
1330
  const keyVal = newKeys[index];
@@ -1321,20 +1352,22 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1321
1352
  const indexSignal = createSignal(index);
1322
1353
  const revisionSignal = createSignal(0);
1323
1354
  slot = renderAndMountSlot(item, index, keyVal, indexSignal, revisionSignal, renderer, type, name, props, container, g, components, componentTypes, api, forCtx, ns, fullKey, options);
1324
- if (slot.el) {
1325
- applyEnterAnimation(slot.el, slot.props);
1326
- callHook(g, name, "onMount");
1355
+ if (!slot.el) {
1356
+ disposeSlot(slot);
1357
+ return;
1327
1358
  }
1359
+ applyEnterAnimation(slot.el, slot.props);
1360
+ callHook(g, name, "onMount");
1328
1361
  slotMap.set(keyVal, slot);
1329
1362
  }
1330
- const refChild = forWrapper.children[index];
1331
- if (slot.el && refChild !== slot.el) {
1332
- forWrapper.insertBefore(slot.el, refChild ?? null);
1363
+ const nextChild = cursor.nextSibling;
1364
+ if (slot.el && nextChild !== slot.el) {
1365
+ container.insertBefore(slot.el, nextChild ?? endAnchor);
1366
+ }
1367
+ if (slot.el) {
1368
+ cursor = slot.el;
1333
1369
  }
1334
1370
  });
1335
- while (forWrapper.children.length > arr.length) {
1336
- forWrapper.lastChild.remove();
1337
- }
1338
1371
  });
1339
1372
  onCleanup(() => {
1340
1373
  for (const slot of Array.from(slotMap.values()))
@@ -1342,7 +1375,8 @@ function renderForNode(fullKey, props, container, g, components, componentTypes,
1342
1375
  slotMap.clear();
1343
1376
  for (const slot of Array.from(leavingSlots))
1344
1377
  disposeSlot(slot);
1345
- forWrapper.remove();
1378
+ startAnchor.remove();
1379
+ endAnchor.remove();
1346
1380
  });
1347
1381
  }
1348
1382
  function renderNormalNode(fullKey, props, container, g, components, componentTypes, api, forCtx, ns, options) {
@@ -1719,7 +1753,7 @@ ${parseSource}
1719
1753
  var parseSlexKitDsl = parseSlexSource;
1720
1754
 
1721
1755
  // src/version.ts
1722
- var SLEXKIT_VERSION = "0.3.0";
1756
+ var SLEXKIT_VERSION = "0.3.2";
1723
1757
  var SLEX_PROTOCOL_VERSION = "0.1";
1724
1758
  var SLEXKIT_COMPONENTS_VERSION = SLEXKIT_VERSION;
1725
1759
  function getSlexKitInfo() {
@@ -2847,7 +2881,6 @@ var inputSpec = component({
2847
2881
  min: { type: "string | number", dynamic: true, description: "Minimum value used by numeric input controls." },
2848
2882
  max: { type: "string | number", dynamic: true, description: "Maximum value used by numeric input controls." },
2849
2883
  step: { type: "string | number", dynamic: true, description: "Step size used by numeric input controls." },
2850
- controls: { type: "boolean", default: true, dynamic: true, description: "Show decrement and increment buttons for numeric inputs." },
2851
2884
  onchange: { type: "write-expression", description: "Write expression invoked when the value changes." }
2852
2885
  },
2853
2886
  children: noChildren,
@@ -3355,6 +3388,8 @@ var textSpec = component({
3355
3388
  content: { type: "string", dynamic: true, description: "Alias for text." },
3356
3389
  label: { type: "string", dynamic: true, description: "Alias for text." },
3357
3390
  variant: { type: "string", values: ["default", "muted"], default: "default", description: "Text visual variant." },
3391
+ color: { type: "string", dynamic: true, description: "Optional CSS color for controlled previews." },
3392
+ size: { type: "string | number", dynamic: true, description: "Optional font size. Numbers are treated as px." },
3358
3393
  class: { type: "string", description: "Additional host-controlled CSS class." }
3359
3394
  },
3360
3395
  children: noChildren,
@@ -3655,7 +3690,11 @@ function isRenderableSource(value) {
3655
3690
  return Object.keys(value).some((key) => key.includes(":"));
3656
3691
  }
3657
3692
  function bareLayoutFromSource(value) {
3658
- const { slex: _slex, namespace: _namespace, g: _g, layout: _layout, ...layout } = value;
3693
+ const layout = { ...value };
3694
+ delete layout.slex;
3695
+ delete layout.namespace;
3696
+ delete layout.g;
3697
+ delete layout.layout;
3659
3698
  return layout;
3660
3699
  }
3661
3700
  function isStateOnlySource(value) {