@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.
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/dist/audit/findings.d.ts +225 -0
- package/dist/audit/findings.d.ts.map +1 -0
- package/dist/audit/findings.js +348 -0
- package/dist/audit/findings.js.map +1 -0
- package/dist/audit/styles.d.ts +37 -0
- package/dist/audit/styles.d.ts.map +1 -0
- package/dist/audit/styles.js +72 -0
- package/dist/audit/styles.js.map +1 -0
- package/dist/audit/surface.d.ts +155 -0
- package/dist/audit/surface.d.ts.map +1 -0
- package/dist/audit/surface.js +202 -0
- package/dist/audit/surface.js.map +1 -0
- package/dist/create/draft.d.ts +119 -0
- package/dist/create/draft.d.ts.map +1 -0
- package/dist/create/draft.js +114 -0
- package/dist/create/draft.js.map +1 -0
- package/dist/create/keys.d.ts +225 -0
- package/dist/create/keys.d.ts.map +1 -0
- package/dist/create/keys.js +228 -0
- package/dist/create/keys.js.map +1 -0
- package/dist/create/placement.d.ts +69 -0
- package/dist/create/placement.d.ts.map +1 -0
- package/dist/create/placement.js +76 -0
- package/dist/create/placement.js.map +1 -0
- package/dist/firstpass/batch.d.ts +190 -0
- package/dist/firstpass/batch.d.ts.map +1 -0
- package/dist/firstpass/batch.js +143 -0
- package/dist/firstpass/batch.js.map +1 -0
- package/dist/firstpass/candidates.d.ts +128 -0
- package/dist/firstpass/candidates.d.ts.map +1 -0
- package/dist/firstpass/candidates.js +39 -0
- package/dist/firstpass/candidates.js.map +1 -0
- package/dist/firstpass/keys.d.ts +90 -0
- package/dist/firstpass/keys.d.ts.map +1 -0
- package/dist/firstpass/keys.js +112 -0
- package/dist/firstpass/keys.js.map +1 -0
- package/dist/firstpass/queue.d.ts +183 -0
- package/dist/firstpass/queue.d.ts.map +1 -0
- package/dist/firstpass/queue.js +159 -0
- package/dist/firstpass/queue.js.map +1 -0
- package/dist/firstpass/render.d.ts +100 -0
- package/dist/firstpass/render.d.ts.map +1 -0
- package/dist/firstpass/render.js +216 -0
- package/dist/firstpass/render.js.map +1 -0
- package/dist/firstpass/styles.d.ts +36 -0
- package/dist/firstpass/styles.d.ts.map +1 -0
- package/dist/firstpass/styles.js +148 -0
- package/dist/firstpass/styles.js.map +1 -0
- package/dist/firstpass/view.d.ts +82 -0
- package/dist/firstpass/view.d.ts.map +1 -0
- package/dist/firstpass/view.js +60 -0
- package/dist/firstpass/view.js.map +1 -0
- package/dist/firstpass/words.d.ts +105 -0
- package/dist/firstpass/words.d.ts.map +1 -0
- package/dist/firstpass/words.js +28 -0
- package/dist/firstpass/words.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay/grammar.d.ts +259 -0
- package/dist/overlay/grammar.d.ts.map +1 -0
- package/dist/overlay/grammar.js +223 -0
- package/dist/overlay/grammar.js.map +1 -0
- package/dist/overlay/render.d.ts +125 -0
- package/dist/overlay/render.d.ts.map +1 -0
- package/dist/overlay/render.js +439 -0
- package/dist/overlay/render.js.map +1 -0
- package/dist/overlay/styles.d.ts +41 -0
- package/dist/overlay/styles.d.ts.map +1 -0
- package/dist/overlay/styles.js +124 -0
- package/dist/overlay/styles.js.map +1 -0
- package/dist/picker/render.d.ts +80 -0
- package/dist/picker/render.d.ts.map +1 -0
- package/dist/picker/render.js +112 -0
- package/dist/picker/render.js.map +1 -0
- package/dist/picker/styles.d.ts +26 -0
- package/dist/picker/styles.d.ts.map +1 -0
- package/dist/picker/styles.js +116 -0
- package/dist/picker/styles.js.map +1 -0
- package/dist/picker/view.d.ts +135 -0
- package/dist/picker/view.d.ts.map +1 -0
- package/dist/picker/view.js +117 -0
- package/dist/picker/view.js.map +1 -0
- package/dist/picker/words.d.ts +53 -0
- package/dist/picker/words.d.ts.map +1 -0
- package/dist/picker/words.js +28 -0
- package/dist/picker/words.js.map +1 -0
- package/dist/reevaluate/render.d.ts +77 -0
- package/dist/reevaluate/render.d.ts.map +1 -0
- package/dist/reevaluate/render.js +211 -0
- package/dist/reevaluate/render.js.map +1 -0
- package/dist/reevaluate/styles.d.ts +24 -0
- package/dist/reevaluate/styles.d.ts.map +1 -0
- package/dist/reevaluate/styles.js +182 -0
- package/dist/reevaluate/styles.js.map +1 -0
- package/dist/reevaluate/view.d.ts +102 -0
- package/dist/reevaluate/view.d.ts.map +1 -0
- package/dist/reevaluate/view.js +111 -0
- package/dist/reevaluate/view.js.map +1 -0
- package/dist/reevaluate/words.d.ts +54 -0
- package/dist/reevaluate/words.d.ts.map +1 -0
- package/dist/reevaluate/words.js +29 -0
- package/dist/reevaluate/words.js.map +1 -0
- package/dist/scale/commands.d.ts +60 -0
- package/dist/scale/commands.d.ts.map +1 -0
- package/dist/scale/commands.js +49 -0
- package/dist/scale/commands.js.map +1 -0
- package/dist/scale/ladder.d.ts +141 -0
- package/dist/scale/ladder.d.ts.map +1 -0
- package/dist/scale/ladder.js +243 -0
- package/dist/scale/ladder.js.map +1 -0
- package/dist/scale/render.d.ts +85 -0
- package/dist/scale/render.d.ts.map +1 -0
- package/dist/scale/render.js +194 -0
- package/dist/scale/render.js.map +1 -0
- package/dist/scale/styles.d.ts +21 -0
- package/dist/scale/styles.d.ts.map +1 -0
- package/dist/scale/styles.js +105 -0
- package/dist/scale/styles.js.map +1 -0
- package/dist/workspace/inspector.d.ts +117 -0
- package/dist/workspace/inspector.d.ts.map +1 -0
- package/dist/workspace/inspector.js +140 -0
- package/dist/workspace/inspector.js.map +1 -0
- package/dist/workspace/rail.d.ts +122 -0
- package/dist/workspace/rail.d.ts.map +1 -0
- package/dist/workspace/rail.js +216 -0
- package/dist/workspace/rail.js.map +1 -0
- package/dist/workspace/render.d.ts +139 -0
- package/dist/workspace/render.d.ts.map +1 -0
- package/dist/workspace/render.js +366 -0
- package/dist/workspace/render.js.map +1 -0
- package/dist/workspace/selection.d.ts +92 -0
- package/dist/workspace/selection.d.ts.map +1 -0
- package/dist/workspace/selection.js +83 -0
- package/dist/workspace/selection.js.map +1 -0
- package/dist/workspace/styles.d.ts +39 -0
- package/dist/workspace/styles.d.ts.map +1 -0
- package/dist/workspace/styles.js +313 -0
- package/dist/workspace/styles.js.map +1 -0
- 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":"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"}
|
|
@@ -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"}
|