@issuegraph/editor 0.1.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +238 -0
  3. package/dist/audit/findings.d.ts +225 -0
  4. package/dist/audit/findings.d.ts.map +1 -0
  5. package/dist/audit/findings.js +348 -0
  6. package/dist/audit/findings.js.map +1 -0
  7. package/dist/audit/styles.d.ts +37 -0
  8. package/dist/audit/styles.d.ts.map +1 -0
  9. package/dist/audit/styles.js +72 -0
  10. package/dist/audit/styles.js.map +1 -0
  11. package/dist/audit/surface.d.ts +155 -0
  12. package/dist/audit/surface.d.ts.map +1 -0
  13. package/dist/audit/surface.js +202 -0
  14. package/dist/audit/surface.js.map +1 -0
  15. package/dist/create/draft.d.ts +119 -0
  16. package/dist/create/draft.d.ts.map +1 -0
  17. package/dist/create/draft.js +114 -0
  18. package/dist/create/draft.js.map +1 -0
  19. package/dist/create/keys.d.ts +225 -0
  20. package/dist/create/keys.d.ts.map +1 -0
  21. package/dist/create/keys.js +228 -0
  22. package/dist/create/keys.js.map +1 -0
  23. package/dist/create/placement.d.ts +69 -0
  24. package/dist/create/placement.d.ts.map +1 -0
  25. package/dist/create/placement.js +76 -0
  26. package/dist/create/placement.js.map +1 -0
  27. package/dist/firstpass/batch.d.ts +190 -0
  28. package/dist/firstpass/batch.d.ts.map +1 -0
  29. package/dist/firstpass/batch.js +143 -0
  30. package/dist/firstpass/batch.js.map +1 -0
  31. package/dist/firstpass/candidates.d.ts +128 -0
  32. package/dist/firstpass/candidates.d.ts.map +1 -0
  33. package/dist/firstpass/candidates.js +39 -0
  34. package/dist/firstpass/candidates.js.map +1 -0
  35. package/dist/firstpass/keys.d.ts +90 -0
  36. package/dist/firstpass/keys.d.ts.map +1 -0
  37. package/dist/firstpass/keys.js +112 -0
  38. package/dist/firstpass/keys.js.map +1 -0
  39. package/dist/firstpass/queue.d.ts +183 -0
  40. package/dist/firstpass/queue.d.ts.map +1 -0
  41. package/dist/firstpass/queue.js +159 -0
  42. package/dist/firstpass/queue.js.map +1 -0
  43. package/dist/firstpass/render.d.ts +100 -0
  44. package/dist/firstpass/render.d.ts.map +1 -0
  45. package/dist/firstpass/render.js +216 -0
  46. package/dist/firstpass/render.js.map +1 -0
  47. package/dist/firstpass/styles.d.ts +36 -0
  48. package/dist/firstpass/styles.d.ts.map +1 -0
  49. package/dist/firstpass/styles.js +148 -0
  50. package/dist/firstpass/styles.js.map +1 -0
  51. package/dist/firstpass/view.d.ts +82 -0
  52. package/dist/firstpass/view.d.ts.map +1 -0
  53. package/dist/firstpass/view.js +60 -0
  54. package/dist/firstpass/view.js.map +1 -0
  55. package/dist/firstpass/words.d.ts +105 -0
  56. package/dist/firstpass/words.d.ts.map +1 -0
  57. package/dist/firstpass/words.js +28 -0
  58. package/dist/firstpass/words.js.map +1 -0
  59. package/dist/index.d.ts +150 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +147 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/overlay/grammar.d.ts +259 -0
  64. package/dist/overlay/grammar.d.ts.map +1 -0
  65. package/dist/overlay/grammar.js +223 -0
  66. package/dist/overlay/grammar.js.map +1 -0
  67. package/dist/overlay/render.d.ts +125 -0
  68. package/dist/overlay/render.d.ts.map +1 -0
  69. package/dist/overlay/render.js +439 -0
  70. package/dist/overlay/render.js.map +1 -0
  71. package/dist/overlay/styles.d.ts +41 -0
  72. package/dist/overlay/styles.d.ts.map +1 -0
  73. package/dist/overlay/styles.js +124 -0
  74. package/dist/overlay/styles.js.map +1 -0
  75. package/dist/picker/render.d.ts +80 -0
  76. package/dist/picker/render.d.ts.map +1 -0
  77. package/dist/picker/render.js +112 -0
  78. package/dist/picker/render.js.map +1 -0
  79. package/dist/picker/styles.d.ts +26 -0
  80. package/dist/picker/styles.d.ts.map +1 -0
  81. package/dist/picker/styles.js +116 -0
  82. package/dist/picker/styles.js.map +1 -0
  83. package/dist/picker/view.d.ts +135 -0
  84. package/dist/picker/view.d.ts.map +1 -0
  85. package/dist/picker/view.js +117 -0
  86. package/dist/picker/view.js.map +1 -0
  87. package/dist/picker/words.d.ts +53 -0
  88. package/dist/picker/words.d.ts.map +1 -0
  89. package/dist/picker/words.js +28 -0
  90. package/dist/picker/words.js.map +1 -0
  91. package/dist/reevaluate/render.d.ts +77 -0
  92. package/dist/reevaluate/render.d.ts.map +1 -0
  93. package/dist/reevaluate/render.js +211 -0
  94. package/dist/reevaluate/render.js.map +1 -0
  95. package/dist/reevaluate/styles.d.ts +24 -0
  96. package/dist/reevaluate/styles.d.ts.map +1 -0
  97. package/dist/reevaluate/styles.js +182 -0
  98. package/dist/reevaluate/styles.js.map +1 -0
  99. package/dist/reevaluate/view.d.ts +102 -0
  100. package/dist/reevaluate/view.d.ts.map +1 -0
  101. package/dist/reevaluate/view.js +111 -0
  102. package/dist/reevaluate/view.js.map +1 -0
  103. package/dist/reevaluate/words.d.ts +54 -0
  104. package/dist/reevaluate/words.d.ts.map +1 -0
  105. package/dist/reevaluate/words.js +29 -0
  106. package/dist/reevaluate/words.js.map +1 -0
  107. package/dist/scale/commands.d.ts +60 -0
  108. package/dist/scale/commands.d.ts.map +1 -0
  109. package/dist/scale/commands.js +49 -0
  110. package/dist/scale/commands.js.map +1 -0
  111. package/dist/scale/ladder.d.ts +141 -0
  112. package/dist/scale/ladder.d.ts.map +1 -0
  113. package/dist/scale/ladder.js +243 -0
  114. package/dist/scale/ladder.js.map +1 -0
  115. package/dist/scale/render.d.ts +85 -0
  116. package/dist/scale/render.d.ts.map +1 -0
  117. package/dist/scale/render.js +194 -0
  118. package/dist/scale/render.js.map +1 -0
  119. package/dist/scale/styles.d.ts +21 -0
  120. package/dist/scale/styles.d.ts.map +1 -0
  121. package/dist/scale/styles.js +105 -0
  122. package/dist/scale/styles.js.map +1 -0
  123. package/dist/workspace/inspector.d.ts +117 -0
  124. package/dist/workspace/inspector.d.ts.map +1 -0
  125. package/dist/workspace/inspector.js +140 -0
  126. package/dist/workspace/inspector.js.map +1 -0
  127. package/dist/workspace/rail.d.ts +122 -0
  128. package/dist/workspace/rail.d.ts.map +1 -0
  129. package/dist/workspace/rail.js +216 -0
  130. package/dist/workspace/rail.js.map +1 -0
  131. package/dist/workspace/render.d.ts +139 -0
  132. package/dist/workspace/render.d.ts.map +1 -0
  133. package/dist/workspace/render.js +366 -0
  134. package/dist/workspace/render.js.map +1 -0
  135. package/dist/workspace/selection.d.ts +92 -0
  136. package/dist/workspace/selection.d.ts.map +1 -0
  137. package/dist/workspace/selection.js +83 -0
  138. package/dist/workspace/selection.js.map +1 -0
  139. package/dist/workspace/styles.d.ts +39 -0
  140. package/dist/workspace/styles.d.ts.map +1 -0
  141. package/dist/workspace/styles.js +313 -0
  142. package/dist/workspace/styles.js.map +1 -0
  143. package/package.json +58 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Creating an edge, as the one thing all three paths do.
3
+ *
4
+ * §17b asks for three **equivalent** create paths — canvas, inspector and
5
+ * keyboard — and is explicit that they are equivalent rather than a primary
6
+ * path with two shortcuts. The inspector is "the only path when the target
7
+ * isn't on canvas", and at any real size most targets are off canvas (§17f:
8
+ * the canvas is a *local* instrument), so a design where drag is the real path
9
+ * and the other two are conveniences is a design that stops working at the
10
+ * size it was built for.
11
+ *
12
+ * ## Equivalence is a property of the SHAPE, not a promise a test keeps
13
+ *
14
+ * The three paths gather the same three facts in different orders:
15
+ *
16
+ * canvas source → target → kind (drag, then the picker at the drop)
17
+ * inspector source → kind → target ("+ add" → type → issue search)
18
+ * keyboard source → kind → target (`R` → `1`–`5` → search → `⏎`)
19
+ *
20
+ * So a draft modelled as a SEQUENCE would need three sequences, and "equivalent"
21
+ * would mean three implementations that agree today. This models it as a SET of
22
+ * gathered facts instead: three slots, each filled by its own command, in any
23
+ * order, and the proposal is emitted on whichever transition completes the set.
24
+ * Order-independence stops being a thing to verify and becomes a thing that
25
+ * cannot be otherwise — there is one emitter, and none of the three paths is
26
+ * spelled out here at all.
27
+ *
28
+ * That is also why the draft carries no path identity. A `source` filled by a
29
+ * drag and one filled by `R` are the same fact, and a field recording which
30
+ * arrived would be the difference between the paths growing back — every reader
31
+ * of it a place where one path could start behaving unlike the others. What
32
+ * genuinely differs between them is where the picker is DRAWN, which is
33
+ * geometry and lives in {@link ./placement.ts}, not state.
34
+ *
35
+ * ## One user act, one `Proposal`
36
+ *
37
+ * The same rule `picker/view.ts` keeps for retype and flip. `@issuegraph/store`
38
+ * closed the operation set at four so that one act is one round trip and one
39
+ * undo entry, and a create that emitted twice — or that emitted a delete plus a
40
+ * create — would undo the property the store went out of its way to model.
41
+ *
42
+ * ## Validity is the store's, and stays there
43
+ *
44
+ * A self-edge, a duplicate edge, an unknown issue: `structuralRefusal` owns all
45
+ * of them and answers with an `InvalidCode`. This module proposes; it does not
46
+ * adjudicate. `picker/view.ts` made the same call for `unchanged-kind` and
47
+ * recorded why — a second validity rule out here is a second place for the
48
+ * answer to drift, and the surface a refusal is drawn on already exists.
49
+ */
50
+ /** Nothing gathered. The state before a create starts and after one emits. */
51
+ export const IDLE_CREATE_DRAFT = Object.freeze({
52
+ source: null,
53
+ target: null,
54
+ kind: null,
55
+ });
56
+ function complete(draft) {
57
+ const { source, target, kind } = draft;
58
+ if (source === null || target === null || kind === null)
59
+ return null;
60
+ return { source, target, kind };
61
+ }
62
+ /**
63
+ * Apply one command. Total, pure, and never mutates what it is given.
64
+ *
65
+ * AN EXHAUSTIVE SWITCH OVER A DISCRIMINATED UNION — the shape `scaleReducer`
66
+ * already uses here and the one branching form `AGENTS.md`'s boundary rule
67
+ * leaves open. Adding a command without a case fails the build rather than
68
+ * silently returning the draft unchanged.
69
+ *
70
+ * The direction a create lands with is the gather order: `from` is the source,
71
+ * `to` is the target. Nothing here infers it. §17b's rule is that direction is
72
+ * STATED rather than guessed, and it is — by the picker, after the edit lands,
73
+ * with a flip beside it, which is one act from correct. That is the same
74
+ * reasoning `picker/view.ts` records for retyping across the directed/symmetric
75
+ * split, and it is why this module does not ask about directedness at all: a
76
+ * symmetric kind keeps the pair too, precisely so an editor knows which issue
77
+ * carries the field.
78
+ */
79
+ export function createReducer(draft, command) {
80
+ const next = (() => {
81
+ switch (command.kind) {
82
+ case 'begin':
83
+ return { source: command.source, target: null, kind: null };
84
+ case 'target':
85
+ return { ...draft, target: command.ref };
86
+ case 'type':
87
+ return { ...draft, kind: command.edgeKind };
88
+ case 'cancel':
89
+ return IDLE_CREATE_DRAFT;
90
+ }
91
+ })();
92
+ // `cancel` CANNOT emit, and it needs no guard here to be sure of it: it
93
+ // empties the draft, so the completeness test below has nothing to find. An
94
+ // earlier revision carried an explicit early return for it and claimed to be
95
+ // tested — a mutation control disproved that in the only way it can be
96
+ // disproved, by deleting the line and watching every test still pass. It was
97
+ // dead code asserting its own necessity, which is worse than no comment.
98
+ //
99
+ // A withdrawal that fired the edit it withdrew would be the worst possible
100
+ // reading of the key, so the property is still pinned by tests; what changed
101
+ // is that they now pin a structural guarantee rather than a branch.
102
+ const ready = complete(next);
103
+ if (ready === null)
104
+ return { draft: next, proposal: null };
105
+ // THE DRAFT RESETS ON EMISSION. Left full, the next command to arrive would
106
+ // complete it again and emit a second proposal for one user act — the "two
107
+ // dispatches, two order re-evaluations" the closed operation set exists to
108
+ // prevent. Emission is the end of the draft, not a state it stays in.
109
+ return {
110
+ draft: IDLE_CREATE_DRAFT,
111
+ proposal: { op: 'create', kind: ready.kind, from: ready.source, to: ready.target },
112
+ };
113
+ }
114
+ //# sourceMappingURL=draft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../../src/create/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAoBH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,iBAAiB,GAAgB,MAAM,CAAC,MAAM,CAAC;IAC1D,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;CACX,CAAC,CAAC;AA2CH,SAAS,QAAQ,CAAC,KAAkB;IAClC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC;IACvC,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAkB,EAAE,OAAsB;IACtE,MAAM,IAAI,GAAG,CAAC,GAAgB,EAAE;QAC9B,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,KAAK,OAAO;gBACV,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAC9D,KAAK,QAAQ;gBACX,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;YAC3C,KAAK,MAAM;gBACT,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC9C,KAAK,QAAQ;gBACX,OAAO,iBAAiB,CAAC;QAC7B,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IAEL,wEAAwE;IACxE,4EAA4E;IAC5E,6EAA6E;IAC7E,uEAAuE;IACvE,6EAA6E;IAC7E,yEAAyE;IACzE,EAAE;IACF,2EAA2E;IAC3E,6EAA6E;IAC7E,oEAAoE;IACpE,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAE3D,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,sEAAsE;IACtE,OAAO;QACL,KAAK,EAAE,iBAAiB;QACxB,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE;KACnF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The keyboard path, as a pure key map.
3
+ *
4
+ * §17b fixes the loop — `R` → `1`–`5` → search → `⏎`, with `⌫` deleting a
5
+ * selected edge and `T` retyping it — and fixes what it is FOR in four words:
6
+ * "full loop, no pointer". An owner encoding a backlog can reach every edit
7
+ * without touching a pointing device, which is what makes the first pass fast.
8
+ *
9
+ * No DOM, no events, no side-effects: a key NAME and a context in, an intent
10
+ * out. Exactly the shape `viewer/navigation.ts` chose, and for the same payoff
11
+ * — the whole map is exhaustively testable on a runtime with no DOM at all, and
12
+ * the shell is left with nothing to get wrong except wiring.
13
+ *
14
+ * ## The digits are read from the format, never listed
15
+ *
16
+ * `1`–`5` select a kind BY ITS POSITION IN `EDGE_FIELDS`, taken from
17
+ * `@issuegraph/core`. A local `['blocked-by', …]` out here is the drifting
18
+ * second implementation the package family removes everywhere it appears:
19
+ * `picker/view.ts` records the same decision for directedness, and the failure
20
+ * it prevents is specific — a sixth field added to the format would leave the
21
+ * list silently wrong, with nothing failing to say so. Built from the vocabulary
22
+ * itself, a sixth field gets a `6` for free and the picker and the keyboard
23
+ * cannot disagree about which digit means what.
24
+ *
25
+ * ## Retype opens the picker; it does not emit a retype
26
+ *
27
+ * `T` answers with the edge to open {@link ../picker/view.ts pickerView} on,
28
+ * whose options already carry one `retype` proposal each. Emitting one from out
29
+ * here would be a second retype emitter, and the two would be free to disagree
30
+ * about what a retype IS — which is the whole reason that module exists.
31
+ *
32
+ * ## `none` means SOMEONE ELSE OWNS THIS PRESS
33
+ *
34
+ * The same contract `NavigationCommand`'s `none` carries, and the single idea
35
+ * this module keeps getting asked about. A host's whole wiring is "reduce a
36
+ * non-`none` intent, and `preventDefault()` it" — so every press this map
37
+ * claims wrongly is a keystroke stolen from its real owner, and one it hands
38
+ * back is simply the platform working.
39
+ *
40
+ * ## Why this asks about OUR interaction rather than about other owners
41
+ *
42
+ * Four review rounds each found a different owner this map had failed to
43
+ * anticipate: the platform's `Cmd+R`, the target search's digits, an input
44
+ * method's `⏎`, and then an unrelated editable control's `Escape`. Each fix was
45
+ * correct and each invited the next, because they were all answers to an
46
+ * unanswerable question — *who else might own this press?* That list is an
47
+ * inventory of the HOST's widgets, and this package cannot see it, cannot bound
48
+ * it, and gets one more entry every time a host grows a control.
49
+ *
50
+ * So the question is inverted. {@link CreateInteraction} enumerates OUR OWN
51
+ * interaction, which §17b fixes at three states, and the host says which one it
52
+ * is in. A fifth widget adds no code here: the host reports `elsewhere` and the
53
+ * map is silent. What was an open-ended list of other people's claims became a
54
+ * closed description of this design's own flow.
55
+ *
56
+ * Two press-level facts remain, and they are bounded in a way the widget list
57
+ * never was — a modifier set and a composition flag are fields on the event
58
+ * itself, not things a host invents. See {@link KeyPress}.
59
+ *
60
+ * That leaves the contract: an unbound key is `none`; a press some other part
61
+ * of the host owns is `none`; everything else is this vocabulary's.
62
+ *
63
+ * Which bindings reach the target search is DATA on the binding table, so no
64
+ * call site decides it and a sixth binding cannot be added without answering.
65
+ */
66
+ import type { EdgeId, IssueRef, Proposal } from '@issuegraph/store';
67
+ import type { CreateCommand } from './draft.ts';
68
+ /**
69
+ * What the shell knows when a key arrives.
70
+ *
71
+ * Every field is nullable because every one of them is genuinely absent
72
+ * sometimes — nothing focused, a search matching nothing, no edge selected —
73
+ * and a key whose subject is missing resolves to {@link KeyIntent} `none`
74
+ * rather than to a guess.
75
+ */
76
+ export interface KeyboardContext {
77
+ /**
78
+ * The issue holding the tab stop. The SOURCE a new relationship starts from.
79
+ *
80
+ * The viewer's roving tab stop is the natural supply for this: its
81
+ * `NavigationState.focused` is exactly "the issue the reader is standing on".
82
+ */
83
+ readonly focused: IssueRef | null;
84
+ /**
85
+ * What the target search currently resolves to, or `null`.
86
+ *
87
+ * The SEARCH ITSELF IS THE HOST'S. Matching a query against a backlog needs
88
+ * the backlog — and at the sizes §17f describes, an index the host already
89
+ * has — so this module takes the answer rather than the question. It also
90
+ * keeps the create path indifferent to *how* a target was named: the same
91
+ * `⏎` works over a fuzzy match, a pasted reference or a picked list row.
92
+ */
93
+ readonly match: IssueRef | null;
94
+ /**
95
+ * The selected edge, or `null` when the selection is an issue or empty.
96
+ *
97
+ * A `together-with` CONNECTOR ARRIVES HERE LIKE ANY OTHER EDGE, and that is
98
+ * why neither `⌫` nor `T` carries a special case for it. The viewer gives the
99
+ * connector its own edge identity (`GROUP_ATTRIBUTE`) precisely so that an
100
+ * enclosure — which has no line to click — is still individually selectable,
101
+ * so by the time a selection reaches this module it is already just an edge
102
+ * id. §17b asks that such an edge be selectable, retypeable and deletable;
103
+ * it is, through the ordinary path, with nothing here to keep in step.
104
+ */
105
+ readonly selectedEdge: EdgeId | null;
106
+ /**
107
+ * Which of this design's own interactions the keyboard is in.
108
+ *
109
+ * REQUIRED, NOT OPTIONAL. Every default is wrong for some host, and the
110
+ * plausible one — "assume the canvas" — is the one that steals keystrokes; a
111
+ * host that has not thought about it should get a compile error, not silence.
112
+ *
113
+ * The host answers it because the host is the only one who can: this package
114
+ * has no DOM, and which control holds focus is visible only to the shell.
115
+ * Crucially, answering it needs no knowledge of THIS package — a host maps
116
+ * its own world onto three states it already understands.
117
+ */
118
+ readonly interaction: CreateInteraction;
119
+ }
120
+ /**
121
+ * Where the keyboard is, in terms of the create flow §17b specifies.
122
+ *
123
+ * Three states, and the set is closed by the DESIGN rather than by the host's
124
+ * inventory of controls — which is the whole reason it replaced a growing list
125
+ * of other owners.
126
+ *
127
+ * - `canvas` — the create vocabulary is live. Every binding reaches it.
128
+ * - `target-search` — the create flow's OWN search box has focus. Only the
129
+ * bindings that must survive it reach: `⏎` commits the target and `Escape`
130
+ * withdraws, which is precisely the middle of `R → digit → search → ⏎`. Its
131
+ * printable keys belong to the box — most issue references carry a digit, so
132
+ * a map that claimed `1`–`5` here would eat nearly every query — and `⌫`
133
+ * deletes a CHARACTER rather than the reader's selected edge.
134
+ * - `elsewhere` — something else owns the keyboard entirely: an inline title,
135
+ * a filter box, a modal, a control this package has never heard of. NOTHING
136
+ * reaches, `Escape` included, because that control needs `Escape` to cancel
137
+ * its own edit.
138
+ *
139
+ * `elsewhere` is what makes the set closed. It is the state for everything not
140
+ * named, so a host growing a fifth control changes nothing here.
141
+ */
142
+ export type CreateInteraction = 'canvas' | 'target-search' | 'elsewhere';
143
+ /**
144
+ * What a key means. `none` leaves the key to the host.
145
+ *
146
+ * `create` carries a {@link CreateCommand } for {@link ./draft.ts createReducer},
147
+ * so the keyboard path feeds the same reducer the other two do rather than
148
+ * building proposals of its own.
149
+ */
150
+ export type KeyIntent = {
151
+ readonly kind: 'none';
152
+ } | {
153
+ readonly kind: 'create';
154
+ readonly command: CreateCommand;
155
+ } | {
156
+ readonly kind: 'propose';
157
+ readonly proposal: Proposal;
158
+ }
159
+ /** Open the type picker on this edge; its options carry the proposals. */
160
+ | {
161
+ readonly kind: 'retype';
162
+ readonly edgeId: EdgeId;
163
+ };
164
+ /**
165
+ * One key press. Structurally a subset of the DOM's `KeyboardEvent`, so a host
166
+ * passes the event straight in rather than unpacking it.
167
+ *
168
+ * TAKING THE PRESS RATHER THAN THE KEY NAME IS THE POINT. An earlier revision
169
+ * took a bare `key: string`, which cannot tell `R` from `Cmd+R` — so a shell
170
+ * that forwarded `event.key` and called `preventDefault()` for any non-`none`
171
+ * intent would hijack reload, new-tab and tab-selection, and no amount of care
172
+ * on the host's part could recover the distinction this function had already
173
+ * discarded. The modifiers are part of what a press IS; asking for them is the
174
+ * only way this map can honestly answer "not mine".
175
+ *
176
+ * The three that BLOCK are optional so `{ key: 'r' }` still means an
177
+ * unmodified press, which is what a test writes and what a synthetic press from
178
+ * a non-DOM shell has.
179
+ */
180
+ export interface KeyPress {
181
+ /** The `KeyboardEvent.key` value. */
182
+ readonly key: string;
183
+ readonly ctrlKey?: boolean;
184
+ readonly metaKey?: boolean;
185
+ readonly altKey?: boolean;
186
+ /**
187
+ * `KeyboardEvent.isComposing` — whether an input method is mid-composition.
188
+ *
189
+ * The two keys this protects are exactly the two that survive editable focus.
190
+ * While an IME is composing, `⏎` CONFIRMS the candidate and `Escape` CANCELS
191
+ * the composition; both belong to the input method, and claiming them commits
192
+ * a stale target or discards the draft while the reader is still spelling the
193
+ * word they meant to search for. Anyone entering CJK text hits it on the
194
+ * ordinary path, which is why it is not an exotic case.
195
+ */
196
+ readonly isComposing?: boolean;
197
+ /**
198
+ * `KeyboardEvent.repeat` — whether the OS generated this event by auto-repeat
199
+ * rather than the reader pressing the key again.
200
+ *
201
+ * EVERY BINDING IN THIS VOCABULARY IS A ONE-SHOT COMMAND, so a repeat is not
202
+ * a second act: holding `⌫` past the repeat delay is one decision to delete
203
+ * one edge, and emitting a proposal per event breaks the one-act/one-proposal
204
+ * contract `draft.ts` and the store are both built on. The store makes it
205
+ * visible rather than harmless — a pending delete keeps its edge drawn and
206
+ * selection is client state, so the queued proposals settle into
207
+ * `unknown-edge` records after the first one lands.
208
+ *
209
+ * It is blanket rather than a per-binding flag because there is no repeatable
210
+ * binding here to distinguish: `R`, `1`–`5`, `⏎`, `⌫` and `T` are all
211
+ * discrete commands, none of them a continuous motion like an arrow key. Add
212
+ * one that genuinely repeats and that is when this earns a column on the
213
+ * table — not before.
214
+ */
215
+ readonly repeat?: boolean;
216
+ }
217
+ /**
218
+ * What one key press means, given what the shell currently holds.
219
+ *
220
+ * Total and pure. The branching is one exhaustive switch over a discriminated
221
+ * union — the boundary rule's permitted shape — and every arm is a single
222
+ * expression, because the decision was already made by the table.
223
+ */
224
+ export declare function keyIntent(press: KeyPress, context: KeyboardContext): KeyIntent;
225
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/create/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAY,QAAQ,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;IAChC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,eAAe,GAAG,WAAW,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE;AAC3D,0EAA0E;GACxE;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA+DzD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,QAAQ;IACvB,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AA0ED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,SAAS,CA+C9E"}
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The keyboard path, as a pure key map.
3
+ *
4
+ * §17b fixes the loop — `R` → `1`–`5` → search → `⏎`, with `⌫` deleting a
5
+ * selected edge and `T` retyping it — and fixes what it is FOR in four words:
6
+ * "full loop, no pointer". An owner encoding a backlog can reach every edit
7
+ * without touching a pointing device, which is what makes the first pass fast.
8
+ *
9
+ * No DOM, no events, no side-effects: a key NAME and a context in, an intent
10
+ * out. Exactly the shape `viewer/navigation.ts` chose, and for the same payoff
11
+ * — the whole map is exhaustively testable on a runtime with no DOM at all, and
12
+ * the shell is left with nothing to get wrong except wiring.
13
+ *
14
+ * ## The digits are read from the format, never listed
15
+ *
16
+ * `1`–`5` select a kind BY ITS POSITION IN `EDGE_FIELDS`, taken from
17
+ * `@issuegraph/core`. A local `['blocked-by', …]` out here is the drifting
18
+ * second implementation the package family removes everywhere it appears:
19
+ * `picker/view.ts` records the same decision for directedness, and the failure
20
+ * it prevents is specific — a sixth field added to the format would leave the
21
+ * list silently wrong, with nothing failing to say so. Built from the vocabulary
22
+ * itself, a sixth field gets a `6` for free and the picker and the keyboard
23
+ * cannot disagree about which digit means what.
24
+ *
25
+ * ## Retype opens the picker; it does not emit a retype
26
+ *
27
+ * `T` answers with the edge to open {@link ../picker/view.ts pickerView} on,
28
+ * whose options already carry one `retype` proposal each. Emitting one from out
29
+ * here would be a second retype emitter, and the two would be free to disagree
30
+ * about what a retype IS — which is the whole reason that module exists.
31
+ *
32
+ * ## `none` means SOMEONE ELSE OWNS THIS PRESS
33
+ *
34
+ * The same contract `NavigationCommand`'s `none` carries, and the single idea
35
+ * this module keeps getting asked about. A host's whole wiring is "reduce a
36
+ * non-`none` intent, and `preventDefault()` it" — so every press this map
37
+ * claims wrongly is a keystroke stolen from its real owner, and one it hands
38
+ * back is simply the platform working.
39
+ *
40
+ * ## Why this asks about OUR interaction rather than about other owners
41
+ *
42
+ * Four review rounds each found a different owner this map had failed to
43
+ * anticipate: the platform's `Cmd+R`, the target search's digits, an input
44
+ * method's `⏎`, and then an unrelated editable control's `Escape`. Each fix was
45
+ * correct and each invited the next, because they were all answers to an
46
+ * unanswerable question — *who else might own this press?* That list is an
47
+ * inventory of the HOST's widgets, and this package cannot see it, cannot bound
48
+ * it, and gets one more entry every time a host grows a control.
49
+ *
50
+ * So the question is inverted. {@link CreateInteraction} enumerates OUR OWN
51
+ * interaction, which §17b fixes at three states, and the host says which one it
52
+ * is in. A fifth widget adds no code here: the host reports `elsewhere` and the
53
+ * map is silent. What was an open-ended list of other people's claims became a
54
+ * closed description of this design's own flow.
55
+ *
56
+ * Two press-level facts remain, and they are bounded in a way the widget list
57
+ * never was — a modifier set and a composition flag are fields on the event
58
+ * itself, not things a host invents. See {@link KeyPress}.
59
+ *
60
+ * That leaves the contract: an unbound key is `none`; a press some other part
61
+ * of the host owns is `none`; everything else is this vocabulary's.
62
+ *
63
+ * Which bindings reach the target search is DATA on the binding table, so no
64
+ * call site decides it and a sixth binding cannot be added without answering.
65
+ */
66
+ import { EDGE_FIELDS } from '@issuegraph/core';
67
+ /**
68
+ * The vocabulary, as data.
69
+ *
70
+ * THE KIND TRAVELS IN THE BINDING, not an index into `EDGE_FIELDS`. Storing a
71
+ * position would put an array lookup on the resolve path, whose miss is
72
+ * `undefined` — a case that cannot happen and would still have to be answered,
73
+ * in a language where answering it wrongly means a cast. Reading the vocabulary
74
+ * once, here, closes it.
75
+ */
76
+ const BINDINGS = new Map([
77
+ // `r`, the digits and `t` are PRINTABLE. While a text control has focus they
78
+ // are that control's characters, so they do not survive editing.
79
+ ['r', { kind: 'relate', reachesTargetSearch: false }],
80
+ ...EDGE_FIELDS.map((edgeKind, index) => [
81
+ String(index + 1),
82
+ { kind: 'choose-type', edgeKind, reachesTargetSearch: false },
83
+ ]),
84
+ // `⏎` and `Escape` SURVIVE, and they are the reason this is a per-binding flag
85
+ // rather than one "printable" test: the flow §17b specifies is
86
+ // `R → digit → search → ⏎`, so the search box is focused at precisely the
87
+ // moment `⏎` has to commit the target. A rule that silenced the whole map
88
+ // while editing would break the loop it exists to deliver. `Escape` survives
89
+ // for the same reason it is always available: a draft you cannot abandon is
90
+ // worse than one you cannot start.
91
+ ['enter', { kind: 'commit-target', reachesTargetSearch: true }],
92
+ // BOTH SPELLINGS OF THE DELETE KEY. §17b writes it `⌫`, which is `Backspace`
93
+ // on the keyboards that have it and `Delete` on those that do not — most
94
+ // notably Apple's, where the key in that position reports `Backspace` and the
95
+ // key labelled `Delete` is the forward one. Binding one of the two would make
96
+ // "no pointer" false on whichever hardware got the other.
97
+ // NOT SURVIVING EDITING is the non-obvious half: in a focused text box `⌫`
98
+ // deletes a CHARACTER, and a map that claimed it would delete the reader's
99
+ // selected edge while they were correcting a typo in the search.
100
+ ['backspace', { kind: 'delete-edge', reachesTargetSearch: false }],
101
+ ['delete', { kind: 'delete-edge', reachesTargetSearch: false }],
102
+ ['t', { kind: 'retype-edge', reachesTargetSearch: false }],
103
+ ['escape', { kind: 'cancel', reachesTargetSearch: true }],
104
+ ]);
105
+ const NONE = Object.freeze({ kind: 'none' });
106
+ /**
107
+ * Whether this press is a chord that belongs to the platform, not to us.
108
+ *
109
+ * `shiftKey` IS DELIBERATELY ABSENT. §17b names its bindings in capitals, and
110
+ * `Shift+r` is how a keyboard reports `R` — so treating shift as a modifier
111
+ * would unbind the very keys the design specifies. Shift changes which key was
112
+ * pressed and is folded by `normalize`; Ctrl, Meta and Alt change whose
113
+ * shortcut it is, which is a different question and the one that matters here.
114
+ */
115
+ function chorded(press) {
116
+ return press.ctrlKey === true || press.metaKey === true || press.altKey === true;
117
+ }
118
+ /**
119
+ * Whether an input method is mid-composition, and therefore owns this press.
120
+ *
121
+ * Its own predicate rather than another clause inside {@link chorded}: a chord
122
+ * is a fact about which MODIFIERS are held, composition is a fact about the
123
+ * INPUT METHOD's state, and folding them together would make the name lie about
124
+ * half of what it tests. They are asked in the same breath below because the
125
+ * question they answer is the same one.
126
+ */
127
+ function composing(press) {
128
+ return press.isComposing === true;
129
+ }
130
+ /**
131
+ * Whether the OS generated this press by auto-repeat.
132
+ *
133
+ * A DIFFERENT QUESTION FROM THE OTHER TWO, which is why it is not folded into
134
+ * either. Those ask who OWNS the press; this one asks whether it is a fresh ACT
135
+ * at all. The reader holding a key means one decision, however many events the
136
+ * repeat delay produces.
137
+ */
138
+ function repeated(press) {
139
+ return press.repeat === true;
140
+ }
141
+ /**
142
+ * Whether a binding is live in the interaction the host reports.
143
+ *
144
+ * A DECISION TABLE OVER THE THREE STATES, exhaustive so a fourth interaction —
145
+ * were the design ever to grow one — is a compile error here rather than a
146
+ * silently permissive default. `elsewhere` returning `false` for EVERY binding
147
+ * is the whole of the fix for an unrelated control's `Escape`: that control
148
+ * needs `Escape` to cancel its own edit, and a create draft it knows nothing
149
+ * about must not consume it.
150
+ */
151
+ function reaches(binding, interaction) {
152
+ switch (interaction) {
153
+ case 'canvas':
154
+ return true;
155
+ case 'target-search':
156
+ return binding.reachesTargetSearch;
157
+ case 'elsewhere':
158
+ return false;
159
+ }
160
+ }
161
+ /**
162
+ * Fold a `KeyboardEvent.key` into the table's spelling.
163
+ *
164
+ * Lower-casing is what makes `R` and `r` the same key, which matters because
165
+ * §17b names the bindings in capitals while a keyboard reports the shifted and
166
+ * unshifted forms differently. `toLowerCase` also folds `Enter`, `Backspace`,
167
+ * `Delete` and `Escape` onto their entries above, so one rule covers both
168
+ * families and there is no second spelling table to keep aligned.
169
+ */
170
+ function normalize(key) {
171
+ return key.toLowerCase();
172
+ }
173
+ /**
174
+ * What one key press means, given what the shell currently holds.
175
+ *
176
+ * Total and pure. The branching is one exhaustive switch over a discriminated
177
+ * union — the boundary rule's permitted shape — and every arm is a single
178
+ * expression, because the decision was already made by the table.
179
+ */
180
+ export function keyIntent(press, context) {
181
+ // THE PRESS ITSELF DISQUALIFIES IT, before any lookup — none of these is a
182
+ // miss to be looked up. All three read a field off the event, so none is the
183
+ // open-ended widget question `CreateInteraction` replaced: `KeyboardEvent`'s
184
+ // shape is fixed by the platform rather than by how many controls a host has.
185
+ // TWO QUESTIONS, NOT ONE. The first two ask who OWNS the press — the platform
186
+ // holds `Cmd+R`, an input method holds `⏎` while composing. The third asks
187
+ // whether it is a fresh ACT at all, which is a different thing and the reason
188
+ // it is its own predicate.
189
+ // COMPOSITION BITES THE TWO BINDINGS THAT REACH THE TARGET SEARCH, which is
190
+ // why it cannot be folded into `reachesTargetSearch`: `⏎` and `Escape` are
191
+ // exactly the two that reach a focused search box, and exactly the two an IME
192
+ // needs while composing.
193
+ if (chorded(press) || composing(press) || repeated(press))
194
+ return NONE;
195
+ const binding = BINDINGS.get(normalize(press.key));
196
+ if (binding === undefined)
197
+ return NONE;
198
+ if (!reaches(binding, context.interaction))
199
+ return NONE;
200
+ switch (binding.kind) {
201
+ case 'relate':
202
+ // Nothing focused is nothing to relate FROM. Answering `none` hands the
203
+ // key back rather than opening a draft with a hole in it.
204
+ return context.focused === null
205
+ ? NONE
206
+ : { kind: 'create', command: { kind: 'begin', source: context.focused } };
207
+ case 'choose-type':
208
+ // NO CONTEXT NEEDED, and deliberately usable before a target exists: the
209
+ // keyboard path gathers kind before target, and the draft is a set, so a
210
+ // kind chosen early is simply a filled slot.
211
+ return { kind: 'create', command: { kind: 'type', edgeKind: binding.edgeKind } };
212
+ case 'commit-target':
213
+ return context.match === null
214
+ ? NONE
215
+ : { kind: 'create', command: { kind: 'target', ref: context.match } };
216
+ case 'delete-edge':
217
+ return context.selectedEdge === null
218
+ ? NONE
219
+ : { kind: 'propose', proposal: { op: 'delete', edgeId: context.selectedEdge } };
220
+ case 'retype-edge':
221
+ return context.selectedEdge === null ? NONE : { kind: 'retype', edgeId: context.selectedEdge };
222
+ case 'cancel':
223
+ // ALWAYS AVAILABLE. Escape withdraws whatever is open, and a draft the
224
+ // reader cannot abandon is worse than one they cannot start.
225
+ return { kind: 'create', command: { kind: 'cancel' } };
226
+ }
227
+ }
228
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/create/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAoH/C;;;;;;;;GAQG;AACH,MAAM,QAAQ,GAAiC,IAAI,GAAG,CAAkB;IACtE,6EAA6E;IAC7E,iEAAiE;IACjE,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC;IACrD,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,EAA8B,EAAE,CAAC;QAClE,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;QACjB,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,mBAAmB,EAAE,KAAK,EAAE;KAC9D,CAAC;IACF,+EAA+E;IAC/E,+DAA+D;IAC/D,0EAA0E;IAC1E,0EAA0E;IAC1E,6EAA6E;IAC7E,4EAA4E;IAC5E,mCAAmC;IACnC,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC;IAC/D,6EAA6E;IAC7E,yEAAyE;IACzE,8EAA8E;IAC9E,8EAA8E;IAC9E,0DAA0D;IAC1D,2EAA2E;IAC3E,2EAA2E;IAC3E,iEAAiE;IACjE,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC;IAClE,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC;IAC/D,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC;IAC1D,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC;CAC1D,CAAC,CAAC;AAEH,MAAM,IAAI,GAAc,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AAwDxD;;;;;;;;GAQG;AACH,SAAS,OAAO,CAAC,KAAe;IAC9B,OAAO,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC;AACnF,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,KAAe;IAChC,OAAO,KAAK,CAAC,WAAW,KAAK,IAAI,CAAC;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,QAAQ,CAAC,KAAe;IAC/B,OAAO,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC;AAC/B,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,OAAO,CAAC,OAAgB,EAAE,WAA8B;IAC/D,QAAQ,WAAW,EAAE,CAAC;QACpB,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC;QACd,KAAK,eAAe;YAClB,OAAO,OAAO,CAAC,mBAAmB,CAAC;QACrC,KAAK,WAAW;YACd,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,KAAe,EAAE,OAAwB;IACjE,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,2EAA2E;IAC3E,8EAA8E;IAC9E,2BAA2B;IAC3B,4EAA4E;IAC5E,2EAA2E;IAC3E,8EAA8E;IAC9E,yBAAyB;IACzB,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvE,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEvC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,IAAI,CAAC;IAExD,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,QAAQ;YACX,wEAAwE;YACxE,0DAA0D;YAC1D,OAAO,OAAO,CAAC,OAAO,KAAK,IAAI;gBAC7B,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9E,KAAK,aAAa;YAChB,yEAAyE;YACzE,yEAAyE;YACzE,6CAA6C;YAC7C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;QACnF,KAAK,eAAe;YAClB,OAAO,OAAO,CAAC,KAAK,KAAK,IAAI;gBAC3B,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC1E,KAAK,aAAa;YAChB,OAAO,OAAO,CAAC,YAAY,KAAK,IAAI;gBAClC,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;QACpF,KAAK,aAAa;YAChB,OAAO,OAAO,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;QACjG,KAAK,QAAQ;YACX,uEAAuE;YACvE,6DAA6D;YAC7D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC;IAC3D,CAAC;AACH,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Where the canvas path draws its type picker.
3
+ *
4
+ * §17b puts the picker "at the drop point", and the kit's implementation note
5
+ * fixes how that coordinate is arrived at: *"Hand-authoring these coordinates
6
+ * against remembered positions is the failure mode; compute them from layout."*
7
+ * So this takes MEASURED bounds — the container's, and the picker's own — and
8
+ * derives a position from them. It holds no constants about how big anything is.
9
+ *
10
+ * ## The one thing it adds to "at the drop point"
11
+ *
12
+ * A drop near the right or bottom edge would put a picker anchored there partly
13
+ * outside the canvas, where the reader cannot reach the options. So the picker
14
+ * FLIPS to the other side of the drop point rather than being nudged: a flip
15
+ * keeps the drop point on a corner of the picker, so the picker still visibly
16
+ * belongs to the gesture that opened it, while a nudge slides it off the point
17
+ * and reads as landing somewhere arbitrary.
18
+ *
19
+ * A flip is reported rather than only applied ({@link AxisPlacement.flipped}),
20
+ * because the side the picker opens on decides which corner its callout points
21
+ * from — and a host that had to re-derive that from the coordinates would be
22
+ * recomputing a decision already made here.
23
+ *
24
+ * ## It is geometry, so it is not in the draft
25
+ *
26
+ * The canvas is the only path with a drop point: the inspector opens its picker
27
+ * in a panel and the keyboard never has a pointer position at all. That is the
28
+ * whole of what distinguishes the three, and keeping it here — rather than as a
29
+ * field on {@link ./draft.ts CreateDraft} — is what stops "which path started
30
+ * this" from becoming state the rest of the create logic could branch on.
31
+ *
32
+ * Pure arithmetic on numbers, so it runs under the purity test like everything
33
+ * else here: it never measures anything itself. Measuring is the shell's, from
34
+ * `getBoundingClientRect` or its own layout output; both arrive as plain data.
35
+ */
36
+ /** A measured rectangle, in whatever coordinate space the caller measured in. */
37
+ export interface Bounds {
38
+ readonly x: number;
39
+ readonly y: number;
40
+ readonly width: number;
41
+ readonly height: number;
42
+ }
43
+ /** A measured extent. The picker's own, once the host has laid it out. */
44
+ export interface Size {
45
+ readonly width: number;
46
+ readonly height: number;
47
+ }
48
+ /** Where the drag was released, in the container's coordinate space. */
49
+ export interface Point {
50
+ readonly x: number;
51
+ readonly y: number;
52
+ }
53
+ /** Where to draw the picker, and which way it opened. */
54
+ export interface PickerPlacement {
55
+ readonly x: number;
56
+ readonly y: number;
57
+ readonly flippedX: boolean;
58
+ readonly flippedY: boolean;
59
+ }
60
+ /**
61
+ * The picker's top-left corner for a drop at `drop`, inside `container`.
62
+ *
63
+ * Every input is measured by the caller; nothing here assumes a size, a margin
64
+ * or a viewport. `container` is the canvas's own measured bounds rather than the
65
+ * window's, because the canvas is the surface the drop happened on and a picker
66
+ * escaping it would sit over the order rail beside it.
67
+ */
68
+ export declare function pickerPlacement(drop: Point, picker: Size, container: Bounds): PickerPlacement;
69
+ //# sourceMappingURL=placement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../src/create/placement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,iFAAiF;AACjF,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,0EAA0E;AAC1E,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wEAAwE;AACxE,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAsBD,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAyBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,CAS7F"}