fluxplugin 1.0.3 → 1.0.4

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 (50) hide show
  1. package/README.md +15 -12
  2. package/dist/browser/chunks/chunk-AV4KI7LI.js +1792 -0
  3. package/dist/browser/chunks/{chunk-TL63576R.js → chunk-OAYWYU5O.js} +48 -11
  4. package/dist/browser/index.js +3 -1
  5. package/dist/browser/manager-ui.js +386 -185
  6. package/dist/browser/react.js +46 -2
  7. package/dist/cli.js +3622 -257
  8. package/dist/node/chunks/{chunk-EVX2O65A.js → chunk-SDCGUBEW.js} +49 -11
  9. package/dist/node/chunks/{chunk-C7VWZZOX.js → chunk-ZLA5XUXN.js} +175 -51
  10. package/dist/node/cli-api.js +742 -76
  11. package/dist/node/index.js +3 -1
  12. package/dist/node/main.js +8 -2
  13. package/dist/node/preload.js +1 -1
  14. package/dist/types/cli/build.d.ts +5 -0
  15. package/dist/types/cli/cli.d.ts +2 -0
  16. package/dist/types/cli/dist.d.ts +18 -0
  17. package/dist/types/cli/host-templates.d.ts +2 -0
  18. package/dist/types/cli/store.d.ts +13 -0
  19. package/dist/types/cli/templates.d.ts +2 -0
  20. package/dist/types/core/manifest.d.ts +15 -1
  21. package/dist/types/core/protocol.d.ts +18 -5
  22. package/dist/types/core/registry.d.ts +11 -0
  23. package/dist/types/core/routes.d.ts +3 -1
  24. package/dist/types/main/adapters.d.ts +3 -0
  25. package/dist/types/main/assets.d.ts +8 -0
  26. package/dist/types/main/github-registry.d.ts +2 -1
  27. package/dist/types/main/index.d.ts +1 -0
  28. package/dist/types/main/registry.d.ts +55 -18
  29. package/dist/types/main/stores.d.ts +7 -1
  30. package/dist/types/manager-ui/index.d.ts +35 -3
  31. package/dist/types/react/base-css.d.ts +6 -2
  32. package/dist/types/react/http-bridge.d.ts +5 -0
  33. package/dist/types/react/i18n.d.ts +33 -0
  34. package/dist/types/react/index.d.ts +5 -0
  35. package/dist/types/react/markdown.d.ts +2 -1
  36. package/dist/types/react/media.d.ts +33 -0
  37. package/dist/types/react/settings.d.ts +6 -0
  38. package/dist/types/react/sidebar.d.ts +42 -0
  39. package/dist/types/react/theme.d.ts +42 -4
  40. package/dist/types/react/ui.d.ts +42 -0
  41. package/docs/CUSTOMIZING.md +56 -0
  42. package/docs/STORE.md +19 -0
  43. package/docs/screenshots/plugin-page.png +0 -0
  44. package/docs/screenshots/settings-appearance.png +0 -0
  45. package/docs/screenshots/site-guide-dark.png +0 -0
  46. package/docs/screenshots/site-home.png +0 -0
  47. package/docs/screenshots/site-mobile.png +0 -0
  48. package/docs/screenshots/site-search.png +0 -0
  49. package/package.json +1 -1
  50. package/dist/browser/chunks/chunk-VXQJIDXW.js +0 -1232
@@ -0,0 +1,1792 @@
1
+ import {
2
+ MGMT,
3
+ PermissionManager,
4
+ PluginManager,
5
+ errorMessage,
6
+ normalizePath,
7
+ s,
8
+ scopeCss
9
+ } from "./chunk-OAYWYU5O.js";
10
+
11
+ // src/react/runtime.ts
12
+ var RemoteError = class extends Error {
13
+ constructor(payload) {
14
+ super(payload.message);
15
+ this.payload = payload;
16
+ this.name = "RemoteError";
17
+ }
18
+ payload;
19
+ get code() {
20
+ return this.payload.code;
21
+ }
22
+ };
23
+ var PluginsClient = class {
24
+ constructor(bridge) {
25
+ this.bridge = bridge;
26
+ }
27
+ bridge;
28
+ call(channel, input) {
29
+ return this.bridge.invoke(channel, input);
30
+ }
31
+ list = () => this.call(MGMT.list);
32
+ enable = (id) => this.call(MGMT.enable, { id });
33
+ disable = (id) => this.call(MGMT.disable, { id });
34
+ reload = (id) => this.call(MGMT.reload, { id });
35
+ grant = (id, permission) => this.call(MGMT.grant, { id, permission });
36
+ revoke = (id, permission) => this.call(MGMT.revoke, { id, permission });
37
+ uninstall = (id) => this.call(MGMT.uninstall, { id });
38
+ search = (query) => this.call(MGMT.search, { query });
39
+ updates = () => this.call(MGMT.updates);
40
+ install = (entry) => this.call(MGMT.install, { entry });
41
+ update = (entry) => this.call(MGMT.update, { entry });
42
+ rollback = (id, version) => this.call(MGMT.rollback, { id, version });
43
+ configGet = (id) => this.call(MGMT.configGet, { id });
44
+ configSet = (id, patch) => this.call(MGMT.configSet, { id, patch });
45
+ configReset = (id) => this.call(MGMT.configReset, { id });
46
+ configSchema = (id) => this.call(MGMT.configSchema, { id });
47
+ logs = (id) => this.call(MGMT.logs, { id });
48
+ details = (id) => this.call(MGMT.details, { id });
49
+ diagnostics = (id) => this.call(MGMT.diagnostics, { id });
50
+ };
51
+ var asDefinition = (mod) => {
52
+ const d = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
53
+ if (!d || typeof d !== "object") throw new Error("renderer entry must default-export definePlugin({...})");
54
+ return d;
55
+ };
56
+ var RendererFramework = class {
57
+ constructor(opts) {
58
+ this.opts = opts;
59
+ this.bridge = opts.bridge;
60
+ this.client = new PluginsClient(opts.bridge);
61
+ this.manager = new PluginManager({
62
+ runtime: "renderer",
63
+ permissions: this.permissions,
64
+ ipcBridge: {
65
+ invoke: (name, input, caller) => opts.bridge.invoke(name, input, caller),
66
+ send: (channel, payload, caller) => opts.bridge.send(channel, payload, caller),
67
+ on: (channel, fn) => ({ dispose: opts.bridge.on(channel, fn) })
68
+ },
69
+ // Renderer plugins that crash must never take the app down; the main side persists auto-disable.
70
+ ...opts.manager
71
+ });
72
+ this.manager.subscribe(() => this.touch());
73
+ }
74
+ opts;
75
+ bridge;
76
+ client;
77
+ manager;
78
+ permissions = new PermissionManager({ autoGrant: false });
79
+ revision = -1;
80
+ bootstrap;
81
+ syncing = Promise.resolve();
82
+ renderErrors = /* @__PURE__ */ new Map();
83
+ unsubscribers = [];
84
+ storeListeners = /* @__PURE__ */ new Set();
85
+ _storeVersion = 0;
86
+ /** `useSyncExternalStore` plumbing for the bootstrap-derived view (plugin list etc.). */
87
+ get storeVersion() {
88
+ return this._storeVersion + this.manager.version;
89
+ }
90
+ subscribe = (l) => {
91
+ this.storeListeners.add(l);
92
+ const off = this.manager.subscribe(l);
93
+ return () => {
94
+ this.storeListeners.delete(l);
95
+ off();
96
+ };
97
+ };
98
+ touch() {
99
+ this._storeVersion++;
100
+ for (const l of [...this.storeListeners]) l();
101
+ }
102
+ get currentBootstrap() {
103
+ return this.bootstrap;
104
+ }
105
+ /** Fetches the bootstrap, activates enabled plugins and starts listening for changes. */
106
+ async start() {
107
+ await this.sync();
108
+ this.unsubscribers.push(
109
+ this.bridge.on(MGMT.changed, () => {
110
+ void this.sync().catch((e) => console.error("[fluxplugin] sync failed", e));
111
+ }),
112
+ this.bridge.on("flux.notification", (n) => void this.manager.notifications.push(n, n?.owner ?? "main")),
113
+ this.bridge.on("flux.event.plugin.hotreload", ({ id } = {}) => void this.hotReload(id))
114
+ );
115
+ }
116
+ stop() {
117
+ for (const u of this.unsubscribers.splice(0)) u();
118
+ }
119
+ hotReload(id) {
120
+ this.cacheBust = Date.now();
121
+ return this.sync(true).then(async () => {
122
+ if (id && this.manager.has(id)) await this.manager.reload(id).catch(() => void 0);
123
+ });
124
+ }
125
+ cacheBust = 0;
126
+ /** Mirrors main-process state into the renderer manager. Calls are serialized. */
127
+ sync(force = false) {
128
+ const run = this.syncing.then(async () => {
129
+ const boot = await this.bridge.bootstrap();
130
+ if (!force && boot.revision === this.revision && this.bootstrap) return;
131
+ this.revision = boot.revision;
132
+ this.bootstrap = boot;
133
+ await this.reconcile(boot);
134
+ this.installWindowPlugins(boot.preload);
135
+ this.touch();
136
+ });
137
+ this.syncing = run.catch(() => void 0);
138
+ return run;
139
+ }
140
+ async reconcile(boot) {
141
+ const seen = /* @__PURE__ */ new Set();
142
+ for (const d of boot.plugins) {
143
+ seen.add(d.id);
144
+ this.permissions.restore({ [d.id]: d.granted });
145
+ if (!this.manager.has(d.id)) {
146
+ if (d.state === "error" && !d.rendererUrl) continue;
147
+ this.manager.register({ manifest: d.manifest, origin: "renderer", load: () => this.load(d) });
148
+ this.permissions.restore({ [d.id]: d.granted });
149
+ } else {
150
+ this.permissions.restore({ [d.id]: d.granted });
151
+ }
152
+ const info = this.manager.info(d.id);
153
+ if (!d.enabled && info.enabled) await this.manager.disable(d.id).catch(() => void 0);
154
+ else if (d.enabled && !info.enabled) await this.manager.enable(d.id, { activate: false }).catch(() => void 0);
155
+ }
156
+ for (const info of this.manager.list()) if (!seen.has(info.id)) await this.manager.unregister(info.id);
157
+ await this.manager.startup();
158
+ }
159
+ async load(d) {
160
+ let def = {};
161
+ if (d.rendererUrl) {
162
+ const url = this.cacheBust ? `${d.rendererUrl}${d.rendererUrl.includes("?") ? "&" : "?"}flux=${this.cacheBust}` : d.rendererUrl;
163
+ const mod = await (this.opts.importPlugin ? this.opts.importPlugin(url, d) : import(
164
+ /* @vite-ignore */
165
+ url
166
+ ));
167
+ def = asDefinition(mod);
168
+ }
169
+ const hrefs = d.styleUrls;
170
+ if (!hrefs.length) return def;
171
+ const activate = def.onActivate ?? def.activate;
172
+ return {
173
+ ...def,
174
+ onActivate: async (ctx) => {
175
+ hrefs.forEach((href, i) => ctx.lifecycle.subscriptions.add(this.manager.styles.register({ id: `manifest-${i}`, href, priority: 50 }, ctx.pluginId)));
176
+ await activate?.(ctx);
177
+ },
178
+ activate: void 0
179
+ };
180
+ }
181
+ /** Called by plugin error boundaries. Repeated render failures disable the plugin. */
182
+ reportRenderError(pluginId, error) {
183
+ this.manager.logs.logger(pluginId).error(`render error: ${errorMessage(error)}`, error instanceof Error ? error.stack : void 0);
184
+ const now = Date.now();
185
+ const list = [...(this.renderErrors.get(pluginId) ?? []).filter((t) => now - t < 6e4), now];
186
+ this.renderErrors.set(pluginId, list);
187
+ if (list.length >= (this.opts.maxRenderErrors ?? 3) && this.manager.has(pluginId) && this.manager.info(pluginId).enabled) {
188
+ this.manager.logs.logger(pluginId).warn("disabled automatically after repeated render errors");
189
+ this.renderErrors.delete(pluginId);
190
+ void this.client.disable(pluginId).catch(() => this.manager.disable(pluginId));
191
+ }
192
+ }
193
+ /** Builds `window.plugins.<ns>.<member>` from the preload manifest. Rebuilt on every sync. */
194
+ installWindowPlugins(spec) {
195
+ if (typeof window === "undefined") return;
196
+ window.plugins = buildPluginApis(this.bridge, spec);
197
+ }
198
+ /** Config handle usable from React (mirrors ctx.config through the management API). */
199
+ configOf(id) {
200
+ return {
201
+ get: () => this.client.configGet(id),
202
+ set: (p) => this.client.configSet(id, p),
203
+ reset: () => this.client.configReset(id)
204
+ };
205
+ }
206
+ };
207
+ function buildPluginApis(bridge, spec) {
208
+ const out = {};
209
+ for (const [ns, members] of Object.entries(spec)) {
210
+ const api = /* @__PURE__ */ Object.create(null);
211
+ for (const [name, m] of Object.entries(members)) {
212
+ if (m.kind === "invoke") api[name] = (input) => bridge.invoke(m.channel, input, ns);
213
+ else if (m.kind === "send") api[name] = (payload) => bridge.send(m.channel, payload, ns);
214
+ else api[name] = (listener) => bridge.on(m.channel, listener);
215
+ }
216
+ out[ns] = Object.freeze(api);
217
+ }
218
+ return Object.freeze(out);
219
+ }
220
+ function installSharedModules(modules) {
221
+ const g = globalThis;
222
+ g.__FLUXPLUGIN_SHARED__ = { ...g.__FLUXPLUGIN_SHARED__ ?? {}, ...modules };
223
+ }
224
+
225
+ // src/react/components.tsx
226
+ import {
227
+ Component,
228
+ createContext,
229
+ useCallback as useCallback2,
230
+ useContext,
231
+ useEffect as useEffect2,
232
+ useMemo as useMemo2,
233
+ useRef,
234
+ useState as useState2,
235
+ useSyncExternalStore as useSyncExternalStore2
236
+ } from "react";
237
+
238
+ // src/react/router.tsx
239
+ import {
240
+ Suspense,
241
+ lazy,
242
+ useCallback,
243
+ useEffect,
244
+ useMemo,
245
+ useState,
246
+ useSyncExternalStore
247
+ } from "react";
248
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
249
+ function currentPath() {
250
+ if (typeof window === "undefined") return "/";
251
+ const h = window.location.hash.replace(/^#/, "");
252
+ return normalizePath(h || "/");
253
+ }
254
+ var listeners = /* @__PURE__ */ new Set();
255
+ if (typeof window !== "undefined") window.addEventListener("hashchange", () => listeners.forEach((l) => l()));
256
+ var subscribeLocation = (l) => (listeners.add(l), () => void listeners.delete(l));
257
+ function navigate(to, opts = {}) {
258
+ const target = "#" + normalizePath(to) + (to.includes("?") ? "?" + to.split("?")[1] : "");
259
+ if (opts.replace) window.location.replace(target);
260
+ else window.location.hash = target;
261
+ listeners.forEach((l) => l());
262
+ }
263
+ function useLocation() {
264
+ return useSyncExternalStore(subscribeLocation, currentPath, () => "/");
265
+ }
266
+ function useNavigate() {
267
+ return navigate;
268
+ }
269
+ function Link({ to, children, ...rest }) {
270
+ const here = useLocation();
271
+ const active = here === normalizePath(to) || to !== "/" && here.startsWith(normalizePath(to) + "/");
272
+ return /* @__PURE__ */ jsx("a", { ...rest, href: "#" + normalizePath(to), "data-active": active || void 0, "aria-current": active ? "page" : void 0, children });
273
+ }
274
+ function usePluginRoute() {
275
+ const fw = usePluginFramework();
276
+ const path = useLocation();
277
+ const routes = fw.manager.routes;
278
+ const version = useSyncExternalStore(routes.subscribe, () => routes.version, () => routes.version);
279
+ return useMemo(() => {
280
+ const match = routes.match(path);
281
+ return { match, params: match?.params ?? {}, breadcrumbs: match ? routes.breadcrumbs(match) : [], path };
282
+ }, [routes, path, version]);
283
+ }
284
+ var useParams = () => usePluginRoute().params;
285
+ function Breadcrumbs({ separator = "/" }) {
286
+ const { breadcrumbs } = usePluginRoute();
287
+ if (!breadcrumbs.length) return null;
288
+ return /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", className: "flux-breadcrumbs", children: breadcrumbs.map((b, i) => /* @__PURE__ */ jsxs("span", { children: [
289
+ i > 0 && /* @__PURE__ */ jsxs("span", { className: "flux-breadcrumb-sep", children: [
290
+ " ",
291
+ separator,
292
+ " "
293
+ ] }),
294
+ i === breadcrumbs.length - 1 ? /* @__PURE__ */ jsx("span", { "aria-current": "page", children: b.label }) : /* @__PURE__ */ jsx(Link, { to: b.path, children: b.label })
295
+ ] }, b.path)) });
296
+ }
297
+ var lazyCache = /* @__PURE__ */ new WeakMap();
298
+ function resolveComponent(def) {
299
+ if (def.component) return def.component;
300
+ const loader = def.lazy;
301
+ let c = lazyCache.get(loader);
302
+ if (!c) {
303
+ c = lazy(async () => {
304
+ const mod = await loader();
305
+ const comp = mod && typeof mod === "object" && "default" in mod ? mod.default : mod;
306
+ return { default: comp };
307
+ });
308
+ lazyCache.set(loader, c);
309
+ }
310
+ return c;
311
+ }
312
+ function PluginRoutes({ notFound, forbidden, loading, defaultLayout: DefaultLayout }) {
313
+ const fw = usePluginFramework();
314
+ const path = useLocation();
315
+ const routes = fw.manager.routes;
316
+ useSyncExternalStore(routes.subscribe, () => routes.version, () => routes.version);
317
+ const [resolution, setResolution] = useState(null);
318
+ const version = routes.version;
319
+ useEffect(() => {
320
+ let live = true;
321
+ (async () => {
322
+ const match2 = await routes.matchAsync(path);
323
+ if (!match2) return live && setResolution({ path });
324
+ const guard2 = await routes.checkGuards(match2, path);
325
+ if (live) setResolution({ path, match: match2, guard: guard2 });
326
+ })().catch(() => live && setResolution({ path }));
327
+ return () => {
328
+ live = false;
329
+ };
330
+ }, [routes, path, version]);
331
+ useEffect(() => {
332
+ if (resolution?.path === path && resolution.guard && !resolution.guard.allow && resolution.guard.redirect) navigate(resolution.guard.redirect, { replace: true });
333
+ }, [resolution, path]);
334
+ if (!resolution || resolution.path !== path) return /* @__PURE__ */ jsx(Fragment, { children: loading ?? null });
335
+ const { match, guard } = resolution;
336
+ if (!match) return /* @__PURE__ */ jsx(Fragment, { children: notFound ?? /* @__PURE__ */ jsxs("div", { className: "flux-not-found", children: [
337
+ "Page not found: ",
338
+ path
339
+ ] }) });
340
+ if (guard && !guard.allow) {
341
+ if (guard.redirect) return /* @__PURE__ */ jsx(Fragment, { children: loading ?? null });
342
+ return /* @__PURE__ */ jsx(Fragment, { children: forbidden ? forbidden(guard.reason) : /* @__PURE__ */ jsxs("div", { className: "flux-forbidden", children: [
343
+ "Access denied: ",
344
+ guard.reason
345
+ ] }) });
346
+ }
347
+ const owner = match.route.owner ?? "host";
348
+ const Page = resolveComponent(match.route.definition);
349
+ let node = /* @__PURE__ */ jsx(Page, { params: match.params });
350
+ for (const r of [...match.chain].reverse()) {
351
+ const l = r.definition.layout;
352
+ if (!l) continue;
353
+ const Layout = typeof l === "string" ? routes.getLayout(l) : l;
354
+ if (Layout) node = /* @__PURE__ */ jsx(Layout, { children: node });
355
+ }
356
+ if (!match.chain.some((r) => r.definition.layout) && DefaultLayout) node = /* @__PURE__ */ jsx(DefaultLayout, { children: node });
357
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(PluginBoundary, { owner, children: /* @__PURE__ */ jsx(Suspense, { fallback: loading ?? /* @__PURE__ */ jsx("div", { className: "flux-loading", children: "Loading\u2026" }), children: node }) }, match.route.fullPath) });
358
+ }
359
+ function useMenu(location2) {
360
+ const fw = usePluginFramework();
361
+ const menus = fw.manager.menus;
362
+ useSyncExternalStore(menus.subscribe, () => menus.version, () => menus.version);
363
+ const run = useCallback(
364
+ (item) => {
365
+ if (item.command) return fw.manager.commands.execute(item.command, ...item.args ?? []);
366
+ if (item.path) navigate(item.path);
367
+ },
368
+ [fw]
369
+ );
370
+ return { items: menus.items(location2), run };
371
+ }
372
+
373
+ // src/react/components.tsx
374
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
375
+ var FrameworkContext = createContext(null);
376
+ function PluginProvider({ framework, children }) {
377
+ return /* @__PURE__ */ jsx2(FrameworkContext.Provider, { value: framework, children });
378
+ }
379
+ function usePluginFramework() {
380
+ const fw = useContext(FrameworkContext);
381
+ if (!fw) throw new Error("usePlugin* hooks must be used inside <PluginProvider>");
382
+ return fw;
383
+ }
384
+ var PluginErrorBoundary = class extends Component {
385
+ state = { error: null };
386
+ static getDerivedStateFromError(error) {
387
+ return { error };
388
+ }
389
+ componentDidCatch(error, _info) {
390
+ this.props.framework.reportRenderError(this.props.owner, error);
391
+ }
392
+ reset = () => this.setState({ error: null });
393
+ render() {
394
+ const { error } = this.state;
395
+ if (!error) return this.props.children;
396
+ if (this.props.fallback) return this.props.fallback(error, this.reset);
397
+ return /* @__PURE__ */ jsxs2("div", { role: "alert", className: "fluxplugin-error", "data-flux-error": this.props.owner, children: [
398
+ /* @__PURE__ */ jsx2("strong", { children: this.props.owner }),
399
+ " failed to render.",
400
+ " ",
401
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: this.reset, children: "Retry" })
402
+ ] });
403
+ }
404
+ };
405
+ function PluginBoundary({ owner, children }) {
406
+ const fw = usePluginFramework();
407
+ return /* @__PURE__ */ jsx2("div", { "data-fluxplugin": owner, style: { display: "contents" }, children: /* @__PURE__ */ jsx2(PluginErrorBoundary, { owner, framework: fw, children }) });
408
+ }
409
+ function useRegistryVersion(subscribe, version) {
410
+ return useSyncExternalStore2(subscribe, version, version);
411
+ }
412
+ function renderContribution(c, props) {
413
+ const C = c.component;
414
+ return /* @__PURE__ */ jsx2(PluginBoundary, { owner: c.owner ?? "unknown", children: /* @__PURE__ */ jsx2(C, { ...c.props ?? {}, ...props }) }, c.id);
415
+ }
416
+ function PluginSlot({ name, children, props = {} }) {
417
+ const fw = usePluginFramework();
418
+ useRegistryVersion(fw.manager.slots.subscribe, () => fw.manager.slots.version);
419
+ const resolved = fw.manager.slots.resolve(name, props);
420
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
421
+ resolved.before.map((c) => renderContribution(c, props)),
422
+ resolved.replacement ? renderContribution(resolved.replacement, { ...props, children }) : children,
423
+ resolved.after.map((c) => renderContribution(c, props))
424
+ ] });
425
+ }
426
+ function useExtensionItems(point, key) {
427
+ const fw = usePluginFramework();
428
+ const version = useRegistryVersion(fw.manager.extensionPoints.subscribe, () => fw.manager.extensionPoints.version);
429
+ return useMemo2(() => fw.manager.extensionPoints.items(point, key), [fw, point, key, version]);
430
+ }
431
+ function usePlugin(id) {
432
+ const fw = usePluginFramework();
433
+ useRegistryVersion(fw.subscribe, () => fw.storeVersion);
434
+ return fw.manager.has(id) ? fw.manager.info(id) : void 0;
435
+ }
436
+ function usePlugins() {
437
+ const fw = usePluginFramework();
438
+ useRegistryVersion(fw.subscribe, () => fw.storeVersion);
439
+ return fw.manager.list();
440
+ }
441
+ function usePluginState(pluginId, key, initial) {
442
+ const fw = usePluginFramework();
443
+ const store = fw.manager.stateOf(pluginId);
444
+ const sub = useCallback2((l) => store.subscribe(l, key).dispose, [store, key]);
445
+ const value = useSyncExternalStore2(
446
+ sub,
447
+ () => store.get(key, initial),
448
+ () => initial
449
+ );
450
+ const set = useCallback2((v) => store.set(key, typeof v === "function" ? v(store.get(key, initial)) : v), [store, key, initial]);
451
+ return [value, set];
452
+ }
453
+ function usePluginEvent(name, onEvent) {
454
+ const fw = usePluginFramework();
455
+ const [last, setLast] = useState2(void 0);
456
+ const cb = useRef(onEvent);
457
+ cb.current = onEvent;
458
+ useEffect2(() => {
459
+ const handle = (p) => {
460
+ setLast(p);
461
+ cb.current?.(p);
462
+ };
463
+ const subs = [fw.manager.events.on(name, (p) => handle(p)), fw.bridge.on(name, handle), fw.bridge.on(`flux.event.${name}`, handle)];
464
+ return () => {
465
+ subs.forEach((s2) => typeof s2 === "function" ? s2() : s2.dispose());
466
+ };
467
+ }, [fw, name]);
468
+ return last;
469
+ }
470
+ function usePluginCommand(id) {
471
+ const fw = usePluginFramework();
472
+ useRegistryVersion(fw.manager.commands.subscribe, () => fw.manager.commands.version);
473
+ const cmd = fw.manager.commands.get(id);
474
+ const run = useCallback2((...args) => fw.manager.commands.execute(id, ...args), [fw, id]);
475
+ return { run, available: !!cmd && (!cmd.when || cmd.when()), title: cmd?.title };
476
+ }
477
+ function usePluginService(name) {
478
+ const fw = usePluginFramework();
479
+ useRegistryVersion(fw.manager.services.subscribe, () => fw.manager.services.version);
480
+ useEffect2(() => {
481
+ if (!fw.manager.services.has(name)) void fw.manager.services.resolveAsync(name).catch(() => void 0);
482
+ }, [fw, name]);
483
+ return fw.manager.services.resolve(name);
484
+ }
485
+ function usePluginIPC(endpoint, opts = {}) {
486
+ const fw = usePluginFramework();
487
+ const name = typeof endpoint === "string" ? endpoint : endpoint.name;
488
+ const [state, setState] = useState2({ data: void 0, error: void 0, loading: false });
489
+ const alive = useRef(true);
490
+ useEffect2(() => () => void (alive.current = false), []);
491
+ const call = useCallback2(
492
+ async (input) => {
493
+ setState((s2) => ({ ...s2, loading: true, error: void 0 }));
494
+ try {
495
+ const data = await fw.bridge.invoke(name, input, opts.caller);
496
+ if (alive.current) setState({ data, error: void 0, loading: false });
497
+ return data;
498
+ } catch (e) {
499
+ const error = e instanceof Error ? e : new Error(String(e));
500
+ if (alive.current) setState({ data: void 0, error, loading: false });
501
+ throw error;
502
+ }
503
+ },
504
+ [fw, name, opts.caller]
505
+ );
506
+ return { ...state, call };
507
+ }
508
+ function usePluginConfig(pluginId) {
509
+ const fw = usePluginFramework();
510
+ const handle = useMemo2(() => fw.configOf(pluginId), [fw, pluginId]);
511
+ const [config, setConfig] = useState2();
512
+ const [error, setError] = useState2();
513
+ const [loading, setLoading] = useState2(true);
514
+ useEffect2(() => {
515
+ let live = true;
516
+ setLoading(true);
517
+ handle.get().then((c) => live && (setConfig(c), setError(void 0))).catch((e) => live && setError(e)).finally(() => live && setLoading(false));
518
+ const sub = fw.manager.events.on("plugin.config.changed", (p) => {
519
+ if (p?.id === pluginId) setConfig(p.next);
520
+ });
521
+ return () => {
522
+ live = false;
523
+ sub.dispose();
524
+ };
525
+ }, [fw, handle, pluginId]);
526
+ return {
527
+ config,
528
+ loading,
529
+ error,
530
+ set: async (patch) => {
531
+ const next = await handle.set(patch);
532
+ setConfig(next);
533
+ return next;
534
+ },
535
+ reset: async () => {
536
+ const next = await handle.reset();
537
+ setConfig(next);
538
+ return next;
539
+ }
540
+ };
541
+ }
542
+ function StyleHost() {
543
+ const fw = usePluginFramework();
544
+ useRegistryVersion(fw.manager.styles.subscribe, () => fw.manager.styles.version);
545
+ const entries = fw.manager.styles.ordered();
546
+ const sig = entries.map((e) => `${e.owner}/${e.id}/${e.priority}/${e.css?.length ?? 0}/${e.href ?? ""}`).join("|");
547
+ useEffect2(() => {
548
+ const created = [];
549
+ for (const e of entries) {
550
+ let el;
551
+ if (e.href) {
552
+ const link = document.createElement("link");
553
+ link.rel = "stylesheet";
554
+ link.href = e.href;
555
+ el = link;
556
+ } else {
557
+ const style = document.createElement("style");
558
+ style.textContent = e.scoped ? scopeCss(e.css ?? "", e.owner) : e.css ?? "";
559
+ el = style;
560
+ }
561
+ el.setAttribute("data-flux-style", `${e.owner}:${e.id}`);
562
+ document.head.appendChild(el);
563
+ created.push(el);
564
+ }
565
+ return () => created.forEach((el) => el.remove());
566
+ }, [sig]);
567
+ return null;
568
+ }
569
+
570
+ // src/react/palette.tsx
571
+ import { useEffect as useEffect4, useMemo as useMemo4, useRef as useRef2, useState as useState4, useSyncExternalStore as useSyncExternalStore3 } from "react";
572
+
573
+ // src/react/i18n.tsx
574
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react";
575
+ import { jsx as jsx3 } from "react/jsx-runtime";
576
+ var interpolate = (text, params) => params ? text.replace(/\{(\w+)\}/g, (m, k) => k in params ? String(params[k]) : m) : text;
577
+ var fallbackCtx = { locale: "en", setLocale: () => {
578
+ }, languages: [{ code: "en", label: "English" }], t: (_k, fb, p) => interpolate(fb ?? _k, p) };
579
+ var Ctx = createContext2(fallbackCtx);
580
+ function declareI18nPoints(framework) {
581
+ const ep = framework.manager.extensionPoints;
582
+ if (ep.definition("i18n")) return;
583
+ ep.declare({
584
+ name: "i18n",
585
+ contributions: {
586
+ languages: s.object({ code: s.string(), label: s.string(), nativeLabel: s.string().optional(), rtl: s.boolean().optional() }),
587
+ messages: s.object({ locale: s.string(), messages: s.record(s.string()) })
588
+ }
589
+ });
590
+ }
591
+ var read = (k) => {
592
+ try {
593
+ return localStorage.getItem(k);
594
+ } catch {
595
+ return null;
596
+ }
597
+ };
598
+ function I18nProvider({ children, defaultLocale = "en", storageKey = "fluxplugin.locale" }) {
599
+ const extra = useExtensionItems("i18n", "languages");
600
+ const bundles = useExtensionItems("i18n", "messages");
601
+ const [locale, setLocaleState] = useState3(() => read(storageKey) ?? (typeof navigator !== "undefined" ? navigator.language : defaultLocale));
602
+ const languages = useMemo3(() => {
603
+ const map = /* @__PURE__ */ new Map([[defaultLocale, { code: defaultLocale, label: "English", nativeLabel: "English" }]]);
604
+ for (const l of extra) map.set(l.code, l);
605
+ return [...map.values()];
606
+ }, [extra, defaultLocale]);
607
+ const active = useMemo3(() => {
608
+ const codes = languages.map((l) => l.code);
609
+ return codes.includes(locale) ? locale : codes.find((c) => locale.toLowerCase().startsWith(c.toLowerCase() + "-")) ?? defaultLocale;
610
+ }, [locale, languages, defaultLocale]);
611
+ const table = useMemo3(() => {
612
+ const m = /* @__PURE__ */ new Map();
613
+ for (const b of bundles) m.set(b.locale, { ...m.get(b.locale) ?? {}, ...b.messages });
614
+ return m;
615
+ }, [bundles]);
616
+ const t = useCallback3((key, fb, params) => interpolate(table.get(active)?.[key] ?? table.get(active.split("-")[0])?.[key] ?? fb ?? key, params), [table, active]);
617
+ const setLocale = useCallback3((code) => {
618
+ setLocaleState(code);
619
+ try {
620
+ localStorage.setItem(storageKey, code);
621
+ } catch {
622
+ }
623
+ }, [storageKey]);
624
+ useEffect3(() => {
625
+ document.documentElement.lang = active;
626
+ document.documentElement.dir = languages.find((l) => l.code === active)?.rtl ? "rtl" : "ltr";
627
+ }, [active, languages]);
628
+ const value = useMemo3(() => ({ locale: active, setLocale, languages, t }), [active, setLocale, languages, t]);
629
+ return /* @__PURE__ */ jsx3(Ctx.Provider, { value, children });
630
+ }
631
+ var useI18n = () => useContext2(Ctx);
632
+ var useT = () => useContext2(Ctx).t;
633
+
634
+ // src/react/palette.tsx
635
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
636
+ function useKeybindings(opts = {}) {
637
+ const fw = usePluginFramework();
638
+ useEffect4(() => {
639
+ if (opts.enabled === false) return;
640
+ const onKey = (e) => {
641
+ if (e.defaultPrevented) return;
642
+ const target = e.target;
643
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) && !(e.ctrlKey || e.metaKey)) return;
644
+ const parts = [];
645
+ if (e.ctrlKey || e.metaKey) parts.push("mod");
646
+ if (e.shiftKey) parts.push("shift");
647
+ if (e.altKey) parts.push("alt");
648
+ const key = e.key.toLowerCase();
649
+ if (["control", "shift", "alt", "meta"].includes(key)) return;
650
+ parts.push(key);
651
+ const cmd = fw.manager.commands.byKeybinding(parts.join("+"));
652
+ if (cmd) {
653
+ e.preventDefault();
654
+ void fw.manager.commands.execute(cmd.id).catch((err) => fw.manager.logs.logger("host").error(`command ${cmd.id} failed: ${String(err)}`));
655
+ }
656
+ };
657
+ window.addEventListener("keydown", onKey);
658
+ return () => window.removeEventListener("keydown", onKey);
659
+ }, [fw, opts.enabled]);
660
+ }
661
+ function CommandPalette({ open, onClose }) {
662
+ const fw = usePluginFramework();
663
+ const t = useT();
664
+ const cmds = fw.manager.commands;
665
+ useSyncExternalStore3(cmds.subscribe, () => cmds.version, () => cmds.version);
666
+ const [internal, setInternal] = useState4(false);
667
+ const isOpen = open ?? internal;
668
+ const close = () => onClose ? onClose() : setInternal(false);
669
+ const [query, setQuery] = useState4("");
670
+ const [index, setIndex] = useState4(0);
671
+ const input = useRef2(null);
672
+ useEffect4(() => {
673
+ const onKey = (e) => {
674
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
675
+ e.preventDefault();
676
+ setInternal((v) => !v);
677
+ } else if (e.key === "Escape") close();
678
+ };
679
+ window.addEventListener("keydown", onKey);
680
+ return () => window.removeEventListener("keydown", onKey);
681
+ }, []);
682
+ useEffect4(() => {
683
+ if (isOpen) {
684
+ setQuery("");
685
+ setIndex(0);
686
+ setTimeout(() => input.current?.focus(), 0);
687
+ }
688
+ }, [isOpen]);
689
+ const results = useMemo4(() => cmds.palette(query), [cmds, query, cmds.version]);
690
+ if (!isOpen) return null;
691
+ const run = (i) => {
692
+ const c = results[i];
693
+ if (!c) return;
694
+ close();
695
+ void cmds.execute(c.id).catch((e) => fw.manager.logs.logger("host").error(String(e)));
696
+ };
697
+ return /* @__PURE__ */ jsx4("div", { className: "flux-palette-backdrop", onMouseDown: close, role: "presentation", children: /* @__PURE__ */ jsxs3("div", { className: "flux-palette", role: "dialog", "aria-label": "Command palette", onMouseDown: (e) => e.stopPropagation(), children: [
698
+ /* @__PURE__ */ jsx4(
699
+ "input",
700
+ {
701
+ ref: input,
702
+ className: "flux-palette-input",
703
+ placeholder: t("palette.placeholder", "Type a command\u2026"),
704
+ value: query,
705
+ onChange: (e) => (setQuery(e.target.value), setIndex(0)),
706
+ onKeyDown: (e) => {
707
+ if (e.key === "ArrowDown") e.preventDefault(), setIndex((i) => Math.min(i + 1, results.length - 1));
708
+ else if (e.key === "ArrowUp") e.preventDefault(), setIndex((i) => Math.max(i - 1, 0));
709
+ else if (e.key === "Enter") run(index);
710
+ }
711
+ }
712
+ ),
713
+ /* @__PURE__ */ jsxs3("ul", { className: "flux-palette-list", role: "listbox", children: [
714
+ results.length === 0 && /* @__PURE__ */ jsx4("li", { className: "flux-palette-empty", children: t("palette.empty", "No matching commands") }),
715
+ results.map((c, i) => /* @__PURE__ */ jsxs3("li", { role: "option", "aria-selected": i === index, className: "flux-palette-item" + (i === index ? " is-active" : ""), onMouseEnter: () => setIndex(i), onClick: () => run(i), children: [
716
+ /* @__PURE__ */ jsxs3("span", { children: [
717
+ c.category && /* @__PURE__ */ jsxs3("span", { className: "flux-palette-cat", children: [
718
+ c.category,
719
+ ": "
720
+ ] }),
721
+ c.title
722
+ ] }),
723
+ c.keybinding && /* @__PURE__ */ jsx4("kbd", { children: c.keybinding.replace("Mod", /Mac/.test(navigator.platform) ? "\u2318" : "Ctrl") })
724
+ ] }, c.id))
725
+ ] })
726
+ ] }) });
727
+ }
728
+
729
+ // src/react/base-css.ts
730
+ var baseCss = `
731
+ :root{--flux-font:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI Variable","Segoe UI",system-ui,Roboto,"Helvetica Neue",sans-serif;--flux-font-size:14px;--flux-line:1.5;--flux-radius:10px;--flux-radius-lg:14px;--flux-pad:1;
732
+ --flux-bg:#fff;--flux-surface:#f5f5f7;--flux-surface-2:#e9e9ee;--flux-fg:#1d1d1f;--flux-muted:#6e6e73;--flux-border:rgba(60,60,67,.16);--flux-accent:#007aff;--flux-accent-fg:#fff;--flux-ok:#34c759;--flux-warn:#ff9f0a;--flux-danger:#ff3b30;--flux-shadow:0 10px 34px rgba(0,0,0,.14);--flux-material:rgba(246,246,248,.78)}
733
+ @media (prefers-color-scheme:dark){:root:not([data-theme]){--flux-bg:#1c1c1e;--flux-surface:#2c2c2e;--flux-surface-2:#3a3a3c;--flux-fg:#f5f5f7;--flux-muted:#98989d;--flux-border:rgba(235,235,245,.16);--flux-accent:#0a84ff;--flux-shadow:0 10px 34px rgba(0,0,0,.55);--flux-material:rgba(44,44,46,.72)}}
734
+ :root[data-theme="dark"]{--flux-bg:#1c1c1e;--flux-surface:#2c2c2e;--flux-surface-2:#3a3a3c;--flux-fg:#f5f5f7;--flux-muted:#98989d;--flux-border:rgba(235,235,245,.16);--flux-accent:#0a84ff;--flux-shadow:0 10px 34px rgba(0,0,0,.55);--flux-material:rgba(44,44,46,.72)}
735
+ :root[data-theme="light"]{--flux-bg:#fff;--flux-surface:#f5f5f7;--flux-surface-2:#e9e9ee;--flux-fg:#1d1d1f;--flux-muted:#6e6e73;--flux-border:rgba(60,60,67,.16);--flux-accent:#007aff;--flux-material:rgba(246,246,248,.78)}
736
+ :root[data-radius="sharp"]{--flux-radius:4px;--flux-radius-lg:6px}:root[data-radius="round"]{--flux-radius:14px;--flux-radius-lg:22px}:root[data-density="compact"]{--flux-pad:.7}
737
+ :root[data-motion="reduced"] *{transition:none!important;animation:none!important}
738
+ html{font-family:var(--flux-font);font-size:var(--flux-font-size);line-height:var(--flux-line);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
739
+ body{font-family:var(--flux-font);font-size:1em;line-height:var(--flux-line);color:var(--flux-fg);background:var(--flux-bg)}
740
+ button,input,select,textarea{font-family:inherit;font-size:inherit}
741
+ .flux-icon{flex:none;vertical-align:middle}
742
+ .flux-large-title{font-size:2em;font-weight:700;letter-spacing:-.025em;margin:0 0 .6em;line-height:1.15}.flux-title{font-size:1.5em;font-weight:700;letter-spacing:-.02em;margin:0 0 .8em}
743
+ .flux-empty,.flux-not-found,.flux-forbidden,.flux-loading{padding:24px;color:var(--flux-muted)}
744
+ .flux-plugin-error{padding:10px 14px;border-radius:var(--flux-radius);background:color-mix(in srgb,var(--flux-danger) 12%,transparent);color:var(--flux-danger);font-size:.92em}
745
+ .flux-breadcrumbs{font-size:.86em;color:var(--flux-muted);margin-bottom:6px}.flux-breadcrumbs a{color:inherit;text-decoration:none}.flux-breadcrumbs a:hover{color:var(--flux-accent)}
746
+ /* ---- materials (vibrancy) */
747
+ .flux-titlebar,.flux-side,.flux-dropdown,.flux-toast,.flux-palette{background:var(--flux-material);-webkit-backdrop-filter:saturate(180%) blur(24px);backdrop-filter:saturate(180%) blur(24px)}
748
+ :root[data-translucency="off"] .flux-titlebar,:root[data-translucency="off"] .flux-side,:root[data-translucency="off"] .flux-dropdown,:root[data-translucency="off"] .flux-toast,:root[data-translucency="off"] .flux-palette{background:var(--flux-surface);-webkit-backdrop-filter:none;backdrop-filter:none}
749
+ /* ---- buttons / switch / segmented */
750
+ .flux-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--flux-border);background:var(--flux-bg);color:var(--flux-fg);border-radius:calc(var(--flux-radius) - 2px);padding:calc(5px*var(--flux-pad)) 14px;font-weight:500;cursor:pointer;transition:background .12s,transform .06s}
751
+ .flux-btn:hover{background:var(--flux-surface)}.flux-btn:active{transform:scale(.97)}.flux-btn:disabled{opacity:.5;cursor:default}
752
+ .flux-btn-primary{background:var(--flux-accent);border-color:transparent;color:var(--flux-accent-fg)}.flux-btn-primary:hover{background:var(--flux-accent);filter:brightness(1.08)}
753
+ .flux-btn-destructive{color:var(--flux-danger)}.flux-btn-plain{border-color:transparent;background:transparent;color:var(--flux-accent)}.flux-btn-plain:hover{background:var(--flux-surface)}
754
+ .flux-btn-sm{padding:3px 11px;font-size:.92em;border-radius:99px}.flux-btn-lg{padding:9px 22px;font-size:1.05em;border-radius:99px}
755
+ .flux-switch{position:relative;width:42px;height:26px;border-radius:99px;border:0;background:var(--flux-surface-2);cursor:pointer;transition:background .18s;flex:none;padding:0}
756
+ .flux-switch span{position:absolute;top:2px;left:2px;width:22px;height:22px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.35);transition:transform .18s}
757
+ .flux-switch[data-on]{background:var(--flux-ok)}.flux-switch[data-on] span{transform:translateX(16px)}.flux-switch:focus-visible{outline:2px solid var(--flux-accent);outline-offset:2px}
758
+ .flux-seg{display:inline-flex;background:var(--flux-surface-2);border-radius:9px;padding:2px;gap:2px}
759
+ .flux-seg button{border:0;background:transparent;color:var(--flux-fg);border-radius:7px;padding:calc(4px*var(--flux-pad)) 14px;font-weight:500;cursor:pointer}
760
+ .flux-seg button[data-on]{background:var(--flux-bg);box-shadow:0 1px 3px rgba(0,0,0,.18)}
761
+ .flux-input{width:100%;box-sizing:border-box;border:1px solid var(--flux-border);background:var(--flux-bg);color:var(--flux-fg);border-radius:var(--flux-radius);padding:calc(7px*var(--flux-pad)) 12px;outline:none}.flux-input:focus{border-color:var(--flux-accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--flux-accent) 25%,transparent)}
762
+ .flux-select{background:var(--flux-bg);color:inherit;border:1px solid var(--flux-border);border-radius:7px;padding:3px 6px}
763
+ /* ---- grouped lists */
764
+ .flux-group-box{margin:0 0 calc(22px*var(--flux-pad))}.flux-group-title{font-size:.78em;text-transform:uppercase;letter-spacing:.06em;color:var(--flux-muted);margin:0 14px 6px;font-weight:600}
765
+ .flux-group-body{background:var(--flux-surface);border-radius:var(--flux-radius-lg);overflow:hidden}.flux-group-footer{font-size:.82em;color:var(--flux-muted);margin:6px 14px 0}
766
+ .flux-row{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;padding:calc(10px*var(--flux-pad)) 14px;border:0;background:transparent;color:inherit;text-align:left;position:relative;min-height:calc(44px*var(--flux-pad))}
767
+ .flux-row+.flux-row::before{content:"";position:absolute;top:0;left:14px;right:0;height:1px;background:var(--flux-border)}
768
+ .flux-row.is-link{cursor:pointer}.flux-row.is-link:hover{background:color-mix(in srgb,var(--flux-fg) 5%,transparent)}.flux-row[data-selected]{background:color-mix(in srgb,var(--flux-accent) 14%,transparent)}
769
+ .flux-row-icon{width:28px;height:28px;border-radius:7px;display:inline-flex;align-items:center;justify-content:center;background:var(--flux-accent);color:#fff;flex:none;overflow:hidden}
770
+ .flux-row-text{display:flex;flex-direction:column;min-width:0;flex:1}.flux-row-title{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.flux-row-sub{font-size:.88em;color:var(--flux-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.flux-row-right{display:flex;align-items:center;gap:8px;color:var(--flux-muted)}
771
+ .flux-img-fallback{display:inline-flex;align-items:center;justify-content:center;background:var(--flux-surface-2);color:var(--flux-muted);border-radius:inherit;min-width:32px;min-height:32px}
772
+ /* ---- title bar */
773
+ .flux-titlebar{--flux-titlebar-h:40px;height:var(--flux-titlebar-h);box-sizing:border-box;display:grid;grid-template-columns:1fr minmax(160px,400px) 1fr;align-items:center;gap:8px;padding:0 10px;border-bottom:1px solid var(--flux-border);color:var(--flux-fg);-webkit-app-region:drag;user-select:none;position:relative;z-index:50}
774
+ .flux-titlebar[data-electron][data-os="mac"]{padding-left:82px}
775
+ .flux-titlebar[data-electron][data-os="windows"],.flux-titlebar[data-electron][data-os="linux"]{padding-right:calc(100vw - env(titlebar-area-x,0px) - env(titlebar-area-width,100vw) + 8px)}
776
+ .flux-titlebar button,.flux-titlebar input,.flux-titlebar select,.flux-titlebar .flux-dropdown{-webkit-app-region:no-drag}
777
+ .flux-tb-left,.flux-tb-right{display:flex;align-items:center;gap:2px;min-width:0}.flux-tb-right{justify-content:flex-end}
778
+ .flux-brand{display:flex;align-items:center;gap:8px;font-weight:650;margin-right:8px;white-space:nowrap}
779
+ .flux-tb-btn{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:inherit;border-radius:6px;padding:4px 9px;font:inherit;cursor:pointer;position:relative}
780
+ .flux-tb-btn:hover,.flux-tb-btn.is-open{background:color-mix(in srgb,var(--flux-fg) 9%,transparent)}
781
+ .flux-menubar{display:flex}.flux-menu{position:relative}
782
+ .flux-dropdown{position:absolute;top:calc(100% + 4px);left:0;min-width:230px;max-width:380px;margin:0;padding:5px;list-style:none;color:var(--flux-fg);border:1px solid var(--flux-border);border-radius:10px;box-shadow:var(--flux-shadow);z-index:60}
783
+ .flux-dd-item{display:flex;justify-content:space-between;align-items:center;gap:18px;width:100%;border:0;background:transparent;color:inherit;font:inherit;text-align:left;padding:5px 10px;border-radius:5px;cursor:pointer}
784
+ .flux-dd-item:hover,.flux-dd-item.is-active{background:var(--flux-accent);color:var(--flux-accent-fg)}.flux-dd-item kbd,.flux-dd-item small{opacity:.6;font:11px ui-monospace,monospace}.flux-sep{height:1px;background:var(--flux-border);margin:5px 6px}
785
+ .flux-search{position:relative;display:flex;align-items:center;gap:8px;background:color-mix(in srgb,var(--flux-fg) 7%,transparent);border-radius:8px;padding:0 10px;height:26px;color:var(--flux-muted)}
786
+ .flux-search input{flex:1;min-width:0;border:0;background:transparent;color:var(--flux-fg);outline:none}.flux-search:focus-within{box-shadow:0 0 0 3px color-mix(in srgb,var(--flux-accent) 30%,transparent)}
787
+ .flux-search-results{left:0;right:0;max-width:none;top:calc(100% + 6px)}.flux-group{font-size:.74em;text-transform:uppercase;letter-spacing:.06em;color:var(--flux-muted);padding:7px 10px 2px;font-weight:600}
788
+ .flux-bell{position:relative}.flux-badge{position:absolute;top:0;right:0;min-width:15px;height:15px;border-radius:99px;background:var(--flux-danger);color:#fff;font-size:10px;line-height:15px;text-align:center}
789
+ .flux-notif-panel{left:auto;right:0;width:350px;max-height:440px;overflow:auto;padding:0}.flux-notif-head{display:flex;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--flux-border)}
790
+ .flux-notif{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--flux-border)}.flux-notif p{margin:2px 0 4px;font-size:.92em}.flux-notif small{color:var(--flux-muted)}.flux-notif-actions{display:flex;flex-direction:column;align-items:flex-end;gap:4px}
791
+ .flux-notif.flux-success{box-shadow:inset 3px 0 var(--flux-ok)}.flux-notif.flux-warning{box-shadow:inset 3px 0 var(--flux-warn)}.flux-notif.flux-error{box-shadow:inset 3px 0 var(--flux-danger)}.flux-notif.flux-info{box-shadow:inset 3px 0 var(--flux-accent)}
792
+ .flux-link{background:none;border:0;color:var(--flux-accent);cursor:pointer;font:inherit;font-size:.88em;padding:0}
793
+ .flux-toasts{position:fixed;top:48px;right:14px;display:flex;flex-direction:column;gap:8px;z-index:40;pointer-events:none}
794
+ .flux-toast{pointer-events:auto;color:var(--flux-fg);border:1px solid var(--flux-border);border-radius:14px;padding:11px 15px;box-shadow:var(--flux-shadow);max-width:350px;font-size:.93em}
795
+ .flux-toast.flux-success{border-left:4px solid var(--flux-ok)}.flux-toast.flux-warning{border-left:4px solid var(--flux-warn)}.flux-toast.flux-error{border-left:4px solid var(--flux-danger)}
796
+ /* ---- command palette (Spotlight) */
797
+ .flux-palette-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.28);display:flex;justify-content:center;align-items:flex-start;padding-top:14vh;z-index:9999}
798
+ .flux-palette{width:min(600px,92vw);color:var(--flux-fg);border:1px solid var(--flux-border);border-radius:16px;box-shadow:var(--flux-shadow);overflow:hidden}
799
+ .flux-palette-input{width:100%;box-sizing:border-box;padding:16px 18px;border:0;border-bottom:1px solid var(--flux-border);background:transparent;color:inherit;font-size:1.25em;outline:none}
800
+ .flux-palette-list{list-style:none;margin:0;padding:6px;max-height:360px;overflow:auto}.flux-palette-item{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;border-radius:8px;cursor:pointer}
801
+ .flux-palette-item.is-active{background:var(--flux-accent);color:var(--flux-accent-fg)}.flux-palette-cat{opacity:.65}.flux-palette-empty{padding:16px;color:var(--flux-muted)}.flux-palette kbd{font:11px ui-monospace,monospace;opacity:.7}
802
+ /* ---- sidebar */
803
+ .flux-side{display:flex;flex-direction:column;justify-content:space-between;height:100%;box-sizing:border-box;padding:12px 10px;border-right:1px solid var(--flux-border);overflow-y:auto;overflow-x:hidden;transition:padding .18s}
804
+ .flux-side-nav,.flux-side-bottom{display:flex;flex-direction:column;gap:2px}.flux-side-sep{height:1px;background:var(--flux-border);margin:8px 8px}
805
+ .flux-nav-group{position:relative}
806
+ .flux-nav{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;padding:calc(6px*var(--flux-pad)) 10px;border:0;background:transparent;color:inherit;text-decoration:none;border-radius:8px;cursor:pointer;font-weight:500;text-align:left;white-space:nowrap}
807
+ .flux-nav:hover{background:color-mix(in srgb,var(--flux-fg) 7%,transparent)}.flux-nav[data-active]{background:var(--flux-accent);color:var(--flux-accent-fg)}.flux-nav[data-parent-active]{font-weight:650}
808
+ .flux-nav-icon{display:inline-flex;width:22px;justify-content:center;flex:none}.flux-nav-label{flex:1;overflow:hidden;text-overflow:ellipsis}
809
+ .flux-nav-badge{font-size:.78em;background:var(--flux-danger);color:#fff;border-radius:99px;padding:0 6px}.flux-nav-chev{display:inline-flex;transition:transform .15s;opacity:.6}.flux-nav-chev[data-open]{transform:rotate(90deg)}
810
+ .flux-subnav{margin-left:14px;padding-left:8px;border-left:1px solid var(--flux-border);display:flex;flex-direction:column;gap:2px;margin-top:2px}.flux-subnav .flux-nav{font-weight:450;padding-top:calc(4px*var(--flux-pad));padding-bottom:calc(4px*var(--flux-pad))}
811
+ .flux-side[data-collapsed] .flux-nav{justify-content:center;padding:9px 0}
812
+ .flux-flyout{display:none;position:absolute;left:calc(100% + 6px);top:0;min-width:190px;padding:6px;background:var(--flux-bg);border:1px solid var(--flux-border);border-radius:10px;box-shadow:var(--flux-shadow);z-index:80}.flux-nav-group:hover>.flux-flyout{display:block}
813
+ .flux-flyout-title{font-size:.75em;text-transform:uppercase;letter-spacing:.06em;color:var(--flux-muted);padding:4px 10px}.flux-flyout-item{display:block;padding:6px 10px;border-radius:6px;color:inherit;text-decoration:none}.flux-flyout-item:hover,.flux-flyout-item[data-active]{background:var(--flux-accent);color:var(--flux-accent-fg)}
814
+ .flux-side-footer{font-size:.78em;color:var(--flux-muted);padding:6px 10px}
815
+ /* ---- settings */
816
+ .flux-settings{display:grid;grid-template-columns:240px minmax(0,1fr);gap:28px;align-items:start}.flux-settings-nav{display:flex;flex-direction:column;gap:2px;position:sticky;top:0}
817
+ .flux-settings-item{display:flex;align-items:center;gap:10px;border:0;background:transparent;color:inherit;border-radius:9px;padding:calc(7px*var(--flux-pad)) 10px;cursor:pointer;text-align:left;font-weight:500}.flux-settings-item:hover{background:color-mix(in srgb,var(--flux-fg) 7%,transparent)}.flux-settings-item[data-active]{background:var(--flux-accent);color:var(--flux-accent-fg)}
818
+ .flux-settings-body{min-width:0;max-width:720px}.flux-swatches{display:flex;gap:10px;flex-wrap:wrap}.flux-swatch{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer;box-shadow:inset 0 0 0 2px var(--flux-bg)}.flux-swatch[data-on]{border-color:var(--flux-fg)}
819
+ .flux-theme-card{border:2px solid var(--flux-border);border-radius:12px;overflow:hidden;cursor:pointer;background:none;padding:0;text-align:left;width:128px}.flux-theme-card[data-on]{border-color:var(--flux-accent)}.flux-theme-card .flux-theme-prev{height:62px;display:flex}.flux-theme-card span.cap{display:block;padding:6px 9px;font-size:.84em;font-weight:500}
820
+ .flux-range{width:180px;accent-color:var(--flux-accent)}
821
+ /* ---- plugin pages */
822
+ .flux-pm{max-width:860px;margin:0 auto}.flux-pm-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
823
+ .flux-pm-back{display:inline-flex;align-items:center;gap:2px;border:0;background:none;color:var(--flux-accent);cursor:pointer;font-size:1em;padding:0;margin-bottom:12px}
824
+ .flux-pm-icon{width:56px;height:56px;border-radius:24%;object-fit:cover;flex:none;box-shadow:0 1px 4px rgba(0,0,0,.2);overflow:hidden;display:inline-flex}.flux-pm-icon.lg{width:104px;height:104px;border-radius:23%;box-shadow:0 4px 18px rgba(0,0,0,.22)}
825
+ .flux-pm-hero{display:flex;gap:22px;align-items:center;margin:4px 0 22px;flex-wrap:wrap}.flux-pm-hero h1{margin:0;font-size:2em;letter-spacing:-.02em;line-height:1.15}.flux-pm-hero .by{color:var(--flux-muted);margin:3px 0 12px}.flux-pm-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
826
+ .flux-pm-facts{display:flex;border-top:1px solid var(--flux-border);border-bottom:1px solid var(--flux-border);margin:0 0 22px;overflow:auto}.flux-pm-fact{flex:1;min-width:104px;padding:12px 8px;text-align:center;border-right:1px solid var(--flux-border)}.flux-pm-fact:last-child{border:0}.flux-pm-fact small{display:block;font-size:.7em;letter-spacing:.06em;text-transform:uppercase;color:var(--flux-muted);font-weight:600}.flux-pm-fact b{font-size:1.15em;font-weight:650}
827
+ .flux-pm-shots{display:flex;gap:14px;overflow-x:auto;padding:4px 2px 14px;scroll-snap-type:x mandatory}.flux-pm-shots img,.flux-pm-banner{height:260px;border-radius:var(--flux-radius-lg);border:1px solid var(--flux-border);scroll-snap-align:start;cursor:zoom-in;flex:none}.flux-pm-banner{width:100%;height:auto;max-height:240px;object-fit:cover;cursor:default;margin-bottom:18px}
828
+ .flux-pm h3.sec{font-size:1.3em;font-weight:700;letter-spacing:-.015em;margin:26px 0 10px}
829
+ .flux-pill{display:inline-block;font-size:.76em;padding:2px 9px;border-radius:99px;background:var(--flux-surface-2);color:var(--flux-muted);font-weight:600}.flux-pill.ok{background:color-mix(in srgb,var(--flux-ok) 18%,transparent);color:var(--flux-ok)}.flux-pill.err{background:color-mix(in srgb,var(--flux-danger) 16%,transparent);color:var(--flux-danger)}.flux-pill.warn{background:color-mix(in srgb,var(--flux-warn) 20%,transparent);color:var(--flux-warn)}
830
+ .flux-zoom{position:fixed;inset:0;background:rgba(0,0,0,.82);display:flex;align-items:center;justify-content:center;z-index:9999;cursor:zoom-out}.flux-zoom img{max-width:92vw;max-height:90vh;border-radius:14px}
831
+ .flux-video{width:100%;max-width:640px;aspect-ratio:16/9;border:0;border-radius:var(--flux-radius-lg);background:#000;display:block}.flux-video-link{display:inline-flex;gap:6px;align-items:center;color:var(--flux-accent);text-decoration:none}
832
+ .flux-pre{max-height:200px;overflow:auto;font:12px ui-monospace,monospace;background:var(--flux-surface);padding:12px;border-radius:var(--flux-radius);margin:0;white-space:pre-wrap}
833
+ .flux-fields label{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:10px 14px}
834
+ /* ---- Markdown: app-store style typography */
835
+ .flux-md{font-size:1.04em;line-height:1.68;color:var(--flux-fg);letter-spacing:-.004em;max-width:70ch;overflow-wrap:anywhere}
836
+ .flux-md>:first-child{margin-top:0}.flux-md p{margin:.85em 0}
837
+ .flux-md h2{font-size:1.6em;font-weight:700;letter-spacing:-.022em;line-height:1.2;margin:1.7em 0 .5em}.flux-md h3{font-size:1.28em;font-weight:650;letter-spacing:-.015em;margin:1.5em 0 .4em}.flux-md h4,.flux-md h5{font-size:.82em;font-weight:650;letter-spacing:.07em;text-transform:uppercase;color:var(--flux-muted);margin:1.6em 0 .4em}
838
+ .flux-md a{color:var(--flux-accent);text-decoration:none}.flux-md a:hover{text-decoration:underline;text-underline-offset:3px}
839
+ .flux-md ul,.flux-md ol{padding-left:1.3em;margin:.8em 0}.flux-md li{margin:.4em 0;padding-left:.2em}.flux-md li::marker{color:var(--flux-muted)}.flux-md li.flux-task{list-style:none;margin-left:-1.2em;display:flex;gap:8px;align-items:baseline}
840
+ .flux-md code{font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace;font-size:.86em;background:var(--flux-surface-2);padding:.14em .42em;border-radius:6px}
841
+ .flux-md pre{background:var(--flux-surface);border:1px solid var(--flux-border);border-radius:14px;padding:14px 18px;overflow:auto;line-height:1.55;margin:1.1em 0}.flux-md pre code{background:none;padding:0;font-size:.85em}
842
+ .flux-md blockquote{margin:1.1em 0;padding:.2em 1em;border-left:3px solid var(--flux-border);color:var(--flux-muted)}
843
+ .flux-md hr{border:0;height:1px;background:var(--flux-border);margin:2em 0}
844
+ .flux-md table{width:100%;border-collapse:separate;border-spacing:0;margin:1.2em 0;font-size:.94em;border:1px solid var(--flux-border);border-radius:14px;overflow:hidden}.flux-md th{text-align:left;font-size:.78em;letter-spacing:.06em;text-transform:uppercase;color:var(--flux-muted);background:var(--flux-surface);padding:9px 14px}.flux-md td{padding:10px 14px;border-top:1px solid var(--flux-border)}
845
+ .flux-md-img{max-width:100%;height:auto;border-radius:14px;border:1px solid var(--flux-border);margin:.9em 0;display:block}
846
+ .flux-callout{display:flex;flex-direction:column;gap:2px;margin:1.2em 0;padding:13px 16px;border-radius:14px;background:color-mix(in srgb,var(--c,var(--flux-accent)) 11%,transparent);border-left:4px solid var(--c,var(--flux-accent))}
847
+ .flux-callout strong{color:var(--c,var(--flux-accent));font-size:.82em;letter-spacing:.05em;text-transform:uppercase}.flux-callout[data-kind="warning"],.flux-callout[data-kind="caution"]{--c:var(--flux-warn)}.flux-callout[data-kind="tip"]{--c:var(--flux-ok)}.flux-callout[data-kind="important"]{--c:#af52de}
848
+ `;
849
+
850
+ // src/react/http-bridge.ts
851
+ var HttpBridgeError = class extends Error {
852
+ constructor(message, code) {
853
+ super(message);
854
+ this.code = code;
855
+ this.name = "HttpBridgeError";
856
+ }
857
+ code;
858
+ };
859
+ function createHttpBridge(baseUrl = "") {
860
+ const listeners2 = /* @__PURE__ */ new Map();
861
+ let source;
862
+ const ensureSource = () => {
863
+ if (source || typeof EventSource === "undefined") return;
864
+ source = new EventSource(`${baseUrl}/flux/events`);
865
+ source.onmessage = (ev) => {
866
+ try {
867
+ const msg = JSON.parse(ev.data);
868
+ for (const l of [...listeners2.get(msg.channel) ?? []]) l(msg.payload);
869
+ } catch {
870
+ }
871
+ };
872
+ };
873
+ const post = (path, body) => fetch(`${baseUrl}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
874
+ return {
875
+ async invoke(channel, input, caller) {
876
+ const res = await (await post("/flux/invoke", { channel, input, caller })).json();
877
+ if (!res.ok) throw new HttpBridgeError(res.error.message, res.error.code);
878
+ return res.value;
879
+ },
880
+ send(channel, payload, caller) {
881
+ void post("/flux/send", { channel, payload, caller });
882
+ },
883
+ on(channel, listener) {
884
+ ensureSource();
885
+ let set = listeners2.get(channel);
886
+ if (!set) listeners2.set(channel, set = /* @__PURE__ */ new Set());
887
+ set.add(listener);
888
+ return () => void set.delete(listener);
889
+ },
890
+ async bootstrap() {
891
+ return await (await fetch(`${baseUrl}/flux/bootstrap`)).json();
892
+ }
893
+ };
894
+ }
895
+ function createStaticBridge(bootstrapUrl = "./bootstrap.json") {
896
+ return {
897
+ async invoke() {
898
+ throw new Error("This is a static website: main-process features (IPC, plugin management) are not available.");
899
+ },
900
+ send() {
901
+ },
902
+ on() {
903
+ return () => {
904
+ };
905
+ },
906
+ async bootstrap() {
907
+ return await (await fetch(bootstrapUrl)).json();
908
+ }
909
+ };
910
+ }
911
+
912
+ // src/react/os.ts
913
+ import { useMemo as useMemo5 } from "react";
914
+ function detectOS(ua = typeof navigator === "undefined" ? "" : navigator.userAgent) {
915
+ if (/Mac|iPhone|iPad/i.test(ua)) return "mac";
916
+ if (/Windows/i.test(ua)) return "windows";
917
+ if (/Linux|X11|CrOS/i.test(ua)) return "linux";
918
+ return "other";
919
+ }
920
+ var useOS = () => useMemo5(() => detectOS(), []);
921
+ function formatShortcut(binding, os = detectOS()) {
922
+ const mac = os === "mac";
923
+ const map = mac ? { mod: "\u2318", ctrl: "\u2303", shift: "\u21E7", alt: "\u2325" } : { mod: "Ctrl", ctrl: "Ctrl", shift: "Shift", alt: "Alt" };
924
+ const parts = binding.split("+").map((p) => map[p.toLowerCase()] ?? (p.length === 1 ? p.toUpperCase() : p));
925
+ return mac ? parts.join("") : parts.join("+");
926
+ }
927
+
928
+ // src/react/icons.tsx
929
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
930
+ var PATHS = {
931
+ search: "M11 4a7 7 0 105.2 11.7l3.5 3.5 1.4-1.4-3.5-3.5A7 7 0 0011 4zm0 2a5 5 0 110 10 5 5 0 010-10z",
932
+ bell: "M12 3a6 6 0 00-6 6v3.6L4.3 15.3A1 1 0 005 17h14a1 1 0 00.7-1.7L18 12.6V9a6 6 0 00-6-6zm-2 16a2 2 0 004 0h-4z",
933
+ sun: "M12 7a5 5 0 100 10 5 5 0 000-10zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM1 11h3v2H1v-2zm19 0h3v2h-3v-2zM4.2 5.6l1.4-1.4 2.1 2.1-1.4 1.4-2.1-2.1zm12.1 12.1l1.4-1.4 2.1 2.1-1.4 1.4-2.1-2.1zM5.6 19.8l-1.4-1.4 2.1-2.1 1.4 1.4-2.1 2.1zM18.4 7.7l-1.4-1.4 2.1-2.1 1.4 1.4-2.1 2.1z",
934
+ moon: "M20 14.5A8.5 8.5 0 019.5 4a8.5 8.5 0 1010.5 10.5z",
935
+ plug: "M9 2v5H7v3a5 5 0 004 4.9V19H8v2h8v-2h-3v-4.1A5 5 0 0017 10V7h-2V2h-2v5h-2V2H9z",
936
+ settings: "M12 8a4 4 0 100 8 4 4 0 000-8zm8.9 5l1.7 1.3-2 3.4-2-.8a7.6 7.6 0 01-1.7 1l-.3 2.1h-4l-.3-2.1a7.6 7.6 0 01-1.7-1l-2 .8-2-3.4L5.3 13a7 7 0 010-2L3.6 9.7l2-3.4 2 .8a7.6 7.6 0 011.7-1L9.6 4h4l.3 2.1a7.6 7.6 0 011.7 1l2-.8 2 3.4L17.9 11a7 7 0 010 2z",
937
+ check: "M9 16.2l-3.5-3.5L4 14.2l5 5 11-11-1.5-1.5L9 16.2z",
938
+ close: "M6.4 5l-1.4 1.4L10.6 12 5 17.6 6.4 19l5.6-5.6 5.6 5.6 1.4-1.4-5.6-5.6L19 6.4 17.6 5 12 10.6 6.4 5z",
939
+ chevron: "M7 10l5 5 5-5H7z",
940
+ play: "M8 5v14l11-7L8 5z",
941
+ image: "M4 5h16a1 1 0 011 1v12a1 1 0 01-1 1H4a1 1 0 01-1-1V6a1 1 0 011-1zm3 3a2 2 0 100 4 2 2 0 000-4zm-3 9h16v-3l-4-4-5 5-3-3-4 4v1z",
942
+ home: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z",
943
+ folder: "M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z",
944
+ grid: "M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z",
945
+ panel: "M3 5v14h18V5H3zm6 12H5V7h4v10zm10 0h-8V7h8v10z",
946
+ globe: "M12 2a10 10 0 100 20 10 10 0 000-20zm6.9 6h-3a15.7 15.7 0 00-1.4-3.6A8 8 0 0118.9 8zM12 4c.8 1.2 1.5 2.5 1.9 4h-3.8c.4-1.5 1.1-2.8 1.9-4zM4.3 14a8.2 8.2 0 010-4h3.4a16.5 16.5 0 000 4H4.3zm.8 2h3a15.7 15.7 0 001.4 3.6A8 8 0 015.1 16zm3-8h-3a8 8 0 014.4-3.6A15.7 15.7 0 008.1 8zM12 20c-.8-1.2-1.5-2.5-1.9-4h3.8c-.4 1.5-1.1 2.8-1.9 4zm2.3-6H9.7a14.7 14.7 0 010-4h4.6a14.7 14.7 0 010 4zm.3 5.6a15.7 15.7 0 001.4-3.6h3a8 8 0 01-4.4 3.6zm1.7-5.6a16.5 16.5 0 000-4h3.4a8.2 8.2 0 010 4h-3.4z",
947
+ palette: "M12 3a9 9 0 000 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.3-.5-.8-.5-1.3 0-1.1.9-2 2-2H18c1.7 0 3-1.3 3-3 0-4.4-4-8-9-8zM6.5 12a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm3-4a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm5 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm3 4a1.5 1.5 0 110-3 1.5 1.5 0 010 3z",
948
+ language: "M12.9 15l-2.5-2.5c1.7-1.9 2.9-4 3.6-6.5H17V4h-7V2H8v2H1v2h11.2c-.7 1.9-1.7 3.7-3.2 5.3-1-1.1-1.8-2.3-2.4-3.3h-2c.7 1.6 1.7 3.2 3 4.7L3.3 17.3l1.4 1.4L9 14.4l2.5 2.5.4-1.9zM18.5 10h-2L12 22h2l1.1-3h4.8l1.1 3h2l-4.5-12zm-2.6 7l1.6-4.3 1.6 4.3h-3.2z",
949
+ info: "M12 2a10 10 0 100 20 10 10 0 000-20zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z",
950
+ sliders: "M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z",
951
+ user: "M12 12a4 4 0 100-8 4 4 0 000 8zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4z",
952
+ star: "M12 17.3l6.2 3.7-1.6-7L22 9.2l-7.2-.6L12 2 9.2 8.6 2 9.2 7.5 14l-1.6 7z",
953
+ download: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
954
+ trash: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
955
+ power: "M13 3h-2v10h2V3zm4.8 2.2l-1.4 1.4A6 6 0 116.6 6.6L5.2 5.2A8 8 0 1017.8 5.2z",
956
+ chevronRight: "M10 17l5-5-5-5v10z",
957
+ back: "M15.4 7.4L14 6l-6 6 6 6 1.4-1.4L10.8 12z",
958
+ type: "M5 4v3h5.5v12h3V7H19V4H5z",
959
+ external: "M19 19H5V5h7V3H5a2 2 0 00-2 2v14a2 2 0 002 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.6l-9.8 9.8 1.4 1.4L19 6.4V10h2V3h-7z",
960
+ bolt: "M7 2v11h3v9l7-12h-4l4-8z",
961
+ shield: "M12 1L3 5v6c0 5.6 3.8 10.7 9 12 5.2-1.3 9-6.4 9-12V5l-9-4z",
962
+ refresh: "M17.7 6.3A8 8 0 104 12h2a6 6 0 119.2-5.1L13 9h7V2l-2.3 2.3z",
963
+ command: "M7 3a4 4 0 00-1 7.9v2.2A4 4 0 107 21a4 4 0 004-4h2a4 4 0 104-4 4 4 0 00-1-.1V10.9A4 4 0 1017 3a4 4 0 00-4 4h-2a4 4 0 00-4-4zm4 9H9V10h2v2zm4 0h-2V10h2v2z"
964
+ };
965
+ function Icon({ name, d, size = 16, title }) {
966
+ const path = d ?? (name ? PATHS[name] : void 0) ?? PATHS["plug"];
967
+ return /* @__PURE__ */ jsxs4("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": title ? void 0 : true, role: title ? "img" : void 0, className: "flux-icon", children: [
968
+ title && /* @__PURE__ */ jsx5("title", { children: title }),
969
+ /* @__PURE__ */ jsx5("path", { d: path })
970
+ ] });
971
+ }
972
+ var iconNames = Object.keys(PATHS);
973
+
974
+ // src/react/markdown.tsx
975
+ import React6 from "react";
976
+
977
+ // src/react/media.tsx
978
+ import { useEffect as useEffect5, useMemo as useMemo6, useState as useState5 } from "react";
979
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
980
+ var IMAGE_MIME = /^image\/(png|jpe?g|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon|svg\+xml)$/i;
981
+ function sniff(bytes) {
982
+ const b = (...v) => v.every((x, i) => bytes[i] === x);
983
+ if (b(137, 80, 78, 71)) return "image/png";
984
+ if (b(255, 216, 255)) return "image/jpeg";
985
+ if (b(71, 73, 70, 56)) return "image/gif";
986
+ if (b(82, 73, 70, 70) && bytes[8] === 87 && bytes[9] === 69) return "image/webp";
987
+ if (b(66, 77)) return "image/bmp";
988
+ const head = new TextDecoder().decode(bytes.slice(0, 120)).trimStart();
989
+ if (head.startsWith("<svg") || head.startsWith("<?xml") && head.includes("<svg")) return "image/svg+xml";
990
+ return void 0;
991
+ }
992
+ function toImageSrc(input, opts = {}) {
993
+ if (input == null || input === "") return void 0;
994
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
995
+ if (input.type && !IMAGE_MIME.test(input.type)) return void 0;
996
+ const url = URL.createObjectURL(input);
997
+ return { src: url, release: () => URL.revokeObjectURL(url) };
998
+ }
999
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
1000
+ const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
1001
+ const mime = sniff(bytes);
1002
+ if (!mime) return void 0;
1003
+ const url = URL.createObjectURL(new Blob([bytes], { type: mime }));
1004
+ return { src: url, release: () => URL.revokeObjectURL(url) };
1005
+ }
1006
+ const s2 = String(input).trim();
1007
+ if (/^data:/i.test(s2)) {
1008
+ const m = /^data:([^;,]+)[;,]/i.exec(s2);
1009
+ return m && IMAGE_MIME.test(m[1]) ? { src: s2 } : void 0;
1010
+ }
1011
+ if (/^blob:/i.test(s2) || /^https?:\/\//i.test(s2) || /^file:\/\//i.test(s2)) return { src: s2 };
1012
+ if (/^[a-z][a-z0-9+.-]*:/i.test(s2) && !/^[a-z]:[\\/]/i.test(s2)) return /^fluxplugin:\/\//i.test(s2) ? { src: s2 } : void 0;
1013
+ if (/^[a-z]:[\\/]/i.test(s2)) return { src: "file:///" + s2.replace(/\\/g, "/") };
1014
+ if (s2.length > 64 && /^[A-Za-z0-9+/=\s]+$/.test(s2)) {
1015
+ try {
1016
+ const bin = atob(s2.replace(/\s/g, ""));
1017
+ const bytes = Uint8Array.from(bin.slice(0, 160), (c) => c.charCodeAt(0));
1018
+ const mime = sniff(bytes);
1019
+ if (mime) return { src: `data:${mime};base64,${s2.replace(/\s/g, "")}` };
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ if (s2.startsWith("/") && typeof location !== "undefined" && location.protocol === "file:") return { src: "file://" + s2 };
1024
+ if (opts.base && !s2.startsWith("/")) {
1025
+ try {
1026
+ return { src: new URL(s2, opts.base).href };
1027
+ } catch {
1028
+ return void 0;
1029
+ }
1030
+ }
1031
+ return { src: s2 };
1032
+ }
1033
+ function Img({ src, base, alt = "", fallback, className, style, ...rest }) {
1034
+ const [failed, setFailed] = useState5(false);
1035
+ const resolved = useMemo6(() => toImageSrc(src, { base }), [src, base]);
1036
+ useEffect5(() => {
1037
+ setFailed(false);
1038
+ return () => resolved?.release?.();
1039
+ }, [resolved]);
1040
+ if (!resolved || failed) return /* @__PURE__ */ jsx6(Fragment3, { children: fallback ?? /* @__PURE__ */ jsx6("span", { className: "flux-img-fallback " + (className ?? ""), style, role: "img", "aria-label": alt, children: /* @__PURE__ */ jsx6(Icon, { name: "image", size: 18 }) }) });
1041
+ return /* @__PURE__ */ jsx6("img", { ...rest, className, style, src: resolved.src, alt, loading: "lazy", decoding: "async", referrerPolicy: "no-referrer", onError: () => setFailed(true) });
1042
+ }
1043
+ function VideoPlayer({ video, poster, title = "Video" }) {
1044
+ if (video.kind === "html5" && video.src) {
1045
+ return /* @__PURE__ */ jsxs5("video", { className: "flux-video", "data-testid": "plugin-video", "data-kind": "html5", controls: true, playsInline: true, preload: "metadata", poster, "aria-label": title, children: [
1046
+ /* @__PURE__ */ jsx6("source", { src: video.src }),
1047
+ /* @__PURE__ */ jsx6("a", { href: video.url, target: "_blank", rel: "noopener noreferrer", children: "Download the video" })
1048
+ ] });
1049
+ }
1050
+ if (video.embedUrl) {
1051
+ return /* @__PURE__ */ jsx6(
1052
+ "iframe",
1053
+ {
1054
+ className: "flux-video",
1055
+ "data-testid": "plugin-video",
1056
+ "data-kind": video.kind,
1057
+ src: video.embedUrl,
1058
+ title,
1059
+ loading: "lazy",
1060
+ allow: "encrypted-media; picture-in-picture; fullscreen; autoplay=(self)",
1061
+ sandbox: "allow-scripts allow-same-origin allow-presentation allow-popups",
1062
+ referrerPolicy: "strict-origin-when-cross-origin",
1063
+ allowFullScreen: true
1064
+ }
1065
+ );
1066
+ }
1067
+ return /* @__PURE__ */ jsxs5("a", { className: "flux-video-link", "data-testid": "plugin-video", "data-kind": "link", href: video.url, target: "_blank", rel: "noopener noreferrer", children: [
1068
+ /* @__PURE__ */ jsx6(Icon, { name: "play", size: 18 }),
1069
+ " Watch the video"
1070
+ ] });
1071
+ }
1072
+
1073
+ // src/react/markdown.tsx
1074
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1075
+ var safeHref = (h) => /^https:\/\/[^\s]+$/i.test(h) || h.startsWith("#") ? h : void 0;
1076
+ function inline(src, key = 0, base) {
1077
+ const out = [];
1078
+ const re = /(!\[[^\]]*\]\([^)\s]+(?:\s+"[^"]*")?\))|(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\s][^*]*\*)|(\[[^\]]+\]\([^)\s]+\))/g;
1079
+ let last = 0;
1080
+ let m;
1081
+ let i = 0;
1082
+ while (m = re.exec(src)) {
1083
+ if (m.index > last) out.push(src.slice(last, m.index));
1084
+ const tok = m[0];
1085
+ const k = `${key}-${i++}`;
1086
+ if (tok.startsWith("![")) {
1087
+ const mm = /^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)$/.exec(tok);
1088
+ out.push(/* @__PURE__ */ jsx7(Img, { className: "flux-md-img", src: mm[2], base, alt: mm[1], title: mm[3] }, k));
1089
+ } else if (tok.startsWith("`")) out.push(/* @__PURE__ */ jsx7("code", { children: tok.slice(1, -1) }, k));
1090
+ else if (tok.startsWith("**")) out.push(/* @__PURE__ */ jsx7("strong", { children: tok.slice(2, -2) }, k));
1091
+ else if (tok.startsWith("*")) out.push(/* @__PURE__ */ jsx7("em", { children: tok.slice(1, -1) }, k));
1092
+ else {
1093
+ const mm = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(tok);
1094
+ const href = safeHref(mm[2]);
1095
+ out.push(href ? /* @__PURE__ */ jsx7("a", { href, target: "_blank", rel: "noopener noreferrer", children: mm[1] }, k) : mm[1]);
1096
+ }
1097
+ last = m.index + tok.length;
1098
+ }
1099
+ if (last < src.length) out.push(src.slice(last));
1100
+ return out;
1101
+ }
1102
+ function Markdown({ source, baseUrl }) {
1103
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
1104
+ const blocks = [];
1105
+ let i = 0;
1106
+ let k = 0;
1107
+ while (i < lines.length) {
1108
+ const line = lines[i];
1109
+ if (!line.trim()) {
1110
+ i++;
1111
+ continue;
1112
+ }
1113
+ if (line.startsWith("```")) {
1114
+ const buf2 = [];
1115
+ i++;
1116
+ while (i < lines.length && !lines[i].startsWith("```")) buf2.push(lines[i++]);
1117
+ i++;
1118
+ blocks.push(/* @__PURE__ */ jsx7("pre", { children: /* @__PURE__ */ jsx7("code", { children: buf2.join("\n") }) }, k++));
1119
+ continue;
1120
+ }
1121
+ const h = /^(#{1,4})\s+(.*)$/.exec(line);
1122
+ if (h) {
1123
+ const L = `h${h[1].length + 1}`;
1124
+ blocks.push(React6.createElement(L, { key: k++ }, inline(h[2], 0, baseUrl)));
1125
+ i++;
1126
+ continue;
1127
+ }
1128
+ if (/^(-{3,}|\*{3,})$/.test(line.trim())) {
1129
+ blocks.push(/* @__PURE__ */ jsx7("hr", {}, k++));
1130
+ i++;
1131
+ continue;
1132
+ }
1133
+ if (line.startsWith(">")) {
1134
+ const buf2 = [];
1135
+ while (i < lines.length && lines[i].startsWith(">")) buf2.push(lines[i++].replace(/^>\s?/, ""));
1136
+ const callout = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(.*)$/i.exec(buf2[0] ?? "");
1137
+ if (callout) {
1138
+ const kind = callout[1].toLowerCase();
1139
+ blocks.push(/* @__PURE__ */ jsxs6("aside", { className: "flux-callout", "data-kind": kind, role: "note", children: [
1140
+ /* @__PURE__ */ jsx7("strong", { children: kind[0].toUpperCase() + kind.slice(1) }),
1141
+ /* @__PURE__ */ jsx7("span", { children: inline([callout[2], ...buf2.slice(1)].filter(Boolean).join(" "), 0, baseUrl) })
1142
+ ] }, k++));
1143
+ } else blocks.push(/* @__PURE__ */ jsx7("blockquote", { children: inline(buf2.join(" "), 0, baseUrl) }, k++));
1144
+ continue;
1145
+ }
1146
+ if (line.includes("|") && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(lines[i + 1] ?? "")) {
1147
+ const cells = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
1148
+ const head = cells(line);
1149
+ i += 2;
1150
+ const rows = [];
1151
+ while (i < lines.length && lines[i].includes("|") && lines[i].trim()) rows.push(cells(lines[i++]));
1152
+ blocks.push(
1153
+ /* @__PURE__ */ jsxs6("table", { children: [
1154
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { children: head.map((c, j) => /* @__PURE__ */ jsx7("th", { children: inline(c, 0, baseUrl) }, j)) }) }),
1155
+ /* @__PURE__ */ jsx7("tbody", { children: rows.map((r, ri) => /* @__PURE__ */ jsx7("tr", { children: r.map((c, j) => /* @__PURE__ */ jsx7("td", { children: inline(c, 0, baseUrl) }, j)) }, ri)) })
1156
+ ] }, k++)
1157
+ );
1158
+ continue;
1159
+ }
1160
+ const li = /^(\s*)([-*]|\d+\.)\s+/;
1161
+ if (li.test(line)) {
1162
+ const ordered = /^\s*\d+\./.test(line);
1163
+ const items = [];
1164
+ while (i < lines.length && li.test(lines[i])) {
1165
+ const text = lines[i++].replace(li, "");
1166
+ const task = /^\[([ xX])\]\s+(.*)$/.exec(text);
1167
+ items.push(task ? /* @__PURE__ */ jsxs6("li", { className: "flux-task", children: [
1168
+ /* @__PURE__ */ jsx7("input", { type: "checkbox", checked: task[1] !== " ", readOnly: true, "aria-label": "task" }),
1169
+ inline(task[2], 0, baseUrl)
1170
+ ] }, items.length) : /* @__PURE__ */ jsx7("li", { children: inline(text, 0, baseUrl) }, items.length));
1171
+ }
1172
+ blocks.push(ordered ? /* @__PURE__ */ jsx7("ol", { children: items }, k++) : /* @__PURE__ */ jsx7("ul", { children: items }, k++));
1173
+ continue;
1174
+ }
1175
+ const buf = [];
1176
+ while (i < lines.length && lines[i].trim() && !/^(#{1,4}\s|```|>|\s*([-*]|\d+\.)\s)/.test(lines[i]) && !(lines[i].includes("|") && /^\s*\|?\s*:?-{2,}/.test(lines[i + 1] ?? ""))) buf.push(lines[i++]);
1177
+ blocks.push(/* @__PURE__ */ jsx7("p", { children: inline(buf.join(" "), 0, baseUrl) }, k++));
1178
+ }
1179
+ return /* @__PURE__ */ jsx7("div", { className: "flux-md", children: blocks });
1180
+ }
1181
+
1182
+ // src/react/theme.tsx
1183
+ import { createContext as createContext3, useCallback as useCallback4, useContext as useContext3, useEffect as useEffect6, useMemo as useMemo7, useState as useState6 } from "react";
1184
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1185
+ var isComponent = (v) => typeof v === "function" || typeof v === "object" && v !== null;
1186
+ function declareHostPoints(framework) {
1187
+ const ep = framework.manager.extensionPoints;
1188
+ if (!ep.definition("theme")) ep.declare({ name: "theme", contributions: { themes: s.object({ id: s.string(), label: s.string(), dark: s.boolean().optional(), tokens: s.record(s.string()) }) } });
1189
+ if (!ep.definition("font")) ep.declare({ name: "font", contributions: { fonts: s.object({ id: s.string(), label: s.string(), stack: s.string(), css: s.string().optional() }) } });
1190
+ if (!ep.definition("titlebar")) ep.declare({ name: "titlebar", contributions: { searchProviders: s.object({ id: s.string(), label: s.string(), search: s.fn() }) } });
1191
+ if (!ep.definition("settings")) {
1192
+ ep.declare({ name: "settings", contributions: { sections: s.object({ id: s.string(), title: s.string(), icon: s.string().optional(), order: s.number().optional(), component: s.custom(isComponent, "expected a React component") }) } });
1193
+ }
1194
+ declareI18nPoints(framework);
1195
+ }
1196
+ var DEFAULT_APPEARANCE = { fontId: "system", customFont: "", fontSize: 14, lineHeight: 1.5, density: "comfortable", translucency: true, radius: "default", reduceMotion: false };
1197
+ var BUILTIN_FONTS = [
1198
+ { id: "system", label: "System (SF Pro)", stack: '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable", "Segoe UI", system-ui, Roboto, "Helvetica Neue", sans-serif' },
1199
+ { id: "rounded", label: "Rounded", stack: 'ui-rounded, "SF Pro Rounded", "Nunito", "Segoe UI", system-ui, sans-serif' },
1200
+ { id: "serif", label: "Serif (New York)", stack: '"New York", "Iowan Old Style", "Palatino Linotype", Georgia, serif' },
1201
+ { id: "humanist", label: "Humanist", stack: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", sans-serif' },
1202
+ { id: "mono", label: "Monospace", stack: 'ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace' }
1203
+ ];
1204
+ var SAFE_FONT = /[^\w\s,.-]/g;
1205
+ var clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, Number.isFinite(n) ? n : lo));
1206
+ function sanitizeFontFamily(input) {
1207
+ return input.split(",").map((f) => f.replace(SAFE_FONT, "").replace(/\s+/g, " ").trim()).filter(Boolean).map((f) => `"${f}"`).join(", ");
1208
+ }
1209
+ var Ctx2 = createContext3(null);
1210
+ var read2 = (k) => {
1211
+ try {
1212
+ return localStorage.getItem(k);
1213
+ } catch {
1214
+ return null;
1215
+ }
1216
+ };
1217
+ var write = (k, v) => {
1218
+ try {
1219
+ v === void 0 ? localStorage.removeItem(k) : localStorage.setItem(k, v);
1220
+ } catch {
1221
+ }
1222
+ };
1223
+ function loadAppearance(key) {
1224
+ try {
1225
+ const raw = JSON.parse(read2(key) ?? "{}");
1226
+ return { ...DEFAULT_APPEARANCE, ...raw, fontSize: clamp(raw.fontSize ?? 14, 12, 20), lineHeight: clamp(raw.lineHeight ?? 1.5, 1.2, 2) };
1227
+ } catch {
1228
+ return DEFAULT_APPEARANCE;
1229
+ }
1230
+ }
1231
+ function ThemeProvider({ children, storageKey = "fluxplugin.theme", onResolved }) {
1232
+ const themes = useExtensionItems("theme", "themes");
1233
+ const pluginFonts = useExtensionItems("font", "fonts");
1234
+ const [mode, setModeState] = useState6(() => read2(storageKey + ".mode") || "system");
1235
+ const [themeId, setThemeIdState] = useState6(() => read2(storageKey + ".id") ?? void 0);
1236
+ const [appearance, setAppearanceState] = useState6(() => loadAppearance(storageKey + ".appearance"));
1237
+ const [systemDark, setSystemDark] = useState6(() => typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches);
1238
+ useEffect6(() => {
1239
+ if (typeof matchMedia === "undefined") return;
1240
+ const mq = matchMedia("(prefers-color-scheme: dark)");
1241
+ const l = (e) => setSystemDark(e.matches);
1242
+ mq.addEventListener("change", l);
1243
+ return () => mq.removeEventListener("change", l);
1244
+ }, []);
1245
+ const theme = themes.find((t) => t.id === themeId);
1246
+ const resolved = theme ? theme.dark ? "dark" : "light" : mode === "system" ? systemDark ? "dark" : "light" : mode;
1247
+ const fonts = useMemo7(() => [...BUILTIN_FONTS, ...pluginFonts], [pluginFonts]);
1248
+ const font = fonts.find((f) => f.id === appearance.fontId) ?? BUILTIN_FONTS[0];
1249
+ useEffect6(() => {
1250
+ const root = document.documentElement;
1251
+ root.setAttribute("data-theme", resolved);
1252
+ const applied = Object.entries(theme?.tokens ?? {}).filter(([k]) => /^--[\w-]+$/.test(k));
1253
+ for (const [k, v] of applied) root.style.setProperty(k, v);
1254
+ if (appearance.accent) root.style.setProperty("--flux-accent", appearance.accent);
1255
+ const cs = getComputedStyle(root);
1256
+ onResolved?.({ dark: resolved === "dark", bg: cs.getPropertyValue("--flux-bg").trim(), fg: cs.getPropertyValue("--flux-fg").trim(), surface: cs.getPropertyValue("--flux-surface").trim() });
1257
+ return () => {
1258
+ applied.forEach(([k]) => root.style.removeProperty(k));
1259
+ if (appearance.accent) root.style.removeProperty("--flux-accent");
1260
+ };
1261
+ }, [resolved, theme, appearance.accent]);
1262
+ useEffect6(() => {
1263
+ const root = document.documentElement;
1264
+ const custom = sanitizeFontFamily(appearance.customFont);
1265
+ root.style.setProperty("--flux-font", custom ? `${custom}, ${font.stack}` : font.stack);
1266
+ root.style.setProperty("--flux-font-size", `${appearance.fontSize}px`);
1267
+ root.style.setProperty("--flux-line", String(appearance.lineHeight));
1268
+ root.setAttribute("data-density", appearance.density);
1269
+ root.setAttribute("data-translucency", appearance.translucency ? "on" : "off");
1270
+ root.setAttribute("data-radius", appearance.radius);
1271
+ root.setAttribute("data-motion", appearance.reduceMotion ? "reduced" : "full");
1272
+ let style;
1273
+ if (font.css) {
1274
+ style = document.createElement("style");
1275
+ style.setAttribute("data-flux-font", font.id);
1276
+ style.textContent = font.css;
1277
+ document.head.appendChild(style);
1278
+ }
1279
+ return () => style?.remove();
1280
+ }, [appearance, font]);
1281
+ const setMode = useCallback4((m) => (setModeState(m), setThemeIdState(void 0), write(storageKey + ".mode", m), write(storageKey + ".id", void 0)), [storageKey]);
1282
+ const setThemeId = useCallback4((id) => (setThemeIdState(id), write(storageKey + ".id", id)), [storageKey]);
1283
+ const setAppearance = useCallback4((patch) => setAppearanceState((cur) => {
1284
+ const next = { ...cur, ...patch };
1285
+ next.fontSize = clamp(next.fontSize, 12, 20);
1286
+ next.lineHeight = clamp(next.lineHeight, 1.2, 2);
1287
+ write(storageKey + ".appearance", JSON.stringify(next));
1288
+ return next;
1289
+ }), [storageKey]);
1290
+ const resetAppearance = useCallback4(() => (setAppearanceState(DEFAULT_APPEARANCE), write(storageKey + ".appearance", void 0)), [storageKey]);
1291
+ const value = useMemo7(
1292
+ () => ({ mode, setMode, themeId, setThemeId, resolved, themes, toggle: () => setMode(resolved === "dark" ? "light" : "dark"), appearance, setAppearance, resetAppearance, fonts }),
1293
+ [mode, setMode, themeId, setThemeId, resolved, themes, appearance, setAppearance, resetAppearance, fonts]
1294
+ );
1295
+ return /* @__PURE__ */ jsx8(Ctx2.Provider, { value, children });
1296
+ }
1297
+ function useTheme() {
1298
+ const c = useContext3(Ctx2);
1299
+ if (!c) throw new Error("useTheme must be used inside <ThemeProvider>");
1300
+ return c;
1301
+ }
1302
+ var useAppearance = () => {
1303
+ const { appearance, setAppearance, resetAppearance, fonts } = useTheme();
1304
+ return { appearance, setAppearance, resetAppearance, fonts };
1305
+ };
1306
+ function ThemeSwitcher() {
1307
+ const { resolved, toggle, themes, themeId, setThemeId, mode, setMode } = useTheme();
1308
+ const t = useT();
1309
+ return /* @__PURE__ */ jsxs7("span", { className: "flux-theme-switch", children: [
1310
+ /* @__PURE__ */ jsx8("button", { type: "button", className: "flux-tb-btn", onClick: toggle, title: t("theme.toggle", "Toggle appearance"), "aria-label": t("theme.toggle", "Toggle appearance"), "data-testid": "theme-toggle", children: /* @__PURE__ */ jsx8(Icon, { name: resolved === "dark" ? "moon" : "sun" }) }),
1311
+ themes.length > 0 && /* @__PURE__ */ jsxs7("select", { className: "flux-select", "aria-label": "Theme", value: themeId ?? `mode:${mode}`, onChange: (e) => e.target.value.startsWith("mode:") ? setMode(e.target.value.slice(5)) : setThemeId(e.target.value), children: [
1312
+ /* @__PURE__ */ jsx8("option", { value: "mode:system", children: t("theme.system", "System") }),
1313
+ /* @__PURE__ */ jsx8("option", { value: "mode:light", children: t("theme.light", "Light") }),
1314
+ /* @__PURE__ */ jsx8("option", { value: "mode:dark", children: t("theme.dark", "Dark") }),
1315
+ themes.map((th) => /* @__PURE__ */ jsx8("option", { value: th.id, children: th.label }, th.id))
1316
+ ] })
1317
+ ] });
1318
+ }
1319
+
1320
+ // src/react/titlebar.tsx
1321
+ import { useEffect as useEffect7, useMemo as useMemo8, useRef as useRef3, useState as useState7, useSyncExternalStore as useSyncExternalStore4 } from "react";
1322
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1323
+ function AppMenuBar() {
1324
+ const fw = usePluginFramework();
1325
+ const os = useOS();
1326
+ const menus = fw.manager.menus;
1327
+ useSyncExternalStore4(menus.subscribe, () => menus.version, () => menus.version);
1328
+ const [open, setOpen] = useState7(null);
1329
+ const ref = useRef3(null);
1330
+ useEffect7(() => {
1331
+ const off = (e) => ref.current && !ref.current.contains(e.target) && setOpen(null);
1332
+ const key = (e) => e.key === "Escape" && setOpen(null);
1333
+ window.addEventListener("mousedown", off);
1334
+ window.addEventListener("keydown", key);
1335
+ return () => (window.removeEventListener("mousedown", off), window.removeEventListener("keydown", key));
1336
+ }, []);
1337
+ const top = menus.items("application");
1338
+ if (!top.length) return null;
1339
+ const run = (item) => {
1340
+ setOpen(null);
1341
+ if (item.command) void fw.manager.commands.execute(item.command, ...item.args ?? []).catch((e) => fw.manager.logs.logger("host").error(String(e)));
1342
+ else if (item.path) navigate(item.path);
1343
+ };
1344
+ const shortcut = (item) => {
1345
+ const k = item.command ? fw.manager.commands.get(item.command)?.keybinding : void 0;
1346
+ return k ? formatShortcut(k, os) : "";
1347
+ };
1348
+ return /* @__PURE__ */ jsx9("div", { className: "flux-menubar", role: "menubar", ref, children: top.map((m) => /* @__PURE__ */ jsxs8("div", { className: "flux-menu", children: [
1349
+ /* @__PURE__ */ jsx9(
1350
+ "button",
1351
+ {
1352
+ type: "button",
1353
+ role: "menuitem",
1354
+ className: "flux-tb-btn" + (open === m.id ? " is-open" : ""),
1355
+ "aria-haspopup": !!m.children?.length,
1356
+ "aria-expanded": open === m.id,
1357
+ onClick: () => m.children?.length ? setOpen(open === m.id ? null : m.id) : run(m),
1358
+ onMouseEnter: () => open && m.children?.length && setOpen(m.id),
1359
+ children: m.label
1360
+ }
1361
+ ),
1362
+ open === m.id && m.children && /* @__PURE__ */ jsx9("ul", { className: "flux-dropdown", role: "menu", children: m.children.map(
1363
+ (c) => c.separator ? /* @__PURE__ */ jsx9("li", { className: "flux-sep", role: "separator" }, c.id) : /* @__PURE__ */ jsx9("li", { role: "none", children: /* @__PURE__ */ jsxs8("button", { type: "button", role: "menuitem", className: "flux-dd-item", onClick: () => run(c), children: [
1364
+ /* @__PURE__ */ jsx9("span", { children: c.label }),
1365
+ /* @__PURE__ */ jsx9("kbd", { children: shortcut(c) })
1366
+ ] }) }, c.id)
1367
+ ) })
1368
+ ] }, m.id)) });
1369
+ }
1370
+ function SearchBox({ placeholder }) {
1371
+ const fw = usePluginFramework();
1372
+ const t = useT();
1373
+ const providers = useExtensionItems("titlebar", "searchProviders");
1374
+ const [q, setQ] = useState7("");
1375
+ const [results, setResults] = useState7([]);
1376
+ const [idx, setIdx] = useState7(0);
1377
+ const [focus, setFocus] = useState7(false);
1378
+ const input = useRef3(null);
1379
+ useEffect7(() => {
1380
+ const k = (e) => (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "f" && !e.shiftKey && (e.preventDefault(), input.current?.focus());
1381
+ window.addEventListener("keydown", k);
1382
+ return () => window.removeEventListener("keydown", k);
1383
+ }, []);
1384
+ useEffect7(() => {
1385
+ const query = q.trim();
1386
+ if (!query) return setResults((prev) => prev.length ? [] : prev);
1387
+ let live = true;
1388
+ (async () => {
1389
+ const lower = query.toLowerCase();
1390
+ const own = [
1391
+ ...fw.manager.commands.palette(query).slice(0, 5).map((c) => ({ id: "cmd:" + c.id, title: c.title, subtitle: c.category, group: t("search.commands", "Commands"), run: () => void fw.manager.commands.execute(c.id) })),
1392
+ ...fw.manager.menus.items("sidebar").filter((m) => m.path && m.label.toLowerCase().includes(lower)).map((m) => ({ id: "page:" + m.id, title: m.label, group: t("search.pages", "Pages"), run: () => navigate(m.path) }))
1393
+ ];
1394
+ const extra = await Promise.all(providers.map(async (p) => {
1395
+ try {
1396
+ return (await p.search(query) ?? []).map((r) => ({ ...r, group: r.group ?? p.label }));
1397
+ } catch {
1398
+ return [];
1399
+ }
1400
+ }));
1401
+ if (live) setResults([...own, ...extra.flat()].slice(0, 12)), setIdx(0);
1402
+ })();
1403
+ return () => {
1404
+ live = false;
1405
+ };
1406
+ }, [q, fw, providers, t]);
1407
+ const go = (r) => {
1408
+ if (!r) return;
1409
+ setQ("");
1410
+ input.current?.blur();
1411
+ void r.run();
1412
+ };
1413
+ return /* @__PURE__ */ jsxs8("div", { className: "flux-search", "data-testid": "searchbox", children: [
1414
+ /* @__PURE__ */ jsx9(Icon, { name: "search" }),
1415
+ /* @__PURE__ */ jsx9(
1416
+ "input",
1417
+ {
1418
+ ref: input,
1419
+ value: q,
1420
+ placeholder: placeholder ?? t("search.placeholder", "Search\u2026"),
1421
+ "aria-label": t("search.label", "Search"),
1422
+ onChange: (e) => setQ(e.target.value),
1423
+ onFocus: () => setFocus(true),
1424
+ onBlur: () => setTimeout(() => setFocus(false), 120),
1425
+ onKeyDown: (e) => {
1426
+ if (e.key === "ArrowDown") e.preventDefault(), setIdx((i) => Math.min(i + 1, results.length - 1));
1427
+ else if (e.key === "ArrowUp") e.preventDefault(), setIdx((i) => Math.max(i - 1, 0));
1428
+ else if (e.key === "Enter") go(results[idx]);
1429
+ else if (e.key === "Escape") setQ(""), input.current?.blur();
1430
+ }
1431
+ }
1432
+ ),
1433
+ focus && q.trim() && /* @__PURE__ */ jsxs8("ul", { className: "flux-dropdown flux-search-results", role: "listbox", children: [
1434
+ results.length === 0 && /* @__PURE__ */ jsx9("li", { className: "flux-empty", children: t("search.noResults", "No results") }),
1435
+ results.map((r, i) => /* @__PURE__ */ jsxs8("li", { role: "option", "aria-selected": i === idx, children: [
1436
+ (i === 0 || results[i - 1].group !== r.group) && /* @__PURE__ */ jsx9("div", { className: "flux-group", children: r.group }),
1437
+ /* @__PURE__ */ jsxs8("button", { type: "button", className: "flux-dd-item" + (i === idx ? " is-active" : ""), onMouseDown: (e) => (e.preventDefault(), go(r)), children: [
1438
+ /* @__PURE__ */ jsx9("span", { children: r.title }),
1439
+ r.subtitle && /* @__PURE__ */ jsx9("small", { children: r.subtitle })
1440
+ ] })
1441
+ ] }, r.id))
1442
+ ] })
1443
+ ] });
1444
+ }
1445
+ function useNotifications() {
1446
+ const { manager } = usePluginFramework();
1447
+ const n = manager.notifications;
1448
+ useSyncExternalStore4(n.subscribe, () => n.version, () => n.version);
1449
+ return n;
1450
+ }
1451
+ function NotificationBell() {
1452
+ const fw = usePluginFramework();
1453
+ const t = useT();
1454
+ const center = useNotifications();
1455
+ const [open, setOpen] = useState7(false);
1456
+ const ref = useRef3(null);
1457
+ useEffect7(() => {
1458
+ const off = (e) => ref.current && !ref.current.contains(e.target) && setOpen(false);
1459
+ window.addEventListener("mousedown", off);
1460
+ return () => window.removeEventListener("mousedown", off);
1461
+ }, []);
1462
+ const unread = center.unread;
1463
+ return /* @__PURE__ */ jsxs8("div", { className: "flux-bell", ref, children: [
1464
+ /* @__PURE__ */ jsxs8("button", { type: "button", className: "flux-tb-btn", "aria-label": t("notifications.title", "Notifications"), "data-testid": "bell", onClick: () => (setOpen(!open), !open && center.markRead()), children: [
1465
+ /* @__PURE__ */ jsx9(Icon, { name: "bell" }),
1466
+ unread > 0 && /* @__PURE__ */ jsx9("span", { className: "flux-badge", "data-testid": "bell-count", children: unread })
1467
+ ] }),
1468
+ open && /* @__PURE__ */ jsxs8("div", { className: "flux-dropdown flux-notif-panel", role: "dialog", "aria-label": t("notifications.title", "Notifications"), children: [
1469
+ /* @__PURE__ */ jsxs8("div", { className: "flux-notif-head", children: [
1470
+ /* @__PURE__ */ jsx9("strong", { children: t("notifications.title", "Notifications") }),
1471
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "flux-link", onClick: () => center.clear(), children: t("notifications.clear", "Clear all") })
1472
+ ] }),
1473
+ center.list().length === 0 && /* @__PURE__ */ jsx9("div", { className: "flux-empty", children: t("notifications.empty", "You're all caught up") }),
1474
+ center.list().map((n) => /* @__PURE__ */ jsxs8("div", { className: "flux-notif flux-" + n.level, children: [
1475
+ /* @__PURE__ */ jsxs8("div", { className: "flux-notif-main", children: [
1476
+ /* @__PURE__ */ jsx9("strong", { children: n.title }),
1477
+ n.body && /* @__PURE__ */ jsx9("p", { children: n.body }),
1478
+ /* @__PURE__ */ jsxs8("small", { children: [
1479
+ n.owner,
1480
+ " \xB7 ",
1481
+ new Date(n.time).toLocaleTimeString()
1482
+ ] })
1483
+ ] }),
1484
+ /* @__PURE__ */ jsxs8("div", { className: "flux-notif-actions", children: [
1485
+ n.actions.map((a) => /* @__PURE__ */ jsx9("button", { type: "button", className: "flux-link", onClick: () => void fw.manager.commands.execute(a.command, ...a.args ?? []), children: a.label }, a.label)),
1486
+ /* @__PURE__ */ jsx9("button", { type: "button", className: "flux-link", "aria-label": "Dismiss", onClick: () => center.dismiss(n.id), children: /* @__PURE__ */ jsx9(Icon, { name: "close", size: 12 }) })
1487
+ ] })
1488
+ ] }, n.id))
1489
+ ] })
1490
+ ] });
1491
+ }
1492
+ function ToastHost() {
1493
+ const { manager } = usePluginFramework();
1494
+ const [toasts, setToasts] = useState7([]);
1495
+ useEffect7(() => {
1496
+ const timers = [];
1497
+ const show = (n, remaining = n.timeoutMs) => {
1498
+ if (n.timeoutMs <= 0 || remaining <= 0) return;
1499
+ setToasts((t) => [n, ...t.filter((x) => x.id !== n.id)].slice(0, 4));
1500
+ timers.push(setTimeout(() => setToasts((t) => t.filter((x) => x.id !== n.id)), remaining));
1501
+ };
1502
+ for (const n of [...manager.notifications.list()].reverse()) if (!n.read) show(n, n.timeoutMs - (Date.now() - n.time));
1503
+ const sub = manager.notifications.onPush((n) => show(n));
1504
+ return () => (sub.dispose(), timers.forEach(clearTimeout));
1505
+ }, [manager]);
1506
+ return /* @__PURE__ */ jsx9("div", { className: "flux-toasts", "aria-live": "polite", "data-testid": "toasts", children: toasts.map((n) => /* @__PURE__ */ jsxs8("div", { className: "flux-toast flux-" + n.level, role: "status", children: [
1507
+ /* @__PURE__ */ jsx9("strong", { children: n.title }),
1508
+ n.body && /* @__PURE__ */ jsxs8("span", { children: [
1509
+ " ",
1510
+ n.body
1511
+ ] })
1512
+ ] }, n.id)) });
1513
+ }
1514
+ function TitleBar({ title, logo, searchPlaceholder, show }) {
1515
+ const os = useOS();
1516
+ const electron = typeof window !== "undefined" && !!window.__fluxplugin;
1517
+ const s2 = { menu: true, search: true, notifications: true, theme: true, ...show };
1518
+ const brand = useMemo8(() => /* @__PURE__ */ jsxs8("div", { className: "flux-brand", children: [
1519
+ logo,
1520
+ title && /* @__PURE__ */ jsx9("span", { children: title })
1521
+ ] }), [logo, title]);
1522
+ return /* @__PURE__ */ jsxs8("header", { className: "flux-titlebar", "data-os": os, "data-electron": electron || void 0, "data-testid": "titlebar", children: [
1523
+ /* @__PURE__ */ jsxs8("div", { className: "flux-tb-left", children: [
1524
+ brand,
1525
+ s2.menu && /* @__PURE__ */ jsx9(AppMenuBar, {}),
1526
+ /* @__PURE__ */ jsx9(PluginSlot, { name: "titlebar.left" })
1527
+ ] }),
1528
+ /* @__PURE__ */ jsx9("div", { className: "flux-tb-center", children: s2.search && /* @__PURE__ */ jsx9(SearchBox, { placeholder: searchPlaceholder }) }),
1529
+ /* @__PURE__ */ jsxs8("div", { className: "flux-tb-right", children: [
1530
+ /* @__PURE__ */ jsx9(PluginSlot, { name: "titlebar.right" }),
1531
+ s2.theme && /* @__PURE__ */ jsx9(ThemeSwitcher, {}),
1532
+ s2.notifications && /* @__PURE__ */ jsx9(NotificationBell, {})
1533
+ ] })
1534
+ ] });
1535
+ }
1536
+
1537
+ // src/react/ui.tsx
1538
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1539
+ function Switch({ checked, onChange, label, disabled }) {
1540
+ return /* @__PURE__ */ jsx10("button", { type: "button", role: "switch", "aria-checked": checked, "aria-label": label, disabled, className: "flux-switch", "data-on": checked || void 0, onClick: () => onChange(!checked), children: /* @__PURE__ */ jsx10("span", {}) });
1541
+ }
1542
+ function Segmented({ value, options, onChange, label }) {
1543
+ return /* @__PURE__ */ jsx10("div", { className: "flux-seg", role: "tablist", "aria-label": label, children: options.map((o) => /* @__PURE__ */ jsx10("button", { type: "button", role: "tab", "aria-selected": o.value === value, "data-on": o.value === value || void 0, onClick: () => onChange(o.value), children: o.label }, o.value)) });
1544
+ }
1545
+ function Group({ title, footer, children, className }) {
1546
+ return /* @__PURE__ */ jsxs9("section", { className: "flux-group-box " + (className ?? ""), children: [
1547
+ title && /* @__PURE__ */ jsx10("h3", { className: "flux-group-title", children: title }),
1548
+ /* @__PURE__ */ jsx10("div", { className: "flux-group-body", children }),
1549
+ footer && /* @__PURE__ */ jsx10("p", { className: "flux-group-footer", children: footer })
1550
+ ] });
1551
+ }
1552
+ function Row({ icon, iconColor, title, subtitle, right, chevron, onClick, selected, ...rest }) {
1553
+ const content = /* @__PURE__ */ jsxs9(Fragment4, { children: [
1554
+ icon !== void 0 && /* @__PURE__ */ jsx10("span", { className: "flux-row-icon", style: iconColor ? { background: iconColor } : void 0, children: typeof icon === "string" ? /* @__PURE__ */ jsx10(Icon, { name: icon, size: 16 }) : icon }),
1555
+ /* @__PURE__ */ jsxs9("span", { className: "flux-row-text", children: [
1556
+ /* @__PURE__ */ jsx10("span", { className: "flux-row-title", children: title }),
1557
+ subtitle && /* @__PURE__ */ jsx10("span", { className: "flux-row-sub", children: subtitle })
1558
+ ] }),
1559
+ right && /* @__PURE__ */ jsx10("span", { className: "flux-row-right", onClick: (e) => e.stopPropagation(), children: right }),
1560
+ chevron && /* @__PURE__ */ jsx10(Icon, { name: "chevronRight", size: 18 })
1561
+ ] });
1562
+ return onClick ? /* @__PURE__ */ jsx10("button", { type: "button", className: "flux-row is-link", "data-selected": selected || void 0, onClick, ...rest, children: content }) : /* @__PURE__ */ jsx10("div", { className: "flux-row", ...rest, children: content });
1563
+ }
1564
+ function Button({ variant = "secondary", size = "md", children, ...rest }) {
1565
+ return /* @__PURE__ */ jsx10("button", { type: "button", ...rest, className: `flux-btn flux-btn-${variant} flux-btn-${size} ${rest.className ?? ""}`, children });
1566
+ }
1567
+
1568
+ // src/react/sidebar.tsx
1569
+ import React9, { createContext as createContext4, useCallback as useCallback5, useContext as useContext4, useEffect as useEffect8, useMemo as useMemo9, useState as useState8 } from "react";
1570
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1571
+ var Ctx3 = createContext4({ collapsed: false, setCollapsed: () => {
1572
+ }, toggle: () => {
1573
+ }, width: 232 });
1574
+ var read3 = (k) => {
1575
+ try {
1576
+ return localStorage.getItem(k);
1577
+ } catch {
1578
+ return null;
1579
+ }
1580
+ };
1581
+ var write2 = (k, v) => {
1582
+ try {
1583
+ localStorage.setItem(k, v);
1584
+ } catch {
1585
+ }
1586
+ };
1587
+ function SidebarProvider({ children, storageKey = "fluxplugin.sidebar", expandedWidth = 232, collapsedWidth = 68 }) {
1588
+ const [collapsed, setCollapsedState] = useState8(() => read3(storageKey) === "collapsed");
1589
+ const width = collapsed ? collapsedWidth : expandedWidth;
1590
+ useEffect8(() => {
1591
+ document.documentElement.style.setProperty("--flux-sidebar-w", `${width}px`);
1592
+ }, [width]);
1593
+ const setCollapsed = useCallback5((v) => (setCollapsedState(v), write2(storageKey, v ? "collapsed" : "expanded")), [storageKey]);
1594
+ useEffect8(() => {
1595
+ const l = () => setCollapsed(!collapsed);
1596
+ window.addEventListener("flux:toggle-sidebar", l);
1597
+ return () => window.removeEventListener("flux:toggle-sidebar", l);
1598
+ }, [collapsed, setCollapsed]);
1599
+ const value = useMemo9(() => ({ collapsed, setCollapsed, toggle: () => setCollapsed(!collapsed), width }), [collapsed, setCollapsed, width]);
1600
+ return /* @__PURE__ */ jsx11(Ctx3.Provider, { value, children });
1601
+ }
1602
+ var useSidebar = () => useContext4(Ctx3);
1603
+ function renderIcon(icon, size = 18) {
1604
+ if (!icon) return /* @__PURE__ */ jsx11(Icon, { name: "grid", size });
1605
+ if (typeof icon === "string") return /* @__PURE__ */ jsx11(Icon, { name: icon, size });
1606
+ if (React9.isValidElement(icon)) return icon;
1607
+ if (typeof icon === "object" && icon && "d" in icon) return /* @__PURE__ */ jsx11(Icon, { d: String(icon.d), size });
1608
+ return /* @__PURE__ */ jsx11(Icon, { name: "grid", size });
1609
+ }
1610
+ var norm = (v) => v.replace(/^#/, "").split("?")[0].replace(/\/+$/, "") || "/";
1611
+ var activeFor = (cur, path) => !!path && (norm(cur) === norm(path) || norm(path) !== "/" && norm(cur).startsWith(norm(path) + "/"));
1612
+ var hasActive = (cur, e) => activeFor(cur, e.path) || !!e.children?.some((c) => hasActive(cur, c));
1613
+ var fromNode = (n) => ({ id: n.id, label: n.label, path: n.path, command: n.command, icon: n.icon, badge: n.badge, children: n.children?.map(fromNode) });
1614
+ function SidebarNav({ top = [], bottom = [], location: location2 = "sidebar", footer }) {
1615
+ const fw = usePluginFramework();
1616
+ const t = useT();
1617
+ const { collapsed, toggle } = useSidebar();
1618
+ const cur = useLocation();
1619
+ const menus = fw.manager.menus;
1620
+ const [version, setVersion] = useState8(menus.version);
1621
+ useEffect8(() => menus.subscribe(() => setVersion(menus.version)), [menus]);
1622
+ const plugin = useMemo9(() => menus.tree(location2).map(fromNode), [menus, location2, version]);
1623
+ const [open, setOpen] = useState8(() => {
1624
+ try {
1625
+ return JSON.parse(read3("fluxplugin.sidebar.open") ?? "{}");
1626
+ } catch {
1627
+ return {};
1628
+ }
1629
+ });
1630
+ const setGroup = (id, v) => setOpen((o) => {
1631
+ const n = { ...o, [id]: v };
1632
+ write2("fluxplugin.sidebar.open", JSON.stringify(n));
1633
+ return n;
1634
+ });
1635
+ const go = (e) => {
1636
+ if (e.command) void fw.manager.commands.execute(e.command).catch((err) => fw.manager.logs.logger("host").error(String(err)));
1637
+ else if (e.path) navigate(e.path);
1638
+ };
1639
+ const label = (e, host) => host ? t(`menu.${e.id}`, e.label) : e.label;
1640
+ const item = (e, host, depth = 0) => {
1641
+ const kids = e.children ?? [];
1642
+ const active = activeFor(cur, e.path);
1643
+ const expanded = kids.length > 0 && (open[e.id] ?? hasActive(cur, e));
1644
+ const text = label(e, host);
1645
+ return /* @__PURE__ */ jsxs10("div", { className: "flux-nav-group", "data-depth": depth, children: [
1646
+ /* @__PURE__ */ jsxs10(
1647
+ "a",
1648
+ {
1649
+ href: "#" + (e.path ?? ""),
1650
+ className: "flux-nav",
1651
+ "data-active": active || void 0,
1652
+ "data-parent-active": !active && hasActive(cur, e) || void 0,
1653
+ "aria-current": active ? "page" : void 0,
1654
+ title: collapsed ? text : void 0,
1655
+ "data-testid": "nav-" + e.id,
1656
+ onClick: (ev) => {
1657
+ ev.preventDefault();
1658
+ if (kids.length && !collapsed) setGroup(e.id, !expanded);
1659
+ if (e.path || e.command) go(e);
1660
+ else if (kids.length && collapsed) go(kids[0]);
1661
+ },
1662
+ children: [
1663
+ /* @__PURE__ */ jsx11("span", { className: "flux-nav-icon", children: renderIcon(e.icon, depth ? 14 : 18) }),
1664
+ !collapsed && /* @__PURE__ */ jsx11("span", { className: "flux-nav-label", children: text }),
1665
+ !collapsed && e.badge !== void 0 && /* @__PURE__ */ jsx11("span", { className: "flux-nav-badge", children: e.badge }),
1666
+ !collapsed && kids.length > 0 && /* @__PURE__ */ jsx11("span", { className: "flux-nav-chev", "data-open": expanded || void 0, children: /* @__PURE__ */ jsx11(Icon, { name: "chevronRight", size: 16 }) })
1667
+ ]
1668
+ }
1669
+ ),
1670
+ !collapsed && expanded && /* @__PURE__ */ jsx11("div", { className: "flux-subnav", children: kids.map((k) => item(k, host, depth + 1)) }),
1671
+ collapsed && kids.length > 0 && /* @__PURE__ */ jsxs10("div", { className: "flux-flyout", role: "menu", children: [
1672
+ /* @__PURE__ */ jsx11("div", { className: "flux-flyout-title", children: text }),
1673
+ kids.map((k) => /* @__PURE__ */ jsx11("a", { href: "#" + (k.path ?? ""), role: "menuitem", className: "flux-flyout-item", "data-active": activeFor(cur, k.path) || void 0, onClick: (ev) => (ev.preventDefault(), go(k)), children: k.label }, k.id))
1674
+ ] })
1675
+ ] }, e.id);
1676
+ };
1677
+ return /* @__PURE__ */ jsxs10("aside", { className: "flux-side side", "data-collapsed": collapsed || void 0, "aria-label": "Sidebar", children: [
1678
+ /* @__PURE__ */ jsxs10("nav", { className: "flux-side-nav", children: [
1679
+ top.map((e) => item(e, true)),
1680
+ plugin.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flux-side-sep" }),
1681
+ plugin.map((e) => item(e, false))
1682
+ ] }),
1683
+ /* @__PURE__ */ jsxs10("div", { className: "flux-side-bottom", children: [
1684
+ bottom.map((e) => item(e, true)),
1685
+ footer && !collapsed && /* @__PURE__ */ jsx11("div", { className: "flux-side-footer", children: footer }),
1686
+ /* @__PURE__ */ jsxs10("button", { type: "button", className: "flux-nav flux-nav-toggle", onClick: toggle, title: collapsed ? t("sidebar.expand", "Expand sidebar") : t("sidebar.collapse", "Collapse sidebar"), "aria-label": collapsed ? t("sidebar.expand", "Expand sidebar") : t("sidebar.collapse", "Collapse sidebar"), "aria-expanded": !collapsed, "data-testid": "sidebar-toggle", children: [
1687
+ /* @__PURE__ */ jsx11("span", { className: "flux-nav-icon", children: /* @__PURE__ */ jsx11(Icon, { name: "panel", size: 18 }) }),
1688
+ !collapsed && /* @__PURE__ */ jsx11("span", { className: "flux-nav-label", children: t("sidebar.collapse", "Collapse sidebar") })
1689
+ ] })
1690
+ ] })
1691
+ ] });
1692
+ }
1693
+
1694
+ // src/react/settings.tsx
1695
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1696
+ var COLORS = ["#007aff", "#34c759", "#ff9f0a", "#af52de", "#ff375f", "#5ac8fa", "#8e8e93"];
1697
+ function SettingsPage() {
1698
+ const t = useT();
1699
+ const raw = useExtensionItems("settings", "sections");
1700
+ const sections = [...raw].sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title));
1701
+ const { section } = useParams();
1702
+ const active = sections.find((s2) => s2.id === section) ?? sections[0];
1703
+ if (!active) return /* @__PURE__ */ jsx12("div", { className: "flux-empty", children: t("settings.empty", "No settings yet") });
1704
+ const Body = active.component;
1705
+ return /* @__PURE__ */ jsxs11("div", { className: "flux-settings", "data-testid": "settings-page", children: [
1706
+ /* @__PURE__ */ jsxs11("nav", { className: "flux-settings-nav", "aria-label": t("settings.title", "Settings"), children: [
1707
+ /* @__PURE__ */ jsx12("h1", { className: "flux-large-title", children: t("settings.title", "Settings") }),
1708
+ sections.map((s2, i) => /* @__PURE__ */ jsxs11("button", { type: "button", className: "flux-settings-item", "data-active": s2.id === active.id || void 0, "data-testid": "settings-" + s2.id, onClick: () => navigate("/settings/" + s2.id), children: [
1709
+ /* @__PURE__ */ jsx12("span", { className: "flux-row-icon", style: { background: COLORS[i % COLORS.length] }, children: /* @__PURE__ */ jsx12(Icon, { name: s2.icon ?? "settings", size: 15 }) }),
1710
+ /* @__PURE__ */ jsx12("span", { children: t(`settings.${s2.id}`, s2.title) })
1711
+ ] }, s2.id))
1712
+ ] }),
1713
+ /* @__PURE__ */ jsxs11("div", { className: "flux-settings-body", children: [
1714
+ /* @__PURE__ */ jsx12("h2", { className: "flux-title", children: t(`settings.${active.id}`, active.title) }),
1715
+ /* @__PURE__ */ jsx12(Body, {})
1716
+ ] })
1717
+ ] });
1718
+ }
1719
+
1720
+ export {
1721
+ RemoteError,
1722
+ PluginsClient,
1723
+ RendererFramework,
1724
+ buildPluginApis,
1725
+ installSharedModules,
1726
+ navigate,
1727
+ useLocation,
1728
+ useNavigate,
1729
+ Link,
1730
+ usePluginRoute,
1731
+ useParams,
1732
+ Breadcrumbs,
1733
+ PluginRoutes,
1734
+ useMenu,
1735
+ PluginProvider,
1736
+ usePluginFramework,
1737
+ PluginErrorBoundary,
1738
+ PluginBoundary,
1739
+ PluginSlot,
1740
+ useExtensionItems,
1741
+ usePlugin,
1742
+ usePlugins,
1743
+ usePluginState,
1744
+ usePluginEvent,
1745
+ usePluginCommand,
1746
+ usePluginService,
1747
+ usePluginIPC,
1748
+ usePluginConfig,
1749
+ StyleHost,
1750
+ declareI18nPoints,
1751
+ I18nProvider,
1752
+ useI18n,
1753
+ useT,
1754
+ useKeybindings,
1755
+ CommandPalette,
1756
+ baseCss,
1757
+ HttpBridgeError,
1758
+ createHttpBridge,
1759
+ createStaticBridge,
1760
+ detectOS,
1761
+ useOS,
1762
+ formatShortcut,
1763
+ Icon,
1764
+ iconNames,
1765
+ toImageSrc,
1766
+ Img,
1767
+ VideoPlayer,
1768
+ Markdown,
1769
+ declareHostPoints,
1770
+ DEFAULT_APPEARANCE,
1771
+ BUILTIN_FONTS,
1772
+ sanitizeFontFamily,
1773
+ ThemeProvider,
1774
+ useTheme,
1775
+ useAppearance,
1776
+ ThemeSwitcher,
1777
+ AppMenuBar,
1778
+ SearchBox,
1779
+ NotificationBell,
1780
+ ToastHost,
1781
+ TitleBar,
1782
+ Switch,
1783
+ Segmented,
1784
+ Group,
1785
+ Row,
1786
+ Button,
1787
+ SidebarProvider,
1788
+ useSidebar,
1789
+ renderIcon,
1790
+ SidebarNav,
1791
+ SettingsPage
1792
+ };