@reddb-io/ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/bin/red-ui.js +259 -0
- package/build/CNAME +1 -0
- package/build/_app/env.js +1 -0
- package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
- package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
- package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
- package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
- package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
- package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
- package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
- package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
- package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
- package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
- package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
- package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
- package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
- package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
- package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
- package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
- package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
- package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
- package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
- package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
- package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
- package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
- package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
- package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
- package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
- package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
- package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
- package/build/_app/version.json +1 -0
- package/build/favicon.png +0 -0
- package/build/index.html +59 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
- package/dist/embed/core-QuRYvMt8.js +118 -0
- package/dist/embed/core-QuRYvMt8.js.map +1 -0
- package/dist/embed/index-B6ZQhCsu.js +577 -0
- package/dist/embed/index-B6ZQhCsu.js.map +1 -0
- package/dist/embed/index-client-ItCEU64C.js +3087 -0
- package/dist/embed/index-client-ItCEU64C.js.map +1 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
- package/dist/embed/index.js +11 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
- package/dist/embed/ui.css +1 -0
- package/package.json +101 -0
- package/src/app.css +238 -0
- package/src/lib/ActivityIndicator.svelte +100 -0
- package/src/lib/ClusterNode.svelte +119 -0
- package/src/lib/ClusterView.svelte +541 -0
- package/src/lib/CollectionHistory.svelte +266 -0
- package/src/lib/CollectionsView.svelte +14 -0
- package/src/lib/CommandPalette.svelte +237 -0
- package/src/lib/ConnectDropdown.svelte +222 -0
- package/src/lib/EmptyState.svelte +27 -0
- package/src/lib/LiveChanges.svelte +195 -0
- package/src/lib/MasterPasswordDialog.svelte +168 -0
- package/src/lib/PageHeader.svelte +32 -0
- package/src/lib/PendingChangesPanel.svelte +153 -0
- package/src/lib/QueryEditor.svelte +122 -0
- package/src/lib/ResultsPane.svelte +747 -0
- package/src/lib/SchemaTree.svelte +193 -0
- package/src/lib/SecurityView.svelte +255 -0
- package/src/lib/ShortcutOverlay.svelte +104 -0
- package/src/lib/Splash.svelte +46 -0
- package/src/lib/StatusBar.svelte +163 -0
- package/src/lib/SubNav.svelte +62 -0
- package/src/lib/Topbar.svelte +119 -0
- package/src/lib/Workspace.svelte +247 -0
- package/src/lib/activity.svelte.ts +91 -0
- package/src/lib/build-info.ts +41 -0
- package/src/lib/capability.ts +221 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/collection-catalog.ts +116 -0
- package/src/lib/collection-pages.ts +33 -0
- package/src/lib/connection-history.ts +301 -0
- package/src/lib/connections.svelte.ts +493 -0
- package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
- package/src/lib/embed/index.ts +234 -0
- package/src/lib/graph-loader/contract.ts +336 -0
- package/src/lib/graph-loader/drilldown.ts +334 -0
- package/src/lib/graph-loader/index.ts +13 -0
- package/src/lib/index.ts +114 -0
- package/src/lib/pending-changes.svelte.ts +149 -0
- package/src/lib/query-tabs.svelte.ts +130 -0
- package/src/lib/reddb/boot-params.ts +254 -0
- package/src/lib/reddb/cdc-stream-client.ts +234 -0
- package/src/lib/reddb/client.ts +698 -0
- package/src/lib/reddb/connection-bootstrap.ts +260 -0
- package/src/lib/reddb/connection-provider.contract.ts +72 -0
- package/src/lib/reddb/connection-provider.ts +72 -0
- package/src/lib/reddb/index.ts +14 -0
- package/src/lib/reddb/injected-client-provider.ts +76 -0
- package/src/lib/reddb/local-url-provider.ts +245 -0
- package/src/lib/reddb/secure-store.ts +41 -0
- package/src/lib/reddb/server-capabilities.ts +71 -0
- package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
- package/src/lib/reddb/transports.ts +89 -0
- package/src/lib/reddb/types.ts +32 -0
- package/src/lib/reddb/uri.ts +54 -0
- package/src/lib/reddb/web-encrypted-store.ts +201 -0
- package/src/lib/renderers/DiffRenderer.svelte +184 -0
- package/src/lib/renderers/DocumentRenderer.svelte +134 -0
- package/src/lib/renderers/GraphRenderer.svelte +1328 -0
- package/src/lib/renderers/HypertableRenderer.svelte +253 -0
- package/src/lib/renderers/JsonRenderer.svelte +30 -0
- package/src/lib/renderers/KvRenderer.svelte +150 -0
- package/src/lib/renderers/QueueRenderer.svelte +286 -0
- package/src/lib/renderers/StatsRenderer.svelte +123 -0
- package/src/lib/renderers/TableRenderer.svelte +440 -0
- package/src/lib/renderers/VectorRenderer.svelte +388 -0
- package/src/lib/renderers/diff-render.ts +122 -0
- package/src/lib/renderers/diff.ts +9 -0
- package/src/lib/renderers/document-render.ts +64 -0
- package/src/lib/renderers/document.ts +9 -0
- package/src/lib/renderers/graph-render.ts +631 -0
- package/src/lib/renderers/graph-sigma.ts +395 -0
- package/src/lib/renderers/graph.ts +9 -0
- package/src/lib/renderers/hypertable-render.ts +320 -0
- package/src/lib/renderers/hypertable.ts +9 -0
- package/src/lib/renderers/index.ts +42 -0
- package/src/lib/renderers/json-render.ts +89 -0
- package/src/lib/renderers/json.ts +10 -0
- package/src/lib/renderers/kv-render.ts +193 -0
- package/src/lib/renderers/kv.ts +9 -0
- package/src/lib/renderers/queue-render.ts +197 -0
- package/src/lib/renderers/queue.ts +9 -0
- package/src/lib/renderers/registry.ts +68 -0
- package/src/lib/renderers/stats-render.ts +73 -0
- package/src/lib/renderers/stats.ts +9 -0
- package/src/lib/renderers/table-render.ts +156 -0
- package/src/lib/renderers/table.ts +11 -0
- package/src/lib/renderers/types.ts +33 -0
- package/src/lib/renderers/vector-render.ts +154 -0
- package/src/lib/renderers/vector.ts +9 -0
- package/src/lib/router.svelte.ts +163 -0
- package/src/lib/secureStore.svelte.ts +148 -0
- package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
- package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
- package/src/lib/shortcuts.ts +22 -0
- package/src/lib/surface.ts +80 -0
- package/src/lib/tabs.svelte.ts +115 -0
- package/src/lib/theme.svelte.ts +63 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte";
|
|
2
|
+
import appCss from "../../app.css?inline";
|
|
3
|
+
import ShadowDomSpikeCore from "./ShadowDomSpikeCore.svelte";
|
|
4
|
+
|
|
5
|
+
const shadowHostCss = `
|
|
6
|
+
:host {
|
|
7
|
+
display: block;
|
|
8
|
+
min-height: 720px;
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
contain: content;
|
|
11
|
+
font-family: var(--font-sans);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:host,
|
|
15
|
+
:host([data-theme='dark']) {
|
|
16
|
+
--color-bg-0: #050607;
|
|
17
|
+
--color-bg-1: #0d0f13;
|
|
18
|
+
--color-bg-2: #181b21;
|
|
19
|
+
--color-bg-3: #232730;
|
|
20
|
+
--color-fg-0: #f7f8fa;
|
|
21
|
+
--color-fg-1: #c8ccd4;
|
|
22
|
+
--color-fg-2: #9ca1a9;
|
|
23
|
+
--color-fg-3: #7a7f87;
|
|
24
|
+
--color-line-1: #181b21;
|
|
25
|
+
--color-line-2: #2a2e36;
|
|
26
|
+
--color-line-3: #3d424b;
|
|
27
|
+
--color-accent: #ff2056;
|
|
28
|
+
--color-accent-soft: rgba(255, 32, 86, 0.12);
|
|
29
|
+
--color-accent-glow: rgba(255, 32, 86, 0.45);
|
|
30
|
+
--color-ok: #4ade80;
|
|
31
|
+
--color-warn: #fbbf24;
|
|
32
|
+
--color-danger: #ff5470;
|
|
33
|
+
--color-info: #60a5fa;
|
|
34
|
+
--color-role-primary: #ff2056;
|
|
35
|
+
--color-role-replica: #60a5fa;
|
|
36
|
+
--color-role-embedded: #4ade80;
|
|
37
|
+
--color-ambient-glow: rgba(255, 32, 86, 0.06);
|
|
38
|
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--color-line-2);
|
|
39
|
+
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--color-line-2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:host * {
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.red-ui-shadow-mount {
|
|
47
|
+
min-height: 720px;
|
|
48
|
+
background: var(--color-bg-0);
|
|
49
|
+
color: var(--color-fg-0);
|
|
50
|
+
font-family: var(--font-sans);
|
|
51
|
+
font-size: 13px;
|
|
52
|
+
line-height: 1.5;
|
|
53
|
+
-webkit-font-smoothing: antialiased;
|
|
54
|
+
text-rendering: optimizeLegibility;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
export type ShadowDomSpikeMount = {
|
|
59
|
+
shadowRoot: ShadowRoot;
|
|
60
|
+
portalTarget: HTMLElement;
|
|
61
|
+
destroy: () => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function mountShadowDomSpike(host: HTMLElement): ShadowDomSpikeMount {
|
|
65
|
+
host.setAttribute("data-theme", "dark");
|
|
66
|
+
|
|
67
|
+
const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: "open" });
|
|
68
|
+
shadowRoot.textContent = "";
|
|
69
|
+
|
|
70
|
+
const style = document.createElement("style");
|
|
71
|
+
style.textContent = `${appCss}\n${shadowHostCss}`;
|
|
72
|
+
|
|
73
|
+
const mountTarget = document.createElement("div");
|
|
74
|
+
mountTarget.className = "red-ui-shadow-mount";
|
|
75
|
+
|
|
76
|
+
const portalTarget = document.createElement("div");
|
|
77
|
+
portalTarget.setAttribute("data-red-ui-shadow-portal", "");
|
|
78
|
+
|
|
79
|
+
shadowRoot.append(style, mountTarget, portalTarget);
|
|
80
|
+
|
|
81
|
+
const instance = mount(ShadowDomSpikeCore, {
|
|
82
|
+
target: mountTarget,
|
|
83
|
+
props: { portalTarget },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
shadowRoot,
|
|
88
|
+
portalTarget,
|
|
89
|
+
destroy: () => {
|
|
90
|
+
unmount(instance);
|
|
91
|
+
shadowRoot.textContent = "";
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ShortcutContext = 'Global' | 'Connection' | 'Workspace' | 'Editing'
|
|
2
|
+
|
|
3
|
+
export interface Shortcut {
|
|
4
|
+
id: string
|
|
5
|
+
context: ShortcutContext
|
|
6
|
+
keys: string[]
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Single source of truth for keyboard shortcuts shown in the `?` overlay.
|
|
12
|
+
* Only list shortcuts that are actually wired somewhere in the app.
|
|
13
|
+
*/
|
|
14
|
+
export const shortcuts: Shortcut[] = [
|
|
15
|
+
{ id: 'palette', context: 'Global', keys: ['⌘', 'K'], label: 'Open command palette' },
|
|
16
|
+
{ id: 'shortcuts', context: 'Global', keys: ['?'], label: 'Show keyboard shortcuts' },
|
|
17
|
+
{ id: 'new-query', context: 'Global', keys: ['⌘', 'T'], label: 'New query' },
|
|
18
|
+
{ id: 'open-connect', context: 'Connection', keys: ['⌘', '⇧', 'C'], label: 'Open Connect dropdown' },
|
|
19
|
+
{ id: 'dismiss', context: 'Editing', keys: ['Esc'], label: 'Cancel / dismiss overlay or palette' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const shortcutContexts: ShortcutContext[] = ['Global', 'Connection', 'Workspace', 'Editing']
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Surface detection + boot gate (#21, ADR-0001). red-ui is one Core delivered
|
|
2
|
+
// by three Surfaces; the boot gate must be Surface-aware so a credential-less
|
|
3
|
+
// Surface is never blocked behind the master-password vault.
|
|
4
|
+
//
|
|
5
|
+
// - embedded — the Embeddable Lib / MCP App, running inside a host that
|
|
6
|
+
// owns auth (or a local, no-secret connection). Persists
|
|
7
|
+
// nothing and boots straight to data.
|
|
8
|
+
// - standalone — the Tauri desktop app. Keeps its vault (the OS keychain,
|
|
9
|
+
// which unlocks automatically — "vault unlock as before").
|
|
10
|
+
// - web — the hosted PWA. Persists only target labels; secrets are
|
|
11
|
+
// session-only and are never restored from storage.
|
|
12
|
+
|
|
13
|
+
export type Surface = "embedded" | "standalone" | "web";
|
|
14
|
+
|
|
15
|
+
/** Runtime signals the Surface is derived from (pure — injectable for tests). */
|
|
16
|
+
export interface SurfaceSignals {
|
|
17
|
+
/** Running inside the Tauri shell. */
|
|
18
|
+
tauri: boolean;
|
|
19
|
+
/** Rendered inside a cross-document frame (embedded in a host page). */
|
|
20
|
+
inFrame: boolean;
|
|
21
|
+
/** The MCP App boot flag (`?mcp=1`) is present. */
|
|
22
|
+
mcp: boolean;
|
|
23
|
+
/** The importable Embeddable Lib marked this page before loading Core. */
|
|
24
|
+
embedLib: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Pure Surface decision from runtime signals. */
|
|
28
|
+
export function surfaceFrom({
|
|
29
|
+
tauri,
|
|
30
|
+
inFrame,
|
|
31
|
+
mcp,
|
|
32
|
+
embedLib,
|
|
33
|
+
}: SurfaceSignals): Surface {
|
|
34
|
+
if (tauri) return "standalone";
|
|
35
|
+
if (inFrame || mcp || embedLib) return "embedded";
|
|
36
|
+
return "web";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Detect the Surface from the live browser globals. Defaults to `web` in SSR. */
|
|
40
|
+
export function detectSurface(): Surface {
|
|
41
|
+
if (typeof window === "undefined") return "web";
|
|
42
|
+
const tauri = "__TAURI_INTERNALS__" in window || "__TAURI__" in window;
|
|
43
|
+
let inFrame = false;
|
|
44
|
+
try {
|
|
45
|
+
inFrame = window.self !== window.top;
|
|
46
|
+
} catch {
|
|
47
|
+
// Cross-origin parent access throws — that itself means we're framed.
|
|
48
|
+
inFrame = true;
|
|
49
|
+
}
|
|
50
|
+
const mcp =
|
|
51
|
+
typeof location !== "undefined" &&
|
|
52
|
+
new URLSearchParams(location.search).has("mcp");
|
|
53
|
+
const embedLib = window.__RED_UI_EMBED_SURFACE__ === true;
|
|
54
|
+
return surfaceFrom({ tauri, inFrame, mcp, embedLib });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The initial lock state for a Surface (#21). `hasSecret` is whether encrypted
|
|
59
|
+
* credentials already exist in a vault.
|
|
60
|
+
*
|
|
61
|
+
* - embedded → never locked (credential-less; the host owns auth).
|
|
62
|
+
* - standalone → locked initially; the Tauri keychain unlocks it (as before).
|
|
63
|
+
* - web → never locked for saved credentials; persisted web history is labels only.
|
|
64
|
+
*/
|
|
65
|
+
export function bootGateLocked(surface: Surface, hasSecret: boolean): boolean {
|
|
66
|
+
switch (surface) {
|
|
67
|
+
case "embedded":
|
|
68
|
+
return false;
|
|
69
|
+
case "standalone":
|
|
70
|
+
return true;
|
|
71
|
+
case "web":
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare global {
|
|
77
|
+
interface Window {
|
|
78
|
+
__RED_UI_EMBED_SURFACE__?: boolean;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ResultsPane tab state — Svelte 5 runes store.
|
|
2
|
+
//
|
|
3
|
+
// Each tab is keyed on (kind, key) so the same collection always
|
|
4
|
+
// collapses to a single tab; Cmd+click bypasses that by minting a fresh
|
|
5
|
+
// id so a duplicate tab opens.
|
|
6
|
+
|
|
7
|
+
import type { Capability } from './renderers'
|
|
8
|
+
|
|
9
|
+
export type TabKind = 'collection' | 'query' | 'live-changes' | 'welcome'
|
|
10
|
+
|
|
11
|
+
export interface Tab {
|
|
12
|
+
/** Unique within the pane. Stable when re-focusing existing tabs. */
|
|
13
|
+
id: string
|
|
14
|
+
kind: TabKind
|
|
15
|
+
/** Display label in the tab header. */
|
|
16
|
+
label: string
|
|
17
|
+
/** Stable identity for collapsing duplicates (collection name, query hash). */
|
|
18
|
+
key: string
|
|
19
|
+
/** Optional capability hint forwarded to the renderer registry. */
|
|
20
|
+
capability?: Capability
|
|
21
|
+
/** URL-level collection subpage (`graph`, `svg`, `table`, `json`, …). */
|
|
22
|
+
subpage?: string
|
|
23
|
+
/** Renderer override (slice #9 wires the UI; the field exists today). */
|
|
24
|
+
overrideCapability?: Capability
|
|
25
|
+
/** Show reddb system fields (rid, collection, kind, tenant, …) as a
|
|
26
|
+
* prefix block ahead of user columns. Default false so the table shows
|
|
27
|
+
* what the user wrote. */
|
|
28
|
+
showSystemColumns?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function uid(): string {
|
|
32
|
+
// Small, debug-friendly id. Crypto.randomUUID is overkill for in-pane state.
|
|
33
|
+
return Math.random().toString(36).slice(2, 10)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class TabsStore {
|
|
37
|
+
tabs = $state<Tab[]>([])
|
|
38
|
+
activeId = $state<string | null>(null)
|
|
39
|
+
|
|
40
|
+
active = $derived(this.tabs.find((t) => t.id === this.activeId) ?? null)
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Open or focus a tab.
|
|
44
|
+
* - If `forceNew` is false (default) and a tab with the same (kind,key)
|
|
45
|
+
* exists, focus that tab. Otherwise add a new one and focus it.
|
|
46
|
+
* - If `forceNew` is true, always mint a fresh tab id.
|
|
47
|
+
*/
|
|
48
|
+
open(spec: Omit<Tab, 'id'>, forceNew = false): Tab {
|
|
49
|
+
if (!forceNew) {
|
|
50
|
+
const existing = this.tabs.find((t) => t.kind === spec.kind && t.key === spec.key)
|
|
51
|
+
if (existing) {
|
|
52
|
+
const updated = { ...existing, ...spec, id: existing.id }
|
|
53
|
+
this.tabs = this.tabs.map((t) =>
|
|
54
|
+
t === existing ? updated : t,
|
|
55
|
+
)
|
|
56
|
+
this.activeId = existing.id
|
|
57
|
+
return updated
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const tab: Tab = { ...spec, id: uid() }
|
|
61
|
+
this.tabs = [...this.tabs, tab]
|
|
62
|
+
this.activeId = tab.id
|
|
63
|
+
return tab
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
focus(id: string): void {
|
|
67
|
+
if (this.tabs.some((t) => t.id === id)) this.activeId = id
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Close a tab. Focus moves to the previously active tab if it still
|
|
72
|
+
* exists, otherwise to the neighbour on the left, otherwise null.
|
|
73
|
+
* `previousActiveId` lets the caller pass a remembered focus so close
|
|
74
|
+
* → reopen → close returns to whatever was focused before the closing.
|
|
75
|
+
*/
|
|
76
|
+
close(id: string, previousActiveId?: string | null): void {
|
|
77
|
+
const idx = this.tabs.findIndex((t) => t.id === id)
|
|
78
|
+
if (idx < 0) return
|
|
79
|
+
const wasActive = this.activeId === id
|
|
80
|
+
this.tabs = this.tabs.filter((t) => t.id !== id)
|
|
81
|
+
if (wasActive) {
|
|
82
|
+
if (previousActiveId && this.tabs.some((t) => t.id === previousActiveId)) {
|
|
83
|
+
this.activeId = previousActiveId
|
|
84
|
+
} else if (this.tabs.length === 0) {
|
|
85
|
+
this.activeId = null
|
|
86
|
+
} else {
|
|
87
|
+
const fallback = this.tabs[Math.max(0, idx - 1)]
|
|
88
|
+
this.activeId = fallback?.id ?? null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
rename(id: string, label: string): void {
|
|
94
|
+
this.tabs = this.tabs.map((t) => (t.id === id ? { ...t, label } : t))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setOverride(id: string, capability: Capability | undefined): void {
|
|
98
|
+
this.tabs = this.tabs.map((t) =>
|
|
99
|
+
t.id === id ? { ...t, overrideCapability: capability } : t,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setShowSystem(id: string, show: boolean): void {
|
|
104
|
+
this.tabs = this.tabs.map((t) =>
|
|
105
|
+
t.id === id ? { ...t, showSystemColumns: show } : t,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
clear(): void {
|
|
110
|
+
this.tabs = []
|
|
111
|
+
this.activeId = null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const tabs = new TabsStore()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type Theme = "light" | "dark";
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = "red-ui-theme";
|
|
4
|
+
const DEFAULT: Theme = "light";
|
|
5
|
+
|
|
6
|
+
interface ThemeInitOptions {
|
|
7
|
+
target?: HTMLElement | null;
|
|
8
|
+
persist?: boolean;
|
|
9
|
+
initial?: Theme;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function read(
|
|
13
|
+
target: HTMLElement | null,
|
|
14
|
+
persist: boolean,
|
|
15
|
+
initial?: Theme
|
|
16
|
+
): Theme {
|
|
17
|
+
const scoped = target?.dataset.theme;
|
|
18
|
+
if (scoped === "dark" || scoped === "light") return scoped;
|
|
19
|
+
if (!persist) return initial ?? DEFAULT;
|
|
20
|
+
if (typeof localStorage === "undefined") return initial ?? DEFAULT;
|
|
21
|
+
const v = localStorage.getItem(STORAGE_KEY);
|
|
22
|
+
return v === "dark" || v === "light" ? v : (initial ?? DEFAULT);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function apply(t: Theme, target: HTMLElement | null) {
|
|
26
|
+
if (target) {
|
|
27
|
+
target.dataset.theme = t;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof document !== "undefined")
|
|
31
|
+
document.documentElement.dataset.theme = t;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function persist(t: Theme, enabled: boolean) {
|
|
35
|
+
if (!enabled) return;
|
|
36
|
+
if (typeof localStorage === "undefined") return;
|
|
37
|
+
localStorage.setItem(STORAGE_KEY, t);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class ThemeStore {
|
|
41
|
+
current = $state<Theme>(DEFAULT);
|
|
42
|
+
target: HTMLElement | null = null;
|
|
43
|
+
persistTheme = true;
|
|
44
|
+
|
|
45
|
+
init(opts: ThemeInitOptions = {}) {
|
|
46
|
+
this.target = opts.target ?? null;
|
|
47
|
+
this.persistTheme = opts.persist ?? true;
|
|
48
|
+
this.current = read(this.target, this.persistTheme, opts.initial);
|
|
49
|
+
apply(this.current, this.target);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set(t: Theme) {
|
|
53
|
+
this.current = t;
|
|
54
|
+
apply(t, this.target);
|
|
55
|
+
persist(t, this.persistTheme);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
toggle() {
|
|
59
|
+
this.set(this.current === "dark" ? "light" : "dark");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const theme = new ThemeStore();
|