taskai-cli 1.6.6__tar.gz → 1.7.0__tar.gz
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.
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/DEVLOG.md +91 -2
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/DEVPLAN.md +100 -23
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/PKG-INFO +1 -1
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/pyproject.toml +1 -1
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/static/canvas.js +34 -21
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/static/console.js +41 -15
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/static/index.html +11 -0
- taskai_cli-1.7.0/taskai/static/shortcuts.js +266 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/static/style.css +119 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/.gitignore +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/.readthedocs.yaml +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/README.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/TRIAGE.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/.gitignore +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/_static/.gitkeep +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/ai-integration.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/browser-mode.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/commands.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/conf.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/configuration.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/getting-started.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/index.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/interactive-mode.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/item-data.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/managing-the-tree.md +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/docs/requirements.txt +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/scripts/bump_version.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/browser.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/cli.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/config.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/errors.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/help_menu.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/json_dir_database.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/llm_models.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/models.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/services/ai.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/services/pomodoro.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/services/user_setup.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/static/editpanel.js +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/taskai/views.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/test/test_cli.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/test/test_execution.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/test/test_json_dir_database.py +0 -0
- {taskai_cli-1.6.6 → taskai_cli-1.7.0}/test/test_view.py +0 -0
|
@@ -33,8 +33,97 @@ Fixed the long-standing `update_item` quirk: it now calls
|
|
|
33
33
|
`update <id> --due_by MM-DD-YYYY` no longer throws. Extended
|
|
34
34
|
`_parse_item_kwargs` in the same pass — maps `depends_on` (`"1,2,3"`) to
|
|
35
35
|
`dependency_ids`, and guarded the `due_by` branch with `isinstance(v, str)`
|
|
36
|
-
so it's idempotent for the recursive re-parse.
|
|
37
|
-
|
|
36
|
+
so it's idempotent for the recursive re-parse. `update <id> --due_by` /
|
|
37
|
+
`--depends_on` round-trip now.
|
|
38
|
+
|
|
39
|
+
Then a long edit-panel pass (DEVPLAN Phase 4). Two bugs first:
|
|
40
|
+
|
|
41
|
+
- Completed checkbox wouldn't uncheck. `_parse_item_kwargs`'s `completed`
|
|
42
|
+
case was `bool(v)`, and `bool("false")` is `True` — a regression from
|
|
43
|
+
routing `update` through `_parse_item_kwargs` above (pydantic used to
|
|
44
|
+
coerce the `"false"` string fine downstream). Now parses the string; real
|
|
45
|
+
bools (`done`/`undone`, the recursive re-parse) pass straight through.
|
|
46
|
+
- Every debounced field POST was unfocusing the field mid-type.
|
|
47
|
+
`renderEditForm` did `innerHTML = ""` and rebuilt the whole form on every
|
|
48
|
+
`applyTree` — including the one after its own debounced update. Now tracks
|
|
49
|
+
`renderedItemId`; a same-item refresh does an in-place `refreshFieldValues`
|
|
50
|
+
that skips whatever field is `document.activeElement`. Value logic pulled
|
|
51
|
+
into a shared `fieldDisplayValue`.
|
|
52
|
+
|
|
53
|
+
Description editor: moved `description` to the bottom of `FIELD_DEFS` and
|
|
54
|
+
made its `textarea` flex to fill the space the fixed fields + comment feed
|
|
55
|
+
leave (`min-height: 96px` floor). Density pass on the form too — gaps and
|
|
56
|
+
paddings pulled in (16→10 / 6→3 / 8·10→5·8), radius 8→6.
|
|
57
|
+
|
|
58
|
+
Comments (Phase 4, "Show comments"):
|
|
59
|
+
|
|
60
|
+
- `browser.py`: `_full_tree()` attaches a resolved `comments` array to each
|
|
61
|
+
item dump (`_resolve_comments`, skips a dangling id). Not a `TodoItem`
|
|
62
|
+
field — rides the same `/api/tree` read.
|
|
63
|
+
- `editpanel.js`: `buildCommentsSection` — flat feed (no cards), each row is
|
|
64
|
+
date on the left + comment right-aligned and wrapping, oldest→newest,
|
|
65
|
+
below the description box. Add box is a 2-row `<textarea>` (fixed 52px,
|
|
66
|
+
`resize: none`): Enter sends `comment <id> "<text>"` through
|
|
67
|
+
`/api/command`, Shift+Enter is a newline. List is a fixed `height` (200px,
|
|
68
|
+
a `:root` var) with a scrollbar on overflow; opens pinned to the newest
|
|
69
|
+
and sticks there on add, but keeps your place if you've scrolled up
|
|
70
|
+
(`stickToBottom` check + `requestAnimationFrame` so it also works on the
|
|
71
|
+
first render).
|
|
72
|
+
|
|
73
|
+
Panel layout:
|
|
74
|
+
|
|
75
|
+
- Labels moved inline — `.edit-field` is a row now, fixed 76px label column
|
|
76
|
+
on the left, control fills the rest; each fixed field is one line.
|
|
77
|
+
`description` / `.edit-comments` opt back out to stacked.
|
|
78
|
+
- Panel width 320 → 448 (`EDIT_PANEL_EXPANDED_WIDTH` + `.edit-panel.expanded`).
|
|
79
|
+
- Panel/console clash: both are `position: fixed` and overlapped bottom-right.
|
|
80
|
+
Console heights are `:root` vars now; `.edit-panel.expanded` height is
|
|
81
|
+
`calc(100vh - <console height>)` toggled by
|
|
82
|
+
`body:has(.console-panel.expanded)`, so it stops exactly at the console's
|
|
83
|
+
top edge and animates with it. The height lost when the console opens is
|
|
84
|
+
split evenly — half off description, half off the comment list
|
|
85
|
+
(`body:has(.console-panel.expanded) .comment-list` shrinks ~200→82px).
|
|
86
|
+
|
|
87
|
+
Not done: comment count on the canvas node (still "consider" in the plan).
|
|
88
|
+
|
|
89
|
+
Keyboard shortcuts + left-hand shortcuts panel (Phase 4, last bullet). New
|
|
90
|
+
`shortcuts.js` — one `SHORTCUTS` registry feeds both a global `keydown`
|
|
91
|
+
handler and the rows of `#shortcut-panel`, a left-side mirror of the edit
|
|
92
|
+
panel (44px chevron / 300px white panel, same `:root` var + `:has()`
|
|
93
|
+
console-clash handling, same inline-label rows).
|
|
94
|
+
|
|
95
|
+
Alex flagged two risks up front; the design answers each with one mechanism:
|
|
96
|
+
|
|
97
|
+
- Don't wreck typing. The handler returns early with no `preventDefault`
|
|
98
|
+
whenever `document.activeElement` is an input/textarea/select/CE — so
|
|
99
|
+
arrows and letters behave normally while editing. The only `whileTyping`
|
|
100
|
+
entries are backtick (and only when the console input itself has focus, so
|
|
101
|
+
it can close it) and `Esc`.
|
|
102
|
+
- Don't shadow OS/browser keys. Only modifier-free keys and `Shift+<key>`
|
|
103
|
+
are bound — nothing with Ctrl/Cmd/Alt or F-keys. So browser zoom stays on
|
|
104
|
+
`Cmd/Ctrl` `+/-/0` and we use bare `+`/`-`/`0`; tab/window/menu combos are
|
|
105
|
+
all untouched.
|
|
106
|
+
|
|
107
|
+
Bindings: `` ` `` terminal (focuses cmdline; also closes from inside), `?`
|
|
108
|
+
shortcuts panel, `e` edit panel, `Esc` (leave field / deselect / show-all,
|
|
109
|
+
context-dependent), arrows navigate, `Shift`+arrows pan, `+`/`-` zoom, `0`
|
|
110
|
+
fit, `a` add child + open edit panel with the name field focused+selected,
|
|
111
|
+
`d` toggle done/undone, `Delete` delete (with `confirm()`). Client-only
|
|
112
|
+
shortcuts leave no console trace; `a`/`d`/`Delete` go through `/api/command`
|
|
113
|
+
and print only their `output` line, no `> echo`. A shortcut never moves
|
|
114
|
+
focus except terminal-open and add-node.
|
|
115
|
+
|
|
116
|
+
Supporting refactors:
|
|
117
|
+
|
|
118
|
+
- `canvas.js`: right-panel width plumbing generalised to a `side` param —
|
|
119
|
+
`setPanelWidth(side, …)` / `setPanelWidthInstant(side, …)`,
|
|
120
|
+
`panelWidthAnimId {left,right}`, and `applyCanvasSize` subtracts both
|
|
121
|
+
reservations + `margin-left`s the canvas past the left panel.
|
|
122
|
+
`setRightPanelWidth[Instant]` kept as wrappers so `editpanel.js` didn't
|
|
123
|
+
change.
|
|
124
|
+
- `console.js`: pulled out `toggleConsole(force)` (focus on open, blur on
|
|
125
|
+
close so the shortcut layer revives), `showAll()`, and `postCommand()`,
|
|
126
|
+
all reused by `shortcuts.js`.
|
|
38
127
|
|
|
39
128
|
---
|
|
40
129
|
|
|
@@ -110,8 +110,14 @@ unless you're told to fix them):**
|
|
|
110
110
|
not just `update` — and its `due_by` branch is guarded with
|
|
111
111
|
`isinstance(v, str)` so the helper is idempotent (safe for the
|
|
112
112
|
already-parsed kwargs `update_item`'s `recursive=True` branch passes back
|
|
113
|
-
down).
|
|
114
|
-
end to end
|
|
113
|
+
down). The edit panel's **Due by** field and `--depends_on` from the
|
|
114
|
+
console/CLI now round-trip end to end (there's no "Depends on" field in
|
|
115
|
+
the panel any more). Follow-up the same session: the `completed` branch was
|
|
116
|
+
`bool(v)`, and `bool("false")` is `True` — so once `update` started going
|
|
117
|
+
through `_parse_item_kwargs`, unchecking the edit panel's Completed box
|
|
118
|
+
(which POSTs `--completed "false"`) always re-checked it. Now parses the
|
|
119
|
+
string (`"true"/"1"/"yes"`), with real bools passing straight through for
|
|
120
|
+
the `done`/`undone` dispatch and the recursive re-parse.
|
|
115
121
|
- **Sync is one-directional: CLI → browser only, not the reverse.**
|
|
116
122
|
`GET /api/tree` (`browser.py`) reloads from disk (`db.flush()`) on every
|
|
117
123
|
call, and `canvas.js` refetches on window focus, so browser-side views
|
|
@@ -330,6 +336,10 @@ add new visual knobs there, not inline in `draw()`.
|
|
|
330
336
|
trips `<input type="date">`'s `YYYY-MM-DD` to/from the CLI's
|
|
331
337
|
`MM-DD-YYYY`; `dependency_ids` round-trips to/from the CLI's
|
|
332
338
|
comma-separated `depends_on` id list.
|
|
339
|
+
(Later: the `dependency_ids` / "Depends on" field was dropped from
|
|
340
|
+
`FIELD_DEFS` — there's no CLI `depend` command creating that data
|
|
341
|
+
anyway, see 1.4 / TRIAGE — and the field order changed so
|
|
342
|
+
`description` sits last, above the Phase 4 comment feed.)
|
|
333
343
|
- [x] **No Save button** — deliberate deviation from the original plan.
|
|
334
344
|
Every field change posts straight to `/api/command` as
|
|
335
345
|
`update <id> --<field> <value>`. Checkbox sends immediately on toggle;
|
|
@@ -341,9 +351,10 @@ add new visual knobs there, not inline in `draw()`.
|
|
|
341
351
|
|
|
342
352
|
While wiring this up, confirmed against a throwaway test item (created and
|
|
343
353
|
deleted, not real data): `name`/`description`/`priority`/`completed` all
|
|
344
|
-
update correctly end to end. `due_by` and
|
|
345
|
-
`update_item`/`_parse_item_kwargs` bug documented
|
|
346
|
-
that entry), so
|
|
354
|
+
update correctly end to end. `due_by` (and `--depends_on` from the console
|
|
355
|
+
/ CLI) originally hit the `update_item`/`_parse_item_kwargs` bug documented
|
|
356
|
+
above — **now fixed** (see that entry), so every field the panel exposes
|
|
357
|
+
round-trips.
|
|
347
358
|
|
|
348
359
|
### 1.6 — Console
|
|
349
360
|
|
|
@@ -620,24 +631,90 @@ Builds on Phase 1's canvas / edit panel / console. Still plain HTML/CSS/JS, no
|
|
|
620
631
|
framework, everything routed through the existing `/api/tree` + `/api/command`
|
|
621
632
|
endpoints.
|
|
622
633
|
|
|
623
|
-
- [
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
634
|
+
- [x] **Show comments.** Backend: `_full_tree()` (`browser.py`) attaches a
|
|
635
|
+
resolved `comments` array to each item dump via `_resolve_comments`
|
|
636
|
+
(skips a dangling comment id rather than 500-ing the tree) — not a
|
|
637
|
+
`TodoItem` field, it just rides along on the same `/api/tree` read.
|
|
638
|
+
Frontend (`editpanel.js`): `buildCommentsSection` renders a flat,
|
|
639
|
+
read-only feed below the description box — no cards, each row is the
|
|
640
|
+
date (left, muted) and the comment text (right-aligned, wraps under
|
|
641
|
+
itself), oldest→newest. The add box is a 2-row `<textarea>` (fixed
|
|
642
|
+
52px, `resize: none`); **Enter** sends `comment <id> "<text>"` through
|
|
643
|
+
`/api/command` like every other field, **Shift+Enter** is a newline.
|
|
644
|
+
The list is a fixed `height` (200px, a `:root` var) with a scrollbar
|
|
645
|
+
on overflow; it opens pinned to the newest comment and stays there on
|
|
646
|
+
add, but preserves scroll position if you've scrolled up into the
|
|
647
|
+
history (`stickToBottom` check + `requestAnimationFrame` for the first
|
|
648
|
+
render). Comment count on the canvas node itself is **not** done —
|
|
649
|
+
still just "consider".
|
|
650
|
+
- [x] **Much larger description editor.** `description` moved to the *bottom*
|
|
651
|
+
of the field list (`FIELD_DEFS` in `editpanel.js`), flagged
|
|
652
|
+
`grow: true` → an `.edit-field-grow` class whose `textarea` flexes to
|
|
653
|
+
fill the vertical space the fixed fields + comment feed leave
|
|
654
|
+
(`min-height: 96px` floor, `resize: vertical` kept). It was always a
|
|
655
|
+
`<textarea>` — just no longer a 60px slit. Also this pass, on the same
|
|
656
|
+
panel:
|
|
657
|
+
- **Inline labels.** `.edit-field` went from stacked to a row layout —
|
|
658
|
+
a fixed 76px label column on the left, control fills the rest — so
|
|
659
|
+
each fixed field is one line instead of two. Description and the
|
|
660
|
+
comment feed opt back out to stacked (`.edit-field-grow`,
|
|
661
|
+
`.edit-comments`).
|
|
662
|
+
- **Wider panel.** `EDIT_PANEL_EXPANDED_WIDTH` 320 → 448 (JS constant
|
|
663
|
+
+ `.edit-panel.expanded` in `style.css`).
|
|
664
|
+
- **No clash with the console.** Both panels are `position: fixed` and
|
|
665
|
+
were overlapping in the bottom-right. Console heights are now `:root`
|
|
666
|
+
vars (`--console-h-collapsed/expanded`); `.edit-panel.expanded`
|
|
667
|
+
height is `calc(100vh - <console height>)`, switched via
|
|
668
|
+
`body:has(.console-panel.expanded) …`, so the edit panel stops
|
|
669
|
+
exactly at the console's top edge in both states and animates with
|
|
670
|
+
it. The height the edit panel loses when the console opens is split
|
|
671
|
+
evenly — half off the description box, half off the comment list
|
|
672
|
+
(`body:has(.console-panel.expanded) .comment-list` shrinks by half
|
|
673
|
+
the delta, ~200→82px).
|
|
674
|
+
- Panel-widen-*further*-on-select was considered and skipped; revisit
|
|
675
|
+
if 448px still feels tight for note-taking.
|
|
676
|
+
Two edit-panel bugs fixed in passing: the Completed checkbox not
|
|
677
|
+
un-checking (see the `_parse_item_kwargs` `completed` note under
|
|
678
|
+
"Known code quirks"), and every debounced field POST tearing down and
|
|
679
|
+
unfocusing the field being typed in — `renderEditForm` now tracks
|
|
680
|
+
`renderedItemId` and does an in-place `refreshFieldValues` for a
|
|
681
|
+
same-item refresh, skipping whatever field is `document.activeElement`
|
|
682
|
+
(value logic shared through a new `fieldDisplayValue`).
|
|
683
|
+
- [x] **Keyboard shortcuts + a shortcuts panel.** New `shortcuts.js`: one
|
|
684
|
+
`SHORTCUTS` registry drives both a global `keydown` handler and the
|
|
685
|
+
rows of a left-hand reference panel (`#shortcut-panel` in
|
|
686
|
+
`index.html`, `.shortcut-panel` in `style.css`) — a mirror of the
|
|
687
|
+
edit panel: 44px chevron collapsed, 300px white panel expanded, same
|
|
688
|
+
`:root` var / `body:has(.console-panel.expanded)` height-clash
|
|
689
|
+
handling, same inline-label row shape.
|
|
690
|
+
**Two rules keep it from fighting the browser or the text fields**
|
|
691
|
+
(see the DEVLOG discussion): (1) the handler bails with no
|
|
692
|
+
`preventDefault` whenever a field is focused — so arrows/letters type
|
|
693
|
+
normally while editing — except entries flagged `whileTyping` (just
|
|
694
|
+
backtick, and only when the console input itself is focused, so it can
|
|
695
|
+
close it; plus `Esc`); (2) only modifier-free keys and `Shift+<key>`
|
|
696
|
+
are ever bound — nothing with Ctrl/Cmd/Alt or a function key — so
|
|
697
|
+
browser zoom (`Cmd/Ctrl` `+/-/0`), tab/window combos, the menu bar,
|
|
698
|
+
etc. are never shadowed. Bindings: `` ` `` terminal (focuses the
|
|
699
|
+
command line; also closes from inside), `?` this panel, `e` edit
|
|
700
|
+
panel, `Esc` (leave field / deselect / show-all, context-dependent),
|
|
701
|
+
arrows navigate the tree, `Shift`+arrows pan, `+`/`-` zoom, `0`
|
|
702
|
+
fit/show-all, `a` add a child + open the edit panel with the name
|
|
703
|
+
field focused, `d` toggle done/undone, `Delete` delete (with a
|
|
704
|
+
`confirm()`). Client-only shortcuts (zoom/pan/nav/show-all) leave no
|
|
705
|
+
console trace; the mutating ones (`a`/`d`/`Delete`) run through the
|
|
706
|
+
same `/api/command` path and surface only their `output` line — no
|
|
707
|
+
`> echo`. A shortcut never moves focus except the two that should
|
|
708
|
+
(terminal open, add-node).
|
|
709
|
+
**`canvas.js` change:** the right-panel width plumbing was
|
|
710
|
+
generalised to a `side` param — `setPanelWidth(side, …)` /
|
|
711
|
+
`setPanelWidthInstant(side, …)`, with `panelWidthAnimId {left,right}`
|
|
712
|
+
and `applyCanvasSize` now subtracting both reservations and shoving
|
|
713
|
+
the canvas over with `margin-left`. `setRightPanelWidth[Instant]`
|
|
714
|
+
kept as thin wrappers so `editpanel.js` was untouched.
|
|
715
|
+
**`console.js` change:** extracted `toggleConsole(force)` (focus on
|
|
716
|
+
open / blur on close), `showAll()`, and a `postCommand(input)` helper,
|
|
717
|
+
all reused by `shortcuts.js`.
|
|
641
718
|
|
|
642
719
|
---
|
|
643
720
|
|
|
@@ -221,16 +221,19 @@ function worldToScreen(wx, wy) {
|
|
|
221
221
|
return { x: wx * view.scale + view.offsetX, y: wy * view.scale + view.offsetY };
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
// width (px) reserved on
|
|
224
|
+
// width (px) reserved on each side — right for the edit panel, left for the
|
|
225
|
+
// shortcuts panel; the canvas fills what's between them
|
|
225
226
|
let rightPanelWidth = 0;
|
|
226
|
-
let
|
|
227
|
+
let leftPanelWidth = 0;
|
|
228
|
+
const panelWidthAnimId = { left: null, right: null };
|
|
227
229
|
|
|
228
|
-
// keep in sync with the CSS width transition
|
|
230
|
+
// keep in sync with the CSS width transition on .edit-panel / .shortcut-panel
|
|
229
231
|
const PANEL_TRANSITION_MS = 200;
|
|
230
232
|
|
|
231
233
|
function applyCanvasSize() {
|
|
232
|
-
canvas.width = window.innerWidth - rightPanelWidth;
|
|
234
|
+
canvas.width = window.innerWidth - rightPanelWidth - leftPanelWidth;
|
|
233
235
|
canvas.height = window.innerHeight;
|
|
236
|
+
canvas.style.marginLeft = leftPanelWidth + "px"; // shove the canvas past the left panel
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
function resizeCanvas() {
|
|
@@ -253,33 +256,39 @@ function _rescaleForWidthChange(oldWidth, startScale, centerWorld) {
|
|
|
253
256
|
view.offsetY = canvas.height / 2 - centerWorld.y * view.scale;
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
function _panelWidth(side) { return side === "left" ? leftPanelWidth : rightPanelWidth; }
|
|
260
|
+
function _setPanelWidthVar(side, w) {
|
|
261
|
+
if (side === "left") leftPanelWidth = w;
|
|
262
|
+
else rightPanelWidth = w;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// jumps a side's reservation straight to `width` with no animation — for
|
|
266
|
+
// initial setup, where there's nothing on screen yet to transition from
|
|
267
|
+
function setPanelWidthInstant(side, width) {
|
|
268
|
+
if (panelWidthAnimId[side] !== null) {
|
|
269
|
+
cancelAnimationFrame(panelWidthAnimId[side]);
|
|
270
|
+
panelWidthAnimId[side] = null;
|
|
262
271
|
}
|
|
263
272
|
|
|
264
273
|
const oldWidth = canvas.width;
|
|
265
274
|
const startScale = view.scale;
|
|
266
275
|
const centerWorld = screenToWorld(canvas.width / 2, canvas.height / 2);
|
|
267
276
|
|
|
268
|
-
|
|
277
|
+
_setPanelWidthVar(side, width);
|
|
269
278
|
applyCanvasSize();
|
|
270
279
|
_rescaleForWidthChange(oldWidth, startScale, centerWorld);
|
|
271
280
|
|
|
272
281
|
draw();
|
|
273
282
|
}
|
|
274
283
|
|
|
275
|
-
// eases
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
function
|
|
279
|
-
if (
|
|
280
|
-
if (
|
|
284
|
+
// eases a side's reservation (and the canvas size/zoom that follow it) to
|
|
285
|
+
// `targetWidth` over `duration`ms, matching the panel's own CSS transition
|
|
286
|
+
// so the graph resizes in step with it rather than snapping.
|
|
287
|
+
function setPanelWidth(side, targetWidth, duration = PANEL_TRANSITION_MS) {
|
|
288
|
+
if (panelWidthAnimId[side] !== null) cancelAnimationFrame(panelWidthAnimId[side]);
|
|
289
|
+
if (_panelWidth(side) === targetWidth) return;
|
|
281
290
|
|
|
282
|
-
const startWidth =
|
|
291
|
+
const startWidth = _panelWidth(side);
|
|
283
292
|
const startScale = view.scale;
|
|
284
293
|
const oldWidth = canvas.width;
|
|
285
294
|
const centerWorld = screenToWorld(canvas.width / 2, canvas.height / 2);
|
|
@@ -289,18 +298,22 @@ function setRightPanelWidth(targetWidth, duration = PANEL_TRANSITION_MS) {
|
|
|
289
298
|
const t = Math.min(1, (now - startTime) / duration);
|
|
290
299
|
const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic
|
|
291
300
|
|
|
292
|
-
|
|
301
|
+
_setPanelWidthVar(side, startWidth + (targetWidth - startWidth) * eased);
|
|
293
302
|
applyCanvasSize();
|
|
294
303
|
_rescaleForWidthChange(oldWidth, startScale, centerWorld);
|
|
295
304
|
|
|
296
305
|
draw();
|
|
297
306
|
|
|
298
|
-
|
|
307
|
+
panelWidthAnimId[side] = t < 1 ? requestAnimationFrame(step) : null;
|
|
299
308
|
}
|
|
300
309
|
|
|
301
|
-
|
|
310
|
+
panelWidthAnimId[side] = requestAnimationFrame(step);
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
// back-compat wrappers — editpanel.js drives the right side through these
|
|
314
|
+
function setRightPanelWidthInstant(width) { setPanelWidthInstant("right", width); }
|
|
315
|
+
function setRightPanelWidth(width, duration) { setPanelWidth("right", width, duration); }
|
|
316
|
+
|
|
304
317
|
// true if the world point (x, y) falls inside node's square
|
|
305
318
|
function hitTest(node, x, y) {
|
|
306
319
|
const half = node.size / 2;
|
|
@@ -7,10 +7,19 @@ const consoleToggle = document.getElementById("console-toggle");
|
|
|
7
7
|
const consoleInput = document.getElementById("console-input");
|
|
8
8
|
const consoleScrollback = document.getElementById("console-scrollback");
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// `force` omitted = toggle; true/false = force that state. Opening focuses
|
|
11
|
+
// the command line so you can type straight away; closing blurs it so the
|
|
12
|
+
// keyboard-shortcut layer (which ignores keys while a field is focused)
|
|
13
|
+
// comes back to life.
|
|
14
|
+
function toggleConsole(force) {
|
|
15
|
+
const expanded = typeof force === "boolean" ? force : !consolePanel.classList.contains("expanded");
|
|
16
|
+
consolePanel.classList.toggle("expanded", expanded);
|
|
12
17
|
consoleToggle.setAttribute("aria-expanded", String(expanded));
|
|
13
|
-
|
|
18
|
+
if (expanded) consoleInput.focus();
|
|
19
|
+
else consoleInput.blur();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
consoleToggle.addEventListener("click", () => toggleConsole());
|
|
14
23
|
|
|
15
24
|
function appendLine(text) {
|
|
16
25
|
const line = document.createElement("div");
|
|
@@ -50,6 +59,34 @@ function resolveDotRefs(command) {
|
|
|
50
59
|
return command.replace(/(^|\s)\.(?=\s|$)/g, `$1${selectedNode.id}`);
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
// POST a raw command string to the shared endpoint; returns the parsed
|
|
63
|
+
// { output, tree, focus } (or throws). Callers decide what to echo.
|
|
64
|
+
async function postCommand(input) {
|
|
65
|
+
const res = await fetch("/api/command", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ input }),
|
|
69
|
+
});
|
|
70
|
+
return res.json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// select the synthetic root and ease the view out to fit the whole forest —
|
|
74
|
+
// the `show` / `show all` console command and the keyboard shortcut share this
|
|
75
|
+
async function showAll() {
|
|
76
|
+
let tree;
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch("/api/tree");
|
|
79
|
+
tree = await res.json();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
appendLine("Error: " + err.message);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
applyTree(tree);
|
|
85
|
+
selectedNode = rootNode;
|
|
86
|
+
if (typeof onNodeSelected === "function") onNodeSelected(null);
|
|
87
|
+
fitAll();
|
|
88
|
+
}
|
|
89
|
+
|
|
53
90
|
const commandHistory = [];
|
|
54
91
|
let historyIndex = commandHistory.length; // length = not currently browsing history
|
|
55
92
|
|
|
@@ -90,18 +127,7 @@ consoleInput.addEventListener("keydown", async (e) => {
|
|
|
90
127
|
|
|
91
128
|
// `show all` / bare `show` — select the synthetic root and fit the forest
|
|
92
129
|
if (/^show(\s+all)?$/i.test(command)) {
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const res = await fetch("/api/tree");
|
|
96
|
-
tree = await res.json();
|
|
97
|
-
} catch (err) {
|
|
98
|
-
appendLine("Error: " + err.message);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
applyTree(tree);
|
|
102
|
-
selectedNode = rootNode;
|
|
103
|
-
if (typeof onNodeSelected === "function") onNodeSelected(null);
|
|
104
|
-
fitAll();
|
|
130
|
+
await showAll();
|
|
105
131
|
return;
|
|
106
132
|
}
|
|
107
133
|
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
<body>
|
|
9
9
|
<canvas id="myCanvas"></canvas>
|
|
10
10
|
|
|
11
|
+
<div id="shortcut-panel" class="shortcut-panel">
|
|
12
|
+
<button id="shortcut-toggle" class="shortcut-toggle" aria-label="Toggle shortcuts panel" aria-expanded="false">
|
|
13
|
+
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
14
|
+
<path d="M6 1L13 8L6 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
15
|
+
<path d="M0 1L7 8L0 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
|
|
16
|
+
</svg>
|
|
17
|
+
</button>
|
|
18
|
+
<div class="shortcut-body"></div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
11
21
|
<div id="edit-panel" class="edit-panel">
|
|
12
22
|
<button id="edit-toggle" class="edit-toggle" aria-label="Toggle edit panel" aria-expanded="false">
|
|
13
23
|
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -34,5 +44,6 @@
|
|
|
34
44
|
<script src="/static/canvas.js"></script>
|
|
35
45
|
<script src="/static/editpanel.js"></script>
|
|
36
46
|
<script src="/static/console.js"></script>
|
|
47
|
+
<script src="/static/shortcuts.js"></script>
|
|
37
48
|
</body>
|
|
38
49
|
</html>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Global keyboard shortcuts + the left-hand reference panel that lists them.
|
|
2
|
+
// One registry (SHORTCUTS) drives both the key handler and the panel rows.
|
|
3
|
+
//
|
|
4
|
+
// Design rules (see the discussion in DEVLOG):
|
|
5
|
+
// 1. Shortcuts are inert while a text field is focused — the handler bails
|
|
6
|
+
// out with no preventDefault, so arrows/letters behave normally when
|
|
7
|
+
// typing. The only exceptions are marked `whileTyping`.
|
|
8
|
+
// 2. Only modifier-free keys and Shift+<key> are bound. Nothing with
|
|
9
|
+
// Ctrl/Cmd/Alt or a function key, so no OS/browser shortcut is shadowed
|
|
10
|
+
// (browser zoom stays on Cmd/Ctrl +/-/0; we use bare +/-/0).
|
|
11
|
+
// 3. A shortcut never moves focus, except the two that are supposed to:
|
|
12
|
+
// opening the terminal, and add-node (which focuses the name field).
|
|
13
|
+
|
|
14
|
+
const shortcutPanel = document.getElementById("shortcut-panel");
|
|
15
|
+
const shortcutToggle = document.getElementById("shortcut-toggle");
|
|
16
|
+
const shortcutBody = document.querySelector("#shortcut-panel .shortcut-body");
|
|
17
|
+
|
|
18
|
+
const SHORTCUT_PANEL_COLLAPSED_WIDTH = 44;
|
|
19
|
+
const SHORTCUT_PANEL_EXPANDED_WIDTH = 300;
|
|
20
|
+
|
|
21
|
+
// reserve the collapsed strip's width in the canvas from the start (instant)
|
|
22
|
+
setPanelWidthInstant("left", SHORTCUT_PANEL_COLLAPSED_WIDTH);
|
|
23
|
+
|
|
24
|
+
function toggleShortcutPanel(force) {
|
|
25
|
+
const expanded = typeof force === "boolean" ? force : !shortcutPanel.classList.contains("expanded");
|
|
26
|
+
shortcutPanel.classList.toggle("expanded", expanded);
|
|
27
|
+
shortcutToggle.setAttribute("aria-expanded", String(expanded));
|
|
28
|
+
setPanelWidth("left", expanded ? SHORTCUT_PANEL_EXPANDED_WIDTH : SHORTCUT_PANEL_COLLAPSED_WIDTH);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
shortcutToggle.addEventListener("click", () => toggleShortcutPanel());
|
|
32
|
+
|
|
33
|
+
function toggleEditPanel() {
|
|
34
|
+
if (editPanel.classList.contains("expanded")) closeEditPanel();
|
|
35
|
+
else openEditPanel();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- action helpers --------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
// the selected node, or null when the selection is the synthetic root
|
|
41
|
+
// (i.e. "nothing / the whole tree")
|
|
42
|
+
function selectedRealNode() {
|
|
43
|
+
return selectedNode && selectedNode !== rootNode ? selectedNode : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// run a DB-mutating command through the shared endpoint. No `> echo` (that's
|
|
47
|
+
// for things the user typed), but we do surface `output` since the command
|
|
48
|
+
// changed the database — matching "not visible unless it modifies the DB".
|
|
49
|
+
async function runMutation(input) {
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
data = await postCommand(input);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
appendLine("Error: " + err.message);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (data.output) appendLine(data.output.trim());
|
|
58
|
+
applyTree(data.tree);
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toggleDoneSelected() {
|
|
63
|
+
const node = selectedRealNode();
|
|
64
|
+
if (!node) return;
|
|
65
|
+
const item = itemForNode(node);
|
|
66
|
+
runMutation(`${item && item.completed ? "undone" : "done"} ${node.id}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function deleteSelected() {
|
|
70
|
+
const node = selectedRealNode();
|
|
71
|
+
if (!node) return;
|
|
72
|
+
const item = itemForNode(node);
|
|
73
|
+
const label = item ? item.name : node.id;
|
|
74
|
+
if (!window.confirm(`Delete "${label}" and everything under it?`)) return;
|
|
75
|
+
runMutation(`delete ${node.id}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// add a child of the selected node (or a new root when nothing is selected),
|
|
79
|
+
// then select it, open the edit panel, and put the cursor in the name field
|
|
80
|
+
async function addNodeAndEdit() {
|
|
81
|
+
const parent = selectedRealNode();
|
|
82
|
+
const input = parent ? `add ${parent.id} "New task"` : `create "New task"`;
|
|
83
|
+
|
|
84
|
+
const before = new Set(Object.keys(latestItemsById));
|
|
85
|
+
const data = await runMutation(input);
|
|
86
|
+
if (!data) return;
|
|
87
|
+
|
|
88
|
+
const newId = Object.keys(latestItemsById).find(id => !before.has(id));
|
|
89
|
+
const node = newId && nodes.find(n => n.id === String(newId));
|
|
90
|
+
if (!node) return;
|
|
91
|
+
|
|
92
|
+
selectedNode = node;
|
|
93
|
+
if (typeof onNodeSelected === "function") onNodeSelected(itemForNode(node));
|
|
94
|
+
focusOnNode(node);
|
|
95
|
+
if (typeof openEditPanel === "function") openEditPanel();
|
|
96
|
+
|
|
97
|
+
requestAnimationFrame(() => {
|
|
98
|
+
const nameInput = document.querySelector('#edit-panel [data-field-key="name"]');
|
|
99
|
+
if (nameInput) {
|
|
100
|
+
nameInput.focus();
|
|
101
|
+
nameInput.select();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleEscape() {
|
|
107
|
+
const el = document.activeElement;
|
|
108
|
+
if (el === consoleInput) {
|
|
109
|
+
toggleConsole(false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)) {
|
|
113
|
+
el.blur();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
showAll(); // deselects (selectedNode = rootNode) and fits the whole forest
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- registry ------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
const ARROW_DIR = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" };
|
|
122
|
+
// screen-space pan vectors, matching console.js's "pan <dir>" commands
|
|
123
|
+
const PAN_VEC = { ArrowUp: [0, 1], ArrowDown: [0, -1], ArrowLeft: [1, 0], ArrowRight: [-1, 0] };
|
|
124
|
+
|
|
125
|
+
const SHORTCUTS = [
|
|
126
|
+
{ section: "Panels" },
|
|
127
|
+
{
|
|
128
|
+
combos: [{ key: "`" }], glyphs: ["`"], desc: "Toggle terminal",
|
|
129
|
+
whileTyping: (el) => el === consoleInput, // also closes it from inside
|
|
130
|
+
run: () => toggleConsole(),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
combos: [{ key: "?" }], glyphs: ["?"], desc: "Toggle this panel",
|
|
134
|
+
run: () => toggleShortcutPanel(),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
combos: [{ key: "e" }], glyphs: ["e"], desc: "Toggle edit panel",
|
|
138
|
+
run: () => toggleEditPanel(),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
combos: [{ key: "Escape" }], glyphs: ["Esc"], desc: "Leave field · deselect · show all",
|
|
142
|
+
whileTyping: true, keepDefault: true,
|
|
143
|
+
run: handleEscape,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
{ section: "Move around" },
|
|
147
|
+
{
|
|
148
|
+
combos: [{ key: "ArrowUp" }, { key: "ArrowDown" }, { key: "ArrowLeft" }, { key: "ArrowRight" }],
|
|
149
|
+
glyphs: ["↑", "↓", "←", "→"], desc: "Navigate the tree",
|
|
150
|
+
run: (e) => navigate(ARROW_DIR[e.key]),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
combos: [
|
|
154
|
+
{ key: "ArrowUp", shift: true }, { key: "ArrowDown", shift: true },
|
|
155
|
+
{ key: "ArrowLeft", shift: true }, { key: "ArrowRight", shift: true },
|
|
156
|
+
],
|
|
157
|
+
glyphs: ["⇧↑", "⇧↓", "⇧←", "⇧→"], desc: "Pan the view",
|
|
158
|
+
run: (e) => { const [dx, dy] = PAN_VEC[e.key]; canvasPan(dx * PAN_AMOUNT, dy * PAN_AMOUNT); },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
combos: [{ key: "+" }, { key: "=" }], glyphs: ["+"], desc: "Zoom in",
|
|
162
|
+
run: () => canvasZoom(ZOOM_FACTOR),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
combos: [{ key: "-" }], glyphs: ["–"], desc: "Zoom out",
|
|
166
|
+
run: () => canvasZoom(1 / ZOOM_FACTOR),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
combos: [{ key: "0" }], glyphs: ["0"], desc: "Fit / show all",
|
|
170
|
+
run: () => showAll(),
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
{ section: "Selected node" },
|
|
174
|
+
{
|
|
175
|
+
combos: [{ key: "a" }], glyphs: ["a"], desc: "Add a child, edit its name",
|
|
176
|
+
run: () => addNodeAndEdit(),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
combos: [{ key: "d" }], glyphs: ["d"], desc: "Toggle done",
|
|
180
|
+
run: () => toggleDoneSelected(),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
combos: [{ key: "Delete" }], glyphs: ["Del"], desc: "Delete (asks first)",
|
|
184
|
+
run: () => deleteSelected(),
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// --- key handling --------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
function isTypingContext() {
|
|
191
|
+
const el = document.activeElement;
|
|
192
|
+
if (!el) return false;
|
|
193
|
+
return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT" || el.isContentEditable;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// a named key (ArrowUp, Delete, Escape) can be gated on Shift; a typed
|
|
197
|
+
// character (a, +, ?, `) already carries Shift in e.key, so don't re-check it
|
|
198
|
+
function comboMatches(combo, e) {
|
|
199
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
200
|
+
if (combo.key.length > 1 && (combo.shift === true) !== e.shiftKey) return false;
|
|
201
|
+
return combo.key === e.key;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
window.addEventListener("keydown", (e) => {
|
|
205
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return; // leave browser/OS combos alone
|
|
206
|
+
|
|
207
|
+
const typing = isTypingContext();
|
|
208
|
+
|
|
209
|
+
for (const sc of SHORTCUTS) {
|
|
210
|
+
if (!sc.combos) continue;
|
|
211
|
+
|
|
212
|
+
const wt = typeof sc.whileTyping === "function"
|
|
213
|
+
? sc.whileTyping(document.activeElement)
|
|
214
|
+
: !!sc.whileTyping;
|
|
215
|
+
if (typing && !wt) continue;
|
|
216
|
+
|
|
217
|
+
if (!sc.combos.some(c => comboMatches(c, e))) continue;
|
|
218
|
+
|
|
219
|
+
if (!sc.keepDefault) e.preventDefault();
|
|
220
|
+
sc.run(e);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// --- panel rendering ---------------------------------------------------
|
|
226
|
+
|
|
227
|
+
function renderShortcutList() {
|
|
228
|
+
shortcutBody.innerHTML = "";
|
|
229
|
+
|
|
230
|
+
const list = document.createElement("div");
|
|
231
|
+
list.className = "shortcut-list";
|
|
232
|
+
|
|
233
|
+
SHORTCUTS.forEach(sc => {
|
|
234
|
+
if (sc.section) {
|
|
235
|
+
const h = document.createElement("div");
|
|
236
|
+
h.className = "shortcut-section";
|
|
237
|
+
h.textContent = sc.section;
|
|
238
|
+
list.appendChild(h);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const row = document.createElement("div");
|
|
243
|
+
row.className = "shortcut-row";
|
|
244
|
+
|
|
245
|
+
const keys = document.createElement("div");
|
|
246
|
+
keys.className = "shortcut-keys";
|
|
247
|
+
sc.glyphs.forEach(g => {
|
|
248
|
+
const k = document.createElement("span");
|
|
249
|
+
k.className = "shortcut-key";
|
|
250
|
+
k.textContent = g;
|
|
251
|
+
keys.appendChild(k);
|
|
252
|
+
});
|
|
253
|
+
row.appendChild(keys);
|
|
254
|
+
|
|
255
|
+
const desc = document.createElement("div");
|
|
256
|
+
desc.className = "shortcut-desc";
|
|
257
|
+
desc.textContent = sc.desc;
|
|
258
|
+
row.appendChild(desc);
|
|
259
|
+
|
|
260
|
+
list.appendChild(row);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
shortcutBody.appendChild(list);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
renderShortcutList();
|
|
@@ -343,3 +343,122 @@ body:has(.console-panel.expanded) .comment-list {
|
|
|
343
343
|
.comment-add-input:focus {
|
|
344
344
|
border-color: #4772fa;
|
|
345
345
|
}
|
|
346
|
+
|
|
347
|
+
/* ── shortcuts panel ──────────────────────────────────────────────────────
|
|
348
|
+
left-side mirror of the edit panel: collapsed is a bare chevron button,
|
|
349
|
+
expanded is a white reference panel. Same collapse/clash rules. */
|
|
350
|
+
.shortcut-panel {
|
|
351
|
+
position: fixed;
|
|
352
|
+
top: 0;
|
|
353
|
+
left: 0;
|
|
354
|
+
z-index: 10;
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-direction: column;
|
|
357
|
+
background: transparent;
|
|
358
|
+
border-right: none;
|
|
359
|
+
box-shadow: none;
|
|
360
|
+
width: 44px;
|
|
361
|
+
height: 44px;
|
|
362
|
+
overflow: hidden;
|
|
363
|
+
transition: width 0.2s ease, height 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.shortcut-panel.expanded {
|
|
367
|
+
width: 300px;
|
|
368
|
+
height: calc(100vh - var(--console-h-collapsed));
|
|
369
|
+
background: #ffffff;
|
|
370
|
+
border-right: 1px solid #e2e4ea;
|
|
371
|
+
box-shadow: 2px 0 10px rgba(15, 23, 42, 0.06);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
body:has(.console-panel.expanded) .shortcut-panel.expanded {
|
|
375
|
+
height: calc(100vh - var(--console-h-expanded));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.shortcut-toggle {
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
flex: none;
|
|
383
|
+
width: 44px;
|
|
384
|
+
height: 44px;
|
|
385
|
+
border: none;
|
|
386
|
+
background: transparent;
|
|
387
|
+
color: #9aa0ab;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
padding: 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.shortcut-toggle:hover {
|
|
393
|
+
color: #23252b;
|
|
394
|
+
background: rgba(15, 23, 42, 0.05);
|
|
395
|
+
border-radius: 10px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.shortcut-toggle svg {
|
|
399
|
+
transition: transform 0.2s ease;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.shortcut-panel.expanded .shortcut-toggle svg {
|
|
403
|
+
transform: rotate(180deg);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.shortcut-body {
|
|
407
|
+
flex: 1;
|
|
408
|
+
overflow: auto;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.shortcut-list {
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-direction: column;
|
|
414
|
+
gap: 10px;
|
|
415
|
+
padding: 12px 14px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.shortcut-section {
|
|
419
|
+
font-weight: 600;
|
|
420
|
+
font-size: 11px;
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
letter-spacing: 0.04em;
|
|
423
|
+
color: #9aa0ab;
|
|
424
|
+
margin-top: 8px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.shortcut-section:first-child {
|
|
428
|
+
margin-top: 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* same inline-label shape as .edit-field: keys in a fixed left column */
|
|
432
|
+
.shortcut-row {
|
|
433
|
+
display: flex;
|
|
434
|
+
flex-direction: row;
|
|
435
|
+
align-items: baseline;
|
|
436
|
+
gap: 10px;
|
|
437
|
+
font-size: 12px;
|
|
438
|
+
color: #6b7280;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.shortcut-keys {
|
|
442
|
+
flex: none;
|
|
443
|
+
width: 92px;
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-wrap: wrap;
|
|
446
|
+
gap: 4px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.shortcut-key {
|
|
450
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
451
|
+
font-size: 11px;
|
|
452
|
+
line-height: 1.4;
|
|
453
|
+
color: #23252b;
|
|
454
|
+
background: #f5f6f8;
|
|
455
|
+
border: 1px solid #e2e4ea;
|
|
456
|
+
border-radius: 5px;
|
|
457
|
+
padding: 0 5px;
|
|
458
|
+
white-space: nowrap;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.shortcut-desc {
|
|
462
|
+
flex: 1;
|
|
463
|
+
min-width: 0;
|
|
464
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|