@dreamboard-games/sdk 0.5.0-alpha.16 → 0.5.0-alpha.18

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 (32) hide show
  1. package/README.md +104 -9
  2. package/dist/{board-element-CRN_hj5F.d.ts → board-element-Bi_CEmNa.d.ts} +1 -1
  3. package/dist/{chunk-TJQTLYDI.js → chunk-BNQF7QXR.js} +73 -11
  4. package/dist/chunk-BNQF7QXR.js.map +1 -0
  5. package/dist/{chunk-ES74C2IN.js → chunk-CZRPUWCT.js} +141 -115
  6. package/dist/chunk-CZRPUWCT.js.map +1 -0
  7. package/dist/{chunk-MSABRKR2.js → chunk-HVNA4RAM.js} +70 -20
  8. package/dist/chunk-HVNA4RAM.js.map +1 -0
  9. package/dist/{chunk-B4RMCLUN.js → chunk-U6R7FAH4.js} +15 -2
  10. package/dist/chunk-U6R7FAH4.js.map +1 -0
  11. package/dist/{chunk-YUEMDRIH.js → chunk-YT3RPO5J.js} +3 -3
  12. package/dist/{diagnostics-BokP7HN3.d.ts → diagnostics-C_07naxM.d.ts} +1 -1
  13. package/dist/{extract-DgzrJ5el.d.ts → extract-DcvrH9lh.d.ts} +103 -5
  14. package/dist/index.d.ts +336 -34
  15. package/dist/index.js +224 -43
  16. package/dist/index.js.map +1 -1
  17. package/dist/react.d.ts +44 -5
  18. package/dist/react.js +334 -87
  19. package/dist/react.js.map +1 -1
  20. package/dist/reducer.d.ts +18 -6
  21. package/dist/reducer.js +43 -4
  22. package/dist/reducer.js.map +1 -1
  23. package/dist/{shortcuts-CpB_mwiq.d.ts → shortcuts-BJz4NvjG.d.ts} +87 -12
  24. package/dist/testing.d.ts +3 -3
  25. package/dist/testing.js +4 -4
  26. package/dist/{types-Xg_RxV1n.d.ts → types-DU9GGPt6.d.ts} +1 -1
  27. package/package.json +1 -1
  28. package/dist/chunk-B4RMCLUN.js.map +0 -1
  29. package/dist/chunk-ES74C2IN.js.map +0 -1
  30. package/dist/chunk-MSABRKR2.js.map +0 -1
  31. package/dist/chunk-TJQTLYDI.js.map +0 -1
  32. /package/dist/{chunk-YUEMDRIH.js.map → chunk-YT3RPO5J.js.map} +0 -0
package/README.md CHANGED
@@ -349,7 +349,7 @@ install the React adapter. The `/react` entry delegates selectors to that packag
349
349
  the application bundler resolves its supported React subscription dependencies.
350
350
 
351
351
  `/react` classifies each card press as a tap, hold, drag or browse. Enable
352
- `dragFeature`, export `useCardGesture`, `useActiveCard`, `useDropArea` and `useDragOverlay` from the
352
+ `dragFeature`, export `useCardGesture`, `useCardRow`, `useActiveCard`, `useDropArea` and `useDragOverlay` from the
353
353
  binding, and use the copied Hand and BoardTargets components or your own renderer.
354
354
  The SDK prescribes no layout. Headless `game.drag` remains browser-free and handles
355
355
  atomic domain routing. A drop area can bind typed inputs with
@@ -360,12 +360,32 @@ manual; unbound drops and many-card drafts keep their normal commit behavior.
360
360
  `game.drag.getIsDropTarget(target)` uses the same admission as dropping.
361
361
  Interaction `getSubmitProps(params)` and `getSubmitHandler(params)` accept the
362
362
  same explicit params as `submit(params)`, retaining pending and connection gates.
363
- `fanLayout` computes arc positions for a hand,
363
+ `fanLayout` computes arc positions that always fit a hand,
364
364
  `handFan` adds a focused, readable card with configurable presets and timing,
365
365
  and
366
366
  `originsFeature` adds `card.getOrigin()` so a newly shown card can animate from
367
367
  the zone or player it came from.
368
368
 
369
+ ## Local hand sorting
370
+
371
+ Install `handFeature(core, context, { zones })` with named sorts for each zone:
372
+ `{ hand: { defaultSort: "rank", sorts: { rank: { compare } } } }`.
373
+ Comparators receive the seat's projected `Card` objects; narrow `card.hidden`
374
+ before reading visible card data. Equal comparisons retain source order.
375
+
376
+ `game.hand.getSortModes(zone)` returns typed mode IDs in declaration order.
377
+ Read `getSortMode(zone)` and `getSortedCardIds(zone)`, then call
378
+ `setSortMode(zone, mode)` from your UI. The zone carries its canonical host;
379
+ each host's choice is independent. Labels, buttons and cycling are authored UI.
380
+ Without a selected or default mode, the order comes directly from the source.
381
+ The feature retains the zone's selected/selectable card ID helpers.
382
+
383
+ Sort changes notify subscribers and preserve drafts and card selection without
384
+ submitting. Choices survive frames and reset on source or seat changes. Captured
385
+ `game.hand` branches retain their modes, and sorted IDs are derived from the
386
+ projected zone passed to that branch. Old lifetime setters and disposed setters
387
+ do nothing. See [Optional features](../../docs/api/features.md).
388
+
369
389
  ## Opt-in game shortcuts
370
390
 
371
391
  Install `shortcutsFeature` in the same `createGameHook<Definition>()` binding as
@@ -378,6 +398,7 @@ function AppShortcuts() {
378
398
  useGameShortcuts({
379
399
  bindings: [
380
400
  {
401
+ kind: "interaction",
381
402
  keys: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
382
403
  label: "Draw cards",
383
404
  target: "zone",
@@ -386,6 +407,7 @@ function AppShortcuts() {
386
407
  inputs: ({ key }) => ({ count: Number(key) }),
387
408
  },
388
409
  {
410
+ kind: "interaction",
389
411
  keys: ["f"],
390
412
  label: "Flip card",
391
413
  target: "card",
@@ -393,18 +415,41 @@ function AppShortcuts() {
393
415
  interaction: "play.flip",
394
416
  inputs: ({ target }) => ({ card: target.value }),
395
417
  },
418
+ {
419
+ kind: "local",
420
+ keys: ["s"],
421
+ label: "Change hand order",
422
+ target: "zone",
423
+ zoneId: "hand",
424
+ getIsAvailable: ({ target }) => target.hostId === playerId,
425
+ run: ({ target }) => changeAuthoredOrder(target.hostId),
426
+ },
396
427
  ],
397
428
  });
398
429
  return null;
399
430
  }
400
431
  ```
401
432
 
402
- These interaction and input names are examples from an authored reducer. The
433
+ These interaction and input names are examples from an authored reducer;
434
+ `playerId` and `changeAuthoredOrder` are supplied by the game. The
403
435
  binding's discriminated types derive from that reducer: interaction names,
404
436
  input keys, target kinds, and every required payload field must match. A game
405
437
  that exposes a counted draw submits one `count` payload; shortcuts never repeat
406
438
  a draw-one action. No actions or keys are supplied by default.
407
439
 
440
+ Local bindings run game-authored presentation callbacks for a focused zone
441
+ control or a focused card in that zone. A focused card resolves its current
442
+ containing zone and actual host, including opaque hidden card references. Local
443
+ bindings always use real DOM focus, including focus acquired by clicking; hover
444
+ alone never activates them. Mouse browsing can change the interaction target
445
+ while local actions retain the focused control's zone. They are unavailable
446
+ during a card pointer gesture or its settling drag overlay. Games own any
447
+ further restrictions through the optional pure `getIsAvailable` predicate.
448
+ They do not submit source commands, alter interaction drafts, or require the
449
+ player's reducer turn; an interaction request does not block a local action.
450
+ `run` returns `void` or `Promise<void>` and is never evaluated for hints.
451
+ The SDK supplies no sorting modes or cycling policy.
452
+
408
453
  Card controls using `useCardGesture` share its canonical pointer and keyboard
409
454
  focus. Zone controls spread
410
455
  `useShortcutTarget({ kind: "zone", zoneId: "deck", hostId: "table" }).props`;
@@ -414,19 +459,29 @@ board and tile controls can use the same hook with their canonical
414
459
 
415
460
  Keys are exact browser `event.key` strings. The adapter ignores held repeats,
416
461
  composition, editing controls, modifier combinations, and open modal or menu
417
- surfaces. It prevents the browser default only when a single eligible binding
462
+ surfaces. Native radio inputs can carry a zone target for authored shortcuts;
463
+ their ordinary arrow and space keys remain native unless explicitly bound.
464
+ It prevents the browser default only when a single eligible binding
418
465
  handles the key. Source and seat changes clear the gesture target; unmounting
419
466
  removes the listener and binding hints.
420
467
 
421
468
  Input factories must be pure: eligibility and hints evaluate them against the
422
- current projected domains. `game.shortcuts.getHints(target)` returns only
423
- currently usable, unambiguous keys with their labels and typed interaction
424
- names, for menus or help. `useShortcutHints(target)` reads those hints reactively
469
+ current projected domains. Availability predicates for local bindings must
470
+ also be pure. `game.shortcuts.getHints(target, activity)` returns only
471
+ currently usable, unambiguous keys with their labels and `kind`. Interaction
472
+ hints also carry their typed interaction name; local hints do not. Both variants
473
+ share key collision handling. Headless callers supply local focus explicitly as
474
+ `activity: { focusedTarget, pointerActive }`; omitting activity admits only
475
+ interaction bindings. `useShortcutHints(target)` reads those hints reactively
425
476
  and returns no hints when the feature is absent. The mounted
426
477
  `useGameShortcuts` hook returns `{ target, hints }` for a reserved help/status
427
478
  area. Rejected actions and transport failures use the binding's existing
428
479
  `onError` handler; callbacks from an old source, seat, or unmounted child are
429
- ignored. `game.shortcuts.handle(key, target)` uses the same
480
+ ignored. `game.shortcuts.handle(key, target, activity)` returns `null` for an
481
+ unhandled
482
+ key, or a promise of `{ kind: "local" }` or
483
+ `{ kind: "interaction", result: SubmitResult }`. Callback failures reject that
484
+ promise and use the same `onError` path. Interaction bindings use the same
430
485
  atomic `interaction.submit(params)` path available to authored action buttons.
431
486
  Explicit params are validated against the current projected descriptor without
432
487
  mutating drafts. Existing `submit()` continues to submit the interaction draft
@@ -492,7 +547,47 @@ Standard playing cards are game-owned definitions with ordinary suit/rank
492
547
  properties; the SDK does not synthesize inventories or assign built-in rules.
493
548
 
494
549
  The exported `createGestureRecognizer(down, callbacks, options)` defaults to
495
- upward touch dragging so hands retain sideways scrolling. Pile controls can use
550
+ upward touch dragging so card containers retain sideways scrolling. A row that
551
+ fits uses `{ scrub: true }`: a sliding finger reports each move through
552
+ `browse`, and turning upward still drags. Pile controls can use
496
553
  `{ dragDirection: "any" }` with `touch-action: none` to drag toward a hand below
497
554
  or beside them. This classifies pointer intent only; submit an authored interaction
498
555
  through the bound game to enforce its rules, without selecting a hidden card ID.
556
+
557
+ ### Local card groups and marquee selection
558
+
559
+ `cardSelectionFeature` holds a local list of projected card IDs, separate from
560
+ interaction drafts. Authors decide which zones allow selection and how to draw
561
+ it. Selection clears on an authoritative frame or source/perspective change.
562
+
563
+ ```ts
564
+ features(core, context) {
565
+ const selection = cardSelectionFeature(core, context);
566
+ return {
567
+ selection,
568
+ drag: dragFeature(core, context, {
569
+ getSelection(cardId) {
570
+ const ids = selection.root.cardSelection.cardIds;
571
+ return ids.includes(cardId) ? ids : undefined;
572
+ },
573
+ }),
574
+ };
575
+ }
576
+ ```
577
+
578
+ Use `game.cardSelection.set(ids)`, `.toggle(id)` and `.clear()` for authored
579
+ controls. A selected group can drop only onto an interaction with a `many(...)`
580
+ card input that admits the entire group. Dropping submits that input once in
581
+ selection order, replacing its draft; it never submits a sequence of single-card
582
+ moves. The reducer remains responsible for validating and applying the group
583
+ atomically. An unselected card retains ordinary single-card draft behavior.
584
+ `useDragOverlay().cardIds` identifies all lifted cards; `cardId` is the anchor.
585
+
586
+ `useMarqueeSelection` from `/react` is an opt-in mouse/pen background gesture.
587
+ Pass `enabled`, `getCards: () => [{ id, element }]`, and
588
+ `onSelect: (ids, additive) => ...`; spread its `props` on the selectable area.
589
+ It reports intersecting cards on release and viewport `bounds` for an authored
590
+ fixed-position overlay. Shift requests additive selection; Escape, pointer
591
+ cancellation, blur or disabling the area cancels. Buttons, inputs and card
592
+ controls keep their own gestures, and touch retains scrolling. A background
593
+ click reports an empty selection. The hook supplies no styling or bulk-action UI.
@@ -1,4 +1,4 @@
1
- import { f as BoardEdgeId, n as BoardVertexId } from './extract-DgzrJ5el.js';
1
+ import { f as BoardEdgeId, n as BoardVertexId } from './extract-DcvrH9lh.js';
2
2
 
3
3
  declare function boardEdgeId<BoardId extends string>(layout: "hex" | "square", boardId: BoardId, latticeId: string): BoardEdgeId<BoardId>;
4
4
  declare function boardVertexId<BoardId extends string>(layout: "hex" | "square", boardId: BoardId, latticeId: string): BoardVertexId<BoardId>;
@@ -8,7 +8,7 @@ import {
8
8
  boardSpaceHostId,
9
9
  isCardReferenceNamespace,
10
10
  parseBoardSpaceHostId
11
- } from "./chunk-B4RMCLUN.js";
11
+ } from "./chunk-U6R7FAH4.js";
12
12
  import {
13
13
  AuthoredTileDisclosureSchema,
14
14
  BoardProjectionSchema,
@@ -30,11 +30,13 @@ import {
30
30
  inputValueInDomain,
31
31
  inputValueKey,
32
32
  isBoardSpaceTarget,
33
+ isPositionTarget,
33
34
  parsePerPlayerInstanceId,
34
35
  parseTileSpaceId,
35
36
  requireLookup,
36
- tileSpaceId
37
- } from "./chunk-ES74C2IN.js";
37
+ tileSpaceId,
38
+ zonePositions
39
+ } from "./chunk-CZRPUWCT.js";
38
40
 
39
41
  // src/reducer/model/error-codes.ts
40
42
  var FrameworkErrorCodes = {
@@ -1344,6 +1346,20 @@ function moveComponentToZoneInPlace(options) {
1344
1346
  destination.ids.splice(index, 0, componentId);
1345
1347
  place(table, definitions, componentId, destination.ref, playedBy);
1346
1348
  }
1349
+ function moveComponentToPositionInPlace(options) {
1350
+ const { table, definitions, componentId, at, playedBy } = options;
1351
+ const to = { zoneId: at.zoneId, hostId: at.hostId };
1352
+ const source = table.componentLocations[componentId];
1353
+ const from = source?.type === "InZone" && source.zoneId === at.zoneId && source.hostId === at.hostId ? resolveZone(table, definitions, to).ids.indexOf(componentId) : -1;
1354
+ moveComponentToZoneInPlace({
1355
+ table,
1356
+ definitions,
1357
+ componentId,
1358
+ to,
1359
+ position: from >= 0 && from < at.index ? at.index - 1 : at.index,
1360
+ playedBy
1361
+ });
1362
+ }
1347
1363
  function dealComponentsInPlace(options) {
1348
1364
  const { table, definitions } = options;
1349
1365
  assertNonNegativeSafeInteger(options.count, "Deal count");
@@ -2448,6 +2464,14 @@ var transactionMutations = {
2448
2464
  moveComponentToZoneInPlace({ table: state.table, definitions, ...args });
2449
2465
  return state;
2450
2466
  },
2467
+ moveComponentToPosition(state, args, definitions) {
2468
+ moveComponentToPositionInPlace({
2469
+ table: state.table,
2470
+ definitions,
2471
+ ...args
2472
+ });
2473
+ return state;
2474
+ },
2451
2475
  deal(state, args, definitions) {
2452
2476
  dealComponentsInPlace({ table: state.table, definitions, ...args });
2453
2477
  return state;
@@ -3806,6 +3830,14 @@ function baseValuesForDomain(domain, collector, context) {
3806
3830
  )
3807
3831
  );
3808
3832
  }
3833
+ case "zonePosition":
3834
+ return finiteValueSource(
3835
+ collector.eligibleTargets ? collector.eligibleTargets(
3836
+ context.domainState,
3837
+ context.playerId,
3838
+ context.queries()
3839
+ ) : zonePositions(domain.zones)
3840
+ );
3809
3841
  case "choiceList":
3810
3842
  return choiceListValueSource(domain);
3811
3843
  case "boundedNumber":
@@ -5772,6 +5804,22 @@ function boardReference(value, domain, disclosure, decode) {
5772
5804
  if (typeof value !== "string") return denied2;
5773
5805
  return map(domain.targetKind, domain.boardId, value) ?? denied2;
5774
5806
  }
5807
+ function zoneHostReference(zoneId, hostId, disclosure, decode) {
5808
+ const zone = disclosure.zones.find(
5809
+ (zone2) => zone2.zoneId === zoneId && (decode ? zone2.seatHostId : zone2.hostId) === hostId
5810
+ );
5811
+ return zone ? decode ? zone.hostId : zone.seatHostId : null;
5812
+ }
5813
+ function positionReference(value, disclosure, decode) {
5814
+ if (!isPositionTarget(value)) return denied2;
5815
+ const hostId = zoneHostReference(
5816
+ value.zoneId,
5817
+ value.hostId,
5818
+ disclosure,
5819
+ decode
5820
+ );
5821
+ return hostId === null ? denied2 : { ...value, hostId };
5822
+ }
5775
5823
  function domainMapper(domain, disclosure, cards, decode) {
5776
5824
  switch (domain.type) {
5777
5825
  case "tileTarget":
@@ -5780,12 +5828,16 @@ function domainMapper(domain, disclosure, cards, decode) {
5780
5828
  return (value) => cardReference(value, disclosure, cards, decode);
5781
5829
  case "boardTarget":
5782
5830
  return (value) => boardReference(value, domain, disclosure, decode);
5831
+ case "zonePosition":
5832
+ return (value) => positionReference(value, disclosure, decode);
5783
5833
  default:
5784
5834
  return void 0;
5785
5835
  }
5786
5836
  }
5787
5837
  function collectorMapper(collector, disclosure, cards, decode) {
5788
5838
  switch (collector.kind) {
5839
+ case "position":
5840
+ return (value) => positionReference(value, disclosure, decode);
5789
5841
  case "tile":
5790
5842
  return (value) => typeof value === "string" ? (decode ? disclosure.tileId(value) : disclosure.tileRef(value)) ?? denied2 : denied2;
5791
5843
  case "card":
@@ -5826,15 +5878,25 @@ function projectInput(input, disclosure, cards) {
5826
5878
  const map = domainMapper(input.domain, disclosure, cards, false);
5827
5879
  if (!map) return input;
5828
5880
  const domain = input.domain;
5829
- if (domain.type !== "tileTarget" && domain.type !== "cardTarget" && domain.type !== "boardTarget")
5881
+ if (domain.type !== "tileTarget" && domain.type !== "cardTarget" && domain.type !== "boardTarget" && domain.type !== "zonePosition")
5830
5882
  return input;
5831
- const eligibleTargets = domain.eligibleTargets.flatMap((value) => {
5832
- const mapped = map(value);
5833
- return mapped === denied2 ? [] : [mapped];
5834
- });
5835
- const projectedDomain = {
5883
+ const projectedDomain = domain.type === "zonePosition" ? {
5884
+ ...domain,
5885
+ zones: domain.zones.flatMap((zone) => {
5886
+ const hostId = zoneHostReference(
5887
+ zone.zoneId,
5888
+ zone.hostId,
5889
+ disclosure,
5890
+ false
5891
+ );
5892
+ return hostId === null ? [] : [{ ...zone, hostId }];
5893
+ })
5894
+ } : {
5836
5895
  ...domain,
5837
- eligibleTargets
5896
+ eligibleTargets: domain.eligibleTargets.flatMap((value) => {
5897
+ const mapped = map(value);
5898
+ return mapped === denied2 ? [] : [mapped];
5899
+ })
5838
5900
  };
5839
5901
  const { defaultValue, ...rest } = input;
5840
5902
  const mappedDefault = defaultValue === void 0 ? denied2 : mapSelection(
@@ -7123,4 +7185,4 @@ export {
7123
7185
  createProjectionBuilder,
7124
7186
  createReducerBundle
7125
7187
  };
7126
- //# sourceMappingURL=chunk-TJQTLYDI.js.map
7188
+ //# sourceMappingURL=chunk-BNQF7QXR.js.map