mirror of
https://github.com/luxfi/work-board.git
synced 2026-07-27 06:45:00 +00:00
work-board: migrate onto @luxfi/ui@7.4.0 (@hanzo/gui Tamagui engine) — Phase 1
Run the live 4-org Dework hub on the @luxfi/ui / @hanzo/gui (Tamagui) engine
without regressing the owner-approved look/feel.
- Vite+Tamagui weld (proven exchange recipe): react-native->react-native-web
alias, dedupe + single-copy of @hanzo/gui/@hanzogui/core/@hanzogui/web,
optimizeDeps pre-bundle, __DEV__/EXPO_OS/IS_WEB defines, commonjsOptions so
the engine stays in React's chunk. guiPlugin extraction deferred (its 7.3.0
peer pins vite@8.0.3); app renders on the runtime style-injection path.
- Wrap in AppProvider (GuiProvider + TanStack Query); brand = VALUE: root theme
row dark_${BRAND_KEY} + --brand sourced from @luxfi/ui LUX_BRAND (hex identical
to brands.ts, zero cross-brand leak).
- tokens.css remap layer maps the @luxfi/ui --color-* contract onto the Dework
palette + brand (no wholesale exchange-palette import).
- Text fields -> @luxfi/ui Input/Textarea (native inputs, onChangeText<->onChange
bridge): CommandPalette ⌘K, Suggestions editor, TaskDetail comment, chrome
Toolbar, Leaderboards search. Keystroke round-trip verified.
- Fix Leaderboards mobile clip: page container was a flex item whose mx-auto
disabled cross-axis stretch, so min-w-[460px] tables forced cards to 460px and
bled past 390px. w-full + min-w-0 chain + overflow-hidden cards -> tables now
scroll inside rounded cards, no horizontal page scroll.
Build green (tsc + vite). Visual-regression: 19/21 golden views pixel-match
(<=2%); 2 mobile bottom-sheet overlays (task-detail, palette) diverge ~3% (under
review). Preview branch — do NOT merge to main until owner review.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -2,3 +2,4 @@ node_modules
|
||||
dist
|
||||
*.local
|
||||
.DS_Store
|
||||
dist/
|
||||
|
||||
Generated
+8788
-33
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,18 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/gui": "7.3.0",
|
||||
"@hanzogui/colors": "7.3.0",
|
||||
"@hanzogui/config": "7.3.0",
|
||||
"@hanzogui/core": "7.3.0",
|
||||
"@hanzogui/lucide-icons-2": "7.3.0",
|
||||
"@hanzogui/themes": "7.3.0",
|
||||
"@hanzogui/web": "7.3.0",
|
||||
"@luxfi/ui": "file:/Users/z/work/luxfi/ui/packages/ui",
|
||||
"@tanstack/react-query": "^5.76.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-native-web": "^0.21.0",
|
||||
"viem": "^2.55.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Reusable capture: load a work-board hash route at a viewport and screenshot it,
|
||||
// matching how the goldens were captured (dark, dsf 2, 1440x900 / 390x844).
|
||||
// Usage: node scripts/capture.mjs <baseUrl> <outDir> [view:hash ...]
|
||||
// node scripts/capture.mjs http://localhost:5188 /tmp/shots overview:#/ board:#/board
|
||||
import pw from '/Users/z/work/luxfi/ui/node_modules/@playwright/test/index.js';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
const { chromium } = pw;
|
||||
|
||||
const base = process.argv[2] || 'http://localhost:5188';
|
||||
const outDir = process.argv[3] || '/private/tmp/claude-501/-Users-z-work-lux-private/a30ef7fe-e14c-464e-8783-67708506331d/scratchpad/shots';
|
||||
const specs = process.argv.slice(4);
|
||||
const DEFAULT = [
|
||||
'overview:#/', 'board:#/board', 'open-tasks:#/space/engineering/tasks',
|
||||
'leaderboards:#/leaderboards', 'suggestions:#/suggestions', 'space:#/space/engineering/board',
|
||||
'task-detail:#/board?task=0', 'explore:#/explore',
|
||||
];
|
||||
const views = (specs.length ? specs : DEFAULT).map((s) => {
|
||||
const i = s.indexOf(':');
|
||||
return { view: s.slice(0, i), hash: s.slice(i + 1) };
|
||||
});
|
||||
const VIEWPORTS = [
|
||||
{ name: 'desktop', width: 1440, height: 900 },
|
||||
{ name: 'mobile', width: 390, height: 844 },
|
||||
];
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const errors = [];
|
||||
const browser = await chromium.launch();
|
||||
for (const vp of VIEWPORTS) {
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: vp.width, height: vp.height },
|
||||
deviceScaleFactor: 2,
|
||||
colorScheme: 'dark',
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
page.on('pageerror', (e) => errors.push(`[${vp.name}] PAGEERROR ${e.message}`));
|
||||
page.on('console', (m) => { if (m.type() === 'error') errors.push(`[${vp.name}] ${m.text()}`); });
|
||||
for (const { view, hash } of views) {
|
||||
await page.goto(`${base}/${hash}`, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(700); // settle fixture render + fonts
|
||||
await page.screenshot({ path: `${outDir}/${view}-${vp.name}.png` });
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await browser.close();
|
||||
console.log(JSON.stringify({ captured: views.map((v) => v.view), viewports: VIEWPORTS.map((v) => v.name), errors: errors.slice(0, 15) }, null, 2));
|
||||
@@ -0,0 +1,38 @@
|
||||
// Isolated weld probe runner: loads /weldcheck.html, asserts the @luxfi/ui
|
||||
// Button rendered and the Input round-trips keystrokes, and screenshots it.
|
||||
// Run: node scripts/weld-probe.mjs (Playwright resolved from ~/work/luxfi/ui)
|
||||
import pw from '/Users/z/work/luxfi/ui/node_modules/@playwright/test/index.js';
|
||||
const { chromium } = pw;
|
||||
|
||||
const URL = process.env.WELD_URL || 'http://localhost:5188/weldcheck.html';
|
||||
const OUT = process.env.WELD_OUT || '/private/tmp/claude-501/-Users-z-work-lux-private/a30ef7fe-e14c-464e-8783-67708506331d/scratchpad/weld.png';
|
||||
|
||||
const errors = [];
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 640, height: 480 }, deviceScaleFactor: 2, colorScheme: 'dark' });
|
||||
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
|
||||
page.on('pageerror', (e) => errors.push('PAGEERROR: ' + e.message));
|
||||
|
||||
await page.goto(URL, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
const btn = page.getByTestId('btn');
|
||||
const btnVisible = await btn.isVisible().catch(() => false);
|
||||
const btnText = (await btn.textContent().catch(() => '')) || '';
|
||||
|
||||
// Round-trip: type into the @luxfi/ui Input, assert the controlled echo updates.
|
||||
const inp = page.getByTestId('inp');
|
||||
await inp.click();
|
||||
await inp.type('hello123', { delay: 20 });
|
||||
const echo = ((await page.getByTestId('echo').textContent().catch(() => '')) || '').trim();
|
||||
const inpValue = await inp.inputValue().catch(() => '');
|
||||
|
||||
await page.screenshot({ path: OUT });
|
||||
await browser.close();
|
||||
|
||||
const roundTrip = echo === 'echo:hello123' && inpValue === 'hello123';
|
||||
console.log(JSON.stringify({
|
||||
btnVisible, btnText: btnText.trim(), inpValue, echo, roundTrip,
|
||||
consoleErrors: errors.slice(0, 10),
|
||||
}, null, 2));
|
||||
process.exit(btnVisible && roundTrip && errors.length === 0 ? 0 : 1);
|
||||
+6
-3
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ORG, CHAIN_ID } from './config';
|
||||
import { LUX_BRAND } from '@luxfi/ui';
|
||||
import { ORG, CHAIN_ID, BRAND_KEY } from './config';
|
||||
import { useWorkspace } from './useWorkspace';
|
||||
import type { Source } from './useWorkspace';
|
||||
import { useRoute } from './router';
|
||||
@@ -80,8 +81,10 @@ export function App() {
|
||||
const route = useRoute();
|
||||
|
||||
// Inject the per-brand accent once; every accented element reads var(--brand).
|
||||
// Sourced from the @luxfi/ui LUX_BRAND value (dark_${BRAND_KEY} row) so brand
|
||||
// is one value in one place — identical hex to the legacy brands.ts accent.
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty('--brand', ORG.accent);
|
||||
document.documentElement.style.setProperty('--brand', LUX_BRAND[BRAND_KEY]);
|
||||
}, []);
|
||||
|
||||
const modalTask = useMemo(
|
||||
@@ -148,7 +151,7 @@ export function App() {
|
||||
<Topbar onSearch={() => setPaletteOpen(true)} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
{loading && ws === null ? <Spinner /> : ws ? <Routed ws={ws} tasks={tasks} /> : null}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ORG } from '../config';
|
||||
import type { Task } from '../types';
|
||||
import { navigate, openTask } from '../router';
|
||||
import { openConnect } from '../auth';
|
||||
import { IconSearch, IconGrid, IconBoard, IconTrophy, IconBulb, IconPlus, IconDoc, IconExternal } from '../ui';
|
||||
import { Input, IconSearch, IconGrid, IconBoard, IconTrophy, IconBulb, IconPlus, IconDoc, IconExternal } from '../ui';
|
||||
|
||||
// ⌘K command palette — global fuzzy search + quick-nav across views, spaces and
|
||||
// tasks, plus quick actions. Keyboard-navigable (↑/↓/Enter, Esc). Centered on
|
||||
@@ -108,10 +108,11 @@ export function CommandPalette({ tasks, onClose }: { tasks: Task[]; onClose: ()
|
||||
<div className="flex max-h-[85vh] w-full max-w-xl flex-col overflow-hidden rounded-t-2xl bg-[var(--surface)] shadow-2xl ring-1 ring-inset ring-white/10 sm:max-h-[70vh] sm:rounded-xl" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-2 border-b border-[var(--border)] px-4 py-3">
|
||||
<IconSearch className="h-4 w-4 text-neutral-500" />
|
||||
<input
|
||||
<Input
|
||||
variant="unstyled"
|
||||
autoFocus
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onChangeText={setQ}
|
||||
placeholder="Search bounties, spaces, actions…"
|
||||
className="w-full bg-transparent text-sm text-neutral-100 placeholder:text-neutral-600 focus:outline-none"
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Address } from 'viem';
|
||||
import type { Workspace } from '../chain';
|
||||
import type { Contributor } from '../types';
|
||||
import { formatAmount, short } from '../format';
|
||||
import { Avatar, IconSearch, IconTrophy } from '../ui';
|
||||
import { Avatar, Input, IconSearch, IconTrophy } from '../ui';
|
||||
|
||||
type Window = 'all' | '30d' | '7d';
|
||||
|
||||
@@ -70,7 +70,7 @@ function Search({ placeholder }: { placeholder: string }) {
|
||||
return (
|
||||
<label className="flex items-center gap-1.5 rounded-md bg-white/5 px-2 py-1 text-xs text-neutral-400 ring-1 ring-inset ring-white/8">
|
||||
<IconSearch className="h-3.5 w-3.5" />
|
||||
<input placeholder={placeholder} className="w-36 bg-transparent text-neutral-200 placeholder:text-neutral-600 focus:outline-none" />
|
||||
<Input variant="unstyled" placeholder={placeholder} className="w-36 bg-transparent text-neutral-200 placeholder:text-neutral-600 focus:outline-none" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -178,11 +178,11 @@ export function Leaderboards({ ws }: { ws: Workspace }) {
|
||||
const contributorRows = useMemo(() => base.filter((c) => c.completed > 0n), [base]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-5 md:px-6 md:py-6">
|
||||
<div className="mx-auto w-full max-w-6xl px-4 py-5 md:px-6 md:py-6">
|
||||
<h1 className="mb-5 text-2xl font-bold text-neutral-100">Leaderboards</h1>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
||||
<div className="min-w-0 rounded-xl bg-[var(--surface)] ring-1 ring-inset ring-white/6">
|
||||
<div className="min-w-0 overflow-hidden rounded-xl bg-[var(--surface)] ring-1 ring-inset ring-white/6">
|
||||
<div className="flex flex-wrap items-center gap-3 px-4 py-3">
|
||||
<h2 className="text-sm font-semibold text-neutral-200">Top Contributors</h2>
|
||||
<WindowToggle value={win} onChange={setWin} />
|
||||
@@ -193,7 +193,7 @@ export function Leaderboards({ ws }: { ws: Workspace }) {
|
||||
<ContributorTable rows={contributorRows} />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 rounded-xl bg-[var(--surface)] ring-1 ring-inset ring-white/6">
|
||||
<div className="min-w-0 overflow-hidden rounded-xl bg-[var(--surface)] ring-1 ring-inset ring-white/6">
|
||||
<div className="flex flex-wrap items-center gap-3 px-4 py-3">
|
||||
<h2 className="text-sm font-semibold text-neutral-200">Top Reviewers</h2>
|
||||
<WindowToggle value={win} onChange={setWin} />
|
||||
@@ -205,7 +205,7 @@ export function Leaderboards({ ws }: { ws: Workspace }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 min-w-0 rounded-xl bg-[var(--surface)] ring-1 ring-inset ring-white/6">
|
||||
<div className="mt-6 min-w-0 overflow-hidden rounded-xl bg-[var(--surface)] ring-1 ring-inset ring-white/6">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<h2 className="text-sm font-semibold text-neutral-200">All Contributors</h2>
|
||||
<Search placeholder="Search contributors..." />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ORG } from '../config';
|
||||
import { PrimaryButton, IconPlus, IconChevronDown, IconBulb, IconX } from '../ui';
|
||||
import { PrimaryButton, Input, Textarea, IconPlus, IconChevronDown, IconBulb, IconX } from '../ui';
|
||||
import { timeAgo } from '../format';
|
||||
|
||||
// Community Suggestions — governance ideation. Persisted client-side (localStorage,
|
||||
@@ -34,10 +34,11 @@ function NewSuggestionModal({ onClose, onCreate }: { onClose: () => void; onCrea
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/60 sm:items-start sm:px-4 sm:pt-28" onClick={onClose}>
|
||||
<div className="w-full max-w-2xl rounded-t-2xl bg-[var(--surface)] p-5 pb-8 shadow-2xl ring-1 ring-inset ring-white/10 sm:rounded-xl sm:pb-5" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-start justify-between">
|
||||
<input
|
||||
<Input
|
||||
variant="unstyled"
|
||||
autoFocus
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onChangeText={setTitle}
|
||||
placeholder="Enter your suggestion here"
|
||||
className="w-full bg-transparent text-lg font-semibold text-neutral-100 placeholder:text-neutral-500 focus:outline-none"
|
||||
/>
|
||||
@@ -45,9 +46,10 @@ function NewSuggestionModal({ onClose, onCreate }: { onClose: () => void; onCrea
|
||||
<IconX className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
<Textarea
|
||||
variant="unstyled"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onChangeText={setBody}
|
||||
placeholder='Add more details about your suggestion here. Type "/" to insert.'
|
||||
rows={4}
|
||||
className="mt-3 w-full resize-none bg-transparent text-sm text-neutral-300 placeholder:text-neutral-600 focus:outline-none"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { closeTask } from '../router';
|
||||
import { short, isZero, absDate, timeAgo, refToLink, formatAmount } from '../format';
|
||||
import {
|
||||
Avatar,
|
||||
Input,
|
||||
SkillTag,
|
||||
RewardBadge,
|
||||
OpenToBadge,
|
||||
@@ -221,9 +222,10 @@ export function TaskDetail({ task, ws }: { task: Task; ws: Workspace }) {
|
||||
))}
|
||||
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
variant="unstyled"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onChangeText={setDraft}
|
||||
onKeyDown={(e) => e.key === 'Enter' && addComment()}
|
||||
placeholder="Add a comment or application…"
|
||||
className="flex-1 rounded-md bg-white/5 px-3 py-2 text-sm text-neutral-200 placeholder:text-neutral-600 ring-1 ring-inset ring-white/8 focus:outline-none"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { GhostButton, IconSort, IconFilter, IconSearch } from '../ui';
|
||||
import { GhostButton, Input, IconSort, IconFilter, IconSearch } from '../ui';
|
||||
import { ConnectButton } from './Connect';
|
||||
|
||||
// Connect / Follow — top-right on every content view. Connect resolves through the
|
||||
@@ -67,9 +67,10 @@ export function Toolbar({ query, onQuery }: { query: string; onQuery: (q: string
|
||||
</button>
|
||||
<label className="flex items-center gap-1.5 rounded-md bg-white/5 px-2 py-1 ring-1 ring-inset ring-white/8">
|
||||
<IconSearch className="h-3.5 w-3.5" />
|
||||
<input
|
||||
<Input
|
||||
variant="unstyled"
|
||||
value={query}
|
||||
onChange={(e) => onQuery(e.target.value)}
|
||||
onChangeText={onQuery}
|
||||
placeholder="Search tasks..."
|
||||
className="w-32 bg-transparent text-neutral-200 placeholder:text-neutral-600 focus:outline-none"
|
||||
/>
|
||||
|
||||
+21
-1
@@ -1,5 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* @luxfi/ui ships its primitives as compiled classes in its dist — Tailwind must
|
||||
scan them so the utilities the adopted Button / Input / Textarea emit exist. */
|
||||
@source "../node_modules/@luxfi/ui/dist";
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
/* Dework dark palette — the one place the surface tokens live. */
|
||||
@@ -10,8 +14,24 @@
|
||||
--border: #232328;
|
||||
--hover: #1e1e24;
|
||||
--muted: #8a8a93;
|
||||
/* --brand is set per-org at runtime from brands.ts (App.tsx). */
|
||||
/* --brand is set per-org at runtime from the @luxfi/ui LUX_BRAND value (App.tsx). */
|
||||
--brand: #6c5ce7;
|
||||
|
||||
/* @luxfi/ui adopted-primitive tokens, remapped onto the Dework palette + brand.
|
||||
Only the tokens the adopted primitives read live here — the exchange
|
||||
tokens.css is deliberately NOT imported wholesale, so no exchange palette
|
||||
bleeds into the Dework surfaces. Static (non-brand-varying) except --brand. */
|
||||
--color-button-solid-bg: var(--brand);
|
||||
--color-button-solid-text: #ffffff;
|
||||
--color-hover: color-mix(in srgb, var(--brand) 88%, #000000);
|
||||
--color-button-subtle-bg: rgba(255, 255, 255, 0.05);
|
||||
--color-button-subtle-fg: #e5e5e5;
|
||||
--color-input-bg: transparent;
|
||||
--color-input-fg: #e5e5e5;
|
||||
--color-input-border: var(--border);
|
||||
--color-input-border-hover: #3a3a42;
|
||||
--color-input-border-focus: var(--brand);
|
||||
--color-input-placeholder: var(--muted);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
+13
-1
@@ -1,12 +1,22 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { AppProvider } from '@luxfi/ui';
|
||||
import './index.css';
|
||||
import { App } from './App';
|
||||
import { BRAND_KEY } from './config';
|
||||
import { isAuthCallback, handleAuthCallback } from './auth';
|
||||
|
||||
const root = document.getElementById('root');
|
||||
if (!root) throw new Error('#root not found');
|
||||
|
||||
// The board runs on the @luxfi/ui / @hanzo/gui (Tamagui) engine. AppProvider
|
||||
// mounts GuiProvider (the Tamagui runtime every @luxfi/ui primitive needs) +
|
||||
// TanStack Query, and selects the per-org brand child-theme row as a VALUE —
|
||||
// `dark_${BRAND_KEY}` resolves to dark_lux | dark_zoo | dark_hanzo | dark_pars.
|
||||
// Bespoke Dework markup still reads the `--brand` CSS var (set in App.tsx from
|
||||
// the same @luxfi/ui LUX_BRAND source), so brand is one value in one place.
|
||||
const brandTheme = `dark_${BRAND_KEY}`;
|
||||
|
||||
// The OAuth redirect (Discord / GitHub) lands on /auth/callback — the static
|
||||
// server's SPA fallback serves index.html, so we finish the PKCE code exchange
|
||||
// here and then navigate back to '/'. The board itself never mounts on this path.
|
||||
@@ -21,7 +31,9 @@ if (isAuthCallback()) {
|
||||
} else {
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<AppProvider defaultTheme={brandTheme}>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ import type { Reward } from './types';
|
||||
import { NATIVE_SYMBOL, ORG, BRAND_KEY } from './config';
|
||||
import { BRAND_LOGOS } from './brand-logos';
|
||||
|
||||
// Text fields = @luxfi/ui primitives (native <input>/<textarea> under the hood,
|
||||
// bridging onChangeText<->onChange so keystrokes always round-trip). Re-exported
|
||||
// here so every view reaches text inputs through the one ../ui primitive surface.
|
||||
export { Input, Textarea } from '@luxfi/ui';
|
||||
|
||||
// ---- Brand accent ----
|
||||
export const accent: CSSProperties = { backgroundColor: 'var(--brand)' };
|
||||
export const accentText: CSSProperties = { color: 'var(--brand)' };
|
||||
|
||||
+54
-5
@@ -11,9 +11,10 @@ const RPC_TARGET = process.env.RPC_TARGET || 'http://127.0.0.1:9631';
|
||||
// build-time brand (VITE_BRAND, default zoo). The CSP is strict in production
|
||||
// (no external hosts; only the brand's own RPC host for data) and relaxed in dev
|
||||
// so Vite HMR works. The app ships zero external CDNs/fonts/scripts, so the
|
||||
// strict policy is a tight fit. The connect-src host is derived from the RPC
|
||||
// actually baked in (VITE_RPC_URL, else the brand default) so the CSP can never
|
||||
// drift from the endpoint the board calls.
|
||||
// strict policy is a tight fit. `style-src 'unsafe-inline'` is required for the
|
||||
// @hanzo/gui (Tamagui) runtime style injection. The connect-src host is derived
|
||||
// from the RPC actually baked in (VITE_RPC_URL, else the brand default) so the
|
||||
// CSP can never drift from the endpoint the board calls.
|
||||
function brandHtml(): Plugin {
|
||||
const brand = resolveBrand(process.env.VITE_BRAND);
|
||||
const rpcOrigin = new URL(process.env.VITE_RPC_URL || brand.rpcUrl).origin;
|
||||
@@ -45,8 +46,56 @@ function brandHtml(): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
// @luxfi/ui is the Lux skin of @hanzo/gui (Tamagui). Welding its cross-platform
|
||||
// primitives into this Vite web app follows the proven exchange recipe:
|
||||
// 1. react-native → react-native-web (Tamagui authors against RN primitives).
|
||||
// 2. dedupe react / react-dom / the gui engine to ONE physical copy each, so
|
||||
// createGui() and getGui() read the same module-level config registry
|
||||
// (else: "Can't find GUI configuration" on first themed render).
|
||||
// 3. pre-bundle react-native-web + the CJS-only @react-native/normalize-color
|
||||
// and the gui engine via optimizeDeps.
|
||||
// 4. define __DEV__ / EXPO_OS / IS_WEB — the RN/Tamagui runtime reads them.
|
||||
// 5. keep the gui engine + RNW in React's chunk (commonjsOptions) so the CJS
|
||||
// interop wrapper never yields an undefined createContext.
|
||||
// Atomic-CSS extraction (@hanzogui/vite-plugin, prod-only) is intentionally NOT
|
||||
// wired here: its published peer pins vite@8.0.3 which conflicts with 8.1.x, and
|
||||
// the app renders correctly on the runtime style-injection path without it. See
|
||||
// LLM.md for how to re-enable extraction once the peer is loosened.
|
||||
export default defineConfig(({ mode }) => ({
|
||||
define: {
|
||||
__DEV__: mode !== 'production',
|
||||
'process.env.NODE_ENV': JSON.stringify(mode === 'production' ? 'production' : 'development'),
|
||||
'process.env.EXPO_OS': JSON.stringify('web'),
|
||||
'process.env.IS_WEB': JSON.stringify('true'),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-native': 'react-native-web',
|
||||
},
|
||||
dedupe: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'@tanstack/react-query',
|
||||
'@hanzo/gui',
|
||||
'@hanzogui/core',
|
||||
'@hanzogui/web',
|
||||
],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'react-native-web',
|
||||
'@react-native/normalize-color',
|
||||
'@hanzo/gui',
|
||||
'@hanzogui/core',
|
||||
'@luxfi/ui',
|
||||
],
|
||||
},
|
||||
plugins: [react(), tailwindcss(), brandHtml()],
|
||||
build: {
|
||||
// Never split the gui engine / RNW out of React's chunk — they use CJS
|
||||
// require('react'); a separate chunk makes the interop createContext undefined.
|
||||
commonjsOptions: { include: [/node_modules/] },
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/rpc': {
|
||||
@@ -56,4 +105,4 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user