@grackle-ai/web-components 0.112.0 → 0.112.2

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.
Files changed (30) hide show
  1. package/.rush/temp/{c8b4fba3a6cfb704f3582f1197cac0fc11abbc50.tar.log → 1421806d07f6b0c455deca4bf89a6412726ffd8b.tar.log} +15 -14
  2. package/.rush/temp/{c8b4fba3a6cfb704f3582f1197cac0fc11abbc50.untar.log → 1421806d07f6b0c455deca4bf89a6412726ffd8b.untar.log} +2 -2
  3. package/.rush/temp/{e2794f6b3dd02e58a0ca34e8c438c22d25296c3e.tar.log → a0341c0f1c835c664217d8a879aa38d780e62122.tar.log} +2 -2
  4. package/.rush/temp/{e2794f6b3dd02e58a0ca34e8c438c22d25296c3e.untar.log → a0341c0f1c835c664217d8a879aa38d780e62122.untar.log} +2 -2
  5. package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +5 -5
  6. package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +24 -24
  7. package/.rush/temp/operation/_phase_build/all.log +5 -5
  8. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +5 -5
  9. package/.rush/temp/operation/_phase_build/state.json +1 -1
  10. package/.rush/temp/operation/_phase_test/all.log +24 -24
  11. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +24 -24
  12. package/.rush/temp/operation/_phase_test/state.json +1 -1
  13. package/dist/index.js +3969 -3964
  14. package/package.json +2 -2
  15. package/rush-logs/web-components._phase_build.cache.log +1 -1
  16. package/rush-logs/web-components._phase_build.log +5 -5
  17. package/rush-logs/web-components._phase_test.cache.log +1 -1
  18. package/rush-logs/web-components._phase_test.log +24 -24
  19. package/src/components/display/SplashScreen.tsx +2 -1
  20. package/src/components/layout/AppNav.stories.tsx +5 -5
  21. package/src/components/layout/AppNav.tsx +23 -14
  22. package/src/components/layout/StatusBar.tsx +2 -1
  23. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +2 -2
  24. package/src/components/panels/KeyboardShortcutsPanel.tsx +3 -2
  25. package/src/hooks/types.ts +1 -1
  26. package/src/index.ts +2 -0
  27. package/src/mocks/MockGrackleProvider.tsx +2 -2
  28. package/src/utils/assetUrl.test.ts +23 -0
  29. package/src/utils/assetUrl.ts +33 -0
  30. package/temp/build/lint/_eslint-5eVG3S6w.json +18 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grackle-ai/web-components",
3
- "version": "0.112.0",
3
+ "version": "0.112.2",
4
4
  "description": "Presentational React component library for the Grackle web UI",
5
5
  "license": "MIT",
6
6
  "sideEffects": [
@@ -46,7 +46,7 @@
46
46
  "remark-gfm": "^4.0.0",
47
47
  "lucide-react": "~0.474.0",
48
48
  "react-router": "^7.0.0",
49
- "@grackle-ai/common": "0.112.0"
49
+ "@grackle-ai/common": "0.112.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@rushstack/heft": "1.2.7",
@@ -1,4 +1,4 @@
1
1
  Build cache hit.
2
- Cache key: c8b4fba3a6cfb704f3582f1197cac0fc11abbc50
2
+ Cache key: 1421806d07f6b0c455deca4bf89a6412726ffd8b
3
3
  Clearing cached folders: dist, storybook-static, .rush/temp/operation/_phase_build
4
4
  Successfully restored output from the build cache.
@@ -7,12 +7,12 @@ Invoking: heft run --only build -- --clean
7
7
  [build:lint] Using ESLint version 9.39.4
8
8
  vite v6.4.2 building for production...
9
9
  transforming...
10
- 2715 modules transformed.
10
+ 2716 modules transformed.
11
11
  rendering chunks...
12
12
  computing gzip size...
13
13
  dist/index.css 158.47 kB │ gzip: 20.60 kB
14
- dist/index.js 1,590.94 kB │ gzip: 401.57 kB
15
- ✓ built in 5.74s
14
+ dist/index.js 1,591.31 kB │ gzip: 401.72 kB
15
+ ✓ built in 5.69s
16
16
  [build:vite-build] Vite build completed.
17
- ---- build finished (74.958s) ----
18
- -------------------- Finished (74.961s) --------------------
17
+ ---- build finished (72.731s) ----
18
+ -------------------- Finished (72.734s) --------------------
@@ -1,4 +1,4 @@
1
1
  Build cache hit.
2
- Cache key: e2794f6b3dd02e58a0ca34e8c438c22d25296c3e
2
+ Cache key: a0341c0f1c835c664217d8a879aa38d780e62122
3
3
  Clearing cached folders: .rush/temp/operation/_phase_test
4
4
  Successfully restored output from the build cache.
@@ -5,30 +5,30 @@ The provided list of phases does not contain all phase dependencies. You may nee
5
5
 
6
6
  RUN v3.2.4 /home/runner/work/grackle/grackle/packages/web-components
7
7
 
8
- ✓ src/utils/sessionEvents.test.ts (14 tests) 54ms
9
- ✓ src/utils/eventContent.test.ts (38 tests) 139ms
10
- ✓ src/utils/dashboard.test.ts (4 tests) 7ms
11
- ✓ src/utils/route-config.test.ts (23 tests) 30ms
12
- ✓ src/utils/scrollUtils.test.ts (11 tests) 24ms
13
- ✓ src/utils/breadcrumbs.test.ts (18 tests) 55ms
14
- ✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 27ms
15
- ✓ src/components/tools/classifyTool.test.ts (6 tests) 28ms
16
- ✓ src/components/display/extractText.test.tsx (8 tests) 32ms
17
- ✓ src/hooks/useEventSelection.test.ts (13 tests) 131ms
18
- ✓ src/components/editable/useEditableField.test.tsx (17 tests) 200ms
19
- ✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 264ms
20
- ✓ src/utils/grackleHostStyleVariables.test.ts (2 tests) 540ms
21
- ✓ grackleHostStyleVariables > always returns the MCP-standard fallback variables 516ms
22
- src/components/display/McpAppWidget.test.tsx (3 tests) 501ms
23
- ✓ McpAppWidget > mounts an iframe host for the widget 321ms
24
-
25
- Test Files 14 passed (14)
26
- Tests 171 passed (171)
27
- Start at 22:02:20
28
- Duration 16.20s (transform 2.92s, setup 0ms, collect 17.59s, tests 2.03s, environment 14.57s, prepare 5.64s)
8
+ ✓ src/utils/sessionEvents.test.ts (14 tests) 60ms
9
+ ✓ src/utils/eventContent.test.ts (38 tests) 155ms
10
+ ✓ src/utils/dashboard.test.ts (4 tests) 37ms
11
+ ✓ src/utils/route-config.test.ts (23 tests) 50ms
12
+ ✓ src/utils/scrollUtils.test.ts (11 tests) 20ms
13
+ ✓ src/utils/breadcrumbs.test.ts (18 tests) 65ms
14
+ ✓ src/components/tools/classifyTool.test.ts (6 tests) 22ms
15
+ ✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 28ms
16
+ ✓ src/utils/assetUrl.test.ts (3 tests) 18ms
17
+ ✓ src/components/display/extractText.test.tsx (8 tests) 17ms
18
+ ✓ src/components/editable/useEditableField.test.tsx (17 tests) 206ms
19
+ ✓ src/hooks/useEventSelection.test.ts (13 tests) 146ms
20
+ ✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 199ms
21
+ src/utils/grackleHostStyleVariables.test.ts (2 tests) 445ms
22
+ grackleHostStyleVariables > always returns the MCP-standard fallback variables 416ms
23
+ src/components/display/McpAppWidget.test.tsx (3 tests) 258ms
24
+
25
+ Test Files 15 passed (15)
26
+ Tests 174 passed (174)
27
+ Start at 23:48:14
28
+ Duration 16.41s (transform 2.75s, setup 0ms, collect 18.15s, tests 1.73s, environment 14.38s, prepare 5.42s)
29
29
 
30
30
  [test:vitest] Vitest completed.
31
- [test:storybook-test] Starting Storybook static server on port 38719...
31
+ [test:storybook-test] Starting Storybook static server on port 36655...
32
32
  [test:storybook-test] Storybook server ready. Running interaction tests...
33
33
  jest-haste-map: duplicate manual mock found: adapter-manager
34
34
  The following files share their name; please delete one of them:
@@ -121,5 +121,5 @@ jest-haste-map: duplicate manual mock found: utils/network
121
121
  * <rootDir>/packages/server/src/__mocks__/utils/network.ts
122
122
 
123
123
  [test:storybook-test] Storybook interaction tests completed.
124
- ---- test finished (115.550s) ----
125
- -------------------- Finished (115.554s) --------------------
124
+ ---- test finished (113.186s) ----
125
+ -------------------- Finished (113.19s) --------------------
@@ -1,5 +1,6 @@
1
1
  import type { JSX } from "react";
2
2
  import { Spinner } from "./Spinner.js";
3
+ import { assetUrl } from "../../utils/assetUrl.js";
3
4
  import styles from "./SplashScreen.module.scss";
4
5
 
5
6
  /**
@@ -9,7 +10,7 @@ import styles from "./SplashScreen.module.scss";
9
10
  export function SplashScreen(): JSX.Element {
10
11
  return (
11
12
  <div className={styles.splash} data-testid="splash-screen">
12
- <img src="/grackle-logo.png" alt="Grackle" className={styles.logo} />
13
+ <img src={assetUrl("grackle-logo.png")} alt="Grackle" className={styles.logo} />
13
14
  <Spinner size="xl" label="Loading Grackle" liveRegion />
14
15
  </div>
15
16
  );
@@ -17,7 +17,7 @@ type Story = StoryObj<typeof meta>;
17
17
  export const AllTabsRendered: Story = {
18
18
  play: async ({ canvas }) => {
19
19
  await expect(canvas.getByRole("tab", { name: /Dashboard/ })).toBeInTheDocument();
20
- await expect(canvas.getByRole("tab", { name: /Chat/ })).toBeInTheDocument();
20
+ await expect(canvas.getByRole("tab", { name: /Sessions/ })).toBeInTheDocument();
21
21
  await expect(canvas.getByRole("tab", { name: /Tasks/ })).toBeInTheDocument();
22
22
  await expect(canvas.getByRole("tab", { name: /Environments/ })).toBeInTheDocument();
23
23
  await expect(canvas.getByRole("tab", { name: /Knowledge/ })).toBeInTheDocument();
@@ -30,14 +30,14 @@ export const CoreOnlyTabs: Story = {
30
30
  args: {
31
31
  tabs: [
32
32
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
33
- { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
33
+ { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
34
34
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
35
35
  { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings" },
36
36
  ],
37
37
  },
38
38
  play: async ({ canvas }) => {
39
39
  await expect(canvas.getByRole("tab", { name: /Dashboard/ })).toBeInTheDocument();
40
- await expect(canvas.getByRole("tab", { name: /Chat/ })).toBeInTheDocument();
40
+ await expect(canvas.getByRole("tab", { name: /Sessions/ })).toBeInTheDocument();
41
41
  await expect(canvas.getByRole("tab", { name: /Environments/ })).toBeInTheDocument();
42
42
  await expect(canvas.getByRole("tab", { name: /Settings/ })).toBeInTheDocument();
43
43
  await expect(canvas.queryByRole("tab", { name: /Tasks/ })).not.toBeInTheDocument();
@@ -51,7 +51,7 @@ export const AllTabsExplicit: Story = {
51
51
  args: {
52
52
  tabs: [
53
53
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
54
- { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
54
+ { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
55
55
  { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
56
56
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
57
57
  { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge" },
@@ -78,7 +78,7 @@ export const SettingsPinnedRight: Story = {
78
78
  args: {
79
79
  tabs: [
80
80
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
81
- { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
81
+ { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
82
82
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
83
83
  { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings", align: "end" },
84
84
  { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
@@ -21,18 +21,24 @@ export interface AppTab {
21
21
  route: string;
22
22
  /** data-testid suffix. */
23
23
  testId: string;
24
+ /**
25
+ * Display order within the nav bar (lower numbers appear first). Applied
26
+ * across all plugins so tab order is explicit rather than dependent on plugin
27
+ * load order. End-aligned tabs ignore this and are always pinned right.
28
+ */
29
+ order?: number;
24
30
  /** Horizontal alignment within the nav bar. `"end"` pins the tab to the right edge. */
25
31
  align?: "end";
26
32
  }
27
33
 
28
34
  /** Ordered list of all app navigation tabs. Exported for plugin registry use. */
29
35
  export const TABS: AppTab[] = [
30
- { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
31
- { view: "chat", label: "Chat", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
32
- { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
33
- { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
34
- { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge" },
35
- { view: "findings", label: "Findings", icon: <Search size={ICON_LG} />, route: FINDINGS_URL, testId: "sidebar-tab-findings" },
36
+ { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard", order: 0 },
37
+ { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks", order: 1 },
38
+ { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments", order: 2 },
39
+ { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat", order: 3 },
40
+ { view: "findings", label: "Findings", icon: <Search size={ICON_LG} />, route: FINDINGS_URL, testId: "sidebar-tab-findings", order: 4 },
41
+ { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge", order: 5 },
36
42
  { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings", align: "end" },
37
43
  ];
38
44
 
@@ -67,15 +73,18 @@ export function AppNav({ tabs = TABS }: { tabs?: AppTab[] }): JSX.Element {
67
73
 
68
74
  const activeView = getActiveView(location.pathname);
69
75
 
70
- // Render end-aligned tabs (e.g. Settings) last regardless of the incoming order,
71
- // so they stay pinned to the right edge no matter which plugins contribute tabs.
72
- const orderedTabs = useMemo(
73
- () => [
74
- ...tabs.filter((t) => t.align !== "end"),
76
+ // Sort by explicit `order`, then render end-aligned tabs (e.g. Settings) last
77
+ // regardless of order, so they stay pinned to the right edge no matter which
78
+ // plugins contribute tabs. Tabs without an `order` keep their incoming order
79
+ // (stable sort) and fall after explicitly-ordered ones.
80
+ const orderedTabs = useMemo(() => {
81
+ const byOrder = (a: AppTab, b: AppTab): number =>
82
+ (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER);
83
+ return [
84
+ ...tabs.filter((t) => t.align !== "end").sort(byOrder),
75
85
  ...tabs.filter((t) => t.align === "end"),
76
- ],
77
- [tabs],
78
- );
86
+ ];
87
+ }, [tabs]);
79
88
  const firstEndAlignedView = orderedTabs.find((t) => t.align === "end")?.view;
80
89
 
81
90
  const handleClick = useCallback((tab: AppTab) => {
@@ -3,6 +3,7 @@ import { Circle, Menu } from "lucide-react";
3
3
  import type { ConnectionStatus, Environment, Session } from "../../hooks/types.js";
4
4
  import { ICON_LG, ICON_XS } from "../../utils/iconSize.js";
5
5
  import { HOME_URL, useAppNavigate } from "../../utils/navigation.js";
6
+ import { assetUrl } from "../../utils/assetUrl.js";
6
7
  import { Tooltip } from "../display/Tooltip.js";
7
8
  import styles from "./StatusBar.module.scss";
8
9
 
@@ -51,7 +52,7 @@ export function StatusBar({ connectionStatus, environments, sessions, onToggleSi
51
52
  )}
52
53
  <Tooltip text="Home" placement="bottom">
53
54
  <button type="button" className={styles.brand} onClick={() => navigate(HOME_URL)} data-testid="statusbar-brand">
54
- <img src="/icon-192x192.png" alt="" className={styles.brandLogo} aria-hidden="true" data-testid="statusbar-logo" />
55
+ <img src={assetUrl("icon-192x192.png")} alt="" className={styles.brandLogo} aria-hidden="true" data-testid="statusbar-logo" />
55
56
  Grackle
56
57
  </button>
57
58
  </Tooltip>
@@ -25,7 +25,7 @@ export const AllCategoriesRendered: Story = {
25
25
  await expect(canvas.getByText("Workspace Page")).toBeInTheDocument();
26
26
  await expect(canvas.getByText("Navigation Lists")).toBeInTheDocument();
27
27
  await expect(canvas.getByText("Editing")).toBeInTheDocument();
28
- await expect(canvas.getByText("Chat")).toBeInTheDocument();
28
+ await expect(canvas.getByText("Sessions")).toBeInTheDocument();
29
29
  },
30
30
  };
31
31
 
@@ -35,6 +35,6 @@ export const ShortcutDescriptions: Story = {
35
35
  await expect(canvas.getByText("Open keyboard shortcuts reference")).toBeInTheDocument();
36
36
  await expect(canvas.getByText("Create a new task")).toBeInTheDocument();
37
37
  await expect(canvas.getByText("Switch to Overview tab")).toBeInTheDocument();
38
- await expect(canvas.getByText("Send message (when input is focused)")).toBeInTheDocument();
38
+ await expect(canvas.getByText("Send message (when the composer is focused)")).toBeInTheDocument();
39
39
  },
40
40
  };
@@ -66,9 +66,10 @@ const SHORTCUT_GROUPS: ShortcutGroup[] = [
66
66
  ],
67
67
  },
68
68
  {
69
- title: "Chat",
69
+ title: "Sessions",
70
70
  shortcuts: [
71
- { keys: ["Enter"], description: "Send message (when input is focused)" },
71
+ { keys: ["Ctrl/Cmd", "Enter"], description: "Send message (when the composer is focused)" },
72
+ { keys: ["Enter"], description: "Insert a new line in the message composer" },
72
73
  ],
73
74
  },
74
75
  ];
@@ -327,7 +327,7 @@ export interface UseWorkspacesResult {
327
327
  defaultPersonaId?: string,
328
328
  useWorktrees?: boolean,
329
329
  workingDirectory?: string,
330
- onSuccess?: () => void,
330
+ onSuccess?: (workspace: Workspace) => void,
331
331
  onError?: (message: string) => void,
332
332
  ) => Promise<void>;
333
333
  /** Archive a workspace by ID. */
package/src/index.ts CHANGED
@@ -202,6 +202,8 @@ export { computeKpis, getAttentionTasks, getActiveSessions, getWorkspaceSnapshot
202
202
 
203
203
  export { isNearAnchor, computeScrollCompensation, SCROLL_ANCHOR_THRESHOLD_PX } from "./utils/scrollUtils.js";
204
204
 
205
+ export { assetUrl } from "./utils/assetUrl.js";
206
+
205
207
  // ─── Themes ──────────────────────────────────────────────────────────────────
206
208
 
207
209
  export { THEMES, THEME_IDS, DEFAULT_THEME_ID, getThemeById } from "./themes.js";
@@ -382,7 +382,7 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
382
382
  defaultPersonaId?: string,
383
383
  useWorktrees?: boolean,
384
384
  workingDirectory?: string,
385
- onSuccess?: () => void,
385
+ onSuccess?: (workspace: Workspace) => void,
386
386
  _onError?: (message: string) => void,
387
387
  ) => {
388
388
  console.log("[MockGrackle] createWorkspace", { name, description });
@@ -405,7 +405,7 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
405
405
 
406
406
  setWorkspaces((prev) => [...prev, newWorkspace]);
407
407
  if (onSuccess) {
408
- onSuccess();
408
+ onSuccess(newWorkspace);
409
409
  }
410
410
  },
411
411
  [nextId],
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
+ import { assetUrl } from "./assetUrl.js";
3
+
4
+ describe("assetUrl", () => {
5
+ afterEach(() => {
6
+ vi.unstubAllEnvs();
7
+ });
8
+
9
+ it("prefixes the file with the default base URL", () => {
10
+ vi.stubEnv("BASE_URL", "/");
11
+ expect(assetUrl("icon-192x192.png")).toBe("/icon-192x192.png");
12
+ });
13
+
14
+ it("prefixes the file with a sub-path base URL (demo deployment)", () => {
15
+ vi.stubEnv("BASE_URL", "/grackle/demo/");
16
+ expect(assetUrl("icon-192x192.png")).toBe("/grackle/demo/icon-192x192.png");
17
+ });
18
+
19
+ it("collapses a leading slash on the file name so the base join never double-slashes", () => {
20
+ vi.stubEnv("BASE_URL", "/grackle/demo/");
21
+ expect(assetUrl("/grackle-logo.png")).toBe("/grackle/demo/grackle-logo.png");
22
+ });
23
+ });
@@ -0,0 +1,33 @@
1
+ // Minimal typing for Vite's `import.meta.env.BASE_URL`. We deliberately avoid a
2
+ // full `/// <reference types="vite/client" />` so that Vite's broad ambient
3
+ // asset-module declarations (`*.png`, `*.css`, ...) don't leak into the type
4
+ // graph of every program that compiles this file — we only need `BASE_URL`.
5
+ declare global {
6
+ interface ImportMetaEnv {
7
+ /** Base public path the app is served from (Vite `base`); always ends in `/`. */
8
+ readonly BASE_URL: string;
9
+ }
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv;
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Resolve a public asset path against the application's configured base URL.
17
+ *
18
+ * Public assets (logos, icons, the web manifest) sit at the site root in local
19
+ * development but under a sub-path when the app is deployed to a base URL — e.g.
20
+ * the GitHub Pages demo served from `/grackle/demo/`. A leading-slash `src` is
21
+ * resolved by the browser against the origin root and ignores Vite's `base`, so
22
+ * such assets 404 on sub-path deployments. Prefixing with
23
+ * `import.meta.env.BASE_URL` (always set by Vite, with a trailing slash) yields
24
+ * a path that is correct in every deployment.
25
+ *
26
+ * @param fileName - The asset's file name relative to the public root, ideally
27
+ * without a leading slash (e.g. `"icon-192x192.png"`). A leading slash is
28
+ * tolerated and stripped to avoid a double slash after the base.
29
+ * @returns The base-prefixed asset URL (e.g. `"/grackle/demo/icon-192x192.png"`).
30
+ */
31
+ export function assetUrl(fileName: string): string {
32
+ return `${import.meta.env.BASE_URL}${fileName.replace(/^\//, "")}`;
33
+ }
@@ -7,7 +7,7 @@
7
7
  ],
8
8
  [
9
9
  "hooks/types.ts",
10
- "/7ttZkjX3bM9oMWNFw/qQOYtrrI=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
10
+ "Q3IfPXTzOZH/42LKa4mjM52/+X0=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
11
11
  ],
12
12
  [
13
13
  "components/chat/ChatInput.tsx",
@@ -161,9 +161,13 @@
161
161
  "components/display/Spinner.tsx",
162
162
  "p7oAA36O26nwdV3QtEJdt9vSMRk=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
163
163
  ],
164
+ [
165
+ "utils/assetUrl.ts",
166
+ "KmzyO2hja0ddDKHVjGabZurZzN0=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
167
+ ],
164
168
  [
165
169
  "components/display/SplashScreen.tsx",
166
- "IfvUpfXP8s5Q2WYmnoIiSlxT2yk=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
170
+ "OTmXaBxnSDqnKvDW1/SajhKz1zU=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
167
171
  ],
168
172
  [
169
173
  "components/display/Tooltip.tsx",
@@ -267,11 +271,11 @@
267
271
  ],
268
272
  [
269
273
  "components/layout/StatusBar.tsx",
270
- "aWpXsJzCfnZ4ljVx5oG3/oCIM+U=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
274
+ "IwwWop/68/zHUpaH0JxUuhhga9Q=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
271
275
  ],
272
276
  [
273
277
  "components/layout/AppNav.tsx",
274
- "+ew0Cx3OJiv9FZ9/2F01S9ZazzQ=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
278
+ "m+cH8YKux2EB/NGH9BzaikTuEw0=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
275
279
  ],
276
280
  [
277
281
  "components/layout/Sidebar.tsx",
@@ -387,7 +391,7 @@
387
391
  ],
388
392
  [
389
393
  "components/panels/KeyboardShortcutsPanel.tsx",
390
- "SnjqqP3Qk5aS/bY+pOhXbkbptec=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
394
+ "0yOtcA8wE4p4ctgFYThvwW0x8Pk=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
391
395
  ],
392
396
  [
393
397
  "components/panels/CredentialProvidersPanel.tsx",
@@ -467,7 +471,7 @@
467
471
  ],
468
472
  [
469
473
  "mocks/MockGrackleProvider.tsx",
470
- "g+iRnCe9AbsL/7cGS7bBSU1JIsQ=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
474
+ "Zrw0hhCq5Bbv4P33XGGTpJjLm/8=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
471
475
  ],
472
476
  [
473
477
  "test-utils/storybook-decorators.tsx",
@@ -479,7 +483,7 @@
479
483
  ],
480
484
  [
481
485
  "index.ts",
482
- "XEgfwrVvQNW7DJQmsu4ZpsFAvgQ=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
486
+ "Lbo23h9QnwqCWZmf6x/aafniLGY=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
483
487
  ],
484
488
  [
485
489
  "components/index.ts",
@@ -607,7 +611,7 @@
607
611
  ],
608
612
  [
609
613
  "components/layout/AppNav.stories.tsx",
610
- "FhVGIPJJt0juJDi04NbgfMGBfSY=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
614
+ "oYykVWEa7SEp4YtAOIe+xpB1D3k=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
611
615
  ],
612
616
  [
613
617
  "components/layout/BottomStatusBar.stories.tsx",
@@ -671,7 +675,7 @@
671
675
  ],
672
676
  [
673
677
  "components/panels/KeyboardShortcutsPanel.stories.tsx",
674
- "uJzJ+fBVMitfnq9Wk0qRLInrMHQ=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
678
+ "gZ9hP3WGlzy58xTJYrj2eHoEpYQ=_7W3TvvwZxl0TGST6/dyQa8DKDDc="
675
679
  ],
676
680
  [
677
681
  "components/panels/TaskActionButtons.stories.tsx",
@@ -797,6 +801,10 @@
797
801
  "hooks/useEventSelection.test.ts",
798
802
  "/ggsjovG/PzoHNtCbMzOcwYEa/k=_orHdc0vDZqoYfD6TIDl1Za3EAL4="
799
803
  ],
804
+ [
805
+ "utils/assetUrl.test.ts",
806
+ "IaZp6S9n0W2AQtaTJgpZV3+L9sQ=_orHdc0vDZqoYfD6TIDl1Za3EAL4="
807
+ ],
800
808
  [
801
809
  "utils/breadcrumbs.test.ts",
802
810
  "uvtwd3hxobqtDMQ8hessqUzyrmc=_orHdc0vDZqoYfD6TIDl1Za3EAL4="
@@ -826,5 +834,5 @@
826
834
  "hL9OyAvsoDAPSh4+TPUEgQrdYMI=_orHdc0vDZqoYfD6TIDl1Za3EAL4="
827
835
  ]
828
836
  ],
829
- "filesHash": "PEVC244sy61mI2jL-TojYg"
837
+ "filesHash": "wzmDK7nkrsOam3n0gh_j3g"
830
838
  }