cicy-desktop 2.1.69 → 2.1.71
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/package.json +7 -7
- package/src/backends/homepage-react/assets/index-BpljolQs.js +365 -0
- package/src/backends/homepage-react/assets/{index-BniEbx_j.css → index-C9AZlTew.css} +1 -1
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/local-teams.js +42 -4
- package/src/backends/sidecar-ipc.js +23 -1
- package/src/i18n/locales/en.json +9 -7
- package/src/i18n/locales/zh-CN.json +9 -7
- package/src/sidecar/cicy-code.js +54 -111
- package/src/sidecar/localbin.js +133 -0
- package/src/sidecar/native.js +4 -2
- package/src/sidecar/runtime.js +7 -3
- package/workers/render/src/App.css +156 -10
- package/workers/render/src/App.jsx +254 -39
- package/.env.dev +0 -7
- package/src/backends/homepage-react/assets/index-B8gGhz8B.js +0 -365
- package/workers/render.bak.20260528-2338/DESIGN_v2.md +0 -254
- package/workers/render.bak.20260528-2338/index.html +0 -12
- package/workers/render.bak.20260528-2338/package-lock.json +0 -827
- package/workers/render.bak.20260528-2338/package.json +0 -19
- package/workers/render.bak.20260528-2338/public/_headers +0 -5
- package/workers/render.bak.20260528-2338/public/manifest.json +0 -6
- package/workers/render.bak.20260528-2338/src/App.css +0 -224
- package/workers/render.bak.20260528-2338/src/App.jsx +0 -1028
- package/workers/render.bak.20260528-2338/src/api.js +0 -285
- package/workers/render.bak.20260528-2338/src/cicycode-ops.js +0 -222
- package/workers/render.bak.20260528-2338/src/components/BackendCard.css +0 -299
- package/workers/render.bak.20260528-2338/src/components/BackendCard.jsx +0 -133
- package/workers/render.bak.20260528-2338/src/components/BackendModal.css +0 -161
- package/workers/render.bak.20260528-2338/src/components/BackendModal.jsx +0 -199
- package/workers/render.bak.20260528-2338/src/components/Button.css +0 -72
- package/workers/render.bak.20260528-2338/src/components/Button.jsx +0 -37
- package/workers/render.bak.20260528-2338/src/components/Card.css +0 -42
- package/workers/render.bak.20260528-2338/src/components/Card.jsx +0 -21
- package/workers/render.bak.20260528-2338/src/components/Icon.jsx +0 -30
- package/workers/render.bak.20260528-2338/src/components/Menu.css +0 -55
- package/workers/render.bak.20260528-2338/src/components/Menu.jsx +0 -91
- package/workers/render.bak.20260528-2338/src/components/SidecarBanner.css +0 -79
- package/workers/render.bak.20260528-2338/src/components/SidecarBanner.jsx +0 -84
- package/workers/render.bak.20260528-2338/src/components/StatusChip.css +0 -19
- package/workers/render.bak.20260528-2338/src/components/StatusChip.jsx +0 -31
- package/workers/render.bak.20260528-2338/src/components/Toast.css +0 -31
- package/workers/render.bak.20260528-2338/src/components/Toast.jsx +0 -23
- package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.css +0 -464
- package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.jsx +0 -716
- package/workers/render.bak.20260528-2338/src/dockerInstaller.js +0 -0
- package/workers/render.bak.20260528-2338/src/i18n/en.json +0 -116
- package/workers/render.bak.20260528-2338/src/i18n/fr.json +0 -116
- package/workers/render.bak.20260528-2338/src/i18n/index.js +0 -69
- package/workers/render.bak.20260528-2338/src/i18n/ja.json +0 -116
- package/workers/render.bak.20260528-2338/src/i18n/zh-CN.json +0 -121
- package/workers/render.bak.20260528-2338/src/main.js +0 -475
- package/workers/render.bak.20260528-2338/src/main.jsx +0 -18
- package/workers/render.bak.20260528-2338/src/style.css +0 -275
- package/workers/render.bak.20260528-2338/src/styles/base.css +0 -98
- package/workers/render.bak.20260528-2338/src/styles/tokens.css +0 -90
- package/workers/render.bak.20260528-2338/src/tos.js +0 -72
- package/workers/render.bak.20260528-2338/src/worker.js +0 -40
- package/workers/render.bak.20260528-2338/src/wslInstaller.js +0 -1563
- package/workers/render.bak.20260528-2338/vite.config.js +0 -36
- package/workers/render.bak.20260528-2338/wrangler.toml +0 -17
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import Button from "./Button.jsx";
|
|
4
|
-
import Icon from "./Icon.jsx";
|
|
5
|
-
import { api } from "../api.js";
|
|
6
|
-
import { useT } from "../i18n";
|
|
7
|
-
import "./BackendModal.css";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* <BackendModal mode="add"|"edit" backend={...} open onClose onSaved />
|
|
11
|
-
*
|
|
12
|
-
* Single dialog for both adding and editing a cloud backend.
|
|
13
|
-
* - mode="add": blank fields, "Probe & Add" button
|
|
14
|
-
* - mode="edit": pre-filled, "Save" button (URL probe optional via test btn)
|
|
15
|
-
*
|
|
16
|
-
* Rendered via React portal so it floats above any overflow:hidden ancestor.
|
|
17
|
-
*/
|
|
18
|
-
export default function BackendModal({ mode = "add", backend, open, onClose, onSaved }) {
|
|
19
|
-
const t = useT();
|
|
20
|
-
const isEdit = mode === "edit";
|
|
21
|
-
const isLocal = isEdit && backend?.kind === "local";
|
|
22
|
-
|
|
23
|
-
const [name, setName] = useState("");
|
|
24
|
-
const [url, setUrl] = useState("");
|
|
25
|
-
const [token, setToken] = useState("");
|
|
26
|
-
const [showToken, setShowToken] = useState(false);
|
|
27
|
-
const [status, setStatus] = useState({ kind: "idle", msg: "" });
|
|
28
|
-
const [submitting, setSubmitting] = useState(false);
|
|
29
|
-
|
|
30
|
-
// Reset / hydrate when opening or switching backend
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (!open) return;
|
|
33
|
-
setStatus({ kind: "idle", msg: "" });
|
|
34
|
-
setShowToken(false);
|
|
35
|
-
if (isEdit && backend) {
|
|
36
|
-
setName(backend.name || "");
|
|
37
|
-
setUrl(backend.url || "");
|
|
38
|
-
setToken(backend.token || "");
|
|
39
|
-
} else {
|
|
40
|
-
setName(""); setUrl(""); setToken("");
|
|
41
|
-
}
|
|
42
|
-
}, [open, isEdit, backend]);
|
|
43
|
-
|
|
44
|
-
// Esc closes
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (!open) return;
|
|
47
|
-
const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
|
|
48
|
-
document.addEventListener("keydown", onKey);
|
|
49
|
-
return () => document.removeEventListener("keydown", onKey);
|
|
50
|
-
}, [open, onClose]);
|
|
51
|
-
|
|
52
|
-
if (!open) return null;
|
|
53
|
-
|
|
54
|
-
const submit = async () => {
|
|
55
|
-
// Local edit path: only the display name. URL/token are managed by the
|
|
56
|
-
// app (window-manager.buildLocalUrl reads global.json live). No probe.
|
|
57
|
-
if (isLocal) {
|
|
58
|
-
setSubmitting(true);
|
|
59
|
-
try {
|
|
60
|
-
const saved = await api.backends.update({ id: backend.id, name: name.trim() || backend.name });
|
|
61
|
-
onSaved && onSaved(saved);
|
|
62
|
-
onClose && onClose();
|
|
63
|
-
} catch (e) {
|
|
64
|
-
setStatus({ kind: "err", msg: e.message });
|
|
65
|
-
} finally {
|
|
66
|
-
setSubmitting(false);
|
|
67
|
-
}
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!url.trim()) { setStatus({ kind: "err", msg: t("modal.url_required") }); return; }
|
|
72
|
-
setSubmitting(true);
|
|
73
|
-
setStatus({ kind: "probing", msg: t("modal.probing") });
|
|
74
|
-
try {
|
|
75
|
-
const probe = await api.backends.probe({ url: url.trim(), token: token.trim() });
|
|
76
|
-
if (!probe.ok) {
|
|
77
|
-
setStatus({ kind: "err", msg: probe.error || `HTTP ${probe.statusCode || "?"}` });
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const finalName = name.trim() || (() => { try { return new URL(url).host; } catch { return "Cloud"; } })();
|
|
81
|
-
let saved;
|
|
82
|
-
if (isEdit) {
|
|
83
|
-
saved = await api.backends.update({
|
|
84
|
-
id: backend.id,
|
|
85
|
-
name: finalName,
|
|
86
|
-
url: url.trim(),
|
|
87
|
-
token: token.trim() || "",
|
|
88
|
-
});
|
|
89
|
-
} else {
|
|
90
|
-
saved = await api.backends.add({
|
|
91
|
-
name: finalName,
|
|
92
|
-
url: url.trim(),
|
|
93
|
-
token: token.trim() || undefined,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
onSaved && onSaved(saved);
|
|
97
|
-
onClose && onClose();
|
|
98
|
-
} catch (e) {
|
|
99
|
-
setStatus({ kind: "err", msg: e.message });
|
|
100
|
-
} finally {
|
|
101
|
-
setSubmitting(false);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const onBackdrop = (e) => {
|
|
106
|
-
if (e.target === e.currentTarget) onClose && onClose();
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
return createPortal(
|
|
110
|
-
<div className="modal-backdrop" onClick={onBackdrop}>
|
|
111
|
-
<div className="modal" role="dialog" aria-modal="true">
|
|
112
|
-
<header className="modal__head">
|
|
113
|
-
<h3 className="modal__title">{
|
|
114
|
-
isLocal ? t("modal.edit_local_title")
|
|
115
|
-
: isEdit ? t("modal.edit_title")
|
|
116
|
-
: t("modal.add_title")
|
|
117
|
-
}</h3>
|
|
118
|
-
<button className="modal__close" onClick={onClose} aria-label="Close">
|
|
119
|
-
<Icon name="close" />
|
|
120
|
-
</button>
|
|
121
|
-
</header>
|
|
122
|
-
|
|
123
|
-
<div className="modal__body">
|
|
124
|
-
<label className="modal__field">
|
|
125
|
-
<span className="modal__label">{t("modal.name")}</span>
|
|
126
|
-
<input
|
|
127
|
-
className="modal__input"
|
|
128
|
-
type="text"
|
|
129
|
-
placeholder={t("modal.name_placeholder")}
|
|
130
|
-
value={name}
|
|
131
|
-
onChange={(e) => setName(e.target.value)}
|
|
132
|
-
autoFocus={isLocal || isEdit}
|
|
133
|
-
/>
|
|
134
|
-
</label>
|
|
135
|
-
|
|
136
|
-
<label className="modal__field">
|
|
137
|
-
<span className="modal__label">
|
|
138
|
-
{t("modal.url")} {!isLocal && <span className="modal__required">*</span>}
|
|
139
|
-
{isLocal && <span className="modal__locked">· {t("modal.locked_local")}</span>}
|
|
140
|
-
</span>
|
|
141
|
-
<input
|
|
142
|
-
className="modal__input"
|
|
143
|
-
type="url"
|
|
144
|
-
placeholder="https://cicy.example.com:8008"
|
|
145
|
-
value={isLocal ? "http://127.0.0.1:8008" : url}
|
|
146
|
-
onChange={(e) => setUrl(e.target.value)}
|
|
147
|
-
readOnly={isLocal}
|
|
148
|
-
autoFocus={!isEdit && !isLocal}
|
|
149
|
-
/>
|
|
150
|
-
</label>
|
|
151
|
-
|
|
152
|
-
<label className="modal__field">
|
|
153
|
-
<span className="modal__label">
|
|
154
|
-
{t("modal.token")}
|
|
155
|
-
{isLocal && <span className="modal__locked">· {t("modal.token_local_hint")}</span>}
|
|
156
|
-
</span>
|
|
157
|
-
<div className="modal__token-wrap">
|
|
158
|
-
<input
|
|
159
|
-
className="modal__input modal__token-input"
|
|
160
|
-
type={showToken ? "text" : "password"}
|
|
161
|
-
placeholder={isLocal ? "—" : t("modal.token_placeholder")}
|
|
162
|
-
value={isLocal ? "" : token}
|
|
163
|
-
onChange={(e) => setToken(e.target.value)}
|
|
164
|
-
readOnly={isLocal}
|
|
165
|
-
autoComplete="off"
|
|
166
|
-
spellCheck={false}
|
|
167
|
-
/>
|
|
168
|
-
{!isLocal && (
|
|
169
|
-
<button
|
|
170
|
-
type="button"
|
|
171
|
-
className="modal__token-toggle"
|
|
172
|
-
onClick={() => setShowToken((v) => !v)}
|
|
173
|
-
aria-label={showToken ? t("modal.hide_token") : t("modal.show_token")}
|
|
174
|
-
title={showToken ? t("modal.hide_token") : t("modal.show_token")}
|
|
175
|
-
>
|
|
176
|
-
<Icon name={showToken ? "eye-off" : "eye"} size={16} />
|
|
177
|
-
</button>
|
|
178
|
-
)}
|
|
179
|
-
</div>
|
|
180
|
-
</label>
|
|
181
|
-
|
|
182
|
-
{status.msg && (
|
|
183
|
-
<div className={`modal__status modal__status--${status.kind}`}>
|
|
184
|
-
{status.msg}
|
|
185
|
-
</div>
|
|
186
|
-
)}
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<footer className="modal__foot">
|
|
190
|
-
<Button variant="ghost" onClick={onClose}>{t("modal.cancel")}</Button>
|
|
191
|
-
<Button variant="primary" loading={submitting} onClick={submit}>
|
|
192
|
-
{isEdit ? t("modal.save") : t("modal.probe_add")}
|
|
193
|
-
</Button>
|
|
194
|
-
</footer>
|
|
195
|
-
</div>
|
|
196
|
-
</div>,
|
|
197
|
-
document.body
|
|
198
|
-
);
|
|
199
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
.btn {
|
|
2
|
-
display: inline-flex;
|
|
3
|
-
align-items: center;
|
|
4
|
-
justify-content: center;
|
|
5
|
-
gap: var(--sp-2);
|
|
6
|
-
padding: 0 var(--sp-4);
|
|
7
|
-
border-radius: var(--r-sm);
|
|
8
|
-
font-weight: 500;
|
|
9
|
-
white-space: nowrap;
|
|
10
|
-
user-select: none;
|
|
11
|
-
transition:
|
|
12
|
-
background var(--dur-fast) var(--ease),
|
|
13
|
-
color var(--dur-fast) var(--ease),
|
|
14
|
-
border-color var(--dur-fast) var(--ease),
|
|
15
|
-
box-shadow var(--dur-fast) var(--ease),
|
|
16
|
-
transform 80ms var(--ease);
|
|
17
|
-
}
|
|
18
|
-
.btn:active:not(:disabled) { transform: scale(0.98); }
|
|
19
|
-
|
|
20
|
-
/* sizes */
|
|
21
|
-
.btn--sm { height: 28px; font-size: var(--t-xs); padding: 0 var(--sp-3); }
|
|
22
|
-
.btn--md { height: 36px; font-size: var(--t-sm); padding: 0 var(--sp-4); }
|
|
23
|
-
|
|
24
|
-
/* variants */
|
|
25
|
-
.btn--primary {
|
|
26
|
-
background: var(--brand);
|
|
27
|
-
color: var(--brand-fg);
|
|
28
|
-
}
|
|
29
|
-
.btn--primary:hover:not(:disabled) { filter: brightness(1.08); }
|
|
30
|
-
.btn--primary:disabled { background: var(--bg-elev-2); color: var(--fg-3); }
|
|
31
|
-
|
|
32
|
-
.btn--ghost {
|
|
33
|
-
background: var(--bg-elev-2);
|
|
34
|
-
color: var(--fg);
|
|
35
|
-
border: 1px solid transparent;
|
|
36
|
-
}
|
|
37
|
-
.btn--ghost:hover:not(:disabled) { background: var(--bg-elev-1); border-color: var(--hairline-2); color: var(--fg); }
|
|
38
|
-
.btn--ghost:disabled { color: var(--fg-3); }
|
|
39
|
-
|
|
40
|
-
.btn--danger {
|
|
41
|
-
background: var(--danger-soft);
|
|
42
|
-
color: var(--danger);
|
|
43
|
-
}
|
|
44
|
-
.btn--danger:hover:not(:disabled) { background: var(--danger); color: #fff; }
|
|
45
|
-
|
|
46
|
-
/* Plain icon button — used for ⋯ / × / refresh — minimal chrome */
|
|
47
|
-
.btn--icon {
|
|
48
|
-
width: 32px; padding: 0;
|
|
49
|
-
background: transparent;
|
|
50
|
-
color: var(--fg-2);
|
|
51
|
-
}
|
|
52
|
-
.btn--icon:hover:not(:disabled) { background: var(--bg-elev-2); color: var(--fg); }
|
|
53
|
-
|
|
54
|
-
.btn:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
55
|
-
|
|
56
|
-
.btn__icon { display: inline-flex; line-height: 0; }
|
|
57
|
-
.btn__icon svg { width: 16px; height: 16px; fill: currentColor; }
|
|
58
|
-
.btn--sm .btn__icon svg { width: 14px; height: 14px; }
|
|
59
|
-
.btn__label { line-height: 1; }
|
|
60
|
-
|
|
61
|
-
.btn.is-loading .btn__label { opacity: 0; }
|
|
62
|
-
.btn__spinner {
|
|
63
|
-
position: absolute;
|
|
64
|
-
width: 14px; height: 14px;
|
|
65
|
-
border: 2px solid currentColor;
|
|
66
|
-
border-top-color: transparent;
|
|
67
|
-
border-radius: 50%;
|
|
68
|
-
animation: btn-spin 0.7s linear infinite;
|
|
69
|
-
}
|
|
70
|
-
.btn { position: relative; }
|
|
71
|
-
|
|
72
|
-
@keyframes btn-spin { to { transform: rotate(360deg); } }
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import "./Button.css";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* <Button variant="primary|ghost|danger" size="sm|md" loading disabled icon={...}>
|
|
5
|
-
* Label
|
|
6
|
-
* </Button>
|
|
7
|
-
*
|
|
8
|
-
* Sizes:
|
|
9
|
-
* sm = 28px tall (compact toolbars)
|
|
10
|
-
* md = 36px tall (default — meets touch target on mobile)
|
|
11
|
-
*/
|
|
12
|
-
export default function Button({
|
|
13
|
-
children,
|
|
14
|
-
variant = "ghost",
|
|
15
|
-
size = "md",
|
|
16
|
-
loading = false,
|
|
17
|
-
disabled = false,
|
|
18
|
-
icon = null,
|
|
19
|
-
iconAfter = null,
|
|
20
|
-
className = "",
|
|
21
|
-
type = "button",
|
|
22
|
-
...rest
|
|
23
|
-
}) {
|
|
24
|
-
return (
|
|
25
|
-
<button
|
|
26
|
-
type={type}
|
|
27
|
-
className={`btn btn--${variant} btn--${size} ${loading ? "is-loading" : ""} no-drag ${className}`}
|
|
28
|
-
disabled={disabled || loading}
|
|
29
|
-
{...rest}
|
|
30
|
-
>
|
|
31
|
-
{loading && <span className="btn__spinner" aria-hidden="true" />}
|
|
32
|
-
{icon && <span className="btn__icon">{icon}</span>}
|
|
33
|
-
{children && <span className="btn__label">{children}</span>}
|
|
34
|
-
{iconAfter && <span className="btn__icon">{iconAfter}</span>}
|
|
35
|
-
</button>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
.card {
|
|
2
|
-
position: relative;
|
|
3
|
-
display: block;
|
|
4
|
-
text-align: left;
|
|
5
|
-
width: 100%;
|
|
6
|
-
background: var(--bg-elev-1);
|
|
7
|
-
border: 1px solid var(--hairline);
|
|
8
|
-
border-radius: var(--r-md);
|
|
9
|
-
padding: var(--sp-4);
|
|
10
|
-
font: inherit;
|
|
11
|
-
color: inherit;
|
|
12
|
-
transition: border-color var(--dur-fast) var(--ease),
|
|
13
|
-
background var(--dur-fast) var(--ease),
|
|
14
|
-
box-shadow var(--dur-fast) var(--ease),
|
|
15
|
-
transform var(--dur-fast) var(--ease);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.card--interactive { cursor: pointer; }
|
|
19
|
-
.card--interactive:hover {
|
|
20
|
-
background: var(--bg-elev-2);
|
|
21
|
-
border-color: var(--hairline-2);
|
|
22
|
-
box-shadow: var(--shadow-2);
|
|
23
|
-
}
|
|
24
|
-
.card--interactive:active { transform: translateY(1px); }
|
|
25
|
-
|
|
26
|
-
.card--accent::before {
|
|
27
|
-
content: "";
|
|
28
|
-
position: absolute;
|
|
29
|
-
left: 0; top: 0; bottom: 0;
|
|
30
|
-
width: 3px;
|
|
31
|
-
background: var(--accent, var(--brand));
|
|
32
|
-
border-radius: var(--r-md) 0 0 var(--r-md);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
button.card { all: unset; display: block; }
|
|
36
|
-
button.card.card--interactive {
|
|
37
|
-
cursor: pointer;
|
|
38
|
-
background: var(--bg-elev-1);
|
|
39
|
-
border: 1px solid var(--hairline);
|
|
40
|
-
border-radius: var(--r-md);
|
|
41
|
-
padding: var(--sp-4);
|
|
42
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import "./Card.css";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* <Card> base surface. Variants:
|
|
5
|
-
* plain — flat, no hover (default)
|
|
6
|
-
* interactive — hover lift + cursor pointer
|
|
7
|
-
* accent — left accent bar (color via prop accentColor)
|
|
8
|
-
*/
|
|
9
|
-
export default function Card({ children, variant = "plain", accentColor, onClick, className = "", ...rest }) {
|
|
10
|
-
const Tag = onClick ? "button" : "div";
|
|
11
|
-
return (
|
|
12
|
-
<Tag
|
|
13
|
-
className={`card card--${variant} ${className}`}
|
|
14
|
-
onClick={onClick}
|
|
15
|
-
style={accentColor ? { "--accent": accentColor } : undefined}
|
|
16
|
-
{...rest}
|
|
17
|
-
>
|
|
18
|
-
{children}
|
|
19
|
-
</Tag>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Tiny inline icons. 16×16 viewBox. Use: <Icon name="open" />
|
|
2
|
-
const ICONS = {
|
|
3
|
-
open: "M14 3v2h3.59L8.29 14.29l1.41 1.41L19 6.41V10h2V3h-7zM19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7z",
|
|
4
|
-
refresh: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z",
|
|
5
|
-
restart: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z",
|
|
6
|
-
trash: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
|
|
7
|
-
add: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
|
|
8
|
-
close: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
|
9
|
-
check: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z",
|
|
10
|
-
more: "M5 12a2 2 0 11-4 0 2 2 0 014 0zm9 0a2 2 0 11-4 0 2 2 0 014 0zm9 0a2 2 0 11-4 0 2 2 0 014 0z",
|
|
11
|
-
download:"M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
|
|
12
|
-
play: "M8 5v14l11-7z",
|
|
13
|
-
warn: "M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z",
|
|
14
|
-
globe: "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM18.92 8h-2.95a15.65 15.65 0 00-1.38-3.56A8.03 8.03 0 0118.92 8zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56A7.987 7.987 0 015.08 16zm2.95-8H5.08a7.987 7.987 0 014.33-3.56A15.65 15.65 0 008.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 01-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z",
|
|
15
|
-
laptop: "M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z",
|
|
16
|
-
edit: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
|
|
17
|
-
eye: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 12.5a5 5 0 110-10 5 5 0 010 10zm0-8a3 3 0 100 6 3 3 0 000-6z",
|
|
18
|
-
"eye-off":"M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z",
|
|
19
|
-
folder: "M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default function Icon({ name, size = 16 }) {
|
|
23
|
-
const d = ICONS[name];
|
|
24
|
-
if (!d) return null;
|
|
25
|
-
return (
|
|
26
|
-
<svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor" aria-hidden="true">
|
|
27
|
-
<path d={d} />
|
|
28
|
-
</svg>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
.menu-trigger {
|
|
2
|
-
display: inline-flex;
|
|
3
|
-
position: relative;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.menu {
|
|
7
|
-
position: fixed;
|
|
8
|
-
z-index: 1000;
|
|
9
|
-
min-width: 180px;
|
|
10
|
-
background: var(--bg-elev-2);
|
|
11
|
-
border: 1px solid var(--hairline-2);
|
|
12
|
-
border-radius: var(--r-md);
|
|
13
|
-
padding: var(--sp-1) 0;
|
|
14
|
-
box-shadow: var(--shadow-3);
|
|
15
|
-
transform-origin: top right;
|
|
16
|
-
animation: menu-pop var(--dur-fast) var(--ease);
|
|
17
|
-
}
|
|
18
|
-
@keyframes menu-pop {
|
|
19
|
-
from { opacity: 0; transform: scale(0.95) translateY(-4px); }
|
|
20
|
-
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.menu-item {
|
|
24
|
-
display: flex; align-items: center; gap: var(--sp-3);
|
|
25
|
-
width: 100%;
|
|
26
|
-
padding: var(--sp-2) var(--sp-4);
|
|
27
|
-
font-size: var(--t-sm);
|
|
28
|
-
color: var(--fg);
|
|
29
|
-
text-align: left;
|
|
30
|
-
transition: background var(--dur-fast) var(--ease);
|
|
31
|
-
}
|
|
32
|
-
.menu-item:hover:not(:disabled) { background: var(--bg-elev-1); }
|
|
33
|
-
.menu-item.is-disabled { color: var(--fg-3); cursor: not-allowed; }
|
|
34
|
-
.menu-item.is-danger { color: var(--danger); }
|
|
35
|
-
.menu-item.is-danger:hover { background: var(--danger-soft); }
|
|
36
|
-
.menu-item__icon {
|
|
37
|
-
display: inline-flex;
|
|
38
|
-
color: var(--fg-2);
|
|
39
|
-
line-height: 0;
|
|
40
|
-
}
|
|
41
|
-
.menu-item:hover .menu-item__icon { color: var(--fg); }
|
|
42
|
-
.menu-item__icon svg { width: 16px; height: 16px; fill: currentColor; }
|
|
43
|
-
.menu-item__label { flex: 1; }
|
|
44
|
-
.menu-item__badge {
|
|
45
|
-
width: 6px; height: 6px;
|
|
46
|
-
border-radius: 50%;
|
|
47
|
-
background: var(--brand);
|
|
48
|
-
flex-shrink: 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.menu-divider {
|
|
52
|
-
height: 1px;
|
|
53
|
-
background: var(--hairline);
|
|
54
|
-
margin: var(--sp-1) 0;
|
|
55
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import "./Menu.css";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* <Menu trigger={<button>⋯</button>} items={[
|
|
7
|
-
* { label: "Restart", icon: ..., onClick: () => {} },
|
|
8
|
-
* "divider",
|
|
9
|
-
* { label: "Update to v2.0.3", icon: ..., onClick: () => {}, badge: true },
|
|
10
|
-
* ]} />
|
|
11
|
-
*
|
|
12
|
-
* Renders the menu in a portal (position: fixed) so parent overflow:hidden
|
|
13
|
-
* never clips it. Closes on outside click + Escape.
|
|
14
|
-
*/
|
|
15
|
-
export default function Menu({ trigger, items, align = "right" }) {
|
|
16
|
-
const [open, setOpen] = useState(false);
|
|
17
|
-
const [pos, setPos] = useState({ top: 0, left: 0 });
|
|
18
|
-
const triggerRef = useRef(null);
|
|
19
|
-
const menuRef = useRef(null);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!open) return;
|
|
23
|
-
const onDoc = (e) => {
|
|
24
|
-
if (menuRef.current && !menuRef.current.contains(e.target)
|
|
25
|
-
&& triggerRef.current && !triggerRef.current.contains(e.target)) {
|
|
26
|
-
setOpen(false);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
30
|
-
document.addEventListener("mousedown", onDoc);
|
|
31
|
-
document.addEventListener("keydown", onKey);
|
|
32
|
-
return () => {
|
|
33
|
-
document.removeEventListener("mousedown", onDoc);
|
|
34
|
-
document.removeEventListener("keydown", onKey);
|
|
35
|
-
};
|
|
36
|
-
}, [open]);
|
|
37
|
-
|
|
38
|
-
const toggle = (e) => {
|
|
39
|
-
e.stopPropagation();
|
|
40
|
-
if (open) { setOpen(false); return; }
|
|
41
|
-
const r = triggerRef.current.getBoundingClientRect();
|
|
42
|
-
// Initial position; refined after first paint when we know menu width.
|
|
43
|
-
setPos({ top: r.bottom + 4, left: align === "right" ? r.right - 180 : r.left });
|
|
44
|
-
setOpen(true);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// After mount, snap to actual width.
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (!open || !menuRef.current || !triggerRef.current) return;
|
|
50
|
-
const r = triggerRef.current.getBoundingClientRect();
|
|
51
|
-
const w = menuRef.current.offsetWidth;
|
|
52
|
-
setPos((p) => ({ ...p, left: align === "right" ? r.right - w : r.left }));
|
|
53
|
-
}, [open, align]);
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<>
|
|
57
|
-
<span ref={triggerRef} onClick={toggle} className="menu-trigger no-drag">
|
|
58
|
-
{trigger}
|
|
59
|
-
</span>
|
|
60
|
-
{open && createPortal((
|
|
61
|
-
<div
|
|
62
|
-
ref={menuRef}
|
|
63
|
-
className="menu"
|
|
64
|
-
role="menu"
|
|
65
|
-
style={{ top: pos.top, left: pos.left }}
|
|
66
|
-
>
|
|
67
|
-
{items.map((it, i) => {
|
|
68
|
-
if (it === "divider") return <div key={i} className="menu-divider" />;
|
|
69
|
-
return (
|
|
70
|
-
<button
|
|
71
|
-
key={i}
|
|
72
|
-
role="menuitem"
|
|
73
|
-
className={`menu-item ${it.disabled ? "is-disabled" : ""} ${it.danger ? "is-danger" : ""}`}
|
|
74
|
-
disabled={it.disabled}
|
|
75
|
-
onClick={(e) => {
|
|
76
|
-
e.stopPropagation();
|
|
77
|
-
setOpen(false);
|
|
78
|
-
it.onClick && it.onClick();
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
{it.icon && <span className="menu-item__icon">{it.icon}</span>}
|
|
82
|
-
<span className="menu-item__label">{it.label}</span>
|
|
83
|
-
{it.badge && <span className="menu-item__badge" />}
|
|
84
|
-
</button>
|
|
85
|
-
);
|
|
86
|
-
})}
|
|
87
|
-
</div>
|
|
88
|
-
), document.body)}
|
|
89
|
-
</>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
.sidecar-banner {
|
|
2
|
-
background: var(--bg-elev-1);
|
|
3
|
-
border: 1px solid var(--hairline-2);
|
|
4
|
-
border-radius: var(--r-md);
|
|
5
|
-
padding: var(--sp-4);
|
|
6
|
-
display: flex;
|
|
7
|
-
flex-direction: column;
|
|
8
|
-
gap: var(--sp-3);
|
|
9
|
-
animation: banner-in var(--dur-base) var(--ease);
|
|
10
|
-
}
|
|
11
|
-
@keyframes banner-in {
|
|
12
|
-
from { opacity: 0; transform: translateY(-4px); }
|
|
13
|
-
to { opacity: 1; transform: translateY(0); }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.sidecar-banner__head {
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
gap: var(--sp-3);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.sidecar-banner__icon {
|
|
23
|
-
width: 36px; height: 36px;
|
|
24
|
-
display: grid; place-items: center;
|
|
25
|
-
background: var(--brand-soft);
|
|
26
|
-
color: var(--brand);
|
|
27
|
-
border-radius: var(--r-sm);
|
|
28
|
-
flex-shrink: 0;
|
|
29
|
-
}
|
|
30
|
-
.sidecar-banner.is-error .sidecar-banner__icon {
|
|
31
|
-
background: var(--danger-soft);
|
|
32
|
-
color: var(--danger);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.sidecar-banner__text { flex: 1; min-width: 0; }
|
|
36
|
-
.sidecar-banner__title {
|
|
37
|
-
font-size: var(--t-md);
|
|
38
|
-
font-weight: 600;
|
|
39
|
-
color: var(--fg);
|
|
40
|
-
}
|
|
41
|
-
.sidecar-banner.is-error .sidecar-banner__title { color: var(--danger); }
|
|
42
|
-
.sidecar-banner__subtitle {
|
|
43
|
-
font-size: var(--t-xs);
|
|
44
|
-
color: var(--fg-2);
|
|
45
|
-
font-family: var(--font-mono);
|
|
46
|
-
margin-top: 2px;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.sidecar-banner__actions { flex-shrink: 0; }
|
|
50
|
-
|
|
51
|
-
.sidecar-banner__bar {
|
|
52
|
-
height: 4px;
|
|
53
|
-
background: var(--bg-elev-2);
|
|
54
|
-
border-radius: var(--r-pill);
|
|
55
|
-
overflow: hidden;
|
|
56
|
-
position: relative;
|
|
57
|
-
}
|
|
58
|
-
.sidecar-banner__bar-fill {
|
|
59
|
-
height: 100%;
|
|
60
|
-
background: linear-gradient(90deg, var(--brand), #7da4ff);
|
|
61
|
-
border-radius: inherit;
|
|
62
|
-
transition: width var(--dur-base) var(--ease);
|
|
63
|
-
}
|
|
64
|
-
.sidecar-banner__bar.is-indeterminate .sidecar-banner__bar-fill {
|
|
65
|
-
animation: banner-slide 1.4s linear infinite;
|
|
66
|
-
width: 30% !important;
|
|
67
|
-
}
|
|
68
|
-
@keyframes banner-slide {
|
|
69
|
-
0% { transform: translateX(-100%); }
|
|
70
|
-
100% { transform: translateX(400%); }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.sidecar-banner__bar-meta {
|
|
74
|
-
display: flex;
|
|
75
|
-
justify-content: space-between;
|
|
76
|
-
font-family: var(--font-mono);
|
|
77
|
-
font-size: var(--t-xs);
|
|
78
|
-
color: var(--fg-3);
|
|
79
|
-
}
|