@veams/status-quo 1.1.0 → 1.3.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 (73) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +252 -18
  3. package/dist/config/status-quo-config.d.ts +21 -0
  4. package/dist/config/status-quo-config.js +48 -0
  5. package/dist/config/status-quo-config.js.map +1 -0
  6. package/dist/hooks/__tests__/state-selector.spec.d.ts +4 -0
  7. package/dist/hooks/__tests__/state-selector.spec.js +384 -0
  8. package/dist/hooks/__tests__/state-selector.spec.js.map +1 -0
  9. package/dist/hooks/__tests__/state-singleton.spec.d.ts +4 -0
  10. package/dist/hooks/__tests__/state-singleton.spec.js +97 -0
  11. package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -0
  12. package/dist/hooks/index.d.ts +3 -0
  13. package/dist/hooks/index.js +3 -0
  14. package/dist/hooks/index.js.map +1 -1
  15. package/dist/hooks/state-actions.d.ts +2 -0
  16. package/dist/hooks/state-actions.js +5 -0
  17. package/dist/hooks/state-actions.js.map +1 -0
  18. package/dist/hooks/state-factory.d.ts +5 -0
  19. package/dist/hooks/state-factory.js +10 -6
  20. package/dist/hooks/state-factory.js.map +1 -1
  21. package/dist/hooks/state-handler.d.ts +2 -0
  22. package/dist/hooks/state-handler.js +9 -0
  23. package/dist/hooks/state-handler.js.map +1 -0
  24. package/dist/hooks/state-singleton.d.ts +4 -0
  25. package/dist/hooks/state-singleton.js +3 -5
  26. package/dist/hooks/state-singleton.js.map +1 -1
  27. package/dist/hooks/state-subscription-selector.d.ts +5 -0
  28. package/dist/hooks/state-subscription-selector.js +29 -0
  29. package/dist/hooks/state-subscription-selector.js.map +1 -0
  30. package/dist/hooks/state-subscription.d.ts +8 -1
  31. package/dist/hooks/state-subscription.js +49 -10
  32. package/dist/hooks/state-subscription.js.map +1 -1
  33. package/dist/index.d.ts +7 -5
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/store/__tests__/observable-state-handler.spec.js +68 -5
  37. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
  38. package/dist/store/__tests__/signal-state-handler.spec.js +64 -5
  39. package/dist/store/__tests__/signal-state-handler.spec.js.map +1 -1
  40. package/dist/store/index.d.ts +1 -1
  41. package/dist/store/observable-state-handler.d.ts +7 -2
  42. package/dist/store/observable-state-handler.js +17 -22
  43. package/dist/store/observable-state-handler.js.map +1 -1
  44. package/dist/store/signal-state-handler.d.ts +3 -1
  45. package/dist/store/signal-state-handler.js +5 -14
  46. package/dist/store/signal-state-handler.js.map +1 -1
  47. package/dist/store/state-singleton.d.ts +4 -1
  48. package/dist/store/state-singleton.js +11 -2
  49. package/dist/store/state-singleton.js.map +1 -1
  50. package/docs/assets/index-BBmpszOW.css +1 -0
  51. package/docs/assets/index-Cf8El_RO.js +194 -0
  52. package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
  53. package/docs/index.html +13 -0
  54. package/package.json +1 -1
  55. package/playground/src/App.tsx +269 -48
  56. package/playground/src/styles.css +123 -0
  57. package/src/config/status-quo-config.ts +76 -0
  58. package/src/hooks/__tests__/state-selector.spec.tsx +607 -0
  59. package/src/hooks/__tests__/state-singleton.spec.tsx +151 -0
  60. package/src/hooks/index.ts +3 -0
  61. package/src/hooks/state-actions.tsx +7 -0
  62. package/src/hooks/state-factory.tsx +32 -6
  63. package/src/hooks/state-handler.tsx +16 -0
  64. package/src/hooks/state-singleton.tsx +17 -7
  65. package/src/hooks/state-subscription-selector.tsx +70 -0
  66. package/src/hooks/state-subscription.tsx +98 -21
  67. package/src/index.ts +23 -4
  68. package/src/store/__tests__/observable-state-handler.spec.ts +98 -11
  69. package/src/store/__tests__/signal-state-handler.spec.ts +92 -11
  70. package/src/store/index.ts +1 -1
  71. package/src/store/observable-state-handler.ts +25 -27
  72. package/src/store/signal-state-handler.ts +7 -16
  73. package/src/store/state-singleton.ts +21 -3
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Status Quo Playground</title>
7
+ <script type="module" crossorigin src="./assets/index-Cf8El_RO.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-BBmpszOW.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veams/status-quo",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "The manager to rule states in frontend.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
3
  import {
4
4
  makeStateSingleton,
@@ -30,7 +30,10 @@ type CounterActions = {
30
30
 
31
31
  class ObservableCounterStateHandler extends ObservableStateHandler<CounterState, CounterActions> {
32
32
  constructor(startCount = 0) {
33
- super({ initialState: { count: startCount } });
33
+ super({
34
+ initialState: { count: startCount },
35
+ options: { devTools: { enabled: true, namespace: 'Counter as Observable' } },
36
+ });
34
37
  }
35
38
 
36
39
  getActions(): CounterActions {
@@ -51,7 +54,9 @@ class ObservableCounterStateHandler extends ObservableStateHandler<CounterState,
51
54
 
52
55
  class SignalCounterStateHandler extends SignalStateHandler<CounterState, CounterActions> {
53
56
  constructor(startCount = 0) {
54
- super({ initialState: { count: startCount } });
57
+ super({ initialState: { count: startCount },
58
+ options: { devTools: { enabled: true, namespace: 'Counter as Signal' } },
59
+ });
55
60
  }
56
61
 
57
62
  getActions(): CounterActions {
@@ -159,6 +164,7 @@ function SingletonDisplay() {
159
164
  }
160
165
 
161
166
  export function App() {
167
+ const [isNavOpen, setIsNavOpen] = useState(false);
162
168
  const [observableState, observableActions] = useStateFactory(ObservableCounterFactory, [0]);
163
169
  const [signalState, signalActions] = useStateFactory(SignalCounterFactory, [0]);
164
170
 
@@ -222,11 +228,64 @@ const [state, actions] = useStateFactory(() => new CounterStore(), []);`;
222
228
  unsubscribe();
223
229
  store.destroy();`;
224
230
 
225
- const singletonSnippet = `import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
231
+ const singletonOptionsSnippet = `import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
232
+
233
+ const SessionSingleton = makeStateSingleton(() => new SessionStore(), {
234
+ // true (default): destroy when no consumers are mounted
235
+ // false: keep singleton instance alive across unmounts
236
+ destroyOnNoConsumers: false
237
+ });
238
+
239
+ const [state, actions] = useStateSingleton(SessionSingleton);`;
240
+
241
+ const apiUseStateHandlerSnippet = `const handler = useStateHandler(createUserStore, []);`;
242
+
243
+ const apiUseStateActionsSnippet = `const handler = useStateHandler(createUserStore, []);
244
+ const actions = useStateActions(handler);
245
+
246
+ actions.rename('Ada');`;
247
+
248
+ const apiUseStateSubscriptionSnippet = `const handler = useStateHandler(createUserStore, []);
249
+
250
+ const [fullState] = useStateSubscription(handler);
251
+ const [name] = useStateSubscription(handler, (state) => state.user.name);
252
+ const [profile] = useStateSubscription(
253
+ handler,
254
+ (state) => state.user.profile,
255
+ (current, next) => current.id === next.id && current.role === next.role
256
+ );`;
226
257
 
227
- const CounterSingleton = makeStateSingleton(() => new CounterStore());
258
+ const apiUseStateFactorySnippet = `const [state, actions] = useStateFactory(createUserStore, []);
259
+ const [name] = useStateFactory(createUserStore, (state) => state.user.name, []);
260
+ const [profile] = useStateFactory(
261
+ createUserStore,
262
+ (state) => state.user.profile,
263
+ (current, next) => current.id === next.id,
264
+ []
265
+ );`;
228
266
 
229
- const [state, actions] = useStateSingleton(CounterSingleton);`;
267
+ const apiMakeStateSingletonSnippet = `const SessionSingleton = makeStateSingleton(() => new SessionStore(), {
268
+ destroyOnNoConsumers: true // default
269
+ });
270
+
271
+ const PersistentSessionSingleton = makeStateSingleton(() => new SessionStore(), {
272
+ destroyOnNoConsumers: false
273
+ });`;
274
+
275
+ const apiUseStateSingletonSnippet = `const [state, actions] = useStateSingleton(SessionSingleton);
276
+ const [userName] = useStateSingleton(SessionSingleton, (state) => state.user.name);
277
+
278
+ // Same behavior via useStateSubscription:
279
+ const [session] = useStateSubscription(SessionSingleton);`;
280
+
281
+ const apiSetupStatusQuoSnippet = `import equal from 'fast-deep-equal';
282
+ import { setupStatusQuo } from '@veams/status-quo';
283
+
284
+ setupStatusQuo({
285
+ distinct: {
286
+ comparator: equal
287
+ }
288
+ });`;
230
289
 
231
290
  const composeSnippet = `import { combineLatest } from "rxjs";
232
291
 
@@ -278,22 +337,59 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
278
337
  Prism.highlightAll();
279
338
  }, []);
280
339
 
340
+ const closeNav = () => {
341
+ setIsNavOpen(false);
342
+ };
343
+
281
344
  return (
282
345
  <div className="app">
283
346
  <div className="brand-bar">
284
347
  <img src={statusQuoLogo} alt="StatusQuo logo" className="brand-logo" />
285
348
  </div>
286
349
  <nav className="nav">
287
- <div className="nav-links">
288
- <a href="#overview">Overview</a>
289
- <a href="#quickstart">Quickstart</a>
290
- <a href="#factory">Factory</a>
291
- <a href="#demo">Demo</a>
292
- <a href="#singleton">Singleton</a>
293
- <a href="#singleton-demo">Singleton demo</a>
294
- <a href="#compose">Compose</a>
295
- <a href="#devtools">Devtools</a>
296
- <a href="#cleanup">Cleanup</a>
350
+ <button
351
+ type="button"
352
+ className="nav-toggle"
353
+ aria-expanded={isNavOpen}
354
+ aria-controls="docs-nav-links"
355
+ onClick={() => setIsNavOpen((open) => !open)}
356
+ >
357
+ Sections
358
+ </button>
359
+ <div id="docs-nav-links" className={`nav-links${isNavOpen ? ' is-open' : ''}`}>
360
+ <a href="#overview" onClick={closeNav}>
361
+ Overview
362
+ </a>
363
+ <a href="#quickstart" onClick={closeNav}>
364
+ Quickstart
365
+ </a>
366
+ <a href="#factory" onClick={closeNav}>
367
+ Factory
368
+ </a>
369
+ <a href="#selectors" onClick={closeNav}>
370
+ Selectors
371
+ </a>
372
+ <a href="#api" onClick={closeNav}>
373
+ API
374
+ </a>
375
+ <a href="#demo" onClick={closeNav}>
376
+ Demo
377
+ </a>
378
+ <a href="#singleton" onClick={closeNav}>
379
+ Singleton
380
+ </a>
381
+ <a href="#singleton-demo" onClick={closeNav}>
382
+ Singleton demo
383
+ </a>
384
+ <a href="#compose" onClick={closeNav}>
385
+ Compose
386
+ </a>
387
+ <a href="#devtools" onClick={closeNav}>
388
+ Devtools
389
+ </a>
390
+ <a href="#cleanup" onClick={closeNav}>
391
+ Cleanup
392
+ </a>
297
393
  </div>
298
394
  </nav>
299
395
  <header id="overview" className="hero intro">
@@ -303,14 +399,13 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
303
399
  <p className="subtext">
304
400
  <span>StatusQuo</span> treats state handlers as small, composable objects with explicit
305
401
  lifecycle and a tiny interface. Components subscribe to snapshots, not
306
- framework‑specific store APIs. That makes it easy to swap the engine under the
307
- hood—RxJS for observable streams or Preact Signals for ultra‑light reactive state.
402
+ framework‑specific store APIs. That makes it easy to swap the engine under the hood—RxJS
403
+ for observable streams or Preact Signals for ultra‑light reactive state.
308
404
  </p>
309
405
  <p className="subtext">
310
- Handlers encapsulate state transitions, expose actions, and clean up after
311
- themselves. You decide whether each component should have its own instance
312
- (factory) or share a singleton, while the UI stays blissfully unaware of the
313
- chosen implementation.
406
+ Handlers encapsulate state transitions, expose actions, and clean up after themselves.
407
+ You decide whether each component should have its own instance (factory) or share a
408
+ singleton, while the UI stays blissfully unaware of the chosen implementation.
314
409
  </p>
315
410
  </div>
316
411
  </header>
@@ -321,8 +416,8 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
321
416
  <div>
322
417
  <h3>Swap the engine, keep the API</h3>
323
418
  <p>
324
- Move between RxJS observables and Signals without rewriting your components.
325
- Stores keep the same shape; only the internal runtime changes.
419
+ Move between RxJS observables and Signals without rewriting your components. Stores
420
+ keep the same shape; only the internal runtime changes.
326
421
  </p>
327
422
  </div>
328
423
  </article>
@@ -331,8 +426,8 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
331
426
  <div>
332
427
  <h3>Separate view and state</h3>
333
428
  <p>
334
- Handlers own transitions and expose actions. Views subscribe to snapshots and
335
- never touch store internals.
429
+ Handlers own transitions and expose actions. Views subscribe to snapshots and never
430
+ touch store internals.
336
431
  </p>
337
432
  </div>
338
433
  </article>
@@ -353,8 +448,8 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
353
448
  <p className="eyebrow">Quickstart</p>
354
449
  <h2>Install and wire up a handler</h2>
355
450
  <p>
356
- Pick either observable or signal stores. The hooks stay the same, so you can
357
- swap the backend without changing your components.
451
+ Pick either observable or signal stores. The hooks stay the same, so you can swap the
452
+ backend without changing your components.
358
453
  </p>
359
454
  </div>
360
455
  <div className="doc-snippets">
@@ -373,30 +468,148 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
373
468
  <p className="eyebrow">Factory</p>
374
469
  <h2>Fresh handler per mount</h2>
375
470
  <p>
376
- Use `useStateFactory` when every component instance needs its own store. You
377
- pass a factory function and dependencies, and the hook handles lifecycle
378
- cleanup for you.
471
+ Use `useStateFactory` when each component instance needs its own store.
379
472
  </p>
473
+ <p className="guide-highlight">Local state, zero cross-talk, predictable lifecycles.</p>
380
474
  </div>
381
- <pre className="code-block compact">
382
- <code className="language-ts">{`const [state, actions] = useStateFactory(CounterFactory, [0]);`}</code>
383
- </pre>
384
475
  </article>
385
476
  <article id="singleton" className="guide-card">
386
477
  <div>
387
478
  <p className="eyebrow">Singleton</p>
388
479
  <h2>One handler, many consumers</h2>
389
480
  <p>
390
- Use `makeStateSingleton` and `useStateSingleton` for shared stores. Ideal for
391
- global counters, user sessions, or any shared UI state.
481
+ Use `makeStateSingleton` + `useStateSingleton` for shared state across components.
392
482
  </p>
483
+ <p className="guide-highlight">One source of truth, synced everywhere.</p>
484
+ </div>
485
+ </article>
486
+ <article id="selectors" className="guide-card">
487
+ <div>
488
+ <p className="eyebrow">Selectors</p>
489
+ <h2>Avoid rerender fanout</h2>
490
+ <p>
491
+ Subscribe only to the state slice you need with selector + equality functions.
492
+ </p>
493
+ <p className="guide-highlight">Read less, rerender less, ship smoother UIs.</p>
393
494
  </div>
394
- <pre className="code-block compact">
395
- <code className="language-ts">{singletonSnippet}</code>
396
- </pre>
397
495
  </article>
398
496
  </section>
399
497
 
498
+ <section id="api" className="doc-section">
499
+ <div className="doc-copy">
500
+ <p className="eyebrow">API</p>
501
+ <h2>Detailed hook and singleton reference</h2>
502
+ </div>
503
+ <div className="api-flow">
504
+ <div className="api-group">
505
+ <article className="api-composition-card">
506
+ <h3>Base composition</h3>
507
+ <p>
508
+ Build explicit lifecycles with `useStateHandler`, pull actions with
509
+ `useStateActions`, and subscribe to exact slices with `useStateSubscription`.
510
+ </p>
511
+ <p className="api-composition-punch">Maximum control, minimum magic.</p>
512
+ </article>
513
+ <div className="api-grid">
514
+ <article className="api-card">
515
+ <h3>useStateHandler(factory, params?)</h3>
516
+ <p>Creates one handler instance per component mount and reuses it across rerenders.</p>
517
+ <pre className="code-block compact">
518
+ <code className="language-ts">{apiUseStateHandlerSnippet}</code>
519
+ </pre>
520
+ </article>
521
+
522
+ <article className="api-card">
523
+ <h3>useStateActions(handler)</h3>
524
+ <p>Returns actions without subscribing to state updates.</p>
525
+ <pre className="code-block compact">
526
+ <code className="language-ts">{apiUseStateActionsSnippet}</code>
527
+ </pre>
528
+ </article>
529
+
530
+ <article className="api-card api-card-wide">
531
+ <h3>useStateSubscription(source, selector?, isEqual?)</h3>
532
+ <p>
533
+ Core subscription hook. Source can be a handler or singleton. Returns
534
+ `[selectedState, actions]`.
535
+ </p>
536
+ <pre className="code-block compact">
537
+ <code className="language-ts">{apiUseStateSubscriptionSnippet}</code>
538
+ </pre>
539
+ </article>
540
+ </div>
541
+ </div>
542
+
543
+ <div className="api-group">
544
+ <article className="api-composition-card">
545
+ <h3>Shortcut composition</h3>
546
+ <p>
547
+ Reduce wiring with `useStateFactory` and `useStateSingleton` when you want
548
+ the same behavior in a smaller API surface.
549
+ </p>
550
+ <p className="api-composition-punch">Less wiring, same behavior.</p>
551
+ </article>
552
+ <div className="api-grid">
553
+ <article className="api-card">
554
+ <h3>useStateFactory(factory, selector?, isEqual?, params?)</h3>
555
+ <p>
556
+ Shortcut for handler creation + subscription. Supports full state and selected
557
+ slices.
558
+ </p>
559
+ <pre className="code-block compact">
560
+ <code className="language-ts">{apiUseStateFactorySnippet}</code>
561
+ </pre>
562
+ </article>
563
+
564
+ <article className="api-card">
565
+ <h3>useStateSingleton(singleton, selector?, isEqual?)</h3>
566
+ <p>
567
+ Shortcut for subscribing to a singleton. Selector and equality semantics match
568
+ `useStateSubscription`.
569
+ </p>
570
+ <pre className="code-block compact">
571
+ <code className="language-ts">{apiUseStateSingletonSnippet}</code>
572
+ </pre>
573
+ </article>
574
+ </div>
575
+ </div>
576
+
577
+ <div className="api-group">
578
+ <article className="api-composition-card">
579
+ <h3>Helper function</h3>
580
+ <p>
581
+ Configure global distinct behavior once with `setupStatusQuo` and create shared
582
+ singleton providers with `makeStateSingleton`.
583
+ </p>
584
+ <p className="api-composition-punch">One setup, consistent behavior.</p>
585
+ </article>
586
+ <div className="api-grid">
587
+ <article className="api-card">
588
+ <h3>setupStatusQuo(config?)</h3>
589
+ <p>
590
+ Defines global runtime defaults for distinct update behavior. Per-handler options
591
+ still override global config.
592
+ </p>
593
+ <pre className="code-block compact">
594
+ <code className="language-ts">{apiSetupStatusQuoSnippet}</code>
595
+ </pre>
596
+ </article>
597
+
598
+ <article className="api-card">
599
+ <h3>makeStateSingleton(factory, options?)</h3>
600
+ <p>
601
+ Creates a shared handler provider. `destroyOnNoConsumers` controls instance
602
+ teardown.
603
+ </p>
604
+ <pre className="code-block compact">
605
+ <code className="language-ts">{apiMakeStateSingletonSnippet}</code>
606
+ </pre>
607
+ </article>
608
+ </div>
609
+ </div>
610
+ </div>
611
+ </section>
612
+
400
613
  <div id="demo" className="grid">
401
614
  <CounterCard
402
615
  title="Observable State Handler"
@@ -419,10 +632,19 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
419
632
  <p className="eyebrow">Singleton demo</p>
420
633
  <h2>One store, multiple components</h2>
421
634
  <p>
422
- Both panels below are subscribed to the same singleton handler. Update the
423
- controller and watch the viewer react instantly.
635
+ Both panels below are subscribed to the same singleton handler. Update the controller
636
+ and watch the viewer react instantly. The handler is only disposed once the last
637
+ consumer unmounts (unless persistence is enabled).
638
+ </p>
639
+ <p>
640
+ `makeStateSingleton(factory, options)` always returns one shared instance.
641
+ `destroyOnNoConsumers` controls whether that instance is torn down when the last
642
+ `useStateSingleton` consumer unmounts.
424
643
  </p>
425
644
  </div>
645
+ <pre className="code-block compact">
646
+ <code className="language-ts">{singletonOptionsSnippet}</code>
647
+ </pre>
426
648
  <div className="singleton-grid">
427
649
  <SingletonControls />
428
650
  <SingletonDisplay />
@@ -435,8 +657,8 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
435
657
  <h2>Combine multiple handlers</h2>
436
658
  <p>
437
659
  Use only the slice you need. RxJS makes multi-source composition powerful and
438
- declarative with operators like `combineLatest`, `switchMap`, or `debounceTime`.
439
- Signals can derive values with `computed` and wire them into a parent store via
660
+ declarative with operators like `combineLatest`, `switchMap`, or `debounceTime`. Signals
661
+ can derive values with `computed` and wire them into a parent store via
440
662
  `bindSubscribable`. This keeps parent stores lean and focused.
441
663
  </p>
442
664
  </div>
@@ -450,8 +672,8 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
450
672
  <p className="eyebrow">Devtools</p>
451
673
  <h2>Redux devtools integration</h2>
452
674
  <p>
453
- Enable devtools to inspect actions and state transitions. Both handlers share
454
- the same options shape.
675
+ Enable devtools to inspect actions and state transitions. Both handlers share the same
676
+ options shape.
455
677
  </p>
456
678
  </div>
457
679
  <pre className="code-block">
@@ -464,15 +686,14 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
464
686
  <p className="eyebrow">Cleanup</p>
465
687
  <h2>Controlled teardown</h2>
466
688
  <p>
467
- Each handler exposes `subscribe`, `getSnapshot`, and `destroy`. Use them when
468
- you wire custom integrations or combine multiple stores manually.
689
+ Each handler exposes `subscribe`, `getSnapshot`, and `destroy`. Use them when you wire
690
+ custom integrations or combine multiple stores manually.
469
691
  </p>
470
692
  </div>
471
693
  <pre className="code-block">
472
694
  <code className="language-ts">{cleanupSnippet}</code>
473
695
  </pre>
474
696
  </section>
475
-
476
697
  </div>
477
698
  );
478
699
  }
@@ -64,15 +64,32 @@ body {
64
64
  display: flex;
65
65
  align-items: center;
66
66
  justify-content: center;
67
+ flex-direction: column;
67
68
  padding: 10px 6px;
69
+ width: 100%;
70
+ }
71
+
72
+ .nav-toggle {
73
+ display: none;
74
+ border: 1px solid rgba(27, 27, 31, 0.18);
75
+ background: rgba(255, 255, 255, 0.85);
76
+ color: var(--ink);
77
+ border-radius: 999px;
78
+ padding: 8px 14px;
79
+ font: inherit;
80
+ font-weight: 600;
81
+ cursor: pointer;
82
+ box-shadow: var(--shadow);
68
83
  }
69
84
 
70
85
  .nav-links {
71
86
  display: inline-flex;
72
87
  align-items: center;
88
+ justify-content: center;
73
89
  gap: 10px;
74
90
  flex-wrap: wrap;
75
91
  padding: 6px 12px;
92
+ width: min(100%, 980px);
76
93
  background: rgba(255, 255, 255, 0.7);
77
94
  border-radius: 999px;
78
95
  border: 1px solid rgba(27, 27, 31, 0.1);
@@ -165,6 +182,7 @@ p {
165
182
  display: grid;
166
183
  gap: 24px;
167
184
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
185
+ align-items: stretch;
168
186
  }
169
187
 
170
188
  .doc-section {
@@ -191,6 +209,7 @@ p {
191
209
  display: grid;
192
210
  gap: 20px;
193
211
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
212
+ align-items: stretch;
194
213
  }
195
214
 
196
215
  .singleton-card {
@@ -201,6 +220,8 @@ p {
201
220
  box-shadow: var(--shadow);
202
221
  display: grid;
203
222
  gap: 14px;
223
+ align-content: start;
224
+ height: 100%;
204
225
  }
205
226
 
206
227
  .singleton-count {
@@ -227,12 +248,75 @@ p {
227
248
  display: grid;
228
249
  gap: 20px;
229
250
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
251
+ align-items: stretch;
252
+ }
253
+
254
+ .api-flow {
255
+ display: grid;
256
+ gap: 30px;
257
+ }
258
+
259
+ .api-group {
260
+ display: grid;
261
+ gap: 18px;
262
+ }
263
+
264
+ .api-grid {
265
+ display: grid;
266
+ gap: 18px;
267
+ grid-template-columns: repeat(2, minmax(0, 1fr));
268
+ align-items: stretch;
269
+ }
270
+
271
+ .api-composition-card {
272
+ padding: 2px 0 18px;
273
+ border-bottom: 1px solid rgba(27, 27, 31, 0.16);
274
+ display: grid;
275
+ gap: 10px;
276
+ align-content: start;
277
+ }
278
+
279
+ .api-composition-card h3 {
280
+ margin: 0;
281
+ font-size: 1rem;
282
+ }
283
+
284
+ .api-composition-punch {
285
+ margin-top: 8px;
286
+ font-weight: 600;
287
+ color: var(--ink);
288
+ background: linear-gradient(120deg, rgba(47, 107, 255, 0.14), rgba(255, 159, 28, 0.14));
289
+ border: 1px solid rgba(47, 107, 255, 0.2);
290
+ border-radius: var(--radius-sm);
291
+ padding: 10px 12px;
292
+ }
293
+
294
+ .api-card {
295
+ background: #fff;
296
+ border-radius: var(--radius-sm);
297
+ padding: 18px;
298
+ border: 1px solid rgba(27, 27, 31, 0.08);
299
+ box-shadow: var(--shadow);
300
+ display: grid;
301
+ gap: 10px;
302
+ align-content: start;
303
+ height: 100%;
304
+ }
305
+
306
+ .api-card h3 {
307
+ margin: 0;
308
+ font-size: 1rem;
309
+ }
310
+
311
+ .api-card-wide {
312
+ grid-column: 1 / -1;
230
313
  }
231
314
 
232
315
  .philosophy-grid {
233
316
  display: grid;
234
317
  gap: 20px;
235
318
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
319
+ align-items: stretch;
236
320
  }
237
321
 
238
322
  .philosophy-card {
@@ -243,6 +327,8 @@ p {
243
327
  overflow: hidden;
244
328
  display: grid;
245
329
  gap: 16px;
330
+ align-content: start;
331
+ height: 100%;
246
332
  }
247
333
 
248
334
  .philosophy-card img {
@@ -266,6 +352,18 @@ p {
266
352
  box-shadow: var(--shadow);
267
353
  display: grid;
268
354
  gap: 16px;
355
+ align-content: start;
356
+ height: 100%;
357
+ }
358
+
359
+ .guide-highlight {
360
+ margin-top: 4px;
361
+ font-weight: 600;
362
+ color: var(--ink);
363
+ background: linear-gradient(120deg, rgba(47, 107, 255, 0.16), rgba(255, 159, 28, 0.16));
364
+ border: 1px solid rgba(47, 107, 255, 0.2);
365
+ border-radius: var(--radius-sm);
366
+ padding: 10px 12px;
269
367
  }
270
368
 
271
369
  .code-block.compact {
@@ -283,6 +381,7 @@ p {
283
381
  flex-direction: column;
284
382
  gap: 20px;
285
383
  animation: fade-up 0.6s ease both;
384
+ height: 100%;
286
385
  }
287
386
 
288
387
  .card:nth-child(2) {
@@ -392,14 +491,38 @@ p {
392
491
  padding: 0;
393
492
  }
394
493
 
494
+ .nav-toggle {
495
+ display: inline-flex;
496
+ align-items: center;
497
+ justify-content: center;
498
+ margin-bottom: 10px;
499
+ }
500
+
395
501
  .nav-links {
502
+ display: none;
503
+ width: 100%;
396
504
  border-radius: var(--radius-md);
505
+ padding: 10px;
506
+ gap: 8px;
507
+ }
508
+
509
+ .nav-links.is-open {
510
+ display: grid;
511
+ grid-template-columns: repeat(2, minmax(0, 1fr));
512
+ }
513
+
514
+ .nav-links a {
515
+ text-align: center;
397
516
  }
398
517
 
399
518
  .doc-section {
400
519
  padding: 22px;
401
520
  }
402
521
 
522
+ .api-grid {
523
+ grid-template-columns: 1fr;
524
+ }
525
+
403
526
  .hero {
404
527
  padding: 26px;
405
528
  }