@playgenx/components 0.2.0 → 0.2.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.
- package/README.md +417 -35
- package/bin/state-cli.mjs +196 -0
- package/dist/index.d.mts +102 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +148 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
package/dist/index.d.mts
CHANGED
|
@@ -281,11 +281,18 @@ declare function usePlaygroundState(): PlaygroundState;
|
|
|
281
281
|
*
|
|
282
282
|
* Implementation note: this hook ALWAYS calls the same number of
|
|
283
283
|
* React hooks regardless of whether `key` is set or a Provider is in
|
|
284
|
-
* scope. The "no binding" / "no Provider" branch
|
|
285
|
-
*
|
|
284
|
+
* scope. The "no binding" / "no Provider" branch returns a static
|
|
285
|
+
* tuple so the hook count stays aligned across renders. That means a
|
|
286
286
|
* component can flip `stateKey` from defined to undefined (or move in
|
|
287
287
|
* or out of a Provider) without crashing with a hooks-order error.
|
|
288
288
|
*
|
|
289
|
+
* The hook uses React 19's `useSyncExternalStore` so SSR renders the
|
|
290
|
+
* Provider's current value synchronously — no "value=fallback on
|
|
291
|
+
* server, value=real after hydration" surprise. The hook count drops
|
|
292
|
+
* from 5 (useContext + useState + useState + useEffect + useCallback)
|
|
293
|
+
* to 3 (useContext + useSyncExternalStore + useCallback) as a side
|
|
294
|
+
* benefit.
|
|
295
|
+
*
|
|
289
296
|
* Components that accept a `stateKey` prop should call this internally
|
|
290
297
|
* and use the returned tuple as a drop-in for the underlying
|
|
291
298
|
* value/onChange pair. The return type widens to `readonly [...] | undefined`
|
|
@@ -298,6 +305,98 @@ declare function usePlaygroundState(): PlaygroundState;
|
|
|
298
305
|
*/
|
|
299
306
|
declare function useBoundValueOrUndefined<T>(key: string | undefined, fallback: T): readonly [T, (next: T) => void] | undefined;
|
|
300
307
|
declare function useBoundValue<T>(key: string, fallback: T): readonly [T, (next: T) => void];
|
|
308
|
+
/**
|
|
309
|
+
* Non-subscribing accessor for the store. Returns a stable tuple of
|
|
310
|
+
* imperative API methods (`get`, `set`, `subscribe`, `snapshot`,
|
|
311
|
+
* `replaceAll`) plus the raw `state` ref — none of which trigger a
|
|
312
|
+
* re-render when used.
|
|
313
|
+
*
|
|
314
|
+
* When to use this instead of `useBoundValue`:
|
|
315
|
+
*
|
|
316
|
+
* - You need to read state inside an event handler (no render needed).
|
|
317
|
+
* - You need to set state from many different handlers, not one bound prop.
|
|
318
|
+
* - You're implementing a custom hook that wraps the store.
|
|
319
|
+
* - You want to subscribe imperatively (for analytics, side-effects).
|
|
320
|
+
*
|
|
321
|
+
* When NOT to use this: if you need the component to RE-RENDER on
|
|
322
|
+
* state changes, use `useBoundValue` (which subscribes for you).
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* function LogoutButton() {
|
|
326
|
+
* const { get, set } = useStoreSnapshot();
|
|
327
|
+
* return (
|
|
328
|
+
* <button onClick={() => {
|
|
329
|
+
* if (get<boolean>('confirmLogout')) set('confirmLogout', false);
|
|
330
|
+
* doLogout();
|
|
331
|
+
* }}>Log out</button>
|
|
332
|
+
* );
|
|
333
|
+
* }
|
|
334
|
+
*/
|
|
335
|
+
declare function useStoreSnapshot(): {
|
|
336
|
+
readonly state: PlaygroundState;
|
|
337
|
+
readonly get: <T = unknown>(key: string) => T | undefined;
|
|
338
|
+
readonly set: <T = unknown>(key: string, value: T) => void;
|
|
339
|
+
readonly subscribe: (key: string, cb: (value: unknown) => void) => () => void;
|
|
340
|
+
readonly snapshot: () => Readonly<Record<string, unknown>>;
|
|
341
|
+
readonly replaceAll: (values: Record<string, unknown>) => void;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Bind a state key with a selector transform. Returns the SELECTED
|
|
345
|
+
* value, not the raw value. The component only re-renders when the
|
|
346
|
+
* selected slice changes — not when OTHER fields on the same key
|
|
347
|
+
* change.
|
|
348
|
+
*
|
|
349
|
+
* Useful for normalized state where one key holds an object and you
|
|
350
|
+
* only care about one property:
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* interface User { name: string; age: number; email: string; }
|
|
354
|
+
* const userName = useBoundSelector<User, string>(
|
|
355
|
+
* 'currentUser',
|
|
356
|
+
* (u) => u?.name ?? '(anonymous)',
|
|
357
|
+
* 'guest'
|
|
358
|
+
* );
|
|
359
|
+
* // Setting `currentUser.age` does NOT re-render this component.
|
|
360
|
+
* // Setting `currentUser.name` DOES re-render this component.
|
|
361
|
+
*
|
|
362
|
+
* The setter writes the SELECTED value as-is to the store. For
|
|
363
|
+
* partial-merge semantics (e.g., "set only the name, keep other
|
|
364
|
+
* fields"), use `useStoreSnapshot()` directly and merge yourself.
|
|
365
|
+
*
|
|
366
|
+
* If the selector throws, the error propagates and the component
|
|
367
|
+
* unmounts via the nearest error boundary. Wrap your selector in
|
|
368
|
+
* a try/catch if you need resilience.
|
|
369
|
+
*/
|
|
370
|
+
declare function useBoundSelector<T, U>(key: string, selector: (raw: T | undefined) => U, fallback: U): readonly [U, (next: U) => void];
|
|
371
|
+
/**
|
|
372
|
+
* Bind multiple keys at once. Returns a `Record<key, [value, setter]>`
|
|
373
|
+
* for the keys you specify. Each setter writes back to its own key.
|
|
374
|
+
*
|
|
375
|
+
* Useful for forms and dashboards where multiple bound inputs share
|
|
376
|
+
* a single Provider.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* const form = useMultiBoundValue({
|
|
380
|
+
* name: { fallback: '' },
|
|
381
|
+
* age: { fallback: 0 },
|
|
382
|
+
* email: { fallback: '' },
|
|
383
|
+
* });
|
|
384
|
+
* // form.name[0] === current name string
|
|
385
|
+
* // form.age[1](42) writes 42 to state.age
|
|
386
|
+
*
|
|
387
|
+
* The hook subscribes ONCE per key (so 5 keys = 5 subscriptions,
|
|
388
|
+
* not a single batched subscription). If you bind a LOT of keys,
|
|
389
|
+
* consider using `useStoreSnapshot()` instead — it doesn't subscribe.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* // 50-row table, each row needs its own value
|
|
393
|
+
* const { get, set } = useStoreSnapshot();
|
|
394
|
+
* rows.map(row => <input value={get(`cell-${row.id}`) ?? ''}
|
|
395
|
+
* onChange={e => set(`cell-${row.id}`, e.target.value)} />);
|
|
396
|
+
*/
|
|
397
|
+
declare function useMultiBoundValue<K extends string>(bindings: Record<K, {
|
|
398
|
+
fallback: unknown;
|
|
399
|
+
}>): { readonly [P in K]: readonly [unknown, (next: unknown) => void]; };
|
|
301
400
|
/**
|
|
302
401
|
* State-binding action shape. Used by `<Button onClickAction={...}>`.
|
|
303
402
|
* { set: { count: 1 } } -> sets state.count = 1
|
|
@@ -506,5 +605,5 @@ declare const componentMap: ComponentMap;
|
|
|
506
605
|
*/
|
|
507
606
|
declare function createRegistry(overrides: Partial<ComponentMap>): ComponentMap;
|
|
508
607
|
//#endregion
|
|
509
|
-
export { ArtifactErrorBoundary, type ArtifactErrorBoundaryProps, Button, type ButtonProps, Card, type CardProps, Chart, type ChartKind, type ChartProps, Code, type CodeProps, type ComponentMap, Container, type ContainerProps, Heading, type HeadingProps, List, type ListProps, type PlaygroundState, PlaygroundStateProvider, type PlaygroundStateProviderProps, ShowSource, type ShowSourceProps, Slider, type SliderProps, type StateAction, type StateDiff, type StateEnvelope, type Step, Stepper, type StepperProps, Text, TextField, type TextFieldProps, type TextProps, batch, clearState, componentMap, createPlaygroundState, createRegistry, diffSnapshots, diffState, dumpState, useBoundValue, useBoundValueOrUndefined, usePlaygroundState, useStateAction, validateStateKey };
|
|
608
|
+
export { ArtifactErrorBoundary, type ArtifactErrorBoundaryProps, Button, type ButtonProps, Card, type CardProps, Chart, type ChartKind, type ChartProps, Code, type CodeProps, type ComponentMap, Container, type ContainerProps, Heading, type HeadingProps, List, type ListProps, type PlaygroundState, PlaygroundStateProvider, type PlaygroundStateProviderProps, ShowSource, type ShowSourceProps, Slider, type SliderProps, type StateAction, type StateDiff, type StateEnvelope, type Step, Stepper, type StepperProps, Text, TextField, type TextFieldProps, type TextProps, batch, clearState, componentMap, createPlaygroundState, createRegistry, diffSnapshots, diffState, dumpState, useBoundSelector, useBoundValue, useBoundValueOrUndefined, useMultiBoundValue, usePlaygroundState, useStateAction, useStoreSnapshot, validateStateKey };
|
|
510
609
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/Button.tsx","../src/TextField.tsx","../src/Slider.tsx","../src/Chart.tsx","../src/Container.tsx","../src/Code.tsx","../src/Heading.tsx","../src/Text.tsx","../src/Stepper.tsx","../src/Card.tsx","../src/List.tsx","../src/state.tsx","../src/stateHelpers.ts","../src/ArtifactErrorBoundary.tsx","../src/ShowSource.tsx","../src/createRegistry.ts"],"mappings":";;;;;;;;;UASiB;;EAEf;;EAEA;;EAEA;;;;;EAKA,UAAU,MAAM,kBAAkB;;iBAGpB,SACd,OACA,SACA,UACA,WACC,cAAc,MAAM,IAAI;;;UCzBV;EACf;EACA;EACA;EACA;;;;;;EAMA,WAAW,MAAM,mBAAmB;;;;;;;iBAQtB,YACd,OACA,OACA,aACA,UACA,YACC,iBAAiB,MAAM,IAAI;;;UCvBb;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Bc,SAAS,KAAK,KAAK,OAAO,MAAU,OAAO,YAAY,cAAc,MAAM,IAAI;;;KC3CnF;;;;;;;;;;;UAYK;EACf,MAAM;EACN;EACA;;iBAGc,QAAQ,MAAM,MAAM,SAAS,aAAa,MAAM,IAAI;;;UCnBnD;EACf;EACA;EACA,WAAW,MAAM;;;;;;iBAOH,YAAY,SAAS,KAAK,YAAY,iBAAiB,MAAM,IAAI;;;UCThE;EACf;EACA,WAAW,MAAM;;;;;;;;iBASH,OAAO,UAAU,YAAY,YAAY,MAAM,IAAI;;;UCXlD;;;;;;EAMf;EACA;EACA,WAAW,MAAM;;iBAYH,UAAU,OAAO,OAAO,YAAY,eAAe,MAAM,IAAI;;;UCpB5D;EACf;EACA;EACA;EACA,WAAW,MAAM;;iBAGH,OAAO,QAAmB,MAAM,OAAO,YAAY,YAAY,MAAM,IAAI;;;UCPxE;EACf;EACA;EACA,OAAO,MAAM;;UAGE;EACf,OAAO;;EAEP;;;;;;iBAOc,UAAU,OAAO,WAAW,eAAe,MAAM,IAAI;;;UChBpD;EACf;;;;;EAKA;EACA,WAAW,MAAM;;iBAGH,OAAO,OAAO,WAAe,YAAY,YAAY,MAAM,IAAI;;;UCV9D;;;;;;EAMf;;EAEA;;EAEA,WAAW,MAAM;;;;;;;iBAQH,OAAO,OAAO,SAAS,YAAY,YAAY,MAAM,IAAI;;;;;;;UCIxD;;EAEf,IAAI,aAAa,cAAc;;;;;EAK/B,IAAI,aAAa,aAAa,OAAO;;;;;;EAMrC,UAAU,aAAa,KAAK;;;;;EAK5B,YAAY,SAAS;;EAErB,WAAW,QAAQ;;;;;;;;;;;;;iBAsCL,sBAAsB,aACpC,UAAS,eAAe,KACvB;UAoKc;;EAEf,UAAU;;;;;;EAMV,OAAO;EACP,UAAU,MAAM;;;;;;;;;iBAUF,wBACd,OAAO,+BACN,MAAM,IAAI;;;;;;;;;;;;;iBAkDG,sBAAsB
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/Button.tsx","../src/TextField.tsx","../src/Slider.tsx","../src/Chart.tsx","../src/Container.tsx","../src/Code.tsx","../src/Heading.tsx","../src/Text.tsx","../src/Stepper.tsx","../src/Card.tsx","../src/List.tsx","../src/state.tsx","../src/stateHelpers.ts","../src/ArtifactErrorBoundary.tsx","../src/ShowSource.tsx","../src/createRegistry.ts"],"mappings":";;;;;;;;;UASiB;;EAEf;;EAEA;;EAEA;;;;;EAKA,UAAU,MAAM,kBAAkB;;iBAGpB,SACd,OACA,SACA,UACA,WACC,cAAc,MAAM,IAAI;;;UCzBV;EACf;EACA;EACA;EACA;;;;;;EAMA,WAAW,MAAM,mBAAmB;;;;;;;iBAQtB,YACd,OACA,OACA,aACA,UACA,YACC,iBAAiB,MAAM,IAAI;;;UCvBb;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;;;;;;;;;;;;;;;;;;;;;;iBA0Bc,SAAS,KAAK,KAAK,OAAO,MAAU,OAAO,YAAY,cAAc,MAAM,IAAI;;;KC3CnF;;;;;;;;;;;UAYK;EACf,MAAM;EACN;EACA;;iBAGc,QAAQ,MAAM,MAAM,SAAS,aAAa,MAAM,IAAI;;;UCnBnD;EACf;EACA;EACA,WAAW,MAAM;;;;;;iBAOH,YAAY,SAAS,KAAK,YAAY,iBAAiB,MAAM,IAAI;;;UCThE;EACf;EACA,WAAW,MAAM;;;;;;;;iBASH,OAAO,UAAU,YAAY,YAAY,MAAM,IAAI;;;UCXlD;;;;;;EAMf;EACA;EACA,WAAW,MAAM;;iBAYH,UAAU,OAAO,OAAO,YAAY,eAAe,MAAM,IAAI;;;UCpB5D;EACf;EACA;EACA;EACA,WAAW,MAAM;;iBAGH,OAAO,QAAmB,MAAM,OAAO,YAAY,YAAY,MAAM,IAAI;;;UCPxE;EACf;EACA;EACA,OAAO,MAAM;;UAGE;EACf,OAAO;;EAEP;;;;;;iBAOc,UAAU,OAAO,WAAW,eAAe,MAAM,IAAI;;;UChBpD;EACf;;;;;EAKA;EACA,WAAW,MAAM;;iBAGH,OAAO,OAAO,WAAe,YAAY,YAAY,MAAM,IAAI;;;UCV9D;;;;;;EAMf;;EAEA;;EAEA,WAAW,MAAM;;;;;;;iBAQH,OAAO,OAAO,SAAS,YAAY,YAAY,MAAM,IAAI;;;;;;;UCIxD;;EAEf,IAAI,aAAa,cAAc;;;;;EAK/B,IAAI,aAAa,aAAa,OAAO;;;;;;EAMrC,UAAU,aAAa,KAAK;;;;;EAK5B,YAAY,SAAS;;EAErB,WAAW,QAAQ;;;;;;;;;;;;;iBAsCL,sBAAsB,aACpC,UAAS,eAAe,KACvB;UAoKc;;EAEf,UAAU;;;;;;EAMV,OAAO;EACP,UAAU,MAAM;;;;;;;;;iBAUF,wBACd,OAAO,+BACN,MAAM,IAAI;;;;;;;;;;;;;iBAkDG,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CtB,yBAAyB,GACvC,yBACA,UAAU,cACC,IAAI,MAAM;iBAwDP,cAAc,GAC5B,aACA,UAAU,cACC,IAAI,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCP;WACL,OAAO;WACP,MAAM,aAAa,gBAAgB;WACnC,MAAM,aAAa,aAAa,OAAO;WACvC,YAAY,aAAa,KAAK;WAC9B,gBAAgB,SAAS;WACzB,aAAa,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiDhB,iBAAiB,GAAG,GAClC,aACA,WAAW,KAAK,kBAAkB,GAClC,UAAU,cACC,IAAI,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwEP,mBAAmB,kBACjC,UAAU,OAAO;EAAK;iBACT,KAAK,wBAAwB;;;;;;KA6ChC;EACN,MAAM;EAAyB;;;;;;;iBAQrB,eACd,QAAQ,eACN;;;;;;;;;;;;;UCnnBa;;EAEf;;EAEA;;EAEA;;iBAGc,cACd,MAAM,SAAS,0BACf,MAAM,SAAS,2BACd;;;;;iBAiCa,UACd,IAAI,iBACJ,IAAI,iBACJ,OAAO,SAAS,0BAChB,OAAO,SAAS,2BACf;;;;;;;;;;;;;;;;iBAqBa,iBAAiB;;;;;iBAiBjB,WAAW,OAAO;;;;;;;;;;UAgBjB;;EAEf;;EAEA;;EAEA,QAAQ;;iBAGM,UAAU,OAAO;;;;;;;;;;;;;;;;;;;;iBA6BjB,MAAM,GACpB,OAAO,iBACP,KAAK,OAAO,oBAAoB,IAC/B;;;UCrJc;EACf,UAAU,MAAM;;;;;;EAMhB,WAAW,OAAO,OAAO;IAAQ;;;;;;;;EAOjC;;;;;EAKA;;;;;;;EAOA,WAAW,MAAM,cAAc,OAAO,UAAU,MAAM;;;;;;EAMtD;;UAGQ;EACR,OAAO;;;;;;;;cAuFI,8BAA8B,MAAM,UAC/C,4BACA;EAEA,OAAO;SAEA,yBAAyB,OAAO,QAAQ;EAItC,kBAAkB,OAAO,OAAO,MAAM,MAAM;EAU5C,UAAU,MAAM;;;;UChJV;;EAEf;;EAEA;;;;;;EAMA,WAAW,oBAAoB,qBAAqB,MAAM;;;;;;iBAO5C,WAAW,OAAO,kBAAkB,MAAM,IAAI;;;;;;;;;;KCLlD,eAAe,eAAe;;;;;;cAqB7B,cAAc;;;;;;;;;;;;;;;;;;;iBAoBX,eAAe,WAAW,QAAQ,gBAAgB"}
|
package/dist/index.mjs
CHANGED
|
@@ -308,11 +308,18 @@ function usePlaygroundState() {
|
|
|
308
308
|
*
|
|
309
309
|
* Implementation note: this hook ALWAYS calls the same number of
|
|
310
310
|
* React hooks regardless of whether `key` is set or a Provider is in
|
|
311
|
-
* scope. The "no binding" / "no Provider" branch
|
|
312
|
-
*
|
|
311
|
+
* scope. The "no binding" / "no Provider" branch returns a static
|
|
312
|
+
* tuple so the hook count stays aligned across renders. That means a
|
|
313
313
|
* component can flip `stateKey` from defined to undefined (or move in
|
|
314
314
|
* or out of a Provider) without crashing with a hooks-order error.
|
|
315
315
|
*
|
|
316
|
+
* The hook uses React 19's `useSyncExternalStore` so SSR renders the
|
|
317
|
+
* Provider's current value synchronously — no "value=fallback on
|
|
318
|
+
* server, value=real after hydration" surprise. The hook count drops
|
|
319
|
+
* from 5 (useContext + useState + useState + useEffect + useCallback)
|
|
320
|
+
* to 3 (useContext + useSyncExternalStore + useCallback) as a side
|
|
321
|
+
* benefit.
|
|
322
|
+
*
|
|
316
323
|
* Components that accept a `stateKey` prop should call this internally
|
|
317
324
|
* and use the returned tuple as a drop-in for the underlying
|
|
318
325
|
* value/onChange pair. The return type widens to `readonly [...] | undefined`
|
|
@@ -326,21 +333,30 @@ function usePlaygroundState() {
|
|
|
326
333
|
function useBoundValueOrUndefined(key, fallback) {
|
|
327
334
|
const ctx = React.useContext(PlaygroundStateContext);
|
|
328
335
|
const bound = key !== void 0 && ctx !== null;
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
});
|
|
336
|
+
const value = React.useSyncExternalStore(React.useCallback((onStoreChange) => {
|
|
337
|
+
if (!bound || key === void 0 || ctx === null) return () => {};
|
|
338
|
+
return ctx.state.subscribe(key, onStoreChange);
|
|
339
339
|
}, [
|
|
340
340
|
bound,
|
|
341
341
|
ctx,
|
|
342
342
|
key
|
|
343
|
-
])
|
|
343
|
+
]), React.useCallback(() => {
|
|
344
|
+
if (!bound || key === void 0 || ctx === null) return fallback;
|
|
345
|
+
return ctx.state.get(key) ?? fallback;
|
|
346
|
+
}, [
|
|
347
|
+
bound,
|
|
348
|
+
ctx,
|
|
349
|
+
key,
|
|
350
|
+
fallback
|
|
351
|
+
]), React.useCallback(() => {
|
|
352
|
+
if (!bound || key === void 0 || ctx === null) return fallback;
|
|
353
|
+
return ctx.state.get(key) ?? fallback;
|
|
354
|
+
}, [
|
|
355
|
+
bound,
|
|
356
|
+
ctx,
|
|
357
|
+
key,
|
|
358
|
+
fallback
|
|
359
|
+
]));
|
|
344
360
|
const set = React.useCallback((next) => {
|
|
345
361
|
if (!bound || key === void 0 || ctx === null) return;
|
|
346
362
|
ctx.state.set(key, next);
|
|
@@ -358,6 +374,124 @@ function useBoundValue(key, fallback) {
|
|
|
358
374
|
return bound;
|
|
359
375
|
}
|
|
360
376
|
/**
|
|
377
|
+
* Non-subscribing accessor for the store. Returns a stable tuple of
|
|
378
|
+
* imperative API methods (`get`, `set`, `subscribe`, `snapshot`,
|
|
379
|
+
* `replaceAll`) plus the raw `state` ref — none of which trigger a
|
|
380
|
+
* re-render when used.
|
|
381
|
+
*
|
|
382
|
+
* When to use this instead of `useBoundValue`:
|
|
383
|
+
*
|
|
384
|
+
* - You need to read state inside an event handler (no render needed).
|
|
385
|
+
* - You need to set state from many different handlers, not one bound prop.
|
|
386
|
+
* - You're implementing a custom hook that wraps the store.
|
|
387
|
+
* - You want to subscribe imperatively (for analytics, side-effects).
|
|
388
|
+
*
|
|
389
|
+
* When NOT to use this: if you need the component to RE-RENDER on
|
|
390
|
+
* state changes, use `useBoundValue` (which subscribes for you).
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* function LogoutButton() {
|
|
394
|
+
* const { get, set } = useStoreSnapshot();
|
|
395
|
+
* return (
|
|
396
|
+
* <button onClick={() => {
|
|
397
|
+
* if (get<boolean>('confirmLogout')) set('confirmLogout', false);
|
|
398
|
+
* doLogout();
|
|
399
|
+
* }}>Log out</button>
|
|
400
|
+
* );
|
|
401
|
+
* }
|
|
402
|
+
*/
|
|
403
|
+
function useStoreSnapshot() {
|
|
404
|
+
const ctx = React.useContext(PlaygroundStateContext);
|
|
405
|
+
if (ctx === null) throw new Error("useStoreSnapshot() must be called inside a <PlaygroundStateProvider>.");
|
|
406
|
+
return {
|
|
407
|
+
state: ctx.state,
|
|
408
|
+
get: ctx.state.get,
|
|
409
|
+
set: ctx.state.set,
|
|
410
|
+
subscribe: ctx.state.subscribe,
|
|
411
|
+
snapshot: ctx.state.snapshot,
|
|
412
|
+
replaceAll: ctx.state.replaceAll
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Bind a state key with a selector transform. Returns the SELECTED
|
|
417
|
+
* value, not the raw value. The component only re-renders when the
|
|
418
|
+
* selected slice changes — not when OTHER fields on the same key
|
|
419
|
+
* change.
|
|
420
|
+
*
|
|
421
|
+
* Useful for normalized state where one key holds an object and you
|
|
422
|
+
* only care about one property:
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* interface User { name: string; age: number; email: string; }
|
|
426
|
+
* const userName = useBoundSelector<User, string>(
|
|
427
|
+
* 'currentUser',
|
|
428
|
+
* (u) => u?.name ?? '(anonymous)',
|
|
429
|
+
* 'guest'
|
|
430
|
+
* );
|
|
431
|
+
* // Setting `currentUser.age` does NOT re-render this component.
|
|
432
|
+
* // Setting `currentUser.name` DOES re-render this component.
|
|
433
|
+
*
|
|
434
|
+
* The setter writes the SELECTED value as-is to the store. For
|
|
435
|
+
* partial-merge semantics (e.g., "set only the name, keep other
|
|
436
|
+
* fields"), use `useStoreSnapshot()` directly and merge yourself.
|
|
437
|
+
*
|
|
438
|
+
* If the selector throws, the error propagates and the component
|
|
439
|
+
* unmounts via the nearest error boundary. Wrap your selector in
|
|
440
|
+
* a try/catch if you need resilience.
|
|
441
|
+
*/
|
|
442
|
+
function useBoundSelector(key, selector, fallback) {
|
|
443
|
+
const bound = useBoundValueOrUndefined(key, fallback);
|
|
444
|
+
const raw = bound === void 0 ? void 0 : bound[0];
|
|
445
|
+
const value = React.useMemo(() => selector(raw), [
|
|
446
|
+
raw,
|
|
447
|
+
selector,
|
|
448
|
+
fallback
|
|
449
|
+
]);
|
|
450
|
+
const set = React.useCallback((next) => {
|
|
451
|
+
if (bound === void 0) return;
|
|
452
|
+
bound[1](next);
|
|
453
|
+
}, [bound]);
|
|
454
|
+
if (bound === void 0) throw new Error("useBoundSelector requires a non-undefined `key` and a <PlaygroundStateProvider> ancestor.");
|
|
455
|
+
return [value, set];
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Bind multiple keys at once. Returns a `Record<key, [value, setter]>`
|
|
459
|
+
* for the keys you specify. Each setter writes back to its own key.
|
|
460
|
+
*
|
|
461
|
+
* Useful for forms and dashboards where multiple bound inputs share
|
|
462
|
+
* a single Provider.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* const form = useMultiBoundValue({
|
|
466
|
+
* name: { fallback: '' },
|
|
467
|
+
* age: { fallback: 0 },
|
|
468
|
+
* email: { fallback: '' },
|
|
469
|
+
* });
|
|
470
|
+
* // form.name[0] === current name string
|
|
471
|
+
* // form.age[1](42) writes 42 to state.age
|
|
472
|
+
*
|
|
473
|
+
* The hook subscribes ONCE per key (so 5 keys = 5 subscriptions,
|
|
474
|
+
* not a single batched subscription). If you bind a LOT of keys,
|
|
475
|
+
* consider using `useStoreSnapshot()` instead — it doesn't subscribe.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* // 50-row table, each row needs its own value
|
|
479
|
+
* const { get, set } = useStoreSnapshot();
|
|
480
|
+
* rows.map(row => <input value={get(`cell-${row.id}`) ?? ''}
|
|
481
|
+
* onChange={e => set(`cell-${row.id}`, e.target.value)} />);
|
|
482
|
+
*/
|
|
483
|
+
function useMultiBoundValue(bindings) {
|
|
484
|
+
if (React.useContext(PlaygroundStateContext) === null) throw new Error("useMultiBoundValue() must be called inside a <PlaygroundStateProvider>.");
|
|
485
|
+
const result = {};
|
|
486
|
+
const keys = Object.keys(bindings);
|
|
487
|
+
for (const k of keys) {
|
|
488
|
+
const b = useBoundValueOrUndefined(k, bindings[k].fallback);
|
|
489
|
+
if (b === void 0) result[k] = [bindings[k].fallback, () => {}];
|
|
490
|
+
else result[k] = b;
|
|
491
|
+
}
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
361
495
|
* Apply a StateAction to the nearest PlaygroundState. Returns an
|
|
362
496
|
* onClick handler that fires the action. If action is undefined
|
|
363
497
|
* the handler is a no-op.
|
|
@@ -1184,6 +1318,6 @@ function createRegistry(overrides) {
|
|
|
1184
1318
|
return Object.freeze(merged);
|
|
1185
1319
|
}
|
|
1186
1320
|
//#endregion
|
|
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 };
|
|
1321
|
+
export { ArtifactErrorBoundary, Button, Card, Chart, Code, Container, Heading, List, PlaygroundStateProvider, ShowSource, Slider, Stepper, Text, TextField, batch, clearState, componentMap, createPlaygroundState, createRegistry, diffSnapshots, diffState, dumpState, useBoundSelector, useBoundValue, useBoundValueOrUndefined, useMultiBoundValue, usePlaygroundState, useStateAction, useStoreSnapshot, validateStateKey };
|
|
1188
1322
|
|
|
1189
1323
|
//# sourceMappingURL=index.mjs.map
|