@playgenx/components 0.1.0 → 0.2.1

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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  //#region src/theme.ts
4
4
  /**
5
5
  * Shared theme tokens for @playgenx/components.
@@ -125,15 +125,285 @@ function TextField({ label, value, placeholder, disabled = false, onChange }) {
125
125
  });
126
126
  }
127
127
  //#endregion
128
+ //#region src/state.tsx
129
+ /**
130
+ * Per-render interactive state store. Used by PlaygroundStateProvider to
131
+ * back interactive components (Slider, TextField, Button, Chart) without
132
+ * requiring them to be fully controlled.
133
+ *
134
+ * Design notes:
135
+ * - The store is a singleton instance per Provider, NOT React context.
136
+ * Components consume it via usePlaygroundState() inside React renders,
137
+ * and via .subscribe() / .get() / .set() from non-React callers.
138
+ * - `set` is synchronous; subscribers fire in registration order on the
139
+ * next microtask (so multiple sets in a single tick produce a single
140
+ * re-render per subscriber).
141
+ * - `set` with `===` equal value is a no-op (no subscriber fires).
142
+ * - Throws if used outside a PlaygroundStateProvider (when required by
143
+ * the caller's contract).
144
+ *
145
+ * @packageDocumentation
146
+ */
147
+ /**
148
+ * Create a fresh state store. Public so PlaygroundStateProvider can
149
+ * construct one, but consumers should use Provider/usePlaygroundState
150
+ * to access stores (not direct construction).
151
+ *
152
+ * The optional type parameter is the "shape" of values you intend to
153
+ * store. It's purely a documentation convenience — the store does
154
+ * not enforce it (so you can store heterogeneous values for
155
+ * playgrounds that mix kinds), but `get<T>()` and `set<T>()` will
156
+ * infer correctly when you pass it here.
157
+ */
158
+ function createPlaygroundState(initial = {}) {
159
+ const values = new Map(Object.entries(initial));
160
+ const subs = /* @__PURE__ */ new Map();
161
+ let pending = [];
162
+ let scheduled = false;
163
+ let batchDepth = 0;
164
+ let batchedPending = [];
165
+ const flush = () => {
166
+ if (pending.length === 0) return;
167
+ const batch = pending;
168
+ pending = [];
169
+ scheduled = false;
170
+ for (const { key, value } of batch) {
171
+ const list = subs.get(key);
172
+ if (list) for (const cb of [...list]) try {
173
+ cb(value);
174
+ } catch (err) {
175
+ if (process.env.NODE_ENV !== "production") console.error("[playgenx] subscriber for key %s threw:", key, err);
176
+ }
177
+ }
178
+ };
179
+ const schedule = () => {
180
+ if (scheduled) return;
181
+ scheduled = true;
182
+ if (typeof queueMicrotask === "function") queueMicrotask(flush);
183
+ else Promise.resolve().then(flush);
184
+ };
185
+ const state = {
186
+ get(key) {
187
+ return values.get(key);
188
+ },
189
+ set(key, value) {
190
+ if (Object.is(values.get(key), value)) return;
191
+ values.set(key, value);
192
+ if (batchDepth > 0) batchedPending.push({
193
+ key,
194
+ value
195
+ });
196
+ else pending.push({
197
+ key,
198
+ value
199
+ });
200
+ schedule();
201
+ },
202
+ subscribe(key, cb) {
203
+ let list = subs.get(key);
204
+ if (!list) {
205
+ list = /* @__PURE__ */ new Set();
206
+ subs.set(key, list);
207
+ }
208
+ list.add(cb);
209
+ return () => {
210
+ const l = subs.get(key);
211
+ if (l) {
212
+ l.delete(cb);
213
+ if (l.size === 0) subs.delete(key);
214
+ }
215
+ };
216
+ },
217
+ snapshot() {
218
+ return Object.fromEntries(values);
219
+ },
220
+ replaceAll(values) {
221
+ for (const [k, v] of Object.entries(values)) state.set(k, v);
222
+ },
223
+ _flush() {
224
+ flush();
225
+ },
226
+ _batch(fn) {
227
+ batchDepth++;
228
+ try {
229
+ return fn();
230
+ } finally {
231
+ batchDepth--;
232
+ if (batchDepth === 0 && batchedPending.length > 0) {
233
+ const lastPerKey = /* @__PURE__ */ new Map();
234
+ for (let i = batchedPending.length - 1; i >= 0; i--) {
235
+ const { key, value } = batchedPending[i];
236
+ if (!lastPerKey.has(key)) {
237
+ lastPerKey.set(key, value);
238
+ pending.push({
239
+ key,
240
+ value
241
+ });
242
+ }
243
+ }
244
+ batchedPending = [];
245
+ }
246
+ }
247
+ },
248
+ _clearAllSubscribers() {
249
+ subs.clear();
250
+ pending = [];
251
+ batchedPending = [];
252
+ scheduled = false;
253
+ }
254
+ };
255
+ return state;
256
+ }
257
+ /**
258
+ * React context. Exposed as a type only — consumers should use
259
+ * PlaygroundStateProvider and usePlaygroundState rather than reading
260
+ * the context directly.
261
+ */
262
+ const PlaygroundStateContext = React.createContext(null);
263
+ /**
264
+ * Provide a PlaygroundState scope to descendant components.
265
+ *
266
+ * Each Provider instance has its own store; nested Providers create
267
+ * independent scopes. Components rendered outside any Provider will
268
+ * throw a clear error if they call usePlaygroundState.
269
+ */
270
+ function PlaygroundStateProvider(props) {
271
+ const [state] = React.useState(() => {
272
+ return createPlaygroundState({
273
+ ...props.seed,
274
+ ...props.initial
275
+ });
276
+ });
277
+ React.useEffect(() => {
278
+ return () => {
279
+ state._clearAllSubscribers?.();
280
+ };
281
+ }, [state]);
282
+ const value = React.useMemo(() => ({ state }), [state]);
283
+ return React.createElement(PlaygroundStateContext.Provider, { value }, props.children);
284
+ }
285
+ /**
286
+ * Hook to access the nearest PlaygroundState store. Throws a clear
287
+ * error if called outside a Provider so consumers get a usable
288
+ * diagnostic instead of a `Cannot read properties of undefined`.
289
+ *
290
+ * Note for @playgenx/renderer users: the renderer's `renderBody`
291
+ * does NOT yet auto-wrap with a PlaygroundStateProvider. You'll
292
+ * need to wrap your own tree with `<PlaygroundStateProvider>` (or
293
+ * use the lower-level `renderNodes` API) before state-bound
294
+ * components can resolve. The `withState` option on `renderBody`
295
+ * is planned for a future renderer release (tracked in v0.6.0).
296
+ */
297
+ function usePlaygroundState() {
298
+ const ctx = React.useContext(PlaygroundStateContext);
299
+ if (ctx === null) throw new Error("usePlaygroundState() must be called inside a <PlaygroundStateProvider>. Wrap your component tree with the Provider. (A future renderer release will auto-wrap; for now, do it explicitly.)");
300
+ return ctx.state;
301
+ }
302
+ /**
303
+ * Optional helper for binding a component prop to a state key. Returns
304
+ * the live value (re-renders subscribers on change) and a setter. Pass
305
+ * `undefined` for `key` to disable the binding — the component behaves
306
+ * as if it had no stateKey, and `set` becomes a no-op. Safe to call
307
+ * outside a Provider.
308
+ *
309
+ * Implementation note: this hook ALWAYS calls the same number of
310
+ * React hooks regardless of whether `key` is set or a Provider is in
311
+ * scope. The "no binding" / "no Provider" branch uses dummy local
312
+ * state so the hook count stays aligned across renders. That means a
313
+ * component can flip `stateKey` from defined to undefined (or move in
314
+ * or out of a Provider) without crashing with a hooks-order error.
315
+ *
316
+ * Components that accept a `stateKey` prop should call this internally
317
+ * and use the returned tuple as a drop-in for the underlying
318
+ * value/onChange pair. The return type widens to `readonly [...] | undefined`
319
+ * so the caller can still branch on `if (bound)` — but they don't have
320
+ * to (the tuple is always safe to index).
321
+ *
322
+ * @example
323
+ * const bound = useBoundValueOrUndefined<number>(stateKey, 50);
324
+ * // ... onChange={(e) => bound?.[1](Number(e.currentTarget.value))}
325
+ */
326
+ function useBoundValueOrUndefined(key, fallback) {
327
+ const ctx = React.useContext(PlaygroundStateContext);
328
+ const bound = key !== void 0 && ctx !== null;
329
+ const [value, setValue] = React.useState(fallback);
330
+ const [, force] = React.useState(0);
331
+ React.useEffect(() => {
332
+ if (!bound || key === void 0 || ctx === null) return;
333
+ const initial = ctx.state.get(key) ?? fallback;
334
+ setValue(initial);
335
+ return ctx.state.subscribe(key, (next) => {
336
+ setValue(next);
337
+ force((n) => n + 1);
338
+ });
339
+ }, [
340
+ bound,
341
+ ctx,
342
+ key
343
+ ]);
344
+ const set = React.useCallback((next) => {
345
+ if (!bound || key === void 0 || ctx === null) return;
346
+ ctx.state.set(key, next);
347
+ }, [
348
+ bound,
349
+ ctx,
350
+ key
351
+ ]);
352
+ if (!bound) return void 0;
353
+ return [value, set];
354
+ }
355
+ function useBoundValue(key, fallback) {
356
+ const bound = useBoundValueOrUndefined(key, fallback);
357
+ if (bound === void 0) throw new Error("useBoundValue(key, ...) requires both a non-undefined `key` and a <PlaygroundStateProvider> ancestor. Use useBoundValueOrUndefined if you want optional binding.");
358
+ return bound;
359
+ }
360
+ /**
361
+ * Apply a StateAction to the nearest PlaygroundState. Returns an
362
+ * onClick handler that fires the action. If action is undefined
363
+ * the handler is a no-op.
364
+ */
365
+ function useStateAction(action) {
366
+ const state = usePlaygroundState();
367
+ return React.useCallback((event) => {
368
+ if (action === void 0) return;
369
+ if (action.set) for (const [k, v] of Object.entries(action.set)) state.set(k, v);
370
+ if (action.toggle !== void 0) {
371
+ const key = action.toggle;
372
+ const current = state.get(key);
373
+ state.set(key, !current);
374
+ }
375
+ }, [action, state]);
376
+ }
377
+ //#endregion
128
378
  //#region src/Slider.tsx
129
379
  /**
130
- * Range input. State: internal `value` mirrors props.value if present
131
- * (otherwise uncontrolled); clamps to [min, max].
380
+ * Range input. Three operating modes:
381
+ *
382
+ * 1. **Uncontrolled** (no `value`, no `stateKey`): the slider owns
383
+ * its value as local React state. The onChange mutates that state.
384
+ * Use this when nothing else needs to observe the value.
385
+ *
386
+ * 2. **Controlled by `value` prop**: the slider is read-only —
387
+ * user input is ignored because there is no `onChange` callback
388
+ * wired by the deterministic renderer. Use this for "static"
389
+ * sliders in artifacts. (If you want fully interactive control,
390
+ * use mode 3.)
391
+ *
392
+ * 3. **Bound to PlaygroundState via `stateKey`**: the slider reads
393
+ * its value from the nearest Provider's store (initial: either
394
+ * the store's current value OR `value` prop, whichever is
395
+ * present; the live store wins when bound). On change, the
396
+ * slider writes to the store. Other components can subscribe
397
+ * to the same key.
398
+ *
399
+ * In all modes the displayed value is clamped to `[min, max]`. NaN
400
+ * inputs clamp to `min`. When `min === max` the input is disabled.
132
401
  */
133
- function Slider({ min, max, value, step = 1, label }) {
402
+ function Slider({ min, max, value, step = 1, label, stateKey }) {
134
403
  const [internal, setInternal] = React.useState(() => clamp(value ?? min, min, max));
404
+ const bound = useBoundValueOrUndefined(stateKey, internal);
135
405
  const controlled = value !== void 0;
136
- const current = controlled ? clamp(value, min, max) : internal;
406
+ const current = stateKey !== void 0 && bound ? clamp(bound[0], min, max) : controlled ? clamp(value, min, max) : internal;
137
407
  const trackStyle = {
138
408
  display: "flex",
139
409
  flexDirection: "column",
@@ -179,7 +449,8 @@ function Slider({ min, max, value, step = 1, label }) {
179
449
  "aria-label": label,
180
450
  onChange: (e) => {
181
451
  const next = Number(e.currentTarget.value);
182
- if (!controlled) setInternal(next);
452
+ if (stateKey !== void 0 && bound) bound[1](next);
453
+ else if (!controlled) setInternal(next);
183
454
  },
184
455
  style: inputStyle
185
456
  }), /* @__PURE__ */ jsx("span", {
@@ -604,35 +875,315 @@ function List({ items, ordered, children }) {
604
875
  }));
605
876
  }
606
877
  //#endregion
607
- //#region src/registry.ts
878
+ //#region src/stateHelpers.ts
879
+ function diffSnapshots(prev, next) {
880
+ const added = [];
881
+ const changed = [];
882
+ const removed = [];
883
+ const prevKeys = Object.keys(prev);
884
+ const nextKeys = Object.keys(next);
885
+ const prevSet = new Set(prevKeys);
886
+ const nextSet = new Set(nextKeys);
887
+ for (const k of nextKeys) {
888
+ if (!prevSet.has(k)) {
889
+ added.push(k);
890
+ continue;
891
+ }
892
+ if (!Object.is(prev[k], next[k])) changed.push(k);
893
+ }
894
+ for (const k of prevKeys) if (!nextSet.has(k)) removed.push(k);
895
+ added.sort();
896
+ changed.sort();
897
+ removed.sort();
898
+ if (added.length === 0 && changed.length === 0 && removed.length === 0) return null;
899
+ return {
900
+ added,
901
+ changed,
902
+ removed
903
+ };
904
+ }
905
+ /**
906
+ * Convenience: take snapshots of both stores and diff them. Returns
907
+ * `null` when equal. Useful in tests and debug endpoints.
908
+ */
909
+ function diffState(_a, _b, prev, next) {
910
+ return diffSnapshots(prev ?? _a.snapshot(), next ?? _b.snapshot());
911
+ }
912
+ /**
913
+ * Validate a state-key string. Returns `null` on success, or a
914
+ * human-readable error message on failure. The rules:
915
+ *
916
+ * - 1..64 characters
917
+ * - No whitespace
918
+ * - Only alphanumerics, dots, dashes, underscores
919
+ * - Must start with an alphabetic character (so a parser can
920
+ * safely infer `<Slider stateKey="...">` from a body without
921
+ * swallowing accidental numeric keys).
922
+ *
923
+ * Note: keys like `with/slash` or `with spaces` are rejected because
924
+ * the schema-driven body parser uses them as attribute names —
925
+ * a key with weird characters would break the round-trip.
926
+ */
927
+ function validateStateKey(key) {
928
+ if (key.length === 0) return "stateKey must be a non-empty string";
929
+ if (key.length > 64) return "stateKey must be at most 64 characters";
930
+ if (/\s/.test(key)) return "stateKey must not contain whitespace";
931
+ if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(key)) return "stateKey contains forbidden characters (allowed: A-Z, a-z, 0-9, dot, dash, underscore; must start with a letter)";
932
+ return null;
933
+ }
934
+ /**
935
+ * Remove every key from the store, firing subscribers with `undefined`.
936
+ * After this call, `state.snapshot()` is `{}`.
937
+ */
938
+ function clearState(state) {
939
+ for (const k of Object.keys(state.snapshot())) state.set(k, void 0);
940
+ }
941
+ function dumpState(state) {
942
+ const snap = state.snapshot();
943
+ const envelope = {
944
+ version: 1,
945
+ keys: Object.keys(snap).sort(),
946
+ values: { ...snap }
947
+ };
948
+ return JSON.stringify(envelope, null, 2);
949
+ }
950
+ /**
951
+ * Apply multiple `set` calls as a single atomic batch. Each
952
+ * subscriber sees only the LAST value written to its key in this
953
+ * batch. The store still batches subscriber fires via the microtask
954
+ * scheduler — `batch` does not flush synchronously, it only ensures
955
+ * that within the batch, repeated `set` calls to the same key don't
956
+ * queue redundant fires.
957
+ *
958
+ * Useful for: undo/redo, multi-field form commits, hydrated state
959
+ * restore where you want each subscriber to fire exactly once with
960
+ * the final value.
961
+ *
962
+ * @example
963
+ * batch(state, (s) => {
964
+ * s.set('x', 1);
965
+ * s.set('y', 2);
966
+ * s.set('x', 3); // overwrites; subscriber sees only 3
967
+ * });
968
+ */
969
+ function batch(state, fn) {
970
+ return state._batch(() => fn(state));
971
+ }
972
+ //#endregion
973
+ //#region src/ShowSource.tsx
608
974
  /**
609
- * Map of PascalCase component name to React component. Use this when
610
- * you have a renderer that walks an AST and needs to pick an
611
- * implementation at runtime:
975
+ * Render-prop component that shows a toggle-able raw-source panel.
976
+ * Consumer controls the trigger UI via a children render prop.
612
977
  *
613
- * const C = componentMap[tagName];
614
- * if (C) el = <C {...props}>{children}</C>;
978
+ * Design notes:
979
+ * - Render-prop pattern (not props.children-as-trigger) because the
980
+ * trigger UI varies wildly across consumers (button, link, icon,
981
+ * inline text). The consumer gets `(toggle, showing)` and decides
982
+ * what to render.
983
+ * - The source panel is a `<pre>` with `<code>` inside. Plain-text;
984
+ * no syntax highlighting (avoiding a 50KB highlight.js dep).
985
+ * Consumers who need highlighting can compose with their own
986
+ * syntax-highlighter.
987
+ * - The panel is rendered INLINE (not in a portal/tooltip) so the
988
+ * toggle's visual feedback is immediate.
989
+ *
990
+ * @packageDocumentation
991
+ */
992
+ /**
993
+ * Toggle button + expandable source panel. The trigger UI is the
994
+ * consumer's choice; the panel is plain `<pre><code>`.
995
+ */
996
+ function ShowSource(props) {
997
+ const { body, language, children } = props;
998
+ const [showing, setShowing] = React.useState(false);
999
+ return /* @__PURE__ */ jsxs(Fragment, { children: [children(React.useCallback(() => {
1000
+ setShowing((s) => !s);
1001
+ }, []), showing), showing ? /* @__PURE__ */ jsx("pre", {
1002
+ "data-pgx": "ShowSource",
1003
+ "data-language": language ?? "text",
1004
+ style: {
1005
+ background: "#0f172a",
1006
+ color: "#e2e8f0",
1007
+ padding: "12px",
1008
+ borderRadius: "6px",
1009
+ overflowX: "auto",
1010
+ fontFamily: "ui-monospace, Menlo, monospace",
1011
+ fontSize: "12px",
1012
+ lineHeight: 1.5,
1013
+ margin: "8px 0 0",
1014
+ maxHeight: "320px",
1015
+ whiteSpace: "pre"
1016
+ },
1017
+ children: /* @__PURE__ */ jsx("code", { children: body })
1018
+ }) : null] });
1019
+ }
1020
+ //#endregion
1021
+ //#region src/ArtifactErrorBoundary.tsx
1022
+ /**
1023
+ * Error boundary for rendered artifacts. Catches render-time errors
1024
+ * (malformed body, throw from a component, etc.) and shows a recoverable
1025
+ * fallback. Without this, a single bad artifact blanks the student's
1026
+ * whole page.
615
1027
  *
616
- * Keys mirror `DEFAULT_REGISTRY.list()`.
1028
+ * Design notes:
1029
+ * - Uses React's class-component error boundary API (the only
1030
+ * first-class option in React 18/19).
1031
+ * - Default fallback shows the error message + an optional "Show source"
1032
+ * toggle that reveals the raw body. Consumer can override with
1033
+ * `fallback` for custom UX.
1034
+ * - `onError` fires before the fallback renders so consumers can log
1035
+ * to Sentry / Datadog / etc.
617
1036
  *
618
- * Type note: components vary in their props shape, so the map is
619
- * typed permissively (`Record<string, ComponentType<any>>`). Each
620
- * individual export above is still strictly typed.
1037
+ * @packageDocumentation
1038
+ */
1039
+ /**
1040
+ * Default fallback panel. Renders the error message and, if `body` is
1041
+ * provided, a ShowSource toggle. Designed to be a calm, recoverable
1042
+ * surface — not a stack-trace dump that panics users.
621
1043
  */
622
- const componentMap = {
1044
+ function DefaultFallback({ error, body, language, kind }) {
1045
+ return /* @__PURE__ */ jsxs("div", {
1046
+ role: "alert",
1047
+ "data-pgx": "ArtifactErrorBoundary",
1048
+ "data-pgx-kind": kind,
1049
+ style: {
1050
+ border: "1px solid #fca5a5",
1051
+ background: "#fef2f2",
1052
+ color: "#7f1d1d",
1053
+ borderRadius: "8px",
1054
+ padding: "12px 16px",
1055
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
1056
+ fontSize: "14px",
1057
+ maxWidth: "720px",
1058
+ margin: "12px 0"
1059
+ },
1060
+ children: [
1061
+ /* @__PURE__ */ jsx("div", {
1062
+ style: {
1063
+ fontWeight: 600,
1064
+ marginBottom: "6px"
1065
+ },
1066
+ children: kind ? `${kind} artifact failed to render` : "Artifact failed to render"
1067
+ }),
1068
+ /* @__PURE__ */ jsx("code", {
1069
+ "data-pgx": "error-message",
1070
+ style: {
1071
+ display: "block",
1072
+ fontFamily: "ui-monospace, Menlo, monospace",
1073
+ fontSize: "12px",
1074
+ background: "rgba(0,0,0,0.04)",
1075
+ padding: "6px 8px",
1076
+ borderRadius: "4px",
1077
+ whiteSpace: "pre-wrap",
1078
+ wordBreak: "break-word"
1079
+ },
1080
+ children: error.message
1081
+ }),
1082
+ body !== void 0 ? /* @__PURE__ */ jsx("div", {
1083
+ style: { marginTop: "8px" },
1084
+ children: /* @__PURE__ */ jsx(ShowSource, {
1085
+ body,
1086
+ language,
1087
+ children: (toggle, showing) => /* @__PURE__ */ jsx("button", {
1088
+ type: "button",
1089
+ onClick: toggle,
1090
+ "data-pgx": "show-source-toggle",
1091
+ style: {
1092
+ background: "transparent",
1093
+ border: "1px solid #fca5a5",
1094
+ color: "#7f1d1d",
1095
+ padding: "4px 10px",
1096
+ fontSize: "12px",
1097
+ borderRadius: "4px",
1098
+ cursor: "pointer"
1099
+ },
1100
+ children: showing ? "Hide source" : "Show source"
1101
+ })
1102
+ })
1103
+ }) : null
1104
+ ]
1105
+ });
1106
+ }
1107
+ /**
1108
+ * Error boundary. Catches render-time errors in `children` and shows
1109
+ * the fallback. The error state is reset on prop change of `children`
1110
+ * (or explicit `resetKey` bump) so the consumer can recover by
1111
+ * re-rendering.
1112
+ */
1113
+ var ArtifactErrorBoundary = class extends React.Component {
1114
+ state = { error: null };
1115
+ static getDerivedStateFromError(error) {
1116
+ return { error };
1117
+ }
1118
+ componentDidCatch(error, info) {
1119
+ if (this.props.onError) try {
1120
+ this.props.onError(error, { componentStack: info.componentStack ?? "" });
1121
+ } catch {}
1122
+ }
1123
+ render() {
1124
+ const { error } = this.state;
1125
+ if (error === null) return this.props.children;
1126
+ const { fallback, body, language, kind } = this.props;
1127
+ if (typeof fallback === "function") return fallback(error);
1128
+ if (fallback !== void 0) return fallback;
1129
+ return /* @__PURE__ */ jsx(DefaultFallback, {
1130
+ error,
1131
+ body,
1132
+ language,
1133
+ kind
1134
+ });
1135
+ }
1136
+ };
1137
+ //#endregion
1138
+ //#region src/createRegistry.ts
1139
+ const DEFAULT_REGISTRY = Object.freeze({
623
1140
  Button,
624
- TextField,
625
- Slider,
1141
+ Card,
626
1142
  Chart,
627
- Container,
628
1143
  Code,
1144
+ Container,
629
1145
  Heading,
630
- Text,
1146
+ List,
1147
+ Slider,
631
1148
  Stepper,
632
- Card,
633
- List
634
- };
1149
+ Text,
1150
+ TextField
1151
+ });
1152
+ /**
1153
+ * The full default registry (Button, Card, Chart, Code, Container,
1154
+ * Heading, List, Slider, Stepper, Text, TextField). Frozen. Use
1155
+ * `createRegistry(overrides)` to extend.
1156
+ */
1157
+ const componentMap = DEFAULT_REGISTRY;
1158
+ /**
1159
+ * Build a new registry by merging the defaults with caller-supplied
1160
+ * overrides. The returned map is frozen (Object.freeze) to prevent
1161
+ * post-hoc mutation that would surprise concurrent renders.
1162
+ *
1163
+ * **Lowercase HTML tags are NOT in this map.** The renderer handles
1164
+ * `div`, `span`, etc. as React intrinsic elements separately. Calling
1165
+ * `createRegistry({ div: StubDiv })` will NOT override the renderer
1166
+ * for `<div>` (the renderer uses the lowercase lookup path first).
1167
+ *
1168
+ * @example
1169
+ * ```ts
1170
+ * const map = createRegistry({
1171
+ * MathExpression: MathExpressionComponent,
1172
+ * LatexBlock: LatexBlockComponent,
1173
+ * });
1174
+ * ```
1175
+ */
1176
+ function createRegistry(overrides) {
1177
+ const merged = { ...DEFAULT_REGISTRY };
1178
+ for (const key of Object.keys(overrides)) {
1179
+ const value = overrides[key];
1180
+ if (value === void 0) continue;
1181
+ if (key[0] !== key[0].toUpperCase()) continue;
1182
+ merged[key] = value;
1183
+ }
1184
+ return Object.freeze(merged);
1185
+ }
635
1186
  //#endregion
636
- export { Button, Card, Chart, Code, Container, Heading, List, Slider, Stepper, Text, TextField, componentMap };
1187
+ export { ArtifactErrorBoundary, Button, Card, Chart, Code, Container, Heading, List, PlaygroundStateProvider, ShowSource, Slider, Stepper, Text, TextField, batch, clearState, componentMap, createPlaygroundState, createRegistry, diffSnapshots, diffState, dumpState, useBoundValue, useBoundValueOrUndefined, usePlaygroundState, useStateAction, validateStateKey };
637
1188
 
638
1189
  //# sourceMappingURL=index.mjs.map