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.
Files changed (61) hide show
  1. package/package.json +7 -7
  2. package/src/backends/homepage-react/assets/index-BpljolQs.js +365 -0
  3. package/src/backends/homepage-react/assets/{index-BniEbx_j.css → index-C9AZlTew.css} +1 -1
  4. package/src/backends/homepage-react/index.html +2 -2
  5. package/src/backends/local-teams.js +42 -4
  6. package/src/backends/sidecar-ipc.js +23 -1
  7. package/src/i18n/locales/en.json +9 -7
  8. package/src/i18n/locales/zh-CN.json +9 -7
  9. package/src/sidecar/cicy-code.js +54 -111
  10. package/src/sidecar/localbin.js +133 -0
  11. package/src/sidecar/native.js +4 -2
  12. package/src/sidecar/runtime.js +7 -3
  13. package/workers/render/src/App.css +156 -10
  14. package/workers/render/src/App.jsx +254 -39
  15. package/.env.dev +0 -7
  16. package/src/backends/homepage-react/assets/index-B8gGhz8B.js +0 -365
  17. package/workers/render.bak.20260528-2338/DESIGN_v2.md +0 -254
  18. package/workers/render.bak.20260528-2338/index.html +0 -12
  19. package/workers/render.bak.20260528-2338/package-lock.json +0 -827
  20. package/workers/render.bak.20260528-2338/package.json +0 -19
  21. package/workers/render.bak.20260528-2338/public/_headers +0 -5
  22. package/workers/render.bak.20260528-2338/public/manifest.json +0 -6
  23. package/workers/render.bak.20260528-2338/src/App.css +0 -224
  24. package/workers/render.bak.20260528-2338/src/App.jsx +0 -1028
  25. package/workers/render.bak.20260528-2338/src/api.js +0 -285
  26. package/workers/render.bak.20260528-2338/src/cicycode-ops.js +0 -222
  27. package/workers/render.bak.20260528-2338/src/components/BackendCard.css +0 -299
  28. package/workers/render.bak.20260528-2338/src/components/BackendCard.jsx +0 -133
  29. package/workers/render.bak.20260528-2338/src/components/BackendModal.css +0 -161
  30. package/workers/render.bak.20260528-2338/src/components/BackendModal.jsx +0 -199
  31. package/workers/render.bak.20260528-2338/src/components/Button.css +0 -72
  32. package/workers/render.bak.20260528-2338/src/components/Button.jsx +0 -37
  33. package/workers/render.bak.20260528-2338/src/components/Card.css +0 -42
  34. package/workers/render.bak.20260528-2338/src/components/Card.jsx +0 -21
  35. package/workers/render.bak.20260528-2338/src/components/Icon.jsx +0 -30
  36. package/workers/render.bak.20260528-2338/src/components/Menu.css +0 -55
  37. package/workers/render.bak.20260528-2338/src/components/Menu.jsx +0 -91
  38. package/workers/render.bak.20260528-2338/src/components/SidecarBanner.css +0 -79
  39. package/workers/render.bak.20260528-2338/src/components/SidecarBanner.jsx +0 -84
  40. package/workers/render.bak.20260528-2338/src/components/StatusChip.css +0 -19
  41. package/workers/render.bak.20260528-2338/src/components/StatusChip.jsx +0 -31
  42. package/workers/render.bak.20260528-2338/src/components/Toast.css +0 -31
  43. package/workers/render.bak.20260528-2338/src/components/Toast.jsx +0 -23
  44. package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.css +0 -464
  45. package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.jsx +0 -716
  46. package/workers/render.bak.20260528-2338/src/dockerInstaller.js +0 -0
  47. package/workers/render.bak.20260528-2338/src/i18n/en.json +0 -116
  48. package/workers/render.bak.20260528-2338/src/i18n/fr.json +0 -116
  49. package/workers/render.bak.20260528-2338/src/i18n/index.js +0 -69
  50. package/workers/render.bak.20260528-2338/src/i18n/ja.json +0 -116
  51. package/workers/render.bak.20260528-2338/src/i18n/zh-CN.json +0 -121
  52. package/workers/render.bak.20260528-2338/src/main.js +0 -475
  53. package/workers/render.bak.20260528-2338/src/main.jsx +0 -18
  54. package/workers/render.bak.20260528-2338/src/style.css +0 -275
  55. package/workers/render.bak.20260528-2338/src/styles/base.css +0 -98
  56. package/workers/render.bak.20260528-2338/src/styles/tokens.css +0 -90
  57. package/workers/render.bak.20260528-2338/src/tos.js +0 -72
  58. package/workers/render.bak.20260528-2338/src/worker.js +0 -40
  59. package/workers/render.bak.20260528-2338/src/wslInstaller.js +0 -1563
  60. package/workers/render.bak.20260528-2338/vite.config.js +0 -36
  61. 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
- }