@ssebastien/tcmd 0.1.3 → 0.1.7
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/README.md +256 -62
- package/bin/tcmd +10 -2
- package/bin/tcmd.js +13 -2
- package/desktop/dist/index.html +13 -0
- package/desktop/dist/main.js +20298 -0
- package/desktop/dist/style.css +1515 -0
- package/desktop/frontend/index.html +13 -0
- package/package.json +19 -6
- package/public/fonts/OFL.txt +93 -0
- package/public/index.html +14 -0
- package/public/licenses/LUCIDE.txt +43 -0
- package/public/main.js +20679 -0
- package/public/style.css +1515 -0
- package/scripts/build-desktop.js +31 -0
- package/scripts/serve-web.js +61 -0
- package/src/js/tcmd/components/App.html +80 -14
- package/src/js/tcmd/components/App.js +339 -114
- package/src/js/tcmd/components/App.xml +76 -22
- package/src/js/tcmd/components/BookmarkList.html +5 -5
- package/src/js/tcmd/components/BookmarkList.js +15 -41
- package/src/js/tcmd/components/BookmarkList.xml +1 -2
- package/src/js/tcmd/components/BookmarkRow.html +2 -1
- package/src/js/tcmd/components/BookmarkRow.js +14 -9
- package/src/js/tcmd/components/BookmarkRow.xml +1 -1
- package/src/js/tcmd/components/Breadcrumbs.js +1 -1
- package/src/js/tcmd/components/CommandArgRow.html +28 -0
- package/src/js/tcmd/components/CommandArgRow.js +146 -30
- package/src/js/tcmd/components/CommandArgRow.xml +55 -4
- package/src/js/tcmd/components/CommandBar.html +2 -2
- package/src/js/tcmd/components/CommandBar.js +33 -27
- package/src/js/tcmd/components/CommandBar.xml +2 -2
- package/src/js/tcmd/components/CommandItem.html +1 -1
- package/src/js/tcmd/components/CommandItem.js +25 -25
- package/src/js/tcmd/components/CommandItem.xml +2 -2
- package/src/js/tcmd/components/CommandPanel.html +29 -0
- package/src/js/tcmd/components/CommandPanel.js +78 -52
- package/src/js/tcmd/components/CommandPanel.xml +49 -25
- package/src/js/tcmd/components/Confirm.html +2 -2
- package/src/js/tcmd/components/EntryDetail.html +7 -7
- package/src/js/tcmd/components/EntryDetail.js +22 -8
- package/src/js/tcmd/components/FileList.html +5 -1
- package/src/js/tcmd/components/FileList.js +2 -1
- package/src/js/tcmd/components/FilePicker.html +10 -0
- package/src/js/tcmd/components/FilePicker.js +43 -0
- package/src/js/tcmd/components/FilePicker.xml +13 -0
- package/src/js/tcmd/components/FilePickerRow.html +6 -0
- package/src/js/tcmd/components/FilePickerRow.js +27 -0
- package/src/js/tcmd/components/FilePickerRow.xml +10 -0
- package/src/js/tcmd/components/FilePreview.html +14 -4
- package/src/js/tcmd/components/FilePreview.js +111 -12
- package/src/js/tcmd/components/FilePreview.xml +28 -8
- package/src/js/tcmd/components/FileRow.html +2 -1
- package/src/js/tcmd/components/FileRow.js +70 -12
- package/src/js/tcmd/components/FilterBar.html +1 -1
- package/src/js/tcmd/components/FilterBar.js +14 -4
- package/src/js/tcmd/components/FilterBar.xml +1 -1
- package/src/js/tcmd/components/Frame.js +324 -0
- package/src/js/tcmd/components/Help.html +15 -0
- package/src/js/tcmd/components/Help.js +260 -0
- package/src/js/tcmd/components/Help.xml +13 -4
- package/src/js/tcmd/components/HelpRow.html +8 -0
- package/src/js/tcmd/components/HelpRow.xml +7 -0
- package/src/js/tcmd/components/OverlayList.html +8 -0
- package/src/js/tcmd/components/OverlayList.js +69 -0
- package/src/js/tcmd/components/{CommandPalette.xml → OverlayList.xml} +2 -5
- package/src/js/tcmd/components/OverlayRow.html +5 -0
- package/src/js/tcmd/components/OverlayRow.js +47 -0
- package/src/js/tcmd/components/OverlayRow.xml +9 -0
- package/src/js/tcmd/components/Summary.html +1 -1
- package/src/js/tcmd/components/Summary.js +1 -6
- package/src/js/tcmd/components/format.js +4 -23
- package/src/js/tcmd/components/icons.js +176 -5
- package/src/js/tcmd/components/index.js +9 -5
- package/src/js/tcmd/components/rows.js +176 -48
- package/src/js/tcmd/core/base64.js +64 -0
- package/src/js/tcmd/core/bookmarks.js +45 -8
- package/src/js/tcmd/core/commands.js +978 -260
- package/src/js/tcmd/core/concurrency.js +59 -0
- package/src/js/tcmd/core/decompress.js +117 -0
- package/src/js/tcmd/core/directory.js +219 -93
- package/src/js/tcmd/core/format.js +36 -0
- package/src/js/tcmd/core/fs.js +438 -54
- package/src/js/tcmd/core/history.js +58 -0
- package/src/js/tcmd/core/index.js +3 -9
- package/src/js/tcmd/core/journal.js +93 -0
- package/src/js/tcmd/core/keymap.js +42 -6
- package/src/js/tcmd/core/operations.js +813 -48
- package/src/js/tcmd/core/os.js +29 -3
- package/src/js/tcmd/core/pane.js +33 -8
- package/src/js/tcmd/core/panel.js +280 -104
- package/src/js/tcmd/core/path.js +30 -3
- package/src/js/tcmd/core/permissions.js +107 -0
- package/src/js/tcmd/core/picker.js +156 -0
- package/src/js/tcmd/core/preview.js +102 -0
- package/src/js/tcmd/core/script.js +7 -95
- package/src/js/tcmd/core/selection.js +9 -1
- package/src/js/tcmd/core/session.js +498 -200
- package/src/js/tcmd/core/watch.js +153 -0
- package/src/js/tcmd/desktop/backend/api.js +165 -0
- package/src/js/tcmd/desktop/backend/fs.js +626 -0
- package/src/js/tcmd/desktop/backend/main.ts +9 -0
- package/src/js/tcmd/desktop/bookmarks.js +23 -0
- package/src/js/tcmd/desktop/fs.js +91 -0
- package/src/js/tcmd/desktop/main.js +194 -0
- package/src/js/tcmd/desktop/os.js +33 -0
- package/src/js/tcmd/index.js +0 -1
- package/src/js/tcmd/tui/app.js +118 -60
- package/src/js/tcmd/tui/catalogue.js +238 -42
- package/src/js/tcmd/tui/index.js +18 -19
- package/src/js/tcmd/tui/opentui.js +33 -0
- package/src/js/tcmd/tui/syntax.js +91 -0
- package/src/js/tcmd/tui/theme.js +32 -2
- package/src/js/tcmd/vendor/bzip2.js +276 -0
- package/src/js/tcmd/web/app.js +225 -41
- package/src/js/tcmd/web/bookmarks.js +36 -0
- package/src/js/tcmd/web/catalogue.js +81 -38
- package/src/js/tcmd/web/effects.js +223 -0
- package/src/js/tcmd/web/header.html +1 -1
- package/src/js/tcmd/web/header.js +15 -11
- package/src/js/tcmd/web/http.js +13 -0
- package/src/js/tcmd/web/index.js +2 -2
- package/src/js/tcmd/web/main.js +6 -66
- package/src/js/tcmd/web/opfs.js +297 -30
- package/src/js/tcmd/web/seed.js +4 -4
- package/src/js/tcmd/web/server.js +124 -21
- package/src/js/tcmd/web/theme.js +4 -0
- package/tinyjs.json +16 -0
- package/src/js/tcmd/components/CommandPalette.html +0 -7
- package/src/js/tcmd/components/CommandPalette.js +0 -58
- package/src/js/tcmd/components/PaletteRow.html +0 -3
- package/src/js/tcmd/components/PaletteRow.js +0 -43
- package/src/js/tcmd/components/PaletteRow.xml +0 -4
- package/src/js/tcmd/tui/frame.js +0 -269
- package/src/js/tcmd/tui/help.js +0 -32
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-02
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/Frame
|
|
7
|
+
// Shared presentation helpers for the workspace chrome: the responsive
|
|
8
|
+
// breakpoints driven by the `uiSize` cell, the pane borders, header rules, the
|
|
9
|
+
// command-bar geometry and the overlay visibility gates. Everything here is
|
|
10
|
+
// injected into each renderer's App binding. Viewport dimensions are in
|
|
11
|
+
// monospace columns/rows; the browser measures them from its own font. The key
|
|
12
|
+
// reference's text lives with its overlay in `Help.js`.
|
|
13
|
+
|
|
14
|
+
import { unwrap } from "select/state/cells"
|
|
15
|
+
import { commandBarStatus } from "./CommandBar.js"
|
|
16
|
+
import pkg from "../../../../package.json" with { type: "json" }
|
|
17
|
+
|
|
18
|
+
// Constant: VERSION
|
|
19
|
+
// The published version, read from package.json so the identity bar can never
|
|
20
|
+
// drift from what ships.
|
|
21
|
+
const VERSION = `${pkg?.version ?? ""}`
|
|
22
|
+
|
|
23
|
+
// Constant: MODE_SLOTS
|
|
24
|
+
// Per-letter slots the identity bar draws the view-mode word through, one
|
|
25
|
+
// `<text>` each so every letter can carry its own gradient colour (the same
|
|
26
|
+
// trick the `TinyCmd` brand uses). Sized for the longest mode name; a shorter
|
|
27
|
+
// one leaves the trailing slots empty, and an empty text node takes no column.
|
|
28
|
+
const MODE_SLOTS = "commander".length
|
|
29
|
+
|
|
30
|
+
// Function: modeWord
|
|
31
|
+
// The view-mode name as it reads in the identity bar.
|
|
32
|
+
function modeWord(viewMode) {
|
|
33
|
+
return viewMode === "explorer" ? "explorer" : "commander"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Function: modeGradient
|
|
37
|
+
// The `[from, to]` brand pair a mode word shimmers along: explorer takes the
|
|
38
|
+
// Tiny blues, commander the Cmd oranges, so the mode reads as part of the
|
|
39
|
+
// logo rather than as trailing status text.
|
|
40
|
+
function modeGradient(env, viewMode) {
|
|
41
|
+
return viewMode === "explorer"
|
|
42
|
+
? [env.theme.brandTinyFrom, env.theme.brandTinyTo]
|
|
43
|
+
: [env.theme.brandCmdFrom, env.theme.brandCmdTo]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Function: versionTrail
|
|
47
|
+
// The identity bar's version tail, ` · 0.1.3`. It carries its leading space
|
|
48
|
+
// so it follows the mode word's slots directly in both renderers.
|
|
49
|
+
function versionTrail() {
|
|
50
|
+
return VERSION ? ` · ${VERSION}` : ""
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Below this the two-pane workspace is replaced by a resize hint.
|
|
54
|
+
const MIN_WIDTH = 64
|
|
55
|
+
const MIN_HEIGHT = 16
|
|
56
|
+
// At or above this the inspector can sit beside the panes; below it becomes a
|
|
57
|
+
// fullscreen overlay.
|
|
58
|
+
const INSPECTOR_INLINE_WIDTH = 100
|
|
59
|
+
|
|
60
|
+
// Function: sizeOf
|
|
61
|
+
// Reads the viewport size from the App data, falling back to the renderer's
|
|
62
|
+
// own dimensions so the gates work even when no `uiSize` cell is injected
|
|
63
|
+
// (tests, web). Returns `null` when neither is known.
|
|
64
|
+
function sizeOf(data, self) {
|
|
65
|
+
const size = unwrap(data?.uiSize)
|
|
66
|
+
if (size && (size.width || size.height)) return size
|
|
67
|
+
const width = Math.floor(self?.ctx?.width ?? 0)
|
|
68
|
+
const height = Math.floor(self?.ctx?.height ?? 0)
|
|
69
|
+
if (!width && !height) return null
|
|
70
|
+
return { width, height }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Function: isNarrow
|
|
74
|
+
// True when the viewport is known and too narrow for the inline inspector.
|
|
75
|
+
function isNarrow(data, self) {
|
|
76
|
+
const size = sizeOf(data, self)
|
|
77
|
+
return Boolean(size && (size.width ?? 0) > 0 && size.width < INSPECTOR_INLINE_WIDTH)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Function: isExplorer
|
|
81
|
+
// True when the workspace is the three-column explorer view. Absent
|
|
82
|
+
// `viewMode` data (tests, host-less bindings) means the two-pane commander.
|
|
83
|
+
function isExplorer(data) {
|
|
84
|
+
return unwrap(data?.viewMode) === "explorer"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Function: currentColumnId
|
|
88
|
+
// The id of the column holding the current directory: the focused pane when
|
|
89
|
+
// focus is on source/destination, the anchor pane otherwise. (`core/pane` has
|
|
90
|
+
// the same question over a session; this one reads the view's data bag.)
|
|
91
|
+
function currentColumnId(data) {
|
|
92
|
+
return `${unwrap(data?.currentId) ?? unwrap(data?.active) ?? "left"}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Constant: PANE_GROW
|
|
96
|
+
// The explorer's current column is this much wider than the parent/preview
|
|
97
|
+
// mirrors — enough to read as the working pane, not enough to starve its
|
|
98
|
+
// neighbours. Both renderers apply the same ratio (the TUI's pane row growers
|
|
99
|
+
// and the web CSS `.pane.is-major`), so the geometry has one source of truth
|
|
100
|
+
// here.
|
|
101
|
+
const PANE_GROW = 1.35
|
|
102
|
+
|
|
103
|
+
// Function: paneColumnWidth
|
|
104
|
+
// The columns a pane's column occupies at viewport `width` for the current
|
|
105
|
+
// view. Explorer leads with the parent mirror and grows the current column by
|
|
106
|
+
// PANE_GROW, so its mirrors are narrower than an even split; commander tiles
|
|
107
|
+
// its two panes evenly. The inline inspector adds an equal column to either.
|
|
108
|
+
// Panes hidden in the view, and an unknown viewport (`width <= 0`), are 0.
|
|
109
|
+
//
|
|
110
|
+
// The caller subtracts its own chrome (border, inset, safety) to get a content
|
|
111
|
+
// budget: feeding every pane an even `width / columns` makes the narrower
|
|
112
|
+
// mirror columns overflow, and the renderer then mangles their breadcrumbs.
|
|
113
|
+
function paneColumnWidth(width, paneId, { viewMode, currentId, showPreview } = {}) {
|
|
114
|
+
const total = Math.floor(width ?? 0)
|
|
115
|
+
if (total <= 0) return 0
|
|
116
|
+
const weights = {}
|
|
117
|
+
if (viewMode === "explorer") {
|
|
118
|
+
weights.parent = 1
|
|
119
|
+
weights[`${currentId}` === "right" ? "right" : "left"] = PANE_GROW
|
|
120
|
+
} else {
|
|
121
|
+
weights.left = 1
|
|
122
|
+
weights.right = 1
|
|
123
|
+
}
|
|
124
|
+
if (Boolean(showPreview) && total >= INSPECTOR_INLINE_WIDTH) weights.preview = 1
|
|
125
|
+
const weight = weights[paneId] ?? 0
|
|
126
|
+
if (weight <= 0) return 0
|
|
127
|
+
const sum = Object.values(weights).reduce((acc, value) => acc + value, 0)
|
|
128
|
+
return Math.floor((total * weight) / sum)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Function: paneListHeight
|
|
132
|
+
// Rows available to a pane's listing: the viewport height minus the app header
|
|
133
|
+
// (1 row in the web UI, 2 for the desktop toolbar), the pane border (2), pane
|
|
134
|
+
// header + rule (2), entry detail (2), and the command bar when visible. Fed to
|
|
135
|
+
// the windowed `filelist` so a large directory mounts only the visible slice.
|
|
136
|
+
// `0` means "unknown — render everything to measure".
|
|
137
|
+
function paneListHeight(data, self) {
|
|
138
|
+
const height = Math.floor(sizeOf(data, self)?.height ?? 0)
|
|
139
|
+
if (height <= 0) return 0
|
|
140
|
+
const desktop = Boolean(unwrap(data?.desktopMode))
|
|
141
|
+
// A notice or error keeps a one-line status in desktop mode; otherwise that
|
|
142
|
+
// renderer has no bottom command bar at all. The bar is always one line.
|
|
143
|
+
const bar = commandBarStatus(data) || !desktop ? 1 : 0
|
|
144
|
+
return Math.max(1, height - (desktop ? 8 : 7) - bar)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Function: mixHex
|
|
148
|
+
// Linear interpolation between two `#rrggbb` colours at `t` in [0, 1]. A
|
|
149
|
+
// missing colour (the mono theme) yields `undefined` so the whole run falls
|
|
150
|
+
// back to the terminal default.
|
|
151
|
+
function mixHex(from, to, t) {
|
|
152
|
+
if (!from || !to) return undefined
|
|
153
|
+
const channel = (index) => {
|
|
154
|
+
const a = Number.parseInt(from.slice(index, index + 2), 16)
|
|
155
|
+
const b = Number.parseInt(to.slice(index, index + 2), 16)
|
|
156
|
+
return Math.round(a + (b - a) * t)
|
|
157
|
+
.toString(16)
|
|
158
|
+
.padStart(2, "0")
|
|
159
|
+
}
|
|
160
|
+
return `#${channel(1)}${channel(3)}${channel(5)}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Function: shineOf
|
|
164
|
+
// One `{ letter, fg }` per letter of a brand word, interpolating `from` → `to`
|
|
165
|
+
// across it so the word reads as a gradient; a missing colour (mono theme)
|
|
166
|
+
// leaves every `fg` undefined and the run falls back to the terminal default.
|
|
167
|
+
function shineOf(letters, from, to) {
|
|
168
|
+
return `${letters}`.split("").map((letter, index, all) => ({
|
|
169
|
+
letter,
|
|
170
|
+
fg: mixHex(from, to, index / (all.length - 1)),
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Function: createFrameBehaviors
|
|
175
|
+
// Behaviors for the App root and its chrome.
|
|
176
|
+
function createFrameBehaviors(env) {
|
|
177
|
+
const tiny = shineOf("Tiny", env.theme.brandTinyFrom, env.theme.brandTinyTo)
|
|
178
|
+
const cmd = shineOf("Cmd", env.theme.brandCmdFrom, env.theme.brandCmdTo)
|
|
179
|
+
// The mode word, letter by letter: `modeL<i>` is the letter, `modeFg<i>`
|
|
180
|
+
// its gradient colour — the same per-letter trick the `TinyCmd` brand
|
|
181
|
+
// uses, since a terminal has no gradient to draw. The run is cached per
|
|
182
|
+
// word (there are only two), and both behaviors read the live `viewMode`,
|
|
183
|
+
// so flipping `t` redraws it in the other brand's colours.
|
|
184
|
+
const modeRuns = new Map()
|
|
185
|
+
const modeRun = (data) => {
|
|
186
|
+
const viewMode = unwrap(data?.viewMode)
|
|
187
|
+
const word = modeWord(viewMode)
|
|
188
|
+
let run = modeRuns.get(word)
|
|
189
|
+
if (!run) {
|
|
190
|
+
run = shineOf(word, ...modeGradient(env, viewMode))
|
|
191
|
+
modeRuns.set(word, run)
|
|
192
|
+
}
|
|
193
|
+
return run
|
|
194
|
+
}
|
|
195
|
+
const modeSlots = {}
|
|
196
|
+
for (let i = 0; i < MODE_SLOTS; i++) {
|
|
197
|
+
// The `when` gate matters: an empty text node still claims a column in
|
|
198
|
+
// the row, so the slots past the end of a shorter word are dropped
|
|
199
|
+
// from the layout instead of padding it.
|
|
200
|
+
modeSlots[`modeHas${i}`] = (_self, data) => modeRun(data)[i] !== undefined
|
|
201
|
+
modeSlots[`modeL${i}`] = (_self, data) => modeRun(data)[i]?.letter ?? ""
|
|
202
|
+
modeSlots[`modeFg${i}`] = (_self, data) => modeRun(data)[i]?.fg
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
// Identity bar: "Tiny" shimmers along the brand blues, "Cmd" along a
|
|
206
|
+
// brighter orange than the mark accent, and the host trail follows in
|
|
207
|
+
// the heading colour. Everything else on screen belongs to the user's
|
|
208
|
+
// files; this one row is the app's own.
|
|
209
|
+
brandT: () => tiny[0].letter,
|
|
210
|
+
brandTFg: () => tiny[0].fg,
|
|
211
|
+
brandI: () => tiny[1].letter,
|
|
212
|
+
brandIFg: () => tiny[1].fg,
|
|
213
|
+
brandN: () => tiny[2].letter,
|
|
214
|
+
brandNFg: () => tiny[2].fg,
|
|
215
|
+
brandY: () => tiny[3].letter,
|
|
216
|
+
brandYFg: () => tiny[3].fg,
|
|
217
|
+
brandC: () => cmd[0].letter,
|
|
218
|
+
brandCFg: () => cmd[0].fg,
|
|
219
|
+
brandM: () => cmd[1].letter,
|
|
220
|
+
brandMFg: () => cmd[1].fg,
|
|
221
|
+
brandD: () => cmd[2].letter,
|
|
222
|
+
brandDFg: () => cmd[2].fg,
|
|
223
|
+
topBar: (_self, data) => {
|
|
224
|
+
const host = `${unwrap(data.appHost) ?? ""}`.trim()
|
|
225
|
+
return host ? ` - ${host} ` : " "
|
|
226
|
+
},
|
|
227
|
+
topBarFg: () => env.theme.textHeadingFg,
|
|
228
|
+
// Right-aligned trail: the version, following the gradiented mode word
|
|
229
|
+
// (`modeL*`) slot by slot. Dimmer than the brand, so the row still
|
|
230
|
+
// reads left to right.
|
|
231
|
+
topBarStatus: () => versionTrail(),
|
|
232
|
+
topBarStatusFg: () => env.theme.textSecondaryFg,
|
|
233
|
+
...modeSlots,
|
|
234
|
+
|
|
235
|
+
// Responsive gates. `uiSize` is absent in tests and the web, so the
|
|
236
|
+
// workspace renders normally there.
|
|
237
|
+
tooSmall: (self, data) => {
|
|
238
|
+
const size = sizeOf(data, self)
|
|
239
|
+
if (!size) return false
|
|
240
|
+
return (size.width ?? MIN_WIDTH) < MIN_WIDTH || (size.height ?? MIN_HEIGHT) < MIN_HEIGHT
|
|
241
|
+
},
|
|
242
|
+
minSizeTitle: () => "Terminal too small",
|
|
243
|
+
minSizeHint: (self, data) => {
|
|
244
|
+
const size = sizeOf(data, self) ?? { width: 0, height: 0 }
|
|
245
|
+
return `need ${MIN_WIDTH}x${MIN_HEIGHT}, have ${size.width ?? 0}x${size.height ?? 0}`
|
|
246
|
+
},
|
|
247
|
+
// The min-size gate text uses the heading/secondary text tokens.
|
|
248
|
+
titleFg: () => env.theme.textHeadingFg,
|
|
249
|
+
dimFg: () => env.theme.textSecondaryFg,
|
|
250
|
+
|
|
251
|
+
// Each pane is a complete box. Both draw all four edges (the middle
|
|
252
|
+
// shows two adjacent rules) so each pane's border colour can track its
|
|
253
|
+
// own focus independently — a shared edge would be owned by one pane
|
|
254
|
+
// and never highlight when the other is active.
|
|
255
|
+
leftBorder: () => ["top", "left", "bottom", "right"],
|
|
256
|
+
rightBorder: () => ["top", "left", "bottom", "right"],
|
|
257
|
+
// The header rule separates the pane header row from the listing body.
|
|
258
|
+
headRule: () => ["top"],
|
|
259
|
+
|
|
260
|
+
// The breadcrumb yields the pane header row to the filter while a
|
|
261
|
+
// filter is being edited or applied.
|
|
262
|
+
leftFilterOn: (_self, data) => Boolean(unwrap(data.leftFilter)),
|
|
263
|
+
rightFilterOn: (_self, data) => Boolean(unwrap(data.rightFilter)),
|
|
264
|
+
parentFilterOn: (_self, data) => Boolean(unwrap(data.parentFilter)),
|
|
265
|
+
|
|
266
|
+
// Which columns the workspace draws. Commander tiles source and
|
|
267
|
+
// destination; explorer drops destination for the parent of the
|
|
268
|
+
// current directory, which leads the row so the current pane stays in
|
|
269
|
+
// the middle. The inspector column follows `showInspectorInline` and
|
|
270
|
+
// completes the three columns in either mode. Maximizing the inspector
|
|
271
|
+
// (`⇧V`) hides every column so the preview can take the row.
|
|
272
|
+
showParentPane: (_self, data) => isExplorer(data) && !unwrap(data?.previewFull),
|
|
273
|
+
showLeftPane: (_self, data) =>
|
|
274
|
+
(!isExplorer(data) || currentColumnId(data) === "left") && !unwrap(data?.previewFull),
|
|
275
|
+
showRightPane: (_self, data) =>
|
|
276
|
+
(!isExplorer(data) || currentColumnId(data) === "right") && !unwrap(data?.previewFull),
|
|
277
|
+
|
|
278
|
+
// The inline inspector shows when the preview is on and the viewport is
|
|
279
|
+
// wide enough; the viewer overlay shows whenever it is open, at any
|
|
280
|
+
// width (`v` on a narrow terminal, or Enter on a file that cannot be
|
|
281
|
+
// edited). Maximizing (`⇧V`) replaces the column with
|
|
282
|
+
// `showPreviewFull` below.
|
|
283
|
+
inspectorNarrow: (self, data) => isNarrow(data, self),
|
|
284
|
+
showInspectorInline: (self, data) =>
|
|
285
|
+
Boolean(unwrap(data.showPreview)) && !isNarrow(data, self) && !unwrap(data?.previewFull),
|
|
286
|
+
showViewer: (_self, data) => Boolean(unwrap(data.viewerOpen)),
|
|
287
|
+
// The maximized inspector: the preview fills the pane row in either
|
|
288
|
+
// view mode, pushed by `view.maximize` (`⇧V`).
|
|
289
|
+
showPreviewFull: (_self, data) => Boolean(unwrap(data?.previewFull)),
|
|
290
|
+
|
|
291
|
+
// Help overlay visibility; the content lives in `Help.js` so the
|
|
292
|
+
// overlay has a single source of truth.
|
|
293
|
+
helpOpen: (_self, data) => Boolean(unwrap(data.helpOpen)),
|
|
294
|
+
|
|
295
|
+
// The shared list overlay (history / trash) is mounted whenever a mode
|
|
296
|
+
// is set; the component itself gates on `open`.
|
|
297
|
+
overlayOpen: (_self, data) => Boolean(unwrap(data.overlay)),
|
|
298
|
+
|
|
299
|
+
// The windowed listing height for both panes (they share the row).
|
|
300
|
+
listViewport: (self, data) => paneListHeight(data, self),
|
|
301
|
+
|
|
302
|
+
// The command bar's height: always a single line, status or key hints.
|
|
303
|
+
barHeight: () => 1,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export {
|
|
308
|
+
createFrameBehaviors,
|
|
309
|
+
currentColumnId,
|
|
310
|
+
INSPECTOR_INLINE_WIDTH,
|
|
311
|
+
isExplorer,
|
|
312
|
+
isNarrow,
|
|
313
|
+
MIN_HEIGHT,
|
|
314
|
+
MIN_WIDTH,
|
|
315
|
+
MODE_SLOTS,
|
|
316
|
+
modeGradient,
|
|
317
|
+
modeWord,
|
|
318
|
+
PANE_GROW,
|
|
319
|
+
paneColumnWidth,
|
|
320
|
+
sizeOf,
|
|
321
|
+
versionTrail,
|
|
322
|
+
VERSION,
|
|
323
|
+
}
|
|
324
|
+
// EOF
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<section class="help" when="open" role="dialog" aria-modal="true" aria-label="Key reference" tabindex="-1" on:click="!dialog:help-cancel">
|
|
2
|
+
<div class="help-dialog" on:click="dialogStop">
|
|
3
|
+
<header out="title"></header>
|
|
4
|
+
<div class="help-columns">
|
|
5
|
+
<div class="help-column" out="leftRows"></div>
|
|
6
|
+
<div class="help-column" out="rightRows"></div>
|
|
7
|
+
</div>
|
|
8
|
+
<footer class="help-footer">
|
|
9
|
+
<span class="help-bracket" out="hintOpen"></span>
|
|
10
|
+
<span class="help-title" out="hintHead"></span>
|
|
11
|
+
<span class="help-key" out="hintKeys"></span>
|
|
12
|
+
<span class="help-bracket" out="hintClose"></span>
|
|
13
|
+
</footer>
|
|
14
|
+
</div>
|
|
15
|
+
</section>
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-02
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/Help
|
|
7
|
+
// Shared behavior for the `?` key reference overlay, plus the reference data
|
|
8
|
+
// itself. The categories are built from the live keymap and command registry,
|
|
9
|
+
// so they cannot drift from the actual bindings. The TUI keeps the bracket
|
|
10
|
+
// shorthand (`[Open ⏎]`); the web renders toolbar-like action chips with Lucide
|
|
11
|
+
// icons and key badges. Rows are balanced across two columns with bright
|
|
12
|
+
// uppercase section headings. Both presentations (`Help.xml` / `Help.html`)
|
|
13
|
+
// render the same row data through `HelpRow`.
|
|
14
|
+
|
|
15
|
+
import { unwrap } from "select/state/cells"
|
|
16
|
+
import { DEFAULT_KEYMAP, keyGlyph } from "../core/keymap.js"
|
|
17
|
+
import { getCommands } from "../core/commands.js"
|
|
18
|
+
import { commandLabel, keyGlyph as keyLabelGlyph } from "./CommandItem.js"
|
|
19
|
+
|
|
20
|
+
// Constant: BRACKET
|
|
21
|
+
// The shorthand delimiters, shared with the command bar.
|
|
22
|
+
const OPEN = "["
|
|
23
|
+
const CLOSE = "]"
|
|
24
|
+
|
|
25
|
+
// Constant: CLOSE_HINT
|
|
26
|
+
// The footer shorthand: Esc dismisses the overlay.
|
|
27
|
+
const CLOSE_HINT = { title: "Close", keys: "Esc" }
|
|
28
|
+
|
|
29
|
+
// Constant: BOLD
|
|
30
|
+
// OpenTUI's `TextAttributes.BOLD` bit, set on section headings through the
|
|
31
|
+
// TUI's `out:attributes` slot. The web bolds through CSS instead.
|
|
32
|
+
const BOLD = 1
|
|
33
|
+
|
|
34
|
+
// Constant: GROUP_ORDER / GROUP_TITLES
|
|
35
|
+
// Help sections, in display order.
|
|
36
|
+
const GROUP_ORDER = ["nav", "pane", "list", "bookmark", "view", "file", "history", "trash", "panel", "filter", "app"]
|
|
37
|
+
const GROUP_TITLES = {
|
|
38
|
+
app: "Session",
|
|
39
|
+
bookmark: "Bookmarks",
|
|
40
|
+
file: "Files",
|
|
41
|
+
filter: "Filter",
|
|
42
|
+
history: "History",
|
|
43
|
+
list: "Selection",
|
|
44
|
+
nav: "Navigation",
|
|
45
|
+
panel: "Commands",
|
|
46
|
+
pane: "Panes",
|
|
47
|
+
trash: "Trash",
|
|
48
|
+
view: "View",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Toolbar actions and key-reference entries share Lucide icons in the web UI.
|
|
52
|
+
// The TUI keeps its compact textual row layout.
|
|
53
|
+
const HELP_ICONS = {
|
|
54
|
+
"nav.up": "arrow-up",
|
|
55
|
+
"nav.down": "arrow-down",
|
|
56
|
+
"nav.jumpUp": "arrow-up",
|
|
57
|
+
"nav.jumpDown": "arrow-down",
|
|
58
|
+
"nav.pageUp": "arrow-up",
|
|
59
|
+
"nav.pageDown": "arrow-down",
|
|
60
|
+
"nav.home": "arrow-up",
|
|
61
|
+
"nav.end": "arrow-down",
|
|
62
|
+
"pane.enter": "arrow-right",
|
|
63
|
+
"pane.up": "arrow-up",
|
|
64
|
+
"pane.back": "arrow-left",
|
|
65
|
+
"pane.forward": "arrow-right",
|
|
66
|
+
"pane.toggle": "columns-2",
|
|
67
|
+
"pane.reload": "refresh-cw",
|
|
68
|
+
"list.mark": "check",
|
|
69
|
+
"bookmark.open": "bookmark",
|
|
70
|
+
"view.togglePreview": "panel-right",
|
|
71
|
+
"view.maximize": "maximize",
|
|
72
|
+
"view.toggleMode": "columns-2",
|
|
73
|
+
"file.mkdir": "folder-plus",
|
|
74
|
+
"file.open": "file",
|
|
75
|
+
"file.edit": "file-text",
|
|
76
|
+
"history.open": "refresh-cw",
|
|
77
|
+
"history.undo": "arrow-left",
|
|
78
|
+
"history.redo": "arrow-right",
|
|
79
|
+
"trash.open": "trash",
|
|
80
|
+
"panel.open": "terminal",
|
|
81
|
+
"panel.shell": "terminal",
|
|
82
|
+
"filter.start": "search",
|
|
83
|
+
"app.quit": "power",
|
|
84
|
+
"app.help": "circle-question-mark",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Function: helpGroups
|
|
88
|
+
// The categorized key reference as `{ title, rows }` groups in display order.
|
|
89
|
+
// One row per command, with every distinct chord from the live keymap. The
|
|
90
|
+
// registry provides the action title.
|
|
91
|
+
function helpGroups() {
|
|
92
|
+
const byId = new Map(getCommands().map((command) => [command.id, command]))
|
|
93
|
+
const groups = new Map()
|
|
94
|
+
for (const binding of DEFAULT_KEYMAP) {
|
|
95
|
+
const command = byId.get(binding.command)
|
|
96
|
+
if (!command) continue
|
|
97
|
+
const group = binding.command.split(".")[0]
|
|
98
|
+
if (!groups.has(group)) groups.set(group, new Map())
|
|
99
|
+
const rows = groups.get(group)
|
|
100
|
+
if (!rows.has(binding.command)) rows.set(binding.command, { keys: [], id: binding.command, title: command.title })
|
|
101
|
+
const row = rows.get(binding.command)
|
|
102
|
+
const glyph = keyGlyph(binding)
|
|
103
|
+
if (!row.keys.includes(glyph)) row.keys.push(glyph)
|
|
104
|
+
}
|
|
105
|
+
const out = []
|
|
106
|
+
for (const group of GROUP_ORDER) {
|
|
107
|
+
const rows = groups.get(group)
|
|
108
|
+
if (!rows) continue
|
|
109
|
+
const list = []
|
|
110
|
+
for (const row of rows.values()) {
|
|
111
|
+
list.push({ id: row.id, title: row.title, keys: row.keys.map(keyLabelGlyph).join("/") })
|
|
112
|
+
}
|
|
113
|
+
out.push({ title: GROUP_TITLES[group] ?? group, rows: list })
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Function: helpLines
|
|
119
|
+
// The flat reference: group headings and rows, one per line, blank line
|
|
120
|
+
// between groups (the single-column fallback).
|
|
121
|
+
function helpLines() {
|
|
122
|
+
const lines = []
|
|
123
|
+
for (const group of helpGroups()) {
|
|
124
|
+
lines.push(group.title)
|
|
125
|
+
for (const row of group.rows) lines.push(commandLabel(row).trim())
|
|
126
|
+
lines.push("")
|
|
127
|
+
}
|
|
128
|
+
return lines
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Function: helpBlocks
|
|
132
|
+
// The reference split into two columns balanced by line count, as flat lists of
|
|
133
|
+
// renderable blocks: `{ type: "section", label }` headings,
|
|
134
|
+
// `{ type: "entry", open, head, keys, close }` rows and `{ type: "spacer" }`
|
|
135
|
+
// blank lines between groups. Every entry title and key is padded to the widest
|
|
136
|
+
// one across both columns, so all brackets share one width and the keys align
|
|
137
|
+
// down each column (keys right-aligned against the closing bracket).
|
|
138
|
+
function helpBlocks() {
|
|
139
|
+
const groups = helpGroups()
|
|
140
|
+
const commands = new Map(getCommands().map((command) => [command.id, command]))
|
|
141
|
+
let titleWidth = 0
|
|
142
|
+
let keyWidth = 0
|
|
143
|
+
for (const group of groups) {
|
|
144
|
+
for (const row of group.rows) {
|
|
145
|
+
titleWidth = Math.max(titleWidth, row.title.length)
|
|
146
|
+
keyWidth = Math.max(keyWidth, keyLabelGlyph(row.keys).length)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const columns = [[], []]
|
|
150
|
+
const loads = [0, 0]
|
|
151
|
+
for (const group of groups) {
|
|
152
|
+
const at = loads[0] <= loads[1] ? 0 : 1
|
|
153
|
+
columns[at].push({ type: "section", key: `section:${group.title}`, label: group.title.toUpperCase() })
|
|
154
|
+
for (const row of group.rows) {
|
|
155
|
+
const command = commands.get(row.id)
|
|
156
|
+
// The compact web badge shows the command's canonical key when the
|
|
157
|
+
// registry has one (lowercase `b`, `Spc`, `⇥`); otherwise fall back
|
|
158
|
+
// to the keymap chords, which may list several alternatives.
|
|
159
|
+
const shortcut = command?.keys ? keyLabelGlyph(command.keys) : keyLabelGlyph(row.keys)
|
|
160
|
+
columns[at].push({
|
|
161
|
+
type: "entry",
|
|
162
|
+
key: `entry:${row.id}`,
|
|
163
|
+
icon: HELP_ICONS[row.id] ?? "chevron-right",
|
|
164
|
+
title: row.title,
|
|
165
|
+
shortcut,
|
|
166
|
+
open: OPEN,
|
|
167
|
+
head: `${row.title.padEnd(titleWidth)} `,
|
|
168
|
+
keys: keyLabelGlyph(row.keys).padStart(keyWidth),
|
|
169
|
+
close: CLOSE,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
// A blank row separates the groups (and costs one line, like the
|
|
173
|
+
// heading).
|
|
174
|
+
columns[at].push({ type: "spacer", key: `spacer:${group.title}` })
|
|
175
|
+
// A group costs its rows plus its heading and the blank after it.
|
|
176
|
+
loads[at] += group.rows.length + 2
|
|
177
|
+
}
|
|
178
|
+
// The blank line separates groups, so the bottom of each column ends on its
|
|
179
|
+
// last entry instead.
|
|
180
|
+
for (const column of columns) {
|
|
181
|
+
if (column[column.length - 1]?.type === "spacer") column.pop()
|
|
182
|
+
}
|
|
183
|
+
return { left: columns[0], right: columns[1] }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Function: createBehaviors
|
|
187
|
+
// Builds the Help behavior map for the given renderer environment.
|
|
188
|
+
function createBehaviors(env) {
|
|
189
|
+
return {
|
|
190
|
+
bg: () => env.theme.surfaceBg,
|
|
191
|
+
borderFg: () => env.theme.borderPrimaryActiveFg,
|
|
192
|
+
dimFg: () => env.theme.textSecondaryFg,
|
|
193
|
+
leftRows: (self, data) => env.rows?.(self, data, "left") ?? [],
|
|
194
|
+
rightRows: (self, data) => env.rows?.(self, data, "right") ?? [],
|
|
195
|
+
// The reference has no cursor for the scrollbox to follow, so its
|
|
196
|
+
// vertical bar is driven here: visible only while the two columns
|
|
197
|
+
// actually overflow the viewport. The viewport height is derived from
|
|
198
|
+
// the injected `size` (so the bar tracks a resize) and the dialog's own
|
|
199
|
+
// geometry (`Help.xml`: 82% tall, minus padding, borders, rule and
|
|
200
|
+
// footer).
|
|
201
|
+
scrollbar: (self, data) => {
|
|
202
|
+
const scroll = self?.ref?.scroll
|
|
203
|
+
const bar = scroll?.verticalScrollBar
|
|
204
|
+
if (!bar) return ""
|
|
205
|
+
const height = unwrap(data?.size)?.height ?? self?.ctx?.height ?? 0
|
|
206
|
+
const viewport = height > 0 ? Math.floor(height * 0.82) - 6 : 0
|
|
207
|
+
const columns = helpBlocks()
|
|
208
|
+
const rows = Math.max(columns.left.length, columns.right.length)
|
|
209
|
+
const want = viewport > 0 && rows > viewport
|
|
210
|
+
if (bar.visible !== want) bar.visible = want
|
|
211
|
+
return ""
|
|
212
|
+
},
|
|
213
|
+
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
214
|
+
// The footer shorthand reads like the bar: dim brackets, bright title,
|
|
215
|
+
// accent key.
|
|
216
|
+
hintOpen: () => OPEN,
|
|
217
|
+
hintHead: () => `${CLOSE_HINT.title} `,
|
|
218
|
+
hintKeys: () => CLOSE_HINT.keys,
|
|
219
|
+
hintClose: () => CLOSE,
|
|
220
|
+
bracketFg: () => env.theme.textSecondaryFg,
|
|
221
|
+
headFg: () => env.theme.textFg,
|
|
222
|
+
keyFg: () => env.theme.accentFg,
|
|
223
|
+
// Separator rule above the footer (a top border on a one-row box).
|
|
224
|
+
rule: () => ["top"],
|
|
225
|
+
ruleFg: () => env.theme.textSecondaryFg,
|
|
226
|
+
textFg: () => env.theme.textFg,
|
|
227
|
+
title: () => " Keys ",
|
|
228
|
+
titleFg: () => env.theme.textHeadingFg,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Function: createRowBehaviors
|
|
233
|
+
// Builds the HelpRow behavior map (one section heading or one key entry).
|
|
234
|
+
function createRowBehaviors(env) {
|
|
235
|
+
return {
|
|
236
|
+
isSection: (_self, data) => data.item?.type === "section",
|
|
237
|
+
isEntry: (_self, data) => data.item?.type === "entry",
|
|
238
|
+
section: (_self, data) => data.item?.label ?? "",
|
|
239
|
+
// Sections are bright; the TUI adds bold via OpenTUI's attribute bit
|
|
240
|
+
// (the web bolds through CSS instead).
|
|
241
|
+
sectionFg: () => env.theme.textHeadingFg,
|
|
242
|
+
sectionAttrs: () => BOLD,
|
|
243
|
+
open: (_self, data) => data.item?.open ?? "",
|
|
244
|
+
head: (_self, data) => data.item?.head ?? "",
|
|
245
|
+
keys: (_self, data) => data.item?.keys ?? "",
|
|
246
|
+
close: (_self, data) => data.item?.close ?? "",
|
|
247
|
+
hasIcon: (_self, data) => Boolean(env.theme.lucideIcons && data.item?.icon),
|
|
248
|
+
iconHref: (_self, data) =>
|
|
249
|
+
env.theme.lucideIcons && data.item?.icon ? `#lucide-${data.item.icon}` : "",
|
|
250
|
+
actionTitle: (_self, data) => data.item?.title ?? `${data.item?.head ?? ""}`.trim(),
|
|
251
|
+
shortcut: (_self, data) => data.item?.shortcut ?? `${data.item?.keys ?? ""}`.trim(),
|
|
252
|
+
bracketFg: () => env.theme.textSecondaryFg,
|
|
253
|
+
headFg: () => env.theme.textFg,
|
|
254
|
+
keyFg: () => env.theme.accentFg,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { createBehaviors, createRowBehaviors, helpBlocks, helpGroups, helpLines, keyGlyph }
|
|
259
|
+
|
|
260
|
+
// EOF
|
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
out:title="title" out:titleColor="titleFg" out:borderColor="borderFg"
|
|
4
4
|
out:backgroundColor="bg" flexDirection="column"
|
|
5
5
|
paddingLeft="2" paddingRight="2" paddingTop="1" paddingBottom="1">
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
<helpscroll out="scrollbar" ref="scroll" flexGrow="1" overflow="hidden">
|
|
7
|
+
<box flexDirection="row" overflow="hidden">
|
|
8
|
+
<box out="leftRows" flexDirection="column" flexGrow="1" flexBasis="0" overflow="hidden"></box>
|
|
9
|
+
<box out="rightRows" flexDirection="column" flexGrow="1" flexBasis="0" overflow="hidden" paddingLeft="2"></box>
|
|
10
|
+
</box>
|
|
11
|
+
</helpscroll>
|
|
12
|
+
<box height="1" flexShrink="0" out:border="rule" out:borderColor="ruleFg"></box>
|
|
13
|
+
<box height="1" flexShrink="0" flexDirection="row" overflow="hidden">
|
|
14
|
+
<text out="hintOpen" out:fg="bracketFg" wrapMode="none"></text>
|
|
15
|
+
<text out="hintHead" out:fg="headFg" wrapMode="none"></text>
|
|
16
|
+
<text out="hintKeys" out:fg="keyFg" wrapMode="none"></text>
|
|
17
|
+
<text out="hintClose" out:fg="bracketFg" wrapMode="none"></text>
|
|
18
|
+
</box>
|
|
10
19
|
</box>
|
|
11
20
|
</box>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="help-block">
|
|
2
|
+
<span class="help-section" when="isSection" out="section"></span>
|
|
3
|
+
<span class="help-entry" when="isEntry" role="group">
|
|
4
|
+
<svg class="help-icon ui-icon" when="hasIcon" aria-hidden="true" focusable="false" viewBox="0 0 24 24"><use out:href="iconHref"></use></svg>
|
|
5
|
+
<span class="help-title" out="actionTitle"></span>
|
|
6
|
+
<kbd class="help-key" out="shortcut"></kbd>
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<box height="1" flexDirection="row" flexShrink="0" overflow="hidden">
|
|
2
|
+
<text when="isSection" out="section" out:fg="sectionFg" out:attributes="sectionAttrs" wrapMode="none"></text>
|
|
3
|
+
<text when="isEntry" out="open" out:fg="bracketFg" wrapMode="none"></text>
|
|
4
|
+
<text when="isEntry" out="head" out:fg="headFg" wrapMode="none"></text>
|
|
5
|
+
<text when="isEntry" out="keys" out:fg="keyFg" wrapMode="none"></text>
|
|
6
|
+
<text when="isEntry" out="close" out:fg="bracketFg" wrapMode="none"></text>
|
|
7
|
+
</box>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="overlay-list" when="open" role="dialog" aria-modal="true" aria-label="History and trash" on:click="!dialog:overlay-cancel">
|
|
2
|
+
<div class="overlay-dialog" on:click="dialogStop">
|
|
3
|
+
<h2 out="title"></h2>
|
|
4
|
+
<p class="overlay-empty" when="emptyVisible" out="empty"></p>
|
|
5
|
+
<ul class="overlay-rows" out="rows"></ul>
|
|
6
|
+
<footer out="footer"></footer>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|