Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46bae13212 | ||
|
|
589e0144e2 | ||
|
|
0061378193 | ||
|
|
1cf4825b1d | ||
|
|
f799aaf564 | ||
|
|
5af5ceb9a8 |
@@ -80,24 +80,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_repo_dir_is_none() {
|
||||
// A temp dir with no `.git` anywhere up the chain. We need a path whose
|
||||
// *entire* ancestor chain is `.git`-free, but `/tmp` itself may live
|
||||
// inside a repo (CI containers, sandbox VMs). Work around this by
|
||||
// creating a tmpfs-like isolated root via an in-memory mount when
|
||||
// possible, or — more portably — by asserting the result is NOT inside
|
||||
// the temp dir we control rather than asserting None outright.
|
||||
fn non_repo_dir_does_not_find_root_inside_fixture() {
|
||||
// The fixture has no `.git`, so `find_root` must not return a path
|
||||
// inside it. On hosts where an ancestor of the temp dir *is* a repo
|
||||
// (e.g. `/tmp` lives inside a checkout), the function legitimately
|
||||
// returns that ancestor — the test only asserts the fixture itself
|
||||
// was not mistaken for a repo.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sub = tmp.path().join("plain/dir");
|
||||
fs::create_dir_all(&sub).unwrap();
|
||||
// If an ancestor of /tmp has .git, find_root will return that — but it
|
||||
// must never return anything at or below our tempdir.
|
||||
let result = find_root(&sub);
|
||||
if let Some(ref root) = result {
|
||||
assert!(
|
||||
match find_root(&sub) {
|
||||
None => {} // expected on most hosts
|
||||
Some(root) => assert!(
|
||||
!root.starts_with(tmp.path()),
|
||||
"find_root returned {root:?} which is inside our .git-free tempdir",
|
||||
);
|
||||
"found .git inside the fixture at {root:?}, but none was created"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -805,6 +805,10 @@ impl SessionManager {
|
||||
});
|
||||
drop(stream);
|
||||
sync_structured_activity(&self.inner, id).await;
|
||||
#[cfg(feature = "haiku-autoname")]
|
||||
if let Ok(true) = self.claim_autoname(id).await {
|
||||
crate::autoname::spawn_naming(self.clone(), id, text);
|
||||
}
|
||||
return Ok(QueueAcpPrompt::Queued);
|
||||
}
|
||||
Err(message) => {
|
||||
|
||||
@@ -1020,12 +1020,15 @@ async fn gitroot_probes_path() {
|
||||
.json()
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(serde_json::Value::String(root)) = resp.get("root") {
|
||||
let root = std::path::Path::new(root);
|
||||
assert!(
|
||||
!root.starts_with(tmp.path()),
|
||||
"gitroot returned {root:?} inside our .git-free tempdir",
|
||||
);
|
||||
// On hosts where an ancestor of the temp dir is a repo (e.g. `/tmp`
|
||||
// inside a checkout), the root legitimately points there. The test only
|
||||
// asserts no root was found *inside* the temp dir.
|
||||
match resp.get("root").and_then(|v| v.as_str()) {
|
||||
None => {}
|
||||
Some(root) => assert!(
|
||||
!root.starts_with(tmp.path().to_string_lossy().as_ref()),
|
||||
"found a repo root inside the fixture at {root:?}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -17,12 +17,22 @@
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"marked": "^18.0.11"
|
||||
"marked": "^18.0.11",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"happy-dom": "^20.11.6",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vitest": "^2.1.9"
|
||||
"vitest": "^2.1.9",
|
||||
"zustand": "^5.0.15"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+984
-11
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div id="react-root" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { PALETTES, ThemeProvider, useTheme } from "./ThemeContext";
|
||||
|
||||
// happy-dom's vitest environment ships neither localStorage nor matchMedia;
|
||||
// back both with plain in-memory fakes so persistence and system-theme
|
||||
// reactivity are observable (mirrors ../theme.test.ts's localStorage fake).
|
||||
let store = new Map<string, string>();
|
||||
let systemPrefersLight = false;
|
||||
let mediaListeners: Array<(e: MediaQueryListEvent) => void> = [];
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: {
|
||||
getItem: (k: string) => store.get(k) ?? null,
|
||||
setItem: (k: string, v: string) => store.set(k, v),
|
||||
removeItem: (k: string) => store.delete(k),
|
||||
},
|
||||
});
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
value: (query: string) => ({
|
||||
matches: query.includes("light") ? systemPrefersLight : false,
|
||||
media: query,
|
||||
addEventListener: (_type: string, cb: (e: MediaQueryListEvent) => void) => {
|
||||
mediaListeners.push(cb);
|
||||
},
|
||||
removeEventListener: (_type: string, cb: (e: MediaQueryListEvent) => void) => {
|
||||
mediaListeners = mediaListeners.filter((l) => l !== cb);
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
function fireSystemThemeChange(matches: boolean): void {
|
||||
systemPrefersLight = matches;
|
||||
for (const cb of mediaListeners) {
|
||||
cb({ matches } as MediaQueryListEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function Probe() {
|
||||
const { theme, palette, setTheme, toggleTheme, setPalette } = useTheme();
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="theme">{theme}</span>
|
||||
<span data-testid="palette">{palette}</span>
|
||||
<button onClick={() => setTheme("light")}>pin-light</button>
|
||||
<button onClick={toggleTheme}>toggle</button>
|
||||
<button onClick={() => setPalette("ember")}>set-ember</button>
|
||||
<button onClick={() => setPalette("graphite")}>set-graphite</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("ThemeProvider / useTheme", () => {
|
||||
beforeEach(() => {
|
||||
store = new Map();
|
||||
mediaListeners = [];
|
||||
systemPrefersLight = false;
|
||||
delete document.documentElement.dataset.theme;
|
||||
delete document.documentElement.dataset.palette;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("defaults to graphite and the system theme, no data-palette attribute", () => {
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<Probe />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(screen.getByTestId("theme").textContent).toBe("dark");
|
||||
expect(screen.getByTestId("palette").textContent).toBe("graphite");
|
||||
expect(document.documentElement.dataset.palette).toBeUndefined();
|
||||
});
|
||||
|
||||
it("follows a system theme change while unpinned", () => {
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<Probe />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
act(() => fireSystemThemeChange(true));
|
||||
expect(screen.getByTestId("theme").textContent).toBe("light");
|
||||
});
|
||||
|
||||
it("setTheme pins, persists, and stops following the system", () => {
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<Probe />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
act(() => screen.getByText("pin-light").click());
|
||||
expect(screen.getByTestId("theme").textContent).toBe("light");
|
||||
expect(document.documentElement.dataset.theme).toBe("light");
|
||||
expect(localStorage.getItem("maestro-theme")).toBe("light");
|
||||
|
||||
act(() => fireSystemThemeChange(true));
|
||||
expect(screen.getByTestId("theme").textContent).toBe("light");
|
||||
});
|
||||
|
||||
it("toggleTheme flips the resolved theme", () => {
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<Probe />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
act(() => screen.getByText("toggle").click());
|
||||
expect(screen.getByTestId("theme").textContent).toBe("light");
|
||||
act(() => screen.getByText("toggle").click());
|
||||
expect(screen.getByTestId("theme").textContent).toBe("dark");
|
||||
});
|
||||
|
||||
it("setPalette stamps data-palette and persists; graphite clears it", () => {
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<Probe />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
act(() => screen.getByText("set-ember").click());
|
||||
expect(document.documentElement.dataset.palette).toBe("ember");
|
||||
expect(localStorage.getItem("maestro-palette")).toBe("ember");
|
||||
|
||||
act(() => screen.getByText("set-graphite").click());
|
||||
expect(document.documentElement.dataset.palette).toBeUndefined();
|
||||
});
|
||||
|
||||
it("a fresh mount re-applies a stored palette", () => {
|
||||
localStorage.setItem("maestro-palette", "periwinkle");
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<Probe />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(screen.getByTestId("palette").textContent).toBe("periwinkle");
|
||||
expect(document.documentElement.dataset.palette).toBe("periwinkle");
|
||||
});
|
||||
|
||||
it("exposes every palette in picker order", () => {
|
||||
expect(PALETTES).toEqual([
|
||||
"graphite",
|
||||
"ice-cyan",
|
||||
"periwinkle",
|
||||
"amber-phosphor",
|
||||
"ember",
|
||||
"orchid",
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws when used outside a provider", () => {
|
||||
const Bare = () => {
|
||||
useTheme();
|
||||
return null;
|
||||
};
|
||||
expect(() => render(<Bare />)).toThrow(
|
||||
"useTheme must be used within a ThemeProvider",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
// React port of ../theme.ts. Same storage keys and `data-theme`/`data-palette`
|
||||
// attributes on <html>, so this can sit alongside the vanilla shell (which
|
||||
// still owns theme.ts) without either one fighting the other over state.
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
const THEME_KEY = "maestro-theme";
|
||||
|
||||
/** Every `data-palette` value, in picker order. Graphite is the default. */
|
||||
export const PALETTES = [
|
||||
"graphite",
|
||||
"ice-cyan",
|
||||
"periwinkle",
|
||||
"amber-phosphor",
|
||||
"ember",
|
||||
"orchid",
|
||||
] as const;
|
||||
export type Palette = (typeof PALETTES)[number];
|
||||
const PALETTE_KEY = "maestro-palette";
|
||||
|
||||
function storedTheme(): Theme | null {
|
||||
const v = localStorage.getItem(THEME_KEY);
|
||||
return v === "light" || v === "dark" ? v : null;
|
||||
}
|
||||
|
||||
function storedPalette(): Palette | null {
|
||||
const v = localStorage.getItem(PALETTE_KEY);
|
||||
return (PALETTES as readonly string[]).includes(v ?? "")
|
||||
? (v as Palette)
|
||||
: null;
|
||||
}
|
||||
|
||||
function systemPrefersLight(): boolean {
|
||||
return window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
}
|
||||
|
||||
// Dev-only `?theme=` override for visual review, same as theme.ts's — never
|
||||
// persisted, just seeds the initial pin for this session.
|
||||
function devThemeOverride(): Theme | null {
|
||||
if (!import.meta.env.DEV) return null;
|
||||
const q = new URLSearchParams(location.search).get("theme");
|
||||
return q === "light" || q === "dark" ? q : null;
|
||||
}
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
palette: Palette;
|
||||
setTheme: (t: Theme) => void;
|
||||
toggleTheme: () => void;
|
||||
setPalette: (p: Palette) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [pinnedTheme, setPinnedTheme] = useState<Theme | null>(
|
||||
() => devThemeOverride() ?? storedTheme(),
|
||||
);
|
||||
const [systemIsLight, setSystemIsLight] = useState(systemPrefersLight);
|
||||
const [palette, setPaletteState] = useState<Palette>(
|
||||
() => storedPalette() ?? "graphite",
|
||||
);
|
||||
|
||||
const theme: Theme = pinnedTheme ?? (systemIsLight ? "light" : "dark");
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const mql = window.matchMedia("(prefers-color-scheme: light)");
|
||||
const onChange = (e: MediaQueryListEvent) => setSystemIsLight(e.matches);
|
||||
mql.addEventListener("change", onChange);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
// Pinning is one-way (no UI un-pins back to "follow system"), so this only
|
||||
// ever needs to set the attribute — never clear it — same as theme.ts.
|
||||
useLayoutEffect(() => {
|
||||
if (pinnedTheme) document.documentElement.dataset.theme = pinnedTheme;
|
||||
}, [pinnedTheme]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (palette === "graphite") delete document.documentElement.dataset.palette;
|
||||
else document.documentElement.dataset.palette = palette;
|
||||
}, [palette]);
|
||||
|
||||
const setTheme = useCallback((t: Theme) => {
|
||||
localStorage.setItem(THEME_KEY, t);
|
||||
setPinnedTheme(t);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}, [theme, setTheme]);
|
||||
|
||||
const setPalette = useCallback((p: Palette) => {
|
||||
localStorage.setItem(PALETTE_KEY, p);
|
||||
setPaletteState(p);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{ theme, palette, setTheme, toggleTheme, setPalette }}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeContextValue {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import "./main.css";
|
||||
|
||||
const container = document.getElementById("root");
|
||||
if (!container) {
|
||||
throw new Error("missing #root element");
|
||||
}
|
||||
|
||||
createRoot(container).render(<App />);
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@config "../tailwind.config.ts";
|
||||
@@ -468,18 +468,40 @@ describe("structured connection: data flow", () => {
|
||||
},
|
||||
});
|
||||
ws.emit({ type: "reduced_state", state: respondingState });
|
||||
// While the turn is active, the last intermediate text (agent:2) stays
|
||||
// outside the collapsed group so it's visible as current narration.
|
||||
expect(conn.el.querySelector('[data-row-id="agent:2"]')?.closest(".st-tool-group")).toBeNull();
|
||||
// Earlier intermediate text (agent:1) is still inside the group.
|
||||
// Intermediate updates stay inside the collapsed activity disclosure,
|
||||
// rather than becoming a second visible transcript alongside its summary.
|
||||
expect(conn.el.querySelector('[data-row-id="agent:2"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||
expect(conn.el.querySelector('[data-row-id="agent:1"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||
// When the turn ends, all intermediate text is absorbed.
|
||||
// It remains collapsed when the turn ends.
|
||||
ws.emit({ type: "reduced_state", state: idleState });
|
||||
expect(conn.el.querySelector('[data-row-id="agent:2"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||
expect(conn.el.querySelector('[data-row-id="agent:1"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||
conn.dispose();
|
||||
});
|
||||
|
||||
it("collapses trailing thought commentary with a turn's tool activity", () => {
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
ws.open();
|
||||
ws.emit({
|
||||
type: "transcript_snapshot",
|
||||
snapshot: {
|
||||
cursor: 5,
|
||||
rows: [
|
||||
row("user:1", "user", { content: { type: "text", text: "do it" } }),
|
||||
toolRow("tool:1", "one"),
|
||||
toolRow("tool:2", "two"),
|
||||
row("thought:1", "agent_thought", { content: { type: "text", text: "Checking the next step" } }),
|
||||
row("thought:2", "agent_thought", { content: { type: "text", text: "Preparing the change" } }),
|
||||
],
|
||||
},
|
||||
});
|
||||
for (const id of ["thought:1", "thought:2"]) {
|
||||
expect(conn.el.querySelector(`[data-row-id="${id}"]`)?.closest(".st-tool-group")).not.toBeNull();
|
||||
}
|
||||
conn.dispose();
|
||||
});
|
||||
|
||||
it("keeps an expanded running tool open without emitting a click when patched", () => {
|
||||
const ws = new FakeSocket();
|
||||
const conn = mount({ sockets: [ws] });
|
||||
|
||||
+13
-18
@@ -488,24 +488,14 @@ export function createStructuredConnection(
|
||||
groups.set(currentGid, group);
|
||||
}
|
||||
}
|
||||
// pending at end of a turn = final text/approvals, stays outside (not flushed).
|
||||
|
||||
// While the turn is still active, keep the last intermediate text/thought
|
||||
// row outside the collapsed group so it's visible as the agent's current
|
||||
// narration. It gets absorbed on the next rebuild once newer content
|
||||
// arrives or the turn ends.
|
||||
const turnActive = state.turn.responding || state.turn.thinking;
|
||||
if (turnActive) {
|
||||
const lastGroup = groups.get(currentGid);
|
||||
if (lastGroup) {
|
||||
for (let i = lastGroup.rows.length - 1; i >= 0; i--) {
|
||||
const kind = lastGroup.rows[i].kind;
|
||||
if (kind === "agent_text" || kind === "agent_thought") {
|
||||
lastGroup.rows.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A final agent text is the answer, so it stays outside. Thought rows are
|
||||
// working commentary, though: when a turn already has a disclosure they
|
||||
// belong with its activity even if the next tool call has not arrived yet.
|
||||
// Leaving that tail loose made a busy agent look like it was publishing a
|
||||
// long italic answer above a collapsed "N tool calls" summary.
|
||||
const lastGroup = groups.get(currentGid);
|
||||
if (lastGroup && pending.length > 0 && pending.every((row) => row.kind === "agent_thought")) {
|
||||
lastGroup.rows.push(...pending);
|
||||
}
|
||||
|
||||
return groups;
|
||||
@@ -634,10 +624,15 @@ export function createStructuredConnection(
|
||||
// visible flashing and breaks text selection.
|
||||
const oldEl = rowEls.get(delta.row.id);
|
||||
if (oldEl) {
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
const hadToolFocus = active?.classList.contains("st-tool-head") && oldEl.contains(active);
|
||||
const newEl = buildRowElement(delta.row);
|
||||
if (newEl) {
|
||||
oldEl.replaceWith(newEl);
|
||||
rowEls.set(delta.row.id, newEl);
|
||||
if (hadToolFocus) {
|
||||
newEl.querySelector<HTMLButtonElement>(".st-tool-head")?.focus();
|
||||
}
|
||||
}
|
||||
syncAvailableCommands();
|
||||
return;
|
||||
|
||||
+2
-1
@@ -2507,7 +2507,8 @@ input.leaf-rename:focus {
|
||||
|
||||
.st-row-thought {
|
||||
color: var(--faint);
|
||||
font-style: italic;
|
||||
font-size: var(--fs-12);
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
// Bridges Tailwind utilities to the OKLCH custom-property tokens defined in
|
||||
// src/styles.css. Every value here is `var(--token)`, never a literal, so
|
||||
// theme/palette switching (data-theme, data-palette on <html>) keeps working
|
||||
// through Tailwind classes exactly as it does through the vanilla CSS.
|
||||
export default {
|
||||
content: ["src/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
bg: "var(--bg)",
|
||||
"bg-grid": "var(--bg-grid)",
|
||||
surface: "var(--surface)",
|
||||
"surface-2": "var(--surface-2)",
|
||||
"surface-3": "var(--surface-3)",
|
||||
border: "var(--border)",
|
||||
"border-strong": "var(--border-strong)",
|
||||
|
||||
ink: "var(--ink)",
|
||||
muted: "var(--muted)",
|
||||
faint: "var(--faint)",
|
||||
|
||||
accent: "var(--accent)",
|
||||
"accent-strong": "var(--accent-strong)",
|
||||
"accent-ink": "var(--accent-ink)",
|
||||
"accent-tint": "var(--accent-tint)",
|
||||
"accent-ring": "var(--accent-ring)",
|
||||
"accent-fill": "var(--accent-fill)",
|
||||
|
||||
preview: "var(--preview)",
|
||||
"preview-ink": "var(--preview-ink)",
|
||||
"preview-tint": "var(--preview-tint)",
|
||||
"preview-ring": "var(--preview-ring)",
|
||||
|
||||
live: "var(--live)",
|
||||
"live-glow": "var(--live-glow)",
|
||||
danger: "var(--danger)",
|
||||
"danger-tint": "var(--danger-tint)",
|
||||
warn: "var(--warn)",
|
||||
|
||||
"term-bg": "var(--term-bg)",
|
||||
},
|
||||
spacing: {
|
||||
"sp-1": "var(--sp-1)",
|
||||
"sp-2": "var(--sp-2)",
|
||||
"sp-3": "var(--sp-3)",
|
||||
"sp-4": "var(--sp-4)",
|
||||
"sp-5": "var(--sp-5)",
|
||||
"sp-6": "var(--sp-6)",
|
||||
"sp-8": "var(--sp-8)",
|
||||
},
|
||||
borderRadius: {
|
||||
"r-1": "var(--r-1)",
|
||||
"r-2": "var(--r-2)",
|
||||
"r-3": "var(--r-3)",
|
||||
"r-pill": "var(--r-pill)",
|
||||
},
|
||||
fontSize: {
|
||||
"fs-11": "var(--fs-11)",
|
||||
"fs-12": "var(--fs-12)",
|
||||
"fs-13": "var(--fs-13)",
|
||||
"fs-14": "var(--fs-14)",
|
||||
"fs-15": "var(--fs-15)",
|
||||
"fs-17": "var(--fs-17)",
|
||||
"fs-20": "var(--fs-20)",
|
||||
},
|
||||
fontFamily: {
|
||||
mono: "var(--font-mono)",
|
||||
sans: "var(--font-sans)",
|
||||
},
|
||||
height: {
|
||||
"row-h": "var(--row-h)",
|
||||
"control-h": "var(--control-h)",
|
||||
tap: "var(--tap)",
|
||||
"bar-h": "var(--bar-h)",
|
||||
},
|
||||
width: {
|
||||
tap: "var(--tap)",
|
||||
},
|
||||
minHeight: {
|
||||
"control-h": "var(--control-h)",
|
||||
tap: "var(--tap)",
|
||||
},
|
||||
zIndex: {
|
||||
sticky: "var(--z-sticky)",
|
||||
backdrop: "var(--z-backdrop)",
|
||||
modal: "var(--z-modal)",
|
||||
toast: "var(--z-toast)",
|
||||
},
|
||||
transitionDuration: {
|
||||
fast: "var(--dur-fast)",
|
||||
DEFAULT: "var(--dur)",
|
||||
slow: "var(--dur-slow)",
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
out: "var(--ease-out)",
|
||||
},
|
||||
boxShadow: {
|
||||
1: "var(--shadow-1)",
|
||||
2: "var(--shadow-2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Config;
|
||||
@@ -4,6 +4,7 @@
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// Where the dev proxy forwards `/api/*` (and the events WS) to. Default to a
|
||||
// local daemon on 127.0.0.1:7070. Override with:
|
||||
@@ -18,6 +20,7 @@ export default defineConfig(({ mode }) => {
|
||||
const target = resolveProxyTarget(env.MAESTRO_DAEMON_URL);
|
||||
const secure = target.startsWith("https://");
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
build: {
|
||||
target: "es2022",
|
||||
outDir: "dist",
|
||||
|
||||
Reference in New Issue
Block a user