@genomicx/ui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # @genomicx/ui
2
+
3
+ Shared UI components, design tokens, WASM loader, and utilities for [GenomicX](https://genomicx.org) browser-based bioinformatics tools.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @genomicx/ui
9
+ ```
10
+
11
+ Peer dependencies (install separately):
12
+
13
+ ```bash
14
+ npm install react react-dom react-router-dom react-hot-toast
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Import the CSS in your app's global stylesheet, **after** your Tailwind directives:
20
+
21
+ ```css
22
+ @import "tailwindcss";
23
+ @import '@genomicx/ui/styles/tokens.css';
24
+ @import '@genomicx/ui/styles/components.css';
25
+ ```
26
+
27
+ Init the theme on load (prevents flash of wrong theme):
28
+
29
+ ```tsx
30
+ useEffect(() => {
31
+ const saved = (localStorage.getItem('gx-theme') as 'light' | 'dark') || 'dark'
32
+ document.documentElement.setAttribute('data-theme', saved)
33
+ }, [])
34
+ ```
35
+
36
+ ## Components
37
+
38
+ ### `NavBar`
39
+
40
+ Sticky top navigation with the GenomicX rings logo, theme toggle, and an About link.
41
+
42
+ ```tsx
43
+ import { NavBar } from '@genomicx/ui'
44
+
45
+ <NavBar
46
+ appName="MYAPP"
47
+ appSubtitle="What it does"
48
+ version="1.2.0"
49
+ />
50
+ ```
51
+
52
+ **Props**
53
+
54
+ | Prop | Type | Default | Description |
55
+ |------|------|---------|-------------|
56
+ | `appName` | `string` | — | App name shown in header (uppercase recommended) |
57
+ | `appSubtitle` | `string` | — | Subtitle shown below app name |
58
+ | `version` | `string` | — | Version shown as `v1.0.0` badge |
59
+ | `actions` | `ReactNode` | — | Extra items in the desktop nav (right side) |
60
+ | `mobileActions` | `ReactNode` | — | Extra items in the mobile dropdown |
61
+
62
+ Tool-specific nav buttons (e.g. Save/Load Session) go in `actions`:
63
+
64
+ ```tsx
65
+ <NavBar
66
+ appName="BRIGX"
67
+ appSubtitle="Browser-based Ring Image Generator"
68
+ version={APP_VERSION}
69
+ actions={
70
+ <>
71
+ <button onClick={handleSave}>Save Session</button>
72
+ <label>Load Session<input type="file" onChange={handleLoad} /></label>
73
+ </>
74
+ }
75
+ />
76
+ ```
77
+
78
+ ---
79
+
80
+ ### `AppFooter`
81
+
82
+ Standard footer with GenomicX branding and an optional bug report link.
83
+
84
+ ```tsx
85
+ import { AppFooter } from '@genomicx/ui'
86
+
87
+ <AppFooter appName="MYAPP" />
88
+ <AppFooter appName="MYAPP" onReportBug={() => setShowBugReport(true)} />
89
+ ```
90
+
91
+ ---
92
+
93
+ ### `LogConsole`
94
+
95
+ Collapsible debug console for displaying WASM stderr/stdout.
96
+
97
+ ```tsx
98
+ import { LogConsole } from '@genomicx/ui'
99
+
100
+ <LogConsole logs={logLines} />
101
+ <LogConsole logs={logLines} progress="Running BLAST..." title="Debug Console" />
102
+ ```
103
+
104
+ **Props**
105
+
106
+ | Prop | Type | Description |
107
+ |------|------|-------------|
108
+ | `logs` | `string[]` | Log lines to display |
109
+ | `progress` | `string` | Optional current progress message shown at top |
110
+ | `title` | `string` | Console title (default: `"Console"`) |
111
+
112
+ ---
113
+
114
+ ### `ThemeToggle`
115
+
116
+ Pill-style dark/light theme switcher. Already included in `NavBar` — use standalone only if building a custom nav.
117
+
118
+ ```tsx
119
+ import { ThemeToggle } from '@genomicx/ui'
120
+
121
+ <ThemeToggle />
122
+ ```
123
+
124
+ ---
125
+
126
+ ### `AppShell`
127
+
128
+ Full-page layout wrapper (nav + main + footer).
129
+
130
+ ```tsx
131
+ import { AppShell } from '@genomicx/ui'
132
+
133
+ <AppShell appName="MYAPP" appSubtitle="What it does" version="0.1.0">
134
+ <YourContent />
135
+ </AppShell>
136
+ ```
137
+
138
+ ---
139
+
140
+ ## WASM Loader
141
+
142
+ Canonical Emscripten loader for GenomicX WASM binaries hosted on `static.genomicx.org`.
143
+
144
+ ```ts
145
+ import { createModuleInstance } from '@genomicx/ui'
146
+
147
+ // Loads mash.js + mash.wasm from static.genomicx.org/wasm/
148
+ const mod = await createModuleInstance('mash')
149
+
150
+ mod.FS.writeFile('/query.fa', fastaBytes)
151
+ mod.callMain(['dist', '/db.msh', '/query.fa'])
152
+ const output = mod._stdout.join('\n')
153
+ ```
154
+
155
+ Available binaries: `mash`, `blastall`, `formatdb`
156
+
157
+ **Low-level API** (custom base URL or caching control):
158
+
159
+ ```ts
160
+ import { loadWasmModule } from '@genomicx/ui'
161
+
162
+ const { factory, wasmBinary } = await loadWasmModule('blastall', 'https://my-cdn.example.com/wasm')
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Utilities
168
+
169
+ ```ts
170
+ import { downloadBlob, downloadText, downloadBuffer } from '@genomicx/ui'
171
+
172
+ downloadText('results.tsv', tsvString)
173
+ downloadBuffer('output.fa', uint8Array, 'text/plain')
174
+ downloadBlob('report.json', new Blob([json], { type: 'application/json' }))
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Design Tokens
180
+
181
+ All tokens are CSS custom properties on `:root` / `[data-theme]`. Key tokens:
182
+
183
+ | Token | Light | Dark | Usage |
184
+ |-------|-------|------|-------|
185
+ | `--gx-accent` | `#0d9488` | `#2dd4bf` | Primary accent (teal) |
186
+ | `--gx-bg` | `#f8fafc` | `#0d1117` | Page background |
187
+ | `--gx-bg-alt` | `#fff` | `#161b22` | Card/panel background |
188
+ | `--gx-text` | `#0f172a` | `#e6edf3` | Body text |
189
+ | `--gx-text-muted` | `#64748b` | `#8b949e` | Secondary text |
190
+ | `--gx-border` | `#e2e8f0` | `#30363d` | Borders |
191
+ | `--gx-gradient` | — | — | Accent gradient (button backgrounds) |
192
+ | `--gx-font-sans` | — | — | `Inter, system-ui, sans-serif` |
193
+ | `--gx-font-mono` | — | — | `JetBrains Mono, monospace` |
194
+
195
+ ---
196
+
197
+ ## Typical app scaffold
198
+
199
+ ```
200
+ src/
201
+ main.tsx ← BrowserRouter + Toaster
202
+ App.tsx ← NavBar + Routes + AppFooter
203
+ index.css ← Tailwind + @genomicx/ui tokens + components
204
+ lib/version.ts ← export const APP_VERSION = '0.1.0'
205
+ pages/About.tsx
206
+ components/
207
+ [tool]/
208
+ pipeline.ts
209
+ types.ts
210
+ databases.ts
211
+ ```
212
+
213
+ `index.css`:
214
+ ```css
215
+ @import '@genomicx/ui/styles/tokens.css';
216
+ @import '@genomicx/ui/styles/components.css';
217
+ @import "tailwindcss";
218
+ ```
219
+
220
+ `App.tsx`:
221
+ ```tsx
222
+ import { NavBar, AppFooter, LogConsole } from '@genomicx/ui'
223
+ import { APP_VERSION } from './lib/version'
224
+
225
+ function App() {
226
+ useEffect(() => {
227
+ const theme = localStorage.getItem('gx-theme') as 'light' | 'dark' || 'dark'
228
+ document.documentElement.setAttribute('data-theme', theme)
229
+ }, [])
230
+
231
+ return (
232
+ <div className="app">
233
+ <NavBar appName="MYAPP" appSubtitle="What it does" version={APP_VERSION} />
234
+ <main className="app-main">
235
+ <Routes>
236
+ <Route path="/" element={<AnalysisPage />} />
237
+ <Route path="/about" element={<About />} />
238
+ </Routes>
239
+ </main>
240
+ <AppFooter appName="MYAPP" />
241
+ </div>
242
+ )
243
+ }
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Tools using this package
249
+
250
+ | Tool | Repo |
251
+ |------|------|
252
+ | BRIGx | [happykhan/brigx](https://github.com/happykhan/brigx) |
253
+ | MashX | [genomicx/mashx](https://github.com/genomicx/mashx) |
254
+ | Genetrax | [genomicx/genetrax](https://github.com/genomicx/genetrax) |
255
+
256
+ ---
257
+
258
+ ## Development
259
+
260
+ ```bash
261
+ git clone https://github.com/genomicx/ui
262
+ cd ui
263
+ npm install
264
+ npm run build # compile to dist/
265
+ npm run dev # watch mode
266
+ ```
267
+
268
+ To test changes locally in a consuming app before publishing:
269
+
270
+ ```bash
271
+ # In this repo
272
+ npm link
273
+
274
+ # In the consuming app
275
+ npm link @genomicx/ui
276
+ ```
277
+
278
+ ## Publishing
279
+
280
+ ```bash
281
+ npm run build
282
+ npm publish --access public
283
+ ```
284
+
285
+ Requires membership in the `@genomicx` npm org and an Automation token in `~/.npmrc`.
package/dist/index.js CHANGED
@@ -1,233 +1,182 @@
1
- import { jsxs as o, jsx as e } from "react/jsx-runtime";
2
- import { useState as h, useRef as f } from "react";
3
- import { Link as m } from "react-router-dom";
4
- import u from "react-hot-toast";
5
- function p({ disabled: t = !1 }) {
6
- const [r, a] = h(
7
- () => document.documentElement.getAttribute("data-theme") || "light"
8
- ), l = (n) => {
9
- a(n), document.documentElement.setAttribute("data-theme", n), localStorage.setItem("gx-theme", n);
1
+ import { jsxs as a, jsx as e } from "react/jsx-runtime";
2
+ import { useState as v, useRef as p } from "react";
3
+ import { Link as d } from "react-router-dom";
4
+ import g from "react-hot-toast";
5
+ function u({ disabled: n = !1 }) {
6
+ const [o, t] = v(
7
+ () => document.documentElement.getAttribute("data-theme") || "dark"
8
+ ), l = (r) => {
9
+ t(r), document.documentElement.setAttribute("data-theme", r), localStorage.setItem("gx-theme", r);
10
10
  };
11
- return /* @__PURE__ */ o(
12
- "div",
13
- {
14
- className: `flex items-center rounded-full border overflow-hidden text-xs font-medium ${t ? "opacity-40 pointer-events-none" : ""}`,
15
- style: { borderColor: "var(--gx-border)" },
16
- title: t ? "Theme switching disabled" : void 0,
17
- children: [
18
- /* @__PURE__ */ e(
19
- "button",
20
- {
21
- onClick: () => !t && l("dark"),
22
- className: "px-3 py-1.5 transition-colors",
23
- style: r === "dark" ? { background: "var(--gx-accent)", color: "var(--gx-text-inverted)" } : { color: "var(--gx-text-muted)" },
24
- "aria-label": "Dark theme",
25
- disabled: t,
26
- children: /* @__PURE__ */ e("svg", { className: "w-3.5 h-3.5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ e("path", { d: "M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" }) })
27
- }
28
- ),
29
- /* @__PURE__ */ e(
30
- "button",
31
- {
32
- onClick: () => !t && l("light"),
33
- className: "px-3 py-1.5 transition-colors",
34
- style: r === "light" ? { background: "var(--gx-accent)", color: "var(--gx-text-inverted)" } : { color: "var(--gx-text-muted)" },
35
- "aria-label": "Light theme",
36
- disabled: t,
37
- children: /* @__PURE__ */ e("svg", { className: "w-3.5 h-3.5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ e("path", { fillRule: "evenodd", d: "M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z", clipRule: "evenodd" }) })
38
- }
39
- )
40
- ]
41
- }
42
- );
11
+ return /* @__PURE__ */ a("div", { className: `gx-theme-toggle${n ? " disabled" : ""}`, title: n ? "Theme switching disabled" : void 0, children: [
12
+ /* @__PURE__ */ e(
13
+ "button",
14
+ {
15
+ onClick: () => !n && l("dark"),
16
+ className: `gx-theme-btn${o === "dark" ? " active" : ""}`,
17
+ "aria-label": "Dark theme",
18
+ disabled: n,
19
+ children: /* @__PURE__ */ e("svg", { className: "gx-theme-btn-icon", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ e("path", { d: "M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" }) })
20
+ }
21
+ ),
22
+ /* @__PURE__ */ e(
23
+ "button",
24
+ {
25
+ onClick: () => !n && l("light"),
26
+ className: `gx-theme-btn${o === "light" ? " active" : ""}`,
27
+ "aria-label": "Light theme",
28
+ disabled: n,
29
+ children: /* @__PURE__ */ e("svg", { className: "gx-theme-btn-icon", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ e("path", { fillRule: "evenodd", d: "M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z", clipRule: "evenodd" }) })
30
+ }
31
+ )
32
+ ] });
43
33
  }
44
- function b({ appName: t, appSubtitle: r, version: a, actions: l, mobileActions: n }) {
45
- const [s, c] = h(!1);
46
- return /* @__PURE__ */ o("nav", { className: "sticky top-0 z-40", style: { background: "var(--gx-nav-bg)", backdropFilter: "blur(12px)", WebkitBackdropFilter: "blur(12px)", borderBottom: "1px solid var(--gx-border)" }, children: [
47
- /* @__PURE__ */ e("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ o("div", { className: "flex items-center justify-between h-[60px]", children: [
48
- /* @__PURE__ */ o(m, { to: "/", className: "flex items-center gap-3 hover:opacity-90 transition-opacity", children: [
49
- /* @__PURE__ */ o("svg", { className: "w-7 h-7", viewBox: "0 0 24 24", fill: "none", stroke: "var(--gx-accent)", strokeWidth: "2", children: [
34
+ function f({ appName: n, appSubtitle: o, version: t, actions: l, mobileActions: r }) {
35
+ const [c, s] = v(!1);
36
+ return /* @__PURE__ */ a("nav", { className: "gx-nav", children: [
37
+ /* @__PURE__ */ e("div", { className: "gx-nav-inner", children: /* @__PURE__ */ a("div", { className: "gx-nav-row", children: [
38
+ /* @__PURE__ */ a(d, { to: "/", className: "gx-nav-logo", children: [
39
+ /* @__PURE__ */ a("svg", { className: "gx-nav-logo-icon", viewBox: "0 0 24 24", fill: "none", stroke: "var(--gx-accent)", strokeWidth: "2", children: [
50
40
  /* @__PURE__ */ e("circle", { cx: "12", cy: "12", r: "10" }),
51
41
  /* @__PURE__ */ e("circle", { cx: "12", cy: "12", r: "6" }),
52
42
  /* @__PURE__ */ e("circle", { cx: "12", cy: "12", r: "2" })
53
43
  ] }),
54
- /* @__PURE__ */ o("div", { children: [
55
- /* @__PURE__ */ o("h1", { className: "text-lg font-bold", style: { color: "var(--gx-text)" }, children: [
56
- t,
57
- a && /* @__PURE__ */ o("span", { className: "text-xs font-normal ml-1", style: { color: "var(--gx-text-muted)" }, children: [
44
+ /* @__PURE__ */ a("div", { children: [
45
+ /* @__PURE__ */ a("h1", { className: "gx-nav-logo-name", children: [
46
+ n,
47
+ t && /* @__PURE__ */ a("span", { className: "gx-nav-logo-version", children: [
58
48
  "v",
59
- a
49
+ t
60
50
  ] })
61
51
  ] }),
62
- r && /* @__PURE__ */ e("p", { className: "text-xs", style: { color: "var(--gx-text-muted)" }, children: r })
52
+ o && /* @__PURE__ */ e("p", { className: "gx-nav-logo-sub", children: o })
63
53
  ] })
64
54
  ] }),
65
- /* @__PURE__ */ o("div", { className: "hidden md:flex items-center gap-6", children: [
55
+ /* @__PURE__ */ a("div", { className: "gx-nav-desktop", children: [
66
56
  l,
67
- /* @__PURE__ */ e(m, { to: "/about", className: "text-sm font-medium transition-colors", style: { color: "var(--gx-text-muted)" }, children: "About" }),
68
- /* @__PURE__ */ o(
69
- "a",
70
- {
71
- href: "https://github.com/happykhan",
72
- target: "_blank",
73
- rel: "noopener noreferrer",
74
- className: "text-sm font-medium transition-colors inline-flex items-center gap-1",
75
- style: { color: "var(--gx-text-muted)" },
76
- children: [
77
- "GitHub",
78
- /* @__PURE__ */ e("svg", { className: "w-3.5 h-3.5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" }) })
79
- ]
80
- }
81
- ),
82
- /* @__PURE__ */ e(p, {})
57
+ /* @__PURE__ */ e(d, { to: "/about", className: "gx-nav-link", children: "About" }),
58
+ /* @__PURE__ */ a("a", { href: "https://github.com/happykhan", target: "_blank", rel: "noopener noreferrer", className: "gx-nav-link", children: [
59
+ "GitHub",
60
+ /* @__PURE__ */ e("svg", { className: "gx-nav-link-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" }) })
61
+ ] }),
62
+ /* @__PURE__ */ e(u, {})
83
63
  ] }),
84
- /* @__PURE__ */ o("div", { className: "flex md:hidden items-center gap-3", children: [
85
- /* @__PURE__ */ e(p, {}),
86
- /* @__PURE__ */ e(
87
- "button",
88
- {
89
- onClick: () => c(!s),
90
- className: "p-2 rounded",
91
- style: { color: "var(--gx-text-muted)" },
92
- "aria-label": "Toggle menu",
93
- children: s ? /* @__PURE__ */ e("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) : /* @__PURE__ */ e("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" }) })
94
- }
95
- )
64
+ /* @__PURE__ */ a("div", { className: "gx-nav-mobile-toggle", children: [
65
+ /* @__PURE__ */ e(u, {}),
66
+ /* @__PURE__ */ e("button", { onClick: () => s(!c), className: "gx-nav-hamburger", "aria-label": "Toggle menu", children: c ? /* @__PURE__ */ e("svg", { className: "gx-nav-hamburger-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) : /* @__PURE__ */ e("svg", { className: "gx-nav-hamburger-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" }) }) })
96
67
  ] })
97
68
  ] }) }),
98
- s && /* @__PURE__ */ o("div", { className: "md:hidden px-4 pb-4 space-y-2", style: { borderTop: "1px solid var(--gx-border)", background: "var(--gx-nav-bg)" }, children: [
99
- n,
100
- /* @__PURE__ */ e(m, { to: "/about", onClick: () => c(!1), className: "block text-sm py-2 transition-colors", style: { color: "var(--gx-text-muted)" }, children: "About" }),
101
- /* @__PURE__ */ o(
102
- "a",
103
- {
104
- href: "https://github.com/happykhan",
105
- target: "_blank",
106
- rel: "noopener noreferrer",
107
- className: "inline-flex items-center gap-1 text-sm py-2 transition-colors",
108
- style: { color: "var(--gx-text-muted)" },
109
- children: [
110
- "GitHub",
111
- /* @__PURE__ */ e("svg", { className: "w-3.5 h-3.5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" }) })
112
- ]
113
- }
114
- )
69
+ c && /* @__PURE__ */ a("div", { className: "gx-nav-dropdown", children: [
70
+ r,
71
+ /* @__PURE__ */ e(d, { to: "/about", onClick: () => s(!1), className: "gx-nav-dropdown-link", children: "About" }),
72
+ /* @__PURE__ */ e("a", { href: "https://github.com/happykhan", target: "_blank", rel: "noopener noreferrer", className: "gx-nav-dropdown-link", children: "GitHub ↗" })
115
73
  ] })
116
74
  ] });
117
75
  }
118
- function y({ appName: t = "GenomicX", onReportBug: r }) {
119
- return /* @__PURE__ */ e("footer", { className: "mt-auto py-6", style: { borderTop: "1px solid var(--gx-border)", background: "var(--gx-bg-alt)" }, children: /* @__PURE__ */ e("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ o("div", { className: "flex flex-col md:flex-row justify-between items-center", children: [
120
- /* @__PURE__ */ o("div", { className: "text-sm mb-4 md:mb-0", style: { color: "var(--gx-text-muted)" }, children: [
121
- /* @__PURE__ */ o("p", { className: "font-semibold", style: { color: "var(--gx-text)" }, children: [
122
- t,
123
- " — Powered by BLAST & WebAssembly"
76
+ function N({ appName: n = "GenomicX", onReportBug: o }) {
77
+ return /* @__PURE__ */ e("footer", { className: "gx-footer", children: /* @__PURE__ */ e("div", { className: "gx-footer-inner", children: /* @__PURE__ */ a("div", { className: "gx-footer-content", children: [
78
+ /* @__PURE__ */ a("div", { className: "gx-footer-text", children: [
79
+ /* @__PURE__ */ a("p", { className: "gx-footer-text-title", children: [
80
+ n,
81
+ " — Powered by WebAssembly"
124
82
  ] }),
125
- /* @__PURE__ */ e("p", { className: "mt-1", children: "All processing runs locally in your browser — no data leaves your computer" })
83
+ /* @__PURE__ */ e("p", { className: "gx-footer-text-sub", children: "All processing runs locally in your browser — no data leaves your computer" })
126
84
  ] }),
127
- /* @__PURE__ */ o("div", { className: "flex gap-6 text-sm", children: [
128
- /* @__PURE__ */ e("a", { href: "https://genomicx.org", target: "_blank", rel: "noopener noreferrer", className: "transition-colors hover:text-[var(--gx-accent)]", style: { color: "var(--gx-text-muted)" }, children: "genomicx.org" }),
129
- r && /* @__PURE__ */ e("button", { onClick: r, className: "transition-colors hover:text-[var(--gx-accent)]", style: { color: "var(--gx-text-muted)" }, children: "Report Bug" })
85
+ /* @__PURE__ */ a("div", { className: "gx-footer-links", children: [
86
+ /* @__PURE__ */ e("a", { href: "https://genomicx.org", target: "_blank", rel: "noopener noreferrer", className: "gx-footer-link", children: "genomicx.org" }),
87
+ o && /* @__PURE__ */ e("button", { onClick: o, className: "gx-footer-link", children: "Report Bug" })
130
88
  ] })
131
89
  ] }) }) });
132
90
  }
133
- function C({ children: t, onReportBug: r, ...a }) {
134
- return /* @__PURE__ */ o("div", { className: "min-h-screen flex flex-col", style: { background: "var(--gx-bg)" }, children: [
135
- /* @__PURE__ */ e(b, { ...a }),
136
- /* @__PURE__ */ e("main", { className: "flex-1", children: t }),
137
- /* @__PURE__ */ e(y, { appName: a.appName, onReportBug: r })
91
+ function B({ children: n, onReportBug: o, ...t }) {
92
+ return /* @__PURE__ */ a("div", { style: { minHeight: "100vh", display: "flex", flexDirection: "column", background: "var(--gx-bg)" }, children: [
93
+ /* @__PURE__ */ e(f, { ...t }),
94
+ /* @__PURE__ */ e("main", { style: { flex: 1 }, children: n }),
95
+ /* @__PURE__ */ e(N, { appName: t.appName, onReportBug: o })
138
96
  ] });
139
97
  }
140
- function j({ logs: t, progress: r, title: a = "Console" }) {
141
- const [l, n] = h(!0), s = f(null), c = () => {
142
- navigator.clipboard.writeText(t.join(`
98
+ function C({ logs: n, progress: o, title: t = "Console" }) {
99
+ const l = p(null), r = () => {
100
+ navigator.clipboard.writeText(n.join(`
143
101
  `)).then(() => {
144
- u.success("Logs copied to clipboard!");
102
+ g.success("Logs copied to clipboard!");
145
103
  }).catch(() => {
146
- u.error("Failed to copy logs");
104
+ g.error("Failed to copy logs");
147
105
  });
148
- }, i = r && r.step !== "idle" && r.step !== "Complete!";
149
- return /* @__PURE__ */ o("div", { className: "card mt-6", children: [
150
- i && /* @__PURE__ */ o("div", { className: "mb-4 pb-4", style: { borderBottom: "1px solid var(--gx-border)" }, children: [
151
- /* @__PURE__ */ o("div", { className: "flex items-center justify-between mb-2", children: [
152
- /* @__PURE__ */ e("span", { className: "text-sm font-medium", style: { color: "var(--gx-text)" }, children: r.step }),
153
- /* @__PURE__ */ o("span", { className: "text-sm", style: { color: "var(--gx-text-muted)" }, children: [
154
- r.percent,
106
+ }, c = o && o.step !== "idle" && o.step !== "Complete!";
107
+ return /* @__PURE__ */ a("div", { className: "gx-console", children: [
108
+ c && /* @__PURE__ */ a("div", { className: "gx-console-progress", children: [
109
+ /* @__PURE__ */ a("div", { className: "gx-console-progress-row", children: [
110
+ /* @__PURE__ */ e("span", { className: "gx-console-progress-step", children: o.step }),
111
+ /* @__PURE__ */ a("span", { className: "gx-console-progress-pct", children: [
112
+ o.percent,
155
113
  "%"
156
114
  ] })
157
115
  ] }),
158
- /* @__PURE__ */ e("div", { className: "progress-bg", children: /* @__PURE__ */ e("div", { className: "progress-bar", style: { width: `${r.percent}%` } }) }),
159
- r.message && /* @__PURE__ */ e("div", { className: "mt-2 text-xs", style: { color: "var(--gx-text-muted)" }, children: r.message })
116
+ /* @__PURE__ */ e("div", { className: "progress-bg", children: /* @__PURE__ */ e("div", { className: "progress-bar", style: { width: `${o.percent}%` } }) }),
117
+ o.message && /* @__PURE__ */ e("div", { className: "gx-console-progress-msg", children: o.message })
160
118
  ] }),
161
- /* @__PURE__ */ o("div", { className: "flex items-center justify-between mb-3", children: [
162
- /* @__PURE__ */ o("div", { className: "flex items-center gap-2", children: [
163
- /* @__PURE__ */ e("button", { onClick: () => n(!l), style: { color: "var(--gx-text-muted)" }, children: l ? "▼" : "▶" }),
164
- /* @__PURE__ */ e("h3", { className: "font-semibold", style: { color: "var(--gx-text)" }, children: a }),
165
- /* @__PURE__ */ o("span", { className: "text-xs", style: { color: "var(--gx-text-muted)" }, children: [
119
+ /* @__PURE__ */ a("div", { className: "gx-console-header", children: [
120
+ /* @__PURE__ */ a("div", { className: "gx-console-title-row", children: [
121
+ /* @__PURE__ */ e("h3", { className: "gx-console-title", children: t }),
122
+ /* @__PURE__ */ a("span", { className: "gx-console-count", children: [
166
123
  "(",
167
- t.length,
124
+ n.length,
168
125
  " messages)"
169
126
  ] })
170
127
  ] }),
171
- /* @__PURE__ */ o("button", { onClick: c, className: "btn-secondary text-xs px-3 py-1", disabled: t.length === 0, children: [
172
- /* @__PURE__ */ e("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" }) }),
128
+ /* @__PURE__ */ a("button", { onClick: r, className: "gx-console-copy", disabled: n.length === 0, children: [
129
+ /* @__PURE__ */ e("svg", { className: "gx-console-copy-icon", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ e("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" }) }),
173
130
  "Copy"
174
131
  ] })
175
132
  ] }),
176
- l && /* @__PURE__ */ e(
177
- "div",
178
- {
179
- ref: s,
180
- className: "font-mono text-xs p-4 rounded max-h-96 overflow-y-auto",
181
- style: { background: "var(--gx-code-bg)", color: "var(--gx-accent)", border: "1px solid var(--gx-border)" },
182
- children: t.length === 0 ? /* @__PURE__ */ e("div", { style: { color: "var(--gx-text-muted)" }, children: "No logs yet..." }) : t.map((d, g) => /* @__PURE__ */ e("div", { className: "mb-1 whitespace-pre-wrap break-all", children: d }, g))
183
- }
184
- )
133
+ /* @__PURE__ */ e("div", { ref: l, className: "gx-console-body", children: n.length === 0 ? /* @__PURE__ */ e("div", { className: "gx-console-empty", children: "No logs yet..." }) : n.map((s, i) => /* @__PURE__ */ e("div", { className: "gx-console-line", children: s }, i)) })
185
134
  ] });
186
135
  }
187
- const k = "https://static.genomicx.org/wasm", x = /* @__PURE__ */ new Map();
188
- async function N(t, r = k) {
189
- const a = `${r}/${t}`;
190
- if (x.has(a)) return x.get(a);
191
- const [l, n] = await Promise.all([
192
- fetch(`${r}/${t}.js`),
193
- fetch(`${r}/${t}.wasm`)
136
+ const k = "https://static.genomicx.org/wasm", h = /* @__PURE__ */ new Map();
137
+ async function b(n, o = k) {
138
+ const t = `${o}/${n}`;
139
+ if (h.has(t)) return h.get(t);
140
+ const [l, r] = await Promise.all([
141
+ fetch(`${o}/${n}.js`),
142
+ fetch(`${o}/${n}.wasm`)
194
143
  ]);
195
- if (!l.ok) throw new Error(`Failed to fetch ${t}.js: ${l.status}`);
196
- if (!n.ok) throw new Error(`Failed to fetch ${t}.wasm: ${n.status}`);
197
- const [s, c] = await Promise.all([
144
+ if (!l.ok) throw new Error(`Failed to fetch ${n}.js: ${l.status}`);
145
+ if (!r.ok) throw new Error(`Failed to fetch ${n}.wasm: ${r.status}`);
146
+ const [c, s] = await Promise.all([
198
147
  l.text(),
199
- n.arrayBuffer()
200
- ]), d = { factory: new Function("Module", s + "; return Module;")({}), wasmBinary: c };
201
- return x.set(a, d), d;
148
+ r.arrayBuffer()
149
+ ]), m = { factory: new Function("Module", c + "; return Module;")({}), wasmBinary: s };
150
+ return h.set(t, m), m;
202
151
  }
203
- async function A(t, r) {
204
- const { factory: a, wasmBinary: l } = await N(t, r), n = [], s = [], c = await a({
152
+ async function $(n, o) {
153
+ const { factory: t, wasmBinary: l } = await b(n, o), r = [], c = [], s = await t({
205
154
  wasmBinary: l.slice(0),
206
- print: (i) => n.push(i),
207
- printErr: (i) => s.push(i),
155
+ print: (i) => r.push(i),
156
+ printErr: (i) => c.push(i),
208
157
  noInitialRun: !0
209
158
  });
210
- return c._stdout = n, c._stderr = s, c;
159
+ return s._stdout = r, s._stderr = c, s;
211
160
  }
212
- function v(t, r) {
213
- const a = URL.createObjectURL(t), l = document.createElement("a");
214
- l.href = a, l.download = r, l.click(), URL.revokeObjectURL(a);
161
+ function x(n, o) {
162
+ const t = URL.createObjectURL(n), l = document.createElement("a");
163
+ l.href = t, l.download = o, l.click(), URL.revokeObjectURL(t);
215
164
  }
216
- function T(t, r, a = "text/plain") {
217
- v(new Blob([t], { type: a }), r);
165
+ function j(n, o, t = "text/plain") {
166
+ x(new Blob([n], { type: t }), o);
218
167
  }
219
- function z(t, r) {
220
- v(new Blob([t]), r);
168
+ function A(n, o) {
169
+ x(new Blob([n]), o);
221
170
  }
222
171
  export {
223
- y as AppFooter,
224
- C as AppShell,
225
- j as LogConsole,
226
- b as NavBar,
227
- p as ThemeToggle,
228
- A as createModuleInstance,
229
- v as downloadBlob,
230
- z as downloadBuffer,
231
- T as downloadText,
232
- N as loadWasmModule
172
+ N as AppFooter,
173
+ B as AppShell,
174
+ C as LogConsole,
175
+ f as NavBar,
176
+ u as ThemeToggle,
177
+ $ as createModuleInstance,
178
+ x as downloadBlob,
179
+ A as downloadBuffer,
180
+ j as downloadText,
181
+ b as loadWasmModule
233
182
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genomicx/ui",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared UI components, styles, and WASM loader for GenomicX tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,8 +23,8 @@
23
23
  "dev": "vite build --watch"
24
24
  },
25
25
  "peerDependencies": {
26
- "react": "^18.0.0",
27
- "react-dom": "^18.0.0",
26
+ "react": "^18.0.0 || ^19.0.0",
27
+ "react-dom": "^18.0.0 || ^19.0.0",
28
28
  "react-router-dom": "^6.0.0 || ^7.0.0",
29
29
  "react-hot-toast": "^2.0.0"
30
30
  },
@@ -1,125 +1,400 @@
1
- /* GenomicX Tailwind component classesimport after @tailwind directives */
2
-
3
- @layer base {
4
- html {
5
- scroll-behavior: smooth;
6
- -webkit-font-smoothing: antialiased;
7
- -moz-osx-font-smoothing: grayscale;
8
- }
9
-
10
- body {
11
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
12
- background-color: var(--gx-bg);
13
- color: var(--gx-text);
14
- line-height: 1.7;
15
- transition: background-color var(--gx-transition), color var(--gx-transition);
16
- margin: 0;
17
- }
18
-
19
- * {
20
- transition: background-color var(--gx-transition), border-color var(--gx-transition), color var(--gx-transition);
21
- }
22
-
23
- code {
24
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
25
- }
26
-
27
- ::-webkit-scrollbar { width: 10px; height: 10px; }
28
- ::-webkit-scrollbar-track { background: var(--gx-bg); }
29
- ::-webkit-scrollbar-thumb { background: var(--gx-border); border-radius: 999px; }
30
- ::-webkit-scrollbar-thumb:hover { background: var(--gx-text-muted); }
31
- }
32
-
33
- @layer components {
34
- .card {
35
- background: var(--gx-bg-alt);
36
- border: 1px solid var(--gx-border);
37
- border-radius: var(--gx-radius-lg);
38
- padding: 1.5rem;
39
- transition: border-color var(--gx-transition);
40
- }
41
-
42
- .card:hover {
43
- border-color: var(--gx-accent);
44
- }
45
-
46
- .btn-primary {
47
- display: inline-flex;
48
- align-items: center;
49
- justify-content: center;
50
- gap: 0.5rem;
51
- background: var(--gx-accent);
52
- color: var(--gx-text-inverted);
53
- font-weight: 600;
54
- font-size: 0.875rem;
55
- padding: 0.7rem 1.4rem;
56
- border-radius: 6px;
57
- border: none;
58
- cursor: pointer;
59
- transition: all var(--gx-transition);
60
- }
61
-
62
- .btn-primary:hover { background: var(--gx-accent-hover); }
63
- .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
64
-
65
- .btn-secondary {
66
- display: inline-flex;
67
- align-items: center;
68
- justify-content: center;
69
- gap: 0.5rem;
70
- background: transparent;
71
- color: var(--gx-text);
72
- font-weight: 600;
73
- font-size: 0.875rem;
74
- padding: 0.7rem 1.4rem;
75
- border-radius: 6px;
76
- border: 1px solid var(--gx-border);
77
- cursor: pointer;
78
- transition: all var(--gx-transition);
79
- }
80
-
81
- .btn-secondary:hover { border-color: var(--gx-accent); color: var(--gx-accent); }
82
-
83
- .input-field {
84
- background: var(--gx-bg);
85
- border: 1px solid var(--gx-border);
86
- color: var(--gx-text);
87
- border-radius: 6px;
88
- padding: 0.5rem 0.75rem;
89
- font-size: 0.875rem;
90
- transition: border-color var(--gx-transition);
91
- outline: none;
92
- }
93
-
94
- .input-field:focus { border-color: var(--gx-accent); }
95
-
96
- .label {
97
- display: block;
98
- font-size: 0.8125rem;
99
- font-weight: 500;
100
- color: var(--gx-text);
101
- margin-bottom: 0.5rem;
102
- }
103
-
104
- .section-title {
105
- font-size: 1.25rem;
106
- font-weight: 700;
107
- color: var(--gx-text);
108
- margin-bottom: 1rem;
109
- letter-spacing: -0.01em;
110
- }
111
-
112
- .progress-bg {
113
- background: var(--gx-bg);
114
- height: 6px;
115
- border-radius: 999px;
116
- overflow: hidden;
117
- }
118
-
119
- .progress-bar {
120
- background: var(--gx-accent);
121
- height: 6px;
122
- border-radius: 999px;
123
- transition: width 0.3s ease;
124
- }
1
+ /* GenomicX component stylesno Tailwind dependency */
2
+
3
+ /* ── Base resets ─────────────────────────────── */
4
+ html {
5
+ scroll-behavior: smooth;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ body {
11
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
12
+ background-color: var(--gx-bg);
13
+ color: var(--gx-text);
14
+ line-height: 1.7;
15
+ transition: background-color var(--gx-transition), color var(--gx-transition);
16
+ margin: 0;
17
+ }
18
+
19
+ * {
20
+ transition: background-color var(--gx-transition), border-color var(--gx-transition), color var(--gx-transition);
21
+ }
22
+
23
+ code {
24
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
25
+ }
26
+
27
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
28
+ ::-webkit-scrollbar-track { background: var(--gx-bg); }
29
+ ::-webkit-scrollbar-thumb { background: var(--gx-border); border-radius: 999px; }
30
+ ::-webkit-scrollbar-thumb:hover { background: var(--gx-text-muted); }
31
+
32
+ /* ── NavBar ──────────────────────────────────── */
33
+ .gx-nav {
34
+ position: sticky;
35
+ top: 0;
36
+ z-index: 40;
37
+ background: var(--gx-nav-bg);
38
+ backdrop-filter: blur(12px);
39
+ -webkit-backdrop-filter: blur(12px);
40
+ border-bottom: 1px solid var(--gx-border);
41
+ }
42
+
43
+ .gx-nav-inner {
44
+ max-width: 80rem;
45
+ margin: 0 auto;
46
+ padding: 0 1rem;
47
+ }
48
+
49
+ @media (min-width: 640px) { .gx-nav-inner { padding: 0 1.5rem; } }
50
+ @media (min-width: 1024px) { .gx-nav-inner { padding: 0 2rem; } }
51
+
52
+ .gx-nav-row {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ height: 60px;
57
+ }
58
+
59
+ .gx-nav-logo {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 0.75rem;
63
+ text-decoration: none;
64
+ opacity: 1;
65
+ transition: opacity var(--gx-transition);
66
+ }
67
+ .gx-nav-logo:hover { opacity: 0.85; }
68
+
69
+ .gx-nav-logo-icon { width: 28px; height: 28px; flex-shrink: 0; }
70
+
71
+ .gx-nav-logo-name {
72
+ font-size: 1.125rem;
73
+ font-weight: 700;
74
+ color: var(--gx-text);
75
+ margin: 0;
76
+ line-height: 1.3;
77
+ }
78
+
79
+ .gx-nav-logo-version {
80
+ font-size: 0.75rem;
81
+ font-weight: 400;
82
+ margin-left: 0.25rem;
83
+ color: var(--gx-text-muted);
84
+ }
85
+
86
+ .gx-nav-logo-sub {
87
+ font-size: 0.75rem;
88
+ color: var(--gx-text-muted);
89
+ margin: 0;
90
+ line-height: 1.2;
91
+ }
92
+
93
+ .gx-nav-desktop {
94
+ display: none;
95
+ align-items: center;
96
+ gap: 1.5rem;
97
+ }
98
+ @media (min-width: 768px) { .gx-nav-desktop { display: flex; } }
99
+
100
+ .gx-nav-link {
101
+ font-size: 0.875rem;
102
+ font-weight: 500;
103
+ color: var(--gx-text-muted);
104
+ text-decoration: none;
105
+ transition: color var(--gx-transition);
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 0.25rem;
109
+ }
110
+ .gx-nav-link:hover { color: var(--gx-text); }
111
+
112
+ .gx-nav-link-icon { width: 14px; height: 14px; }
113
+
114
+ .gx-nav-mobile-toggle {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 0.75rem;
118
+ }
119
+ @media (min-width: 768px) { .gx-nav-mobile-toggle { display: none; } }
120
+
121
+ .gx-nav-hamburger {
122
+ background: none;
123
+ border: none;
124
+ cursor: pointer;
125
+ padding: 0.5rem;
126
+ border-radius: 4px;
127
+ color: var(--gx-text-muted);
128
+ display: flex;
129
+ align-items: center;
130
+ }
131
+ .gx-nav-hamburger:hover { color: var(--gx-text); }
132
+
133
+ .gx-nav-hamburger-icon { width: 20px; height: 20px; }
134
+
135
+ .gx-nav-dropdown {
136
+ display: block;
137
+ padding: 0 1rem 1rem;
138
+ border-top: 1px solid var(--gx-border);
139
+ background: var(--gx-nav-bg);
140
+ }
141
+ @media (min-width: 768px) { .gx-nav-dropdown { display: none; } }
142
+
143
+ .gx-nav-dropdown-link {
144
+ display: block;
145
+ font-size: 0.875rem;
146
+ padding: 0.5rem 0;
147
+ color: var(--gx-text-muted);
148
+ text-decoration: none;
149
+ transition: color var(--gx-transition);
150
+ }
151
+ .gx-nav-dropdown-link:hover { color: var(--gx-text); }
152
+
153
+ /* ── Footer ──────────────────────────────────── */
154
+ .gx-footer {
155
+ margin-top: auto;
156
+ padding: 1.5rem 0;
157
+ border-top: 1px solid var(--gx-border);
158
+ background: var(--gx-bg-alt);
159
+ }
160
+
161
+ .gx-footer-inner {
162
+ max-width: 80rem;
163
+ margin: 0 auto;
164
+ padding: 0 1rem;
165
+ }
166
+ @media (min-width: 640px) { .gx-footer-inner { padding: 0 1.5rem; } }
167
+ @media (min-width: 1024px) { .gx-footer-inner { padding: 0 2rem; } }
168
+
169
+ .gx-footer-content {
170
+ display: flex;
171
+ flex-direction: column;
172
+ align-items: center;
173
+ gap: 1rem;
174
+ }
175
+ @media (min-width: 768px) {
176
+ .gx-footer-content { flex-direction: row; justify-content: space-between; }
177
+ }
178
+
179
+ .gx-footer-text { font-size: 0.875rem; color: var(--gx-text-muted); }
180
+ .gx-footer-text-title { font-weight: 600; color: var(--gx-text); margin: 0 0 0.25rem; }
181
+ .gx-footer-text-sub { margin: 0; }
182
+
183
+ .gx-footer-links {
184
+ display: flex;
185
+ gap: 1.5rem;
186
+ font-size: 0.875rem;
187
+ }
188
+
189
+ .gx-footer-link {
190
+ color: var(--gx-text-muted);
191
+ text-decoration: none;
192
+ background: none;
193
+ border: none;
194
+ cursor: pointer;
195
+ padding: 0;
196
+ font-size: 0.875rem;
197
+ transition: color var(--gx-transition);
198
+ }
199
+ .gx-footer-link:hover { color: var(--gx-accent); }
200
+
201
+ /* ── ThemeToggle ─────────────────────────────── */
202
+ .gx-theme-toggle {
203
+ display: flex;
204
+ align-items: center;
205
+ border-radius: 999px;
206
+ border: 1px solid var(--gx-border);
207
+ overflow: hidden;
208
+ font-size: 0.75rem;
209
+ font-weight: 500;
210
+ }
211
+ .gx-theme-toggle.disabled { opacity: 0.4; pointer-events: none; }
212
+
213
+ .gx-theme-btn {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ padding: 0.375rem 0.75rem;
218
+ background: none;
219
+ border: none;
220
+ cursor: pointer;
221
+ transition: background var(--gx-transition), color var(--gx-transition);
222
+ color: var(--gx-text-muted);
223
+ }
224
+ .gx-theme-btn:hover { color: var(--gx-text); }
225
+ .gx-theme-btn-icon { width: 14px; height: 14px; }
226
+ .gx-theme-btn.active {
227
+ background: var(--gx-accent);
228
+ color: var(--gx-text-inverted);
229
+ }
230
+
231
+ /* ── LogConsole ──────────────────────────────── */
232
+ .gx-console {
233
+ background: var(--gx-bg-alt);
234
+ border: 1px solid var(--gx-border);
235
+ border-radius: var(--gx-radius-lg);
236
+ padding: 1.5rem;
237
+ margin-top: 1.5rem;
238
+ }
239
+
240
+ .gx-console-progress {
241
+ margin-bottom: 1rem;
242
+ padding-bottom: 1rem;
243
+ border-bottom: 1px solid var(--gx-border);
244
+ }
245
+
246
+ .gx-console-progress-row {
247
+ display: flex;
248
+ align-items: center;
249
+ justify-content: space-between;
250
+ margin-bottom: 0.5rem;
251
+ }
252
+
253
+ .gx-console-progress-step { font-size: 0.875rem; font-weight: 500; color: var(--gx-text); }
254
+ .gx-console-progress-pct { font-size: 0.875rem; color: var(--gx-text-muted); }
255
+ .gx-console-progress-msg { margin-top: 0.5rem; font-size: 0.75rem; color: var(--gx-text-muted); }
256
+
257
+ .gx-console-header {
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: space-between;
261
+ margin-bottom: 0.75rem;
262
+ }
263
+
264
+ .gx-console-title-row {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 0.5rem;
268
+ }
269
+
270
+ .gx-console-toggle {
271
+ background: none;
272
+ border: none;
273
+ cursor: pointer;
274
+ color: var(--gx-text-muted);
275
+ padding: 0;
276
+ font-size: 0.75rem;
277
+ }
278
+
279
+ .gx-console-title { font-weight: 600; color: var(--gx-text); margin: 0; font-size: 0.875rem; }
280
+ .gx-console-count { font-size: 0.75rem; color: var(--gx-text-muted); }
281
+
282
+ .gx-console-copy {
283
+ display: inline-flex;
284
+ align-items: center;
285
+ gap: 0.25rem;
286
+ font-size: 0.75rem;
287
+ padding: 0.25rem 0.75rem;
288
+ background: transparent;
289
+ color: var(--gx-text);
290
+ border: 1px solid var(--gx-border);
291
+ border-radius: 6px;
292
+ cursor: pointer;
293
+ transition: all var(--gx-transition);
294
+ font-weight: 600;
295
+ }
296
+ .gx-console-copy:hover { border-color: var(--gx-accent); color: var(--gx-accent); }
297
+ .gx-console-copy:disabled { opacity: 0.5; cursor: not-allowed; }
298
+ .gx-console-copy-icon { width: 16px; height: 16px; }
299
+
300
+ .gx-console-body {
301
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
302
+ font-size: 0.75rem;
303
+ padding: 1rem;
304
+ border-radius: 6px;
305
+ max-height: 24rem;
306
+ overflow-y: auto;
307
+ background: var(--gx-code-bg);
308
+ color: var(--gx-accent);
309
+ border: 1px solid var(--gx-border);
310
+ }
311
+
312
+ .gx-console-empty { color: var(--gx-text-muted); }
313
+ .gx-console-line { margin-bottom: 0.25rem; white-space: pre-wrap; word-break: break-all; }
314
+
315
+ /* ── Shared utility classes ──────────────────── */
316
+ .card {
317
+ background: var(--gx-bg-alt);
318
+ border: 1px solid var(--gx-border);
319
+ border-radius: var(--gx-radius-lg);
320
+ padding: 1.5rem;
321
+ transition: border-color var(--gx-transition);
322
+ }
323
+ .card:hover { border-color: var(--gx-accent); }
324
+
325
+ .btn-primary {
326
+ display: inline-flex;
327
+ align-items: center;
328
+ justify-content: center;
329
+ gap: 0.5rem;
330
+ background: var(--gx-accent);
331
+ color: var(--gx-text-inverted);
332
+ font-weight: 600;
333
+ font-size: 0.875rem;
334
+ padding: 0.7rem 1.4rem;
335
+ border-radius: 6px;
336
+ border: none;
337
+ cursor: pointer;
338
+ transition: all var(--gx-transition);
339
+ }
340
+ .btn-primary:hover { background: var(--gx-accent-hover); }
341
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
342
+
343
+ .btn-secondary {
344
+ display: inline-flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ gap: 0.5rem;
348
+ background: transparent;
349
+ color: var(--gx-text);
350
+ font-weight: 600;
351
+ font-size: 0.875rem;
352
+ padding: 0.7rem 1.4rem;
353
+ border-radius: 6px;
354
+ border: 1px solid var(--gx-border);
355
+ cursor: pointer;
356
+ transition: all var(--gx-transition);
357
+ }
358
+ .btn-secondary:hover { border-color: var(--gx-accent); color: var(--gx-accent); }
359
+
360
+ .input-field {
361
+ background: var(--gx-bg);
362
+ border: 1px solid var(--gx-border);
363
+ color: var(--gx-text);
364
+ border-radius: 6px;
365
+ padding: 0.5rem 0.75rem;
366
+ font-size: 0.875rem;
367
+ transition: border-color var(--gx-transition);
368
+ outline: none;
369
+ }
370
+ .input-field:focus { border-color: var(--gx-accent); }
371
+
372
+ .label {
373
+ display: block;
374
+ font-size: 0.8125rem;
375
+ font-weight: 500;
376
+ color: var(--gx-text);
377
+ margin-bottom: 0.5rem;
378
+ }
379
+
380
+ .section-title {
381
+ font-size: 1.25rem;
382
+ font-weight: 700;
383
+ color: var(--gx-text);
384
+ margin-bottom: 1rem;
385
+ letter-spacing: -0.01em;
386
+ }
387
+
388
+ .progress-bg {
389
+ background: var(--gx-bg);
390
+ height: 6px;
391
+ border-radius: 999px;
392
+ overflow: hidden;
393
+ }
394
+
395
+ .progress-bar {
396
+ background: var(--gx-accent);
397
+ height: 6px;
398
+ border-radius: 999px;
399
+ transition: width 0.3s ease;
125
400
  }