@vibe-x/dsh-html-workbench 0.1.2 → 0.2.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/lib/client.js +531 -126
- package/package.json +1 -1
- package/src/client.js +521 -123
- package/src/host.js +25 -0
package/lib/client.js
CHANGED
|
@@ -6,11 +6,15 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
|
|
7
7
|
// Closure symbols — the same names the dynamic runner injects.
|
|
8
8
|
const React = require('react');
|
|
9
|
+
// Replace-on-insert (never bail when the id already exists): a stale
|
|
10
|
+
// stylesheet left over from a previous plugin generation would otherwise
|
|
11
|
+
// silently win and break the layout while the JS behaves correctly.
|
|
9
12
|
const styles = {
|
|
10
13
|
insert(css) {
|
|
11
14
|
if (typeof document === 'undefined') return () => {};
|
|
12
15
|
const id = 'html-workbench-dsh-plugin-styles';
|
|
13
|
-
|
|
16
|
+
const prev = document.getElementById(id);
|
|
17
|
+
if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
|
|
14
18
|
const el = document.createElement('style');
|
|
15
19
|
el.id = id;
|
|
16
20
|
el.textContent = css;
|
|
@@ -21,8 +25,11 @@ window.__ModuleLoader__.load({
|
|
|
21
25
|
const host = {
|
|
22
26
|
call(method, args) {
|
|
23
27
|
const a = args || {};
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
const params = Object.keys(a)
|
|
29
|
+
.filter((k) => a[k] !== undefined && a[k] !== null)
|
|
30
|
+
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(String(a[k])))
|
|
31
|
+
.join('&');
|
|
32
|
+
const url = '/html-workbench/' + method + (params ? '?' + params : '');
|
|
26
33
|
return fetch(url).then((r) => r.json());
|
|
27
34
|
},
|
|
28
35
|
};
|
|
@@ -38,10 +45,17 @@ window.__ModuleLoader__.load({
|
|
|
38
45
|
*
|
|
39
46
|
* Responsibilities (runs in the browser):
|
|
40
47
|
* - Register a right-side panel + corner trigger in `shell.overlay`.
|
|
41
|
-
* -
|
|
42
|
-
*
|
|
48
|
+
* - Layout is browser-like: a two-row chrome at the TOP (identity row +
|
|
49
|
+
* address/toolbar row) and the preview filling everything below. Nothing
|
|
50
|
+
* sits at the bottom, so the panel never visually competes with the chat
|
|
51
|
+
* composer on the left.
|
|
43
52
|
* - Open a file by calling Host `open`, then embed the workbench URL in an
|
|
44
|
-
* iframe. Left edge is draggable to resize the panel width.
|
|
53
|
+
* iframe. Left edge is draggable to resize the panel width (persisted).
|
|
54
|
+
*
|
|
55
|
+
* Styling contract: every colour/shadow comes from the host's `--dsw-alias-*`
|
|
56
|
+
* design tokens (see the DSH first-party plugins) so light/dark themes and
|
|
57
|
+
* future re-skins are inherited for free. No hard-coded palette, no magic
|
|
58
|
+
* offsets — all overlays are laid out with flex/grid.
|
|
45
59
|
*/
|
|
46
60
|
|
|
47
61
|
return {
|
|
@@ -50,66 +64,213 @@ return {
|
|
|
50
64
|
const slots = ctx.get('slots')
|
|
51
65
|
if (slots === undefined) return
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
// ── Styles ───────────────────────────────────────────────────────────────
|
|
68
|
+
// NOTE: both style shims (dynamic runner + static bundle) de-duplicate by a
|
|
69
|
+
// fixed element id and BAIL OUT when it already exists. During iterative
|
|
70
|
+
// development that silently keeps a STALE stylesheet alive — the symptom is
|
|
71
|
+
// "functionality works but the layout is broken" (e.g. an absolutely
|
|
72
|
+
// positioned chevron collapsing into a block below the input). So manage the
|
|
73
|
+
// <style> element here: drop every previous generation, then insert fresh.
|
|
74
|
+
const STYLE_MARK = 'data-hwb-styles'
|
|
75
|
+
const STYLE_VERSION = '2'
|
|
76
|
+
|
|
77
|
+
const CSS = `
|
|
78
|
+
html #root {
|
|
79
|
+
margin-right: calc(var(--dsh-sidebar-width, 0px) + var(--hwb-panel-width, 0px));
|
|
80
|
+
transition: margin-right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease);
|
|
81
|
+
}
|
|
55
82
|
body[data-hwb-dragging] #root { transition: none; }
|
|
56
|
-
body[data-hwb-dragging] { user-select: none; }
|
|
83
|
+
body[data-hwb-dragging] { user-select: none; cursor: col-resize; }
|
|
84
|
+
|
|
85
|
+
/* Reserve right-side clearance in the session header so the corner trigger
|
|
86
|
+
never overlaps its right-aligned utilities (e.g. "Session log"). The
|
|
87
|
+
clearance relaxes as the panel opens, because the trigger then hides. */
|
|
88
|
+
header:has([data-slot="conversation.session.header.utilities"]) {
|
|
89
|
+
padding-right: max(28px, calc(60px - var(--hwb-panel-width, 0px)));
|
|
90
|
+
transition: padding-right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease);
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
.hwb-panel, .hwb-panel * { box-sizing: border-box; }
|
|
58
|
-
.hwb-panel {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.hwb-
|
|
73
|
-
.hwb-
|
|
74
|
-
.hwb-
|
|
75
|
-
.hwb-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.hwb-
|
|
79
|
-
.hwb-
|
|
80
|
-
.hwb-
|
|
81
|
-
.hwb-
|
|
82
|
-
.hwb-
|
|
83
|
-
.hwb-
|
|
84
|
-
.hwb-
|
|
85
|
-
.hwb-
|
|
86
|
-
.hwb-
|
|
87
|
-
.hwb-
|
|
88
|
-
.hwb-
|
|
89
|
-
.hwb-
|
|
90
|
-
.hwb-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.hwb-
|
|
95
|
-
.hwb-
|
|
96
|
-
|
|
94
|
+
.hwb-panel {
|
|
95
|
+
position: fixed; top: 0; right: var(--dsh-sidebar-width, 0px); bottom: 0;
|
|
96
|
+
display: flex; flex-direction: column; min-width: 0;
|
|
97
|
+
background: var(--dsw-alias-bg-base);
|
|
98
|
+
color: var(--dsw-alias-label-primary);
|
|
99
|
+
border-left: 1px solid var(--dsw-alias-border-l1);
|
|
100
|
+
box-shadow: var(--dsw-shadow-lv2);
|
|
101
|
+
z-index: 9999; pointer-events: auto;
|
|
102
|
+
font-family: var(--dsw-font-family, ui-sans-serif, system-ui, sans-serif);
|
|
103
|
+
font-size: 13px; line-height: 1.5;
|
|
104
|
+
--hwb-mono: var(--dsh-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Resize handle — a wide invisible hit area with a thin visible rail. */
|
|
108
|
+
.hwb-resize { position: absolute; left: -4px; top: 0; bottom: 0; width: 9px; z-index: 5; cursor: col-resize; touch-action: none; border: none; padding: 0; background: transparent; }
|
|
109
|
+
.hwb-resize::after { content: ""; position: absolute; left: 4px; top: 0; bottom: 0; width: 2px; border-radius: 2px; background: transparent; transition: background 150ms ease; }
|
|
110
|
+
.hwb-resize:hover::after, .hwb-resize:focus-visible::after, .hwb-resize[data-active]::after { background: var(--dsw-alias-interactive-bg-hover-accent); }
|
|
111
|
+
.hwb-resize:focus-visible { outline: none; }
|
|
112
|
+
|
|
113
|
+
/* ── Chrome: identity row + toolbar row ───────────────────────────────────── */
|
|
114
|
+
.hwb-chrome { flex: none; display: flex; flex-direction: column; background: var(--dsw-alias-bg-layer-1); border-bottom: 1px solid var(--dsw-alias-border-l2); }
|
|
115
|
+
.hwb-idrow { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 8px 0 14px; }
|
|
116
|
+
.hwb-brand { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
117
|
+
.hwb-dot { position: relative; width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--dsw-alias-label-dimmed, var(--dsw-alias-label-tertiary)); box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 0%, transparent); }
|
|
118
|
+
.hwb-dot[data-on] { background: var(--dsw-alias-state-success-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary) 16%, transparent); }
|
|
119
|
+
.hwb-dot[data-off] { background: var(--dsw-alias-state-error-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent); }
|
|
120
|
+
.hwb-name { font-weight: 600; font-size: 13px; white-space: nowrap; }
|
|
121
|
+
.hwb-sep { width: 1px; height: 14px; flex: none; background: var(--dsw-alias-border-l1); }
|
|
122
|
+
.hwb-filechip { display: flex; align-items: center; gap: 6px; min-width: 0; height: 22px; padding: 0 8px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-interactive-bg-hover)); color: var(--dsw-alias-label-secondary); font-size: 11.5px; }
|
|
123
|
+
.hwb-filechip > svg { flex: none; opacity: .7; }
|
|
124
|
+
.hwb-filechip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--hwb-mono); }
|
|
125
|
+
.hwb-spacer { flex: 1 1 auto; min-width: 8px; }
|
|
126
|
+
.hwb-actions { display: flex; align-items: center; gap: 2px; flex: none; }
|
|
127
|
+
|
|
128
|
+
.hwb-icon { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; flex: none; border: none; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; transition: background 120ms ease, color 120ms ease; }
|
|
129
|
+
.hwb-icon:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
130
|
+
.hwb-icon:disabled { opacity: .4; cursor: default; }
|
|
131
|
+
.hwb-icon:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #4d6bfe); outline-offset: 1px; }
|
|
132
|
+
.hwb-icon[data-spin] > svg { animation: hwb-spin 800ms linear infinite; }
|
|
133
|
+
|
|
134
|
+
.hwb-toolbar { display: flex; align-items: center; gap: 8px; padding: 0 10px 10px; }
|
|
135
|
+
|
|
136
|
+
/* Combobox field: input + status + chevron composed with flex — no absolute
|
|
137
|
+
positioning, so nothing can escape the field box. */
|
|
138
|
+
.hwb-fieldwrap { position: relative; flex: 1 1 auto; min-width: 0; }
|
|
139
|
+
.hwb-field { display: flex; align-items: center; gap: 2px; height: 32px; padding: 0 2px 0 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 9px; background: var(--dsw-alias-bg-base); transition: border-color 150ms ease, box-shadow 150ms ease; }
|
|
140
|
+
.hwb-field:hover { border-color: var(--dsw-alias-border-l1); }
|
|
141
|
+
.hwb-field[data-focus] { border-color: var(--dsw-alias-state-business-primary, #4d6bfe); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-business-primary, #4d6bfe) 16%, transparent); }
|
|
142
|
+
.hwb-input { flex: 1 1 auto; min-width: 0; height: 100%; border: none; outline: none; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; font-family: var(--hwb-mono); }
|
|
143
|
+
.hwb-input::placeholder { color: var(--dsw-alias-label-tertiary); font-family: var(--dsw-font-family, ui-sans-serif, system-ui, sans-serif); }
|
|
144
|
+
.hwb-state { display: grid; place-items: center; width: 20px; height: 20px; flex: none; }
|
|
145
|
+
.hwb-state[data-state="exists"] { color: var(--dsw-alias-state-success-primary); }
|
|
146
|
+
.hwb-state[data-state="missing"] { color: var(--dsw-alias-state-error-primary); }
|
|
147
|
+
.hwb-state[data-state="invalid"] { color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-warn-label)); }
|
|
148
|
+
.hwb-state[data-state="checking"] { color: var(--dsw-alias-label-tertiary); }
|
|
149
|
+
.hwb-state[data-state="checking"] > svg { animation: hwb-spin 800ms linear infinite; }
|
|
150
|
+
.hwb-caret { display: grid; place-items: center; width: 26px; height: 26px; flex: none; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; transition: background 120ms ease, color 120ms ease; }
|
|
151
|
+
.hwb-caret:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
152
|
+
.hwb-caret > svg { transition: transform 180ms ease; }
|
|
153
|
+
.hwb-caret[aria-expanded="true"] > svg { transform: rotate(180deg); }
|
|
154
|
+
|
|
155
|
+
.hwb-btn { flex: none; height: 32px; padding: 0 14px; border-radius: 9px; border: 1px solid transparent; font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; transition: background 120ms ease, opacity 120ms ease; }
|
|
156
|
+
.hwb-btn-primary { background: var(--dsw-alias-button-primary-fill, #4d6bfe); color: var(--dsw-alias-button-primary-label, #fff); }
|
|
157
|
+
.hwb-btn-primary:hover:not(:disabled) { opacity: .88; }
|
|
158
|
+
.hwb-btn:disabled { opacity: .45; cursor: default; }
|
|
159
|
+
.hwb-btn:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #4d6bfe); outline-offset: 2px; }
|
|
160
|
+
|
|
161
|
+
/* Dropdown opens DOWNWARD (the field lives at the top of the panel). */
|
|
162
|
+
.hwb-menu { position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 30; display: flex; flex-direction: column; max-height: min(340px, 50vh); overflow: hidden; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); box-shadow: var(--dsw-shadow-lv2); animation: hwb-pop 140ms var(--ds-ease-in-out, ease); }
|
|
163
|
+
.hwb-menu-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-tertiary); font-size: 11px; }
|
|
164
|
+
.hwb-menu-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px; }
|
|
165
|
+
.hwb-menu-empty { padding: 22px 16px; color: var(--dsw-alias-label-tertiary); text-align: center; font-size: 12px; line-height: 1.7; }
|
|
166
|
+
|
|
167
|
+
.hwb-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 8px; border: none; border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
|
|
168
|
+
.hwb-item:hover, .hwb-item:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
|
|
169
|
+
.hwb-item[data-active] { background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover)); }
|
|
170
|
+
.hwb-item > svg { flex: none; color: var(--dsw-alias-label-tertiary); }
|
|
171
|
+
.hwb-item-main { flex: 1 1 auto; min-width: 0; display: block; }
|
|
172
|
+
.hwb-item-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
|
173
|
+
.hwb-item-path { display: block; margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary); font-size: 11px; font-family: var(--hwb-mono); }
|
|
174
|
+
.hwb-tag { flex: none; padding: 1px 6px; border-radius: 5px; font-size: 10px; font-weight: 600; }
|
|
175
|
+
.hwb-tag[data-kind="create"] { background: var(--dsw-alias-state-success-tertiary, color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)); color: var(--dsw-alias-state-success-primary); }
|
|
176
|
+
.hwb-tag[data-kind="edit"] { background: var(--dsw-alias-state-warn-tertiary, color-mix(in srgb, var(--dsw-alias-state-warning-primary, #b7791f) 14%, transparent)); color: var(--dsw-alias-state-warn-label, var(--dsw-alias-state-warning-primary, #b7791f)); }
|
|
177
|
+
|
|
178
|
+
/* ── Banner ───────────────────────────────────────────────────────────────── */
|
|
179
|
+
.hwb-banner { flex: none; display: flex; align-items: flex-start; gap: 8px; padding: 9px 10px 9px 14px; border-bottom: 1px solid var(--dsw-alias-border-l2); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); color: var(--dsw-alias-state-error-primary); font-size: 12px; }
|
|
180
|
+
.hwb-banner > svg { flex: none; margin-top: 1px; }
|
|
181
|
+
.hwb-banner-text { flex: 1 1 auto; min-width: 0; word-break: break-word; }
|
|
182
|
+
|
|
183
|
+
/* ── Body ─────────────────────────────────────────────────────────────────── */
|
|
184
|
+
.hwb-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-base); }
|
|
185
|
+
.hwb-frame { flex: 1 1 auto; width: 100%; border: 0; background: #fff; }
|
|
186
|
+
|
|
187
|
+
.hwb-center { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; padding: 24px; overflow-y: auto; text-align: center; }
|
|
188
|
+
.hwb-blank { display: grid; place-items: center; width: 46px; height: 46px; border-radius: 14px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-tertiary); }
|
|
189
|
+
.hwb-blank-title { font-size: 13.5px; font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
190
|
+
.hwb-blank-desc { max-width: 320px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 1.7; }
|
|
191
|
+
.hwb-recent { width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: 4px; margin-top: 2px; text-align: left; }
|
|
192
|
+
.hwb-recent-label { padding: 0 8px 2px; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
|
|
193
|
+
.hwb-spinner { width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--dsw-alias-border-l1); border-top-color: var(--dsw-alias-state-business-primary, #4d6bfe); animation: hwb-spin 700ms linear infinite; }
|
|
97
194
|
|
|
195
|
+
/* ── Corner trigger ───────────────────────────────────────────────────────── */
|
|
196
|
+
.hwb-trigger {
|
|
197
|
+
position: fixed; top: 8px;
|
|
198
|
+
right: calc(var(--dsh-sidebar-width, 0px) + var(--hwb-panel-width, 0px) + 12px);
|
|
199
|
+
z-index: 10000; display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
|
|
200
|
+
border: none; border-radius: 8px; background: transparent;
|
|
201
|
+
color: var(--dsw-alias-label-secondary); cursor: pointer; pointer-events: auto;
|
|
202
|
+
transition: right var(--ds-transition-duration-slow, 200ms) var(--ds-ease-in-out, ease), background 120ms ease, color 120ms ease;
|
|
203
|
+
}
|
|
204
|
+
.hwb-trigger:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
205
|
+
.hwb-trigger:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #4d6bfe); outline-offset: 1px; }
|
|
206
|
+
|
|
207
|
+
@keyframes hwb-spin { to { transform: rotate(360deg); } }
|
|
208
|
+
@keyframes hwb-pop { from { opacity: 0; transform: translateY(-4px); } }
|
|
209
|
+
@media (prefers-reduced-motion: reduce) {
|
|
210
|
+
html #root, .hwb-trigger, header:has([data-slot="conversation.session.header.utilities"]) { transition: none; }
|
|
211
|
+
.hwb-menu { animation: none; }
|
|
212
|
+
}
|
|
213
|
+
`
|
|
214
|
+
|
|
215
|
+
const applyStyles = () => {
|
|
216
|
+
if (typeof document === 'undefined') { try { styles.insert(CSS) } catch (e) {} return () => {} }
|
|
217
|
+
const stale = document.querySelectorAll('style[' + STYLE_MARK + '], style#html-workbench-dsh-plugin-styles')
|
|
218
|
+
for (let i = 0; i < stale.length; i += 1) {
|
|
219
|
+
const node = stale[i]
|
|
220
|
+
if (node.parentNode) node.parentNode.removeChild(node)
|
|
221
|
+
}
|
|
222
|
+
const el = document.createElement('style')
|
|
223
|
+
el.setAttribute(STYLE_MARK, STYLE_VERSION)
|
|
224
|
+
el.textContent = CSS
|
|
225
|
+
document.head.appendChild(el)
|
|
226
|
+
return () => { if (el.parentNode) el.parentNode.removeChild(el) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof ctx.effect === 'function') ctx.effect(applyStyles, 'html-workbench: styles')
|
|
230
|
+
else applyStyles()
|
|
231
|
+
|
|
232
|
+
// ── Store ────────────────────────────────────────────────────────────────
|
|
98
233
|
const DEFAULT_WIDTH = 820
|
|
99
|
-
const MIN_WIDTH =
|
|
234
|
+
const MIN_WIDTH = 460
|
|
235
|
+
const WIDTH_KEY = 'hwb:panel-width'
|
|
236
|
+
|
|
237
|
+
const readWidth = () => {
|
|
238
|
+
try {
|
|
239
|
+
const raw = window.localStorage.getItem(WIDTH_KEY)
|
|
240
|
+
const n = raw == null ? NaN : parseInt(raw, 10)
|
|
241
|
+
return Number.isFinite(n) && n >= MIN_WIDTH ? n : DEFAULT_WIDTH
|
|
242
|
+
} catch (e) { return DEFAULT_WIDTH }
|
|
243
|
+
}
|
|
244
|
+
const writeWidth = (w) => { try { window.localStorage.setItem(WIDTH_KEY, String(w)) } catch (e) {} }
|
|
245
|
+
|
|
246
|
+
let resolveTimer = null
|
|
100
247
|
|
|
101
248
|
const store = {
|
|
102
249
|
open: false,
|
|
103
|
-
panelWidth:
|
|
104
|
-
expanded: false,
|
|
250
|
+
panelWidth: readWidth(),
|
|
105
251
|
assets: [],
|
|
106
252
|
running: false,
|
|
107
253
|
current: null,
|
|
254
|
+
nonce: 0,
|
|
108
255
|
loading: false,
|
|
256
|
+
refreshing: false,
|
|
109
257
|
error: null,
|
|
258
|
+
pathInput: '',
|
|
259
|
+
resolveState: 'idle',
|
|
110
260
|
listeners: [],
|
|
111
261
|
subscribe(fn) { this.listeners.push(fn); return () => { this.listeners = this.listeners.filter((f) => f !== fn) } },
|
|
112
|
-
set(
|
|
262
|
+
set(patch) {
|
|
263
|
+
// Only notify when something actually changed: the panel polls `list`
|
|
264
|
+
// every few seconds, and blindly re-rendering on every tick makes the
|
|
265
|
+
// input feel laggy and needlessly churns the preview subtree.
|
|
266
|
+
let dirty = false
|
|
267
|
+
const keys = Object.keys(patch)
|
|
268
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
269
|
+
const k = keys[i]
|
|
270
|
+
if (this[k] !== patch[k]) { this[k] = patch[k]; dirty = true }
|
|
271
|
+
}
|
|
272
|
+
if (dirty) this.listeners.forEach((fn) => { try { fn() } catch (e) {} })
|
|
273
|
+
},
|
|
113
274
|
}
|
|
114
275
|
|
|
115
276
|
const useStore = () => {
|
|
@@ -119,117 +280,364 @@ body[data-hwb-dragging] { user-select: none; }
|
|
|
119
280
|
}
|
|
120
281
|
|
|
121
282
|
const basename = (p) => {
|
|
122
|
-
const parts = String(p).split('/')
|
|
123
|
-
return parts[parts.length - 1] || p
|
|
283
|
+
const parts = String(p || '').split('/')
|
|
284
|
+
return parts[parts.length - 1] || String(p || '')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const dirname = (p) => {
|
|
288
|
+
const s = String(p || '')
|
|
289
|
+
const i = s.lastIndexOf('/')
|
|
290
|
+
return i <= 0 ? '/' : s.slice(0, i)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Paths are long and their INFORMATIVE half is the tail, so clip the head
|
|
294
|
+
// (CSS ellipsis would eat the tail). The CSS ellipsis stays as a backstop
|
|
295
|
+
// for narrow panels.
|
|
296
|
+
const clipHead = (s, max) => {
|
|
297
|
+
const str = String(s || '')
|
|
298
|
+
return str.length <= max ? str : '…' + str.slice(str.length - max + 1)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const sameAssets = (a, b) => {
|
|
302
|
+
if (a === b) return true
|
|
303
|
+
if (!a || !b || a.length !== b.length) return false
|
|
304
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
305
|
+
if (a[i].path !== b[i].path || a[i].kind !== b[i].kind || a[i].seq !== b[i].seq) return false
|
|
306
|
+
}
|
|
307
|
+
return true
|
|
124
308
|
}
|
|
125
309
|
|
|
126
|
-
const refresh = () => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
310
|
+
const refresh = (explicit) => {
|
|
311
|
+
if (explicit) store.set({ refreshing: true })
|
|
312
|
+
return host.call('list').then((res) => {
|
|
313
|
+
if (res && res.ok) {
|
|
314
|
+
const next = res.assets || []
|
|
315
|
+
store.set({
|
|
316
|
+
assets: sameAssets(store.assets, next) ? store.assets : next,
|
|
317
|
+
running: !!res.running,
|
|
318
|
+
})
|
|
319
|
+
} else {
|
|
320
|
+
store.set({ error: (res && res.error) || 'list failed' })
|
|
321
|
+
}
|
|
130
322
|
}).catch((e) => store.set({ error: String(e && e.message ? e.message : e) }))
|
|
323
|
+
.then(() => { if (explicit) store.set({ refreshing: false }) })
|
|
131
324
|
}
|
|
132
325
|
|
|
133
326
|
const openFile = (path) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
327
|
+
const file = String(path || '').trim()
|
|
328
|
+
if (!file) return
|
|
329
|
+
store.set({ loading: true, error: null, pathInput: file })
|
|
330
|
+
host.call('open', { file: file }).then((res) => {
|
|
331
|
+
if (res && res.ok) {
|
|
332
|
+
// Bump the nonce so re-opening the SAME file remounts the iframe:
|
|
333
|
+
// with an unchanged `src` the browser would otherwise keep the old
|
|
334
|
+
// document and "打开" would look like a no-op.
|
|
335
|
+
store.set({
|
|
336
|
+
loading: false,
|
|
337
|
+
running: true,
|
|
338
|
+
resolveState: 'exists',
|
|
339
|
+
current: { path: file, url: res.url },
|
|
340
|
+
nonce: store.nonce + 1,
|
|
341
|
+
})
|
|
342
|
+
} else {
|
|
343
|
+
store.set({ loading: false, error: (res && res.error) || 'open failed' })
|
|
344
|
+
}
|
|
138
345
|
}).catch((e) => store.set({ loading: false, error: String(e && e.message ? e.message : e) }))
|
|
139
346
|
}
|
|
140
347
|
|
|
348
|
+
const STATE_TITLE = {
|
|
349
|
+
exists: '文件存在',
|
|
350
|
+
missing: '未找到该文件',
|
|
351
|
+
invalid: '仅支持 .html / .htm 文件',
|
|
352
|
+
checking: '检测中…',
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const checkPath = (value) => {
|
|
356
|
+
const trimmed = (value || '').trim()
|
|
357
|
+
if (resolveTimer) { clearTimeout(resolveTimer); resolveTimer = null }
|
|
358
|
+
if (!trimmed) { store.set({ resolveState: 'idle' }); return }
|
|
359
|
+
store.set({ resolveState: 'checking' })
|
|
360
|
+
resolveTimer = setTimeout(() => {
|
|
361
|
+
resolveTimer = null
|
|
362
|
+
host.call('resolve', { file: trimmed }).then((res) => {
|
|
363
|
+
if (!res || res.ok === false) { store.set({ resolveState: 'idle' }); return }
|
|
364
|
+
if (!res.isHtml) store.set({ resolveState: 'invalid' })
|
|
365
|
+
else if (res.exists === true) store.set({ resolveState: 'exists' })
|
|
366
|
+
else if (res.exists === false) store.set({ resolveState: 'missing' })
|
|
367
|
+
else store.set({ resolveState: 'idle' })
|
|
368
|
+
}).catch(() => store.set({ resolveState: 'idle' }))
|
|
369
|
+
}, 300)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Icons ────────────────────────────────────────────────────────────────
|
|
373
|
+
const icon = (size, children, extra) => {
|
|
374
|
+
const props = {
|
|
375
|
+
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
|
376
|
+
stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round',
|
|
377
|
+
strokeLinejoin: 'round', 'aria-hidden': true,
|
|
378
|
+
}
|
|
379
|
+
if (extra) Object.assign(props, extra)
|
|
380
|
+
return React.createElement('svg', props, children)
|
|
381
|
+
}
|
|
382
|
+
const path = (d, key) => React.createElement('path', { d: d, key: key })
|
|
383
|
+
|
|
384
|
+
const I_CLOSE = icon(15, [path('M18 6 6 18', 'a'), path('m6 6 12 12', 'b')])
|
|
385
|
+
const I_REFRESH = icon(15, [path('M21 12a9 9 0 1 1-3.2-6.9', 'a'), path('M21 4v5h-5', 'b')])
|
|
386
|
+
const I_EXTERNAL = icon(15, [path('M15 3h6v6', 'a'), path('M10 14 21 3', 'b'), path('M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5', 'c')])
|
|
387
|
+
const I_CARET = icon(14, [path('m6 9 6 6 6-6', 'a')])
|
|
388
|
+
const I_CHECK = icon(14, [path('M20 6 9 17l-5-5', 'a')], { strokeWidth: 2.2 })
|
|
389
|
+
const I_MISSING = icon(14, [path('M18 6 6 18', 'a'), path('m6 6 12 12', 'b')], { strokeWidth: 2.2 })
|
|
390
|
+
const I_WARN = icon(14, [path('M12 9v4', 'a'), path('M12 17h.01', 'b'), path('M10.3 3.9 2.4 17.5A1.8 1.8 0 0 0 4 20.2h16a1.8 1.8 0 0 0 1.6-2.7L13.7 3.9a1.8 1.8 0 0 0-3.4 0Z', 'c')])
|
|
391
|
+
const I_LOADING = icon(14, [React.createElement('circle', { cx: 12, cy: 12, r: 9, strokeOpacity: .25, key: 'a' }), path('M21 12a9 9 0 0 0-9-9', 'b')], { strokeWidth: 2.2 })
|
|
392
|
+
const I_FILE = icon(14, [path('M14 3v5h5', 'a'), path('M15 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Z', 'b')])
|
|
393
|
+
const I_ALERT = icon(14, [React.createElement('circle', { cx: 12, cy: 12, r: 9, key: 'a' }), path('M12 8v4', 'b'), path('M12 16h.01', 'c')])
|
|
394
|
+
const I_BLANK = icon(22, [path('M14 3v5h5', 'a'), path('M15 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Z', 'b'), path('m9 15 1.8-2.2L12.4 15l1.4-1.8', 'c')])
|
|
395
|
+
const I_TRIGGER = icon(17, [
|
|
396
|
+
React.createElement('rect', { x: 2.5, y: 4, width: 19, height: 14, rx: 2.4, key: 'a' }),
|
|
397
|
+
path('M2.5 8.4h19', 'b'),
|
|
398
|
+
path('M8 21h8', 'c'),
|
|
399
|
+
path('M12 18v3', 'd'),
|
|
400
|
+
])
|
|
401
|
+
|
|
402
|
+
const STATE_ICON = { exists: I_CHECK, missing: I_MISSING, invalid: I_WARN, checking: I_LOADING }
|
|
403
|
+
|
|
404
|
+
// ── Resize ───────────────────────────────────────────────────────────────
|
|
141
405
|
const startResize = (e) => {
|
|
406
|
+
if (e.button !== undefined && e.button !== 0) return
|
|
142
407
|
e.preventDefault()
|
|
408
|
+
const handle = e.currentTarget
|
|
409
|
+
handle.setAttribute('data-active', '')
|
|
143
410
|
document.body.setAttribute('data-hwb-dragging', '')
|
|
144
|
-
const maxW = Math.max(MIN_WIDTH, window.innerWidth -
|
|
411
|
+
const maxW = () => Math.max(MIN_WIDTH, window.innerWidth - 80)
|
|
145
412
|
const onMove = (ev) => {
|
|
146
|
-
const w = window.innerWidth - ev.clientX
|
|
147
|
-
store.set({ panelWidth: Math.max(MIN_WIDTH, Math.min(w, maxW)) })
|
|
413
|
+
const w = Math.round(window.innerWidth - ev.clientX)
|
|
414
|
+
store.set({ panelWidth: Math.max(MIN_WIDTH, Math.min(w, maxW())) })
|
|
148
415
|
}
|
|
149
416
|
const onUp = () => {
|
|
417
|
+
handle.removeAttribute('data-active')
|
|
150
418
|
document.body.removeAttribute('data-hwb-dragging')
|
|
151
419
|
document.removeEventListener('mousemove', onMove)
|
|
152
420
|
document.removeEventListener('mouseup', onUp)
|
|
421
|
+
writeWidth(store.panelWidth)
|
|
153
422
|
}
|
|
154
423
|
document.addEventListener('mousemove', onMove)
|
|
155
424
|
document.addEventListener('mouseup', onUp)
|
|
156
425
|
}
|
|
157
426
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
427
|
+
const nudgeWidth = (delta) => {
|
|
428
|
+
const maxW = Math.max(MIN_WIDTH, window.innerWidth - 80)
|
|
429
|
+
const w = Math.max(MIN_WIDTH, Math.min(store.panelWidth + delta, maxW))
|
|
430
|
+
store.set({ panelWidth: w })
|
|
431
|
+
writeWidth(w)
|
|
432
|
+
}
|
|
162
433
|
|
|
434
|
+
// ── Panel ────────────────────────────────────────────────────────────────
|
|
163
435
|
const Panel = () => {
|
|
164
436
|
const s = useStore()
|
|
437
|
+
const [menuOpen, setMenuOpen] = React.useState(false)
|
|
438
|
+
const [focused, setFocused] = React.useState(false)
|
|
439
|
+
const fieldRef = React.useRef(null)
|
|
440
|
+
const inputRef = React.useRef(null)
|
|
441
|
+
|
|
165
442
|
React.useEffect(() => {
|
|
166
443
|
const root = document.documentElement
|
|
167
|
-
root.style.setProperty('--hwb-panel-width', s.open ?
|
|
444
|
+
root.style.setProperty('--hwb-panel-width', s.open ? s.panelWidth + 'px' : '0px')
|
|
168
445
|
return () => { root.style.setProperty('--hwb-panel-width', '0px') }
|
|
169
446
|
}, [s.open, s.panelWidth])
|
|
447
|
+
|
|
170
448
|
React.useEffect(() => {
|
|
171
|
-
if (!s.open) return
|
|
449
|
+
if (!s.open) return undefined
|
|
172
450
|
refresh()
|
|
173
|
-
const dispose = ctx.interval(refresh,
|
|
451
|
+
const dispose = ctx.interval(refresh, 4000)
|
|
174
452
|
return () => { if (dispose) dispose() }
|
|
175
453
|
}, [s.open])
|
|
454
|
+
|
|
455
|
+
// Drop any in-flight path check when the panel closes.
|
|
456
|
+
React.useEffect(() => () => { if (resolveTimer) { clearTimeout(resolveTimer); resolveTimer = null } }, [])
|
|
457
|
+
|
|
458
|
+
React.useEffect(() => {
|
|
459
|
+
if (!menuOpen) return undefined
|
|
460
|
+
const onDown = (e) => { if (fieldRef.current && !fieldRef.current.contains(e.target)) setMenuOpen(false) }
|
|
461
|
+
const onKey = (e) => { if (e.key === 'Escape') { setMenuOpen(false); if (inputRef.current) inputRef.current.focus() } }
|
|
462
|
+
document.addEventListener('mousedown', onDown)
|
|
463
|
+
document.addEventListener('keydown', onKey)
|
|
464
|
+
return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey) }
|
|
465
|
+
}, [menuOpen])
|
|
466
|
+
|
|
176
467
|
if (!s.open) return null
|
|
177
468
|
|
|
178
469
|
const assets = s.assets || []
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
470
|
+
const trimmed = (s.pathInput || '').trim()
|
|
471
|
+
const currentPath = s.current ? s.current.path : null
|
|
472
|
+
const submit = () => { if (trimmed) { setMenuOpen(false); openFile(trimmed) } }
|
|
473
|
+
const pick = (p) => { setMenuOpen(false); store.set({ pathInput: p }); openFile(p) }
|
|
474
|
+
|
|
475
|
+
const assetRow = (a, inMenu) => React.createElement('button', {
|
|
476
|
+
key: a.id || a.path,
|
|
477
|
+
type: 'button',
|
|
478
|
+
className: 'hwb-item',
|
|
479
|
+
'data-active': a.path === currentPath ? '' : undefined,
|
|
480
|
+
title: a.path,
|
|
481
|
+
onClick: () => pick(a.path),
|
|
482
|
+
},
|
|
483
|
+
I_FILE,
|
|
484
|
+
React.createElement('span', { className: 'hwb-item-main' },
|
|
485
|
+
React.createElement('span', { className: 'hwb-item-name' }, basename(a.path)),
|
|
486
|
+
React.createElement('span', { className: 'hwb-item-path' }, clipHead(dirname(a.path), 52)),
|
|
487
|
+
),
|
|
488
|
+
inMenu && a.kind ? React.createElement('span', { className: 'hwb-tag', 'data-kind': a.kind }, a.kind === 'create' ? '新建' : '编辑') : null,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
const body = s.loading
|
|
492
|
+
? React.createElement('div', { className: 'hwb-center' },
|
|
493
|
+
React.createElement('div', { className: 'hwb-spinner' }),
|
|
494
|
+
React.createElement('div', { className: 'hwb-blank-desc' }, '正在启动 Workbench 服务…'),
|
|
189
495
|
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
React.createElement('div', { className: 'hwb-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
React.createElement('path', { d: 'M6 6l12 12' }),
|
|
496
|
+
: s.current && s.current.url
|
|
497
|
+
? React.createElement('iframe', {
|
|
498
|
+
key: 'frame-' + s.nonce,
|
|
499
|
+
className: 'hwb-frame',
|
|
500
|
+
src: s.current.url,
|
|
501
|
+
title: 'HTML Workbench — ' + basename(s.current.path),
|
|
502
|
+
})
|
|
503
|
+
: React.createElement('div', { className: 'hwb-center' },
|
|
504
|
+
React.createElement('div', { className: 'hwb-blank' }, I_BLANK),
|
|
505
|
+
React.createElement('div', null,
|
|
506
|
+
React.createElement('div', { className: 'hwb-blank-title' }, '还没有打开文件'),
|
|
507
|
+
React.createElement('div', { className: 'hwb-blank-desc' }, assets.length
|
|
508
|
+
? '在上方地址栏填入 .html 的绝对路径,或从下面的产物里挑一个开始可视化编辑。'
|
|
509
|
+
: '在上方地址栏填入 .html 的绝对路径开始可视化编辑。代理 write / edit 过的 .html 之后也会自动出现在这里。'),
|
|
205
510
|
),
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
511
|
+
assets.length
|
|
512
|
+
? React.createElement('div', { className: 'hwb-recent' },
|
|
513
|
+
React.createElement('div', { className: 'hwb-recent-label' }, '最近的 HTML 产物'),
|
|
514
|
+
assets.slice(0, 6).map((a) => assetRow(a, false)),
|
|
515
|
+
)
|
|
516
|
+
: null,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return React.createElement('div', {
|
|
520
|
+
className: 'hwb-panel',
|
|
521
|
+
style: { width: s.panelWidth + 'px', maxWidth: 'calc(100vw - 24px)' },
|
|
522
|
+
role: 'complementary',
|
|
523
|
+
'aria-label': 'HTML Workbench',
|
|
524
|
+
},
|
|
525
|
+
React.createElement('div', {
|
|
526
|
+
className: 'hwb-resize',
|
|
527
|
+
role: 'separator',
|
|
528
|
+
'aria-orientation': 'vertical',
|
|
529
|
+
'aria-label': '调整面板宽度',
|
|
530
|
+
tabIndex: 0,
|
|
531
|
+
title: '拖动调整宽度(双击复位)',
|
|
532
|
+
onMouseDown: startResize,
|
|
533
|
+
onDoubleClick: () => { store.set({ panelWidth: DEFAULT_WIDTH }); writeWidth(DEFAULT_WIDTH) },
|
|
534
|
+
onKeyDown: (e) => {
|
|
535
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); nudgeWidth(32) }
|
|
536
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); nudgeWidth(-32) }
|
|
537
|
+
},
|
|
538
|
+
}),
|
|
539
|
+
|
|
540
|
+
React.createElement('div', { className: 'hwb-chrome' },
|
|
541
|
+
React.createElement('div', { className: 'hwb-idrow' },
|
|
542
|
+
React.createElement('div', { className: 'hwb-brand' },
|
|
543
|
+
React.createElement('span', {
|
|
544
|
+
className: 'hwb-dot',
|
|
545
|
+
'data-on': s.running ? '' : undefined,
|
|
546
|
+
'data-off': !s.running ? '' : undefined,
|
|
547
|
+
title: s.running ? '本地服务运行中' : '本地服务未就绪',
|
|
548
|
+
}),
|
|
549
|
+
React.createElement('span', { className: 'hwb-name' }, 'HTML Workbench'),
|
|
222
550
|
),
|
|
223
|
-
React.createElement('span', { className: 'hwb-
|
|
224
|
-
|
|
225
|
-
|
|
551
|
+
currentPath ? React.createElement('span', { className: 'hwb-sep' }) : null,
|
|
552
|
+
currentPath
|
|
553
|
+
? React.createElement('span', { className: 'hwb-filechip', title: currentPath }, I_FILE, React.createElement('span', null, basename(currentPath)))
|
|
554
|
+
: null,
|
|
555
|
+
React.createElement('span', { className: 'hwb-spacer' }),
|
|
556
|
+
React.createElement('div', { className: 'hwb-actions' },
|
|
557
|
+
React.createElement('button', {
|
|
558
|
+
type: 'button', className: 'hwb-icon', title: '刷新产物列表',
|
|
559
|
+
'aria-label': '刷新产物列表', 'data-spin': s.refreshing ? '' : undefined,
|
|
560
|
+
onClick: () => refresh(true),
|
|
561
|
+
}, I_REFRESH),
|
|
562
|
+
React.createElement('button', {
|
|
563
|
+
type: 'button', className: 'hwb-icon', title: '在浏览器标签页中打开',
|
|
564
|
+
'aria-label': '在浏览器标签页中打开', disabled: !(s.current && s.current.url),
|
|
565
|
+
onClick: () => { if (s.current && s.current.url) window.open(s.current.url, '_blank', 'noopener') },
|
|
566
|
+
}, I_EXTERNAL),
|
|
567
|
+
React.createElement('button', {
|
|
568
|
+
type: 'button', className: 'hwb-icon', title: '关闭面板',
|
|
569
|
+
'aria-label': '关闭面板', onClick: () => store.set({ open: false }),
|
|
570
|
+
}, I_CLOSE),
|
|
226
571
|
),
|
|
227
572
|
),
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
573
|
+
|
|
574
|
+
React.createElement('div', { className: 'hwb-toolbar' },
|
|
575
|
+
React.createElement('div', { className: 'hwb-fieldwrap', ref: fieldRef },
|
|
576
|
+
React.createElement('div', { className: 'hwb-field', 'data-focus': focused ? '' : undefined },
|
|
577
|
+
React.createElement('input', {
|
|
578
|
+
ref: inputRef,
|
|
579
|
+
className: 'hwb-input',
|
|
580
|
+
type: 'text',
|
|
581
|
+
placeholder: '/absolute/path/page.html',
|
|
582
|
+
value: s.pathInput,
|
|
583
|
+
spellCheck: false,
|
|
584
|
+
autoComplete: 'off',
|
|
585
|
+
'aria-label': 'HTML 文件绝对路径',
|
|
586
|
+
onChange: (e) => { const v = e.target.value; store.set({ pathInput: v }); checkPath(v) },
|
|
587
|
+
onKeyDown: (e) => {
|
|
588
|
+
if (e.key === 'Enter') submit()
|
|
589
|
+
else if (e.key === 'ArrowDown' && assets.length) { e.preventDefault(); setMenuOpen(true) }
|
|
590
|
+
},
|
|
591
|
+
onFocus: () => { setFocused(true); if (assets.length && !trimmed) setMenuOpen(true) },
|
|
592
|
+
onBlur: () => setFocused(false),
|
|
593
|
+
}),
|
|
594
|
+
s.resolveState !== 'idle'
|
|
595
|
+
? React.createElement('span', {
|
|
596
|
+
className: 'hwb-state',
|
|
597
|
+
'data-state': s.resolveState,
|
|
598
|
+
title: STATE_TITLE[s.resolveState] || '',
|
|
599
|
+
}, STATE_ICON[s.resolveState] || null)
|
|
600
|
+
: null,
|
|
601
|
+
React.createElement('button', {
|
|
602
|
+
type: 'button',
|
|
603
|
+
className: 'hwb-caret',
|
|
604
|
+
'aria-expanded': menuOpen,
|
|
605
|
+
'aria-haspopup': 'listbox',
|
|
606
|
+
title: '历史 HTML 产物' + (assets.length ? '(' + assets.length + ')' : ''),
|
|
607
|
+
onClick: () => setMenuOpen((v) => !v),
|
|
608
|
+
}, I_CARET),
|
|
609
|
+
),
|
|
610
|
+
menuOpen
|
|
611
|
+
? React.createElement('div', { className: 'hwb-menu', role: 'listbox' },
|
|
612
|
+
React.createElement('div', { className: 'hwb-menu-head' },
|
|
613
|
+
React.createElement('span', null, 'HTML 产物'),
|
|
614
|
+
React.createElement('span', null, assets.length ? assets.length + ' 个' : '空'),
|
|
615
|
+
),
|
|
616
|
+
assets.length
|
|
617
|
+
? React.createElement('div', { className: 'hwb-menu-body' }, assets.map((a) => assetRow(a, true)))
|
|
618
|
+
: React.createElement('div', { className: 'hwb-menu-empty' }, '暂无产物', React.createElement('br'), '代理 write / edit 过的 .html 会出现在这里'),
|
|
619
|
+
)
|
|
620
|
+
: null,
|
|
621
|
+
),
|
|
622
|
+
React.createElement('button', {
|
|
623
|
+
type: 'button', className: 'hwb-btn hwb-btn-primary',
|
|
624
|
+
disabled: !trimmed || s.loading, onClick: submit,
|
|
625
|
+
}, '打开'),
|
|
626
|
+
),
|
|
232
627
|
),
|
|
628
|
+
|
|
629
|
+
s.error
|
|
630
|
+
? React.createElement('div', { className: 'hwb-banner', role: 'alert' },
|
|
631
|
+
I_ALERT,
|
|
632
|
+
React.createElement('span', { className: 'hwb-banner-text' }, s.error),
|
|
633
|
+
React.createElement('button', {
|
|
634
|
+
type: 'button', className: 'hwb-icon', title: '忽略', 'aria-label': '忽略',
|
|
635
|
+
onClick: () => store.set({ error: null }),
|
|
636
|
+
}, I_CLOSE),
|
|
637
|
+
)
|
|
638
|
+
: null,
|
|
639
|
+
|
|
640
|
+
React.createElement('div', { className: 'hwb-body' }, body),
|
|
233
641
|
)
|
|
234
642
|
}
|
|
235
643
|
|
|
@@ -240,13 +648,10 @@ body[data-hwb-dragging] { user-select: none; }
|
|
|
240
648
|
type: 'button',
|
|
241
649
|
className: 'hwb-trigger',
|
|
242
650
|
title: 'HTML Workbench',
|
|
243
|
-
'aria-
|
|
651
|
+
'aria-label': '打开 HTML Workbench',
|
|
652
|
+
'aria-expanded': false,
|
|
244
653
|
onClick: () => { store.set({ open: true }); refresh() },
|
|
245
|
-
},
|
|
246
|
-
React.createElement('rect', { x: 2, y: 4, width: 20, height: 14, rx: 2 }),
|
|
247
|
-
React.createElement('path', { d: 'M8 21h8' }),
|
|
248
|
-
React.createElement('path', { d: 'M12 18v3' }),
|
|
249
|
-
))
|
|
654
|
+
}, I_TRIGGER)
|
|
250
655
|
}
|
|
251
656
|
|
|
252
657
|
slots.inject('shell.overlay', () => slots.register(
|