feat(FN-1529): add failing regression tests for search query propagation
Tests added: - api-node.test.ts: q parameter forwarding in fetchRemoteNodeTasks - useRemoteNodeData.test.ts: searchQuery propagation and refetch on change - App.test.tsx: searchQuery passed to useRemoteNodeData in remote mode All tests fail as expected because: - fetchRemoteNodeTasks doesn't accept searchQuery parameter - useRemoteNodeData doesn't forward searchQuery - App.tsx doesn't pass searchQuery to useRemoteNodeData
This commit is contained in:
@@ -40,6 +40,7 @@
|
|||||||
- There are **54 unique color themes** in `packages/dashboard/app/public/theme-data.css` (default, ocean, forest, sunset, zen, berry, high-contrast, industrial, monochrome, slate, ash, graphite, silver, solarized, factory, ayu, one-dark, nord, dracula, gruvbox, tokyo-night, catppuccin-mocha, github-dark, everforest, rose-pine, kanagawa, night-owl, palenight, monokai-pro, slime, brutalist, neon-city, parchment, terminal, glass, horizon, vitesse, outrun, snazzy, porple, espresso, mars, poimandres, ember, rust, copper, foundry, carbon, sandstone, lagoon, frost, lavender, neon-bloom, sepia). Each has a dark variant `[data-color-theme="<name>"]` and a light variant `[data-color-theme="<name>"][data-theme="light"]`. Theme blocks were extracted to a separate file in FN-1409 to enable lazy loading — theme-data.css is only loaded when a non-default color theme is active.
|
- There are **54 unique color themes** in `packages/dashboard/app/public/theme-data.css` (default, ocean, forest, sunset, zen, berry, high-contrast, industrial, monochrome, slate, ash, graphite, silver, solarized, factory, ayu, one-dark, nord, dracula, gruvbox, tokyo-night, catppuccin-mocha, github-dark, everforest, rose-pine, kanagawa, night-owl, palenight, monokai-pro, slime, brutalist, neon-city, parchment, terminal, glass, horizon, vitesse, outrun, snazzy, porple, espresso, mars, poimandres, ember, rust, copper, foundry, carbon, sandstone, lagoon, frost, lavender, neon-bloom, sepia). Each has a dark variant `[data-color-theme="<name>"]` and a light variant `[data-color-theme="<name>"][data-theme="light"]`. Theme blocks were extracted to a separate file in FN-1409 to enable lazy loading — theme-data.css is only loaded when a non-default color theme is active.
|
||||||
- When adding CSS custom properties that should be theme-aware (like `--accent`, `--status-*-bg`), add them to all 54 theme blocks plus `:root` and `[data-theme="light"]` base blocks. The test in `status-colors-theme.test.ts` iterates all blocks programmatically to prevent regressions.
|
- When adding CSS custom properties that should be theme-aware (like `--accent`, `--status-*-bg`), add them to all 54 theme blocks plus `:root` and `[data-theme="light"]` base blocks. The test in `status-colors-theme.test.ts` iterates all blocks programmatically to prevent regressions.
|
||||||
- **Semantic tokens** (tokens describing purpose, not appearance) that maintain consistent meaning across all color themes (e.g., "autopilot active" is always green-tinted, "event error" is always red-tinted) only need dark/light adaptation via the base `[data-theme="light"]` block. They do NOT need per-color-theme overrides because the semantic meaning is consistent. Examples from FN-1357: `--autopilot-pulse`, `--event-*-text`, `--event-*-bg`, `--terminal-bg`, `--star-idle`, `--star-active`, `--badge-mission-*`, `--fab-*`.
|
- **Semantic tokens** (tokens describing purpose, not appearance) that maintain consistent meaning across all color themes (e.g., "autopilot active" is always green-tinted, "event error" is always red-tinted) only need dark/light adaptation via the base `[data-theme="light"]` block. They do NOT need per-color-theme overrides because the semantic meaning is consistent. Examples from FN-1357: `--autopilot-pulse`, `--event-*-text`, `--event-*-bg`, `--terminal-bg`, `--star-idle`, `--star-active`, `--badge-mission-*`, `--fab-*`.
|
||||||
|
- **Runtime-safe theme loading (FN-1526)**: The `theme-data.css` stylesheet URL is derived from `document.baseURI` rather than hardcoded paths. This ensures correct resolution in both HTTP/HTTPS contexts (uses `/theme-data.css`) and Electron `file://` contexts (derives path relative to HTML file directory). The same `getThemeDataUrl()` helper is used by both the pre-hydration inline script in `index.html` and the runtime `useTheme.ts` hook.
|
||||||
|
|
||||||
## Plugin System (FN-1111 / FN-1400)
|
## Plugin System (FN-1111 / FN-1400)
|
||||||
|
|
||||||
|
|||||||
@@ -472,6 +472,15 @@ To add a new color theme:
|
|||||||
|
|
||||||
**Note:** Theme variable blocks are stored in a separate `theme-data.css` file for optimized loading. This file is only loaded when a non-default color theme is active, reducing the initial payload for users with the default theme.
|
**Note:** Theme variable blocks are stored in a separate `theme-data.css` file for optimized loading. This file is only loaded when a non-default color theme is active, reducing the initial payload for users with the default theme.
|
||||||
|
|
||||||
|
### Dynamic Stylesheet Loading
|
||||||
|
|
||||||
|
The `theme-data.css` file is loaded dynamically using `document.baseURI` for path resolution, which ensures correct behavior across all runtime contexts:
|
||||||
|
|
||||||
|
- **HTTP/HTTPS serving** (development server, production web deployment): Uses standard root-relative path resolution (`/theme-data.css`)
|
||||||
|
- **Electron file:// context** (desktop production): Derives the path relative to the HTML file's directory
|
||||||
|
|
||||||
|
This approach avoids hardcoded absolute paths that would break in different deployment contexts. Both the pre-hydration inline script in `index.html` and the runtime hook (`useTheme.ts`) use the same path resolution strategy.
|
||||||
|
|
||||||
### Theme-Driven Logo and Task-Creation CTAs
|
### Theme-Driven Logo and Task-Creation CTAs
|
||||||
|
|
||||||
The Fusion logo and all task-creation action buttons (including the "+ New Task" and "Save" buttons in board view, list view, and inline creation surfaces) are fully tokenized and respond to the active color theme. This ensures branding and task-creation affordances stay visually consistent across all 12 color themes and both light/dark modes.
|
The Fusion logo and all task-creation action buttons (including the "+ New Task" and "Save" buttons in board view, list view, and inline creation surfaces) are fully tokenized and respond to the active color theme. This ensures branding and task-creation affordances stay visually consistent across all 12 color themes and both light/dark modes.
|
||||||
|
|||||||
@@ -449,7 +449,8 @@ describe("useTheme", () => {
|
|||||||
expect(link).not.toBeNull();
|
expect(link).not.toBeNull();
|
||||||
expect(link?.tagName.toLowerCase()).toBe("link");
|
expect(link?.tagName.toLowerCase()).toBe("link");
|
||||||
expect(link?.getAttribute("rel")).toBe("stylesheet");
|
expect(link?.getAttribute("rel")).toBe("stylesheet");
|
||||||
expect(link?.getAttribute("href")).toBe("/theme-data.css");
|
// href should resolve to theme-data.css via document.baseURI
|
||||||
|
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes theme-data.css when switching back to default theme", () => {
|
it("removes theme-data.css when switching back to default theme", () => {
|
||||||
@@ -521,9 +522,109 @@ describe("useTheme", () => {
|
|||||||
|
|
||||||
const link = document.getElementById("theme-data");
|
const link = document.getElementById("theme-data");
|
||||||
expect(link).not.toBeNull();
|
expect(link).not.toBeNull();
|
||||||
expect(link?.getAttribute("href")).toBe("/theme-data.css");
|
// href should resolve to theme-data.css via document.baseURI
|
||||||
|
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves theme-data.css relative to document.baseURI for HTTP paths", () => {
|
||||||
|
// Simulate a non-root HTTP path like http://localhost:3000/some/path/
|
||||||
|
Object.defineProperty(document, "baseURI", {
|
||||||
|
value: "http://localhost:3000/some/path/",
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setColorTheme("ocean");
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = document.getElementById("theme-data");
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
// URL resolution should work with non-root base path
|
||||||
|
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
Object.defineProperty(document, "baseURI", {
|
||||||
|
value: "http://localhost:3000/",
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves theme-data.css for file:// URLs (Electron production)", () => {
|
||||||
|
// Simulate Electron production file:// context
|
||||||
|
Object.defineProperty(document, "baseURI", {
|
||||||
|
value: "file:///Users/me/Projects/kb/packages/dashboard/dist/client/index.html",
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setColorTheme("factory");
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = document.getElementById("theme-data");
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
// For file:// URLs, href should resolve to the local file path
|
||||||
|
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
|
||||||
|
// The href should be a valid file:// URL or path
|
||||||
|
expect(link?.getAttribute("href")).toMatch(/^file:\/\/|^\//);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
Object.defineProperty(document, "baseURI", {
|
||||||
|
value: "http://localhost:3000/",
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves theme-data.css for nested file:// paths", () => {
|
||||||
|
// Simulate file:// with nested directory structure
|
||||||
|
Object.defineProperty(document, "baseURI", {
|
||||||
|
value: "file:///app/fusion/node/dashboard/dist/index.html",
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setColorTheme("nord");
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = document.getElementById("theme-data");
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.getAttribute("href")?.endsWith("theme-data.css")).toBe(true);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
Object.defineProperty(document, "baseURI", {
|
||||||
|
value: "http://localhost:3000/",
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not duplicate theme-data link when pre-existing in DOM", () => {
|
||||||
|
// Simulate index.html inline script already injected the link
|
||||||
|
const existingLink = document.createElement("link");
|
||||||
|
existingLink.id = "theme-data";
|
||||||
|
existingLink.rel = "stylesheet";
|
||||||
|
existingLink.href = "/theme-data.css";
|
||||||
|
document.head.appendChild(existingLink);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTheme());
|
||||||
|
|
||||||
|
// Switch to non-default theme
|
||||||
|
act(() => {
|
||||||
|
result.current.setColorTheme("ocean");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still only have one link (the pre-existing one)
|
||||||
|
const links = document.querySelectorAll('link[id="theme-data"]');
|
||||||
|
expect(links.length).toBe(1);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
existingLink.remove();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,45 @@ const THEME_MODE_STORAGE_KEY = "kb-dashboard-theme-mode";
|
|||||||
const COLOR_THEME_STORAGE_KEY = "kb-dashboard-color-theme";
|
const COLOR_THEME_STORAGE_KEY = "kb-dashboard-color-theme";
|
||||||
const VALID_COLOR_THEMES = [...COLOR_THEMES] satisfies ColorTheme[];
|
const VALID_COLOR_THEMES = [...COLOR_THEMES] satisfies ColorTheme[];
|
||||||
const THEME_DATA_ID = "theme-data";
|
const THEME_DATA_ID = "theme-data";
|
||||||
|
const THEME_DATA_FILENAME = "theme-data.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resolved URL for theme-data.css.
|
||||||
|
*
|
||||||
|
* This function handles both HTTP/HTTPS origins and Electron file:// contexts.
|
||||||
|
* Using document.baseURI ensures the stylesheet path resolves correctly regardless
|
||||||
|
* of whether the app is served over HTTP or loaded from a file:// URL.
|
||||||
|
*
|
||||||
|
* For file:// URLs, the path is derived relative to the HTML file's directory.
|
||||||
|
* For HTTP/HTTPS URLs, the path resolves to the server root.
|
||||||
|
*/
|
||||||
|
function getThemeDataUrl(): string {
|
||||||
|
// Get base URL from document.baseURI (most reliable across contexts)
|
||||||
|
// Falls back to document.location.href if baseURI is unavailable
|
||||||
|
const base = document.baseURI || (typeof document.location !== "undefined" ? document.location.href : "");
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
// Fallback to absolute path if no base available
|
||||||
|
return `/${THEME_DATA_FILENAME}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file:// URLs specially - derive path relative to HTML file directory
|
||||||
|
if (base.startsWith("file://")) {
|
||||||
|
// Extract directory from file:// path (e.g., file:///path/to/app/index.html → /path/to/app/)
|
||||||
|
const pathMatch = base.match(/^file:\/\/[^\/]*(\/.*?)?\/[^\/]*$/);
|
||||||
|
if (pathMatch && pathMatch[1]) {
|
||||||
|
// base ends with filename, use the directory portion
|
||||||
|
const dirPath = pathMatch[1];
|
||||||
|
return `${base.substring(0, 7)}${dirPath}${THEME_DATA_FILENAME}`;
|
||||||
|
}
|
||||||
|
// Fallback: just use the base without the filename
|
||||||
|
return base.replace(/\/[^\/]+$/, `/${THEME_DATA_FILENAME}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HTTP/HTTPS URLs, resolve relative to server root
|
||||||
|
const url = new URL(`/${THEME_DATA_FILENAME}`, base);
|
||||||
|
return url.href;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're in a browser environment
|
// Check if we're in a browser environment
|
||||||
const isBrowser = typeof window !== "undefined";
|
const isBrowser = typeof window !== "undefined";
|
||||||
@@ -101,7 +140,7 @@ function loadThemeDataStylesheet(): void {
|
|||||||
|
|
||||||
const link = document.createElement("link");
|
const link = document.createElement("link");
|
||||||
link.rel = "stylesheet";
|
link.rel = "stylesheet";
|
||||||
link.href = "/theme-data.css";
|
link.href = getThemeDataUrl();
|
||||||
link.id = THEME_DATA_ID;
|
link.id = THEME_DATA_ID;
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,26 @@
|
|||||||
document.documentElement.setAttribute('data-theme', effectiveMode);
|
document.documentElement.setAttribute('data-theme', effectiveMode);
|
||||||
document.documentElement.setAttribute('data-color-theme', colorTheme);
|
document.documentElement.setAttribute('data-color-theme', colorTheme);
|
||||||
// Load theme-data.css for non-default themes to prevent flash
|
// Load theme-data.css for non-default themes to prevent flash
|
||||||
|
// Use path-safe resolution that works in both HTTP and file:// contexts
|
||||||
if (colorTheme !== 'default') {
|
if (colorTheme !== 'default') {
|
||||||
|
var themeDataUrl;
|
||||||
|
var base = document.baseURI || (document.location && document.location.href) || '';
|
||||||
|
if (base.indexOf('file://') === 0) {
|
||||||
|
// For file:// URLs, derive path relative to the HTML file directory
|
||||||
|
var pathMatch = base.match(/^file:\/\/[^\/]*(\/.*?)?\/[^\/]*$/);
|
||||||
|
if (pathMatch && pathMatch[1]) {
|
||||||
|
var dirPath = pathMatch[1];
|
||||||
|
themeDataUrl = base.substring(0, 7) + dirPath + 'theme-data.css';
|
||||||
|
} else {
|
||||||
|
themeDataUrl = base.replace(/\/[^\/]+$/, '/theme-data.css');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For HTTP/HTTPS URLs, use URL resolution
|
||||||
|
themeDataUrl = new URL('/theme-data.css', base).href;
|
||||||
|
}
|
||||||
var link = document.createElement('link');
|
var link = document.createElement('link');
|
||||||
link.rel = 'stylesheet';
|
link.rel = 'stylesheet';
|
||||||
link.href = '/theme-data.css';
|
link.href = themeDataUrl;
|
||||||
link.id = 'theme-data';
|
link.id = 'theme-data';
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user