Add router-based navigation and refresh workspace switch overlay (#244)

* feat: enable URL-based navigation for web QA and restore workspace switch branding

* fix: use solid router Route list for compatibility

* fix: mount app inside root route for router context

* fix: patch solid router exports

* fix: render views via switch while syncing URL

* push

* fix: allow remote workspace creation in web
This commit is contained in:
ben
2026-01-24 12:26:21 -08:00
committed by GitHub
parent 79e13fd5c8
commit db9d7fe12f
12 changed files with 333 additions and 143 deletions

9
opencode.jsonc Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"chrome-devtools": {
"type": "local",
"command": ["npx", "-y", "chrome-devtools-mcp@latest"]
}
}
}

View File

@@ -32,7 +32,10 @@
"better-sqlite3",
"esbuild",
"protobufjs"
]
],
"patchedDependencies": {
"@solidjs/router@0.15.4": "patches/@solidjs__router@0.15.4.patch"
}
},
"packageManager": "pnpm@10.27.0"
}

View File

@@ -29,6 +29,7 @@
"@radix-ui/colors": "^3.0.0",
"@solid-primitives/event-bus": "^1.1.2",
"@solid-primitives/storage": "^4.3.3",
"@solidjs/router": "^0.15.4",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "~2.6.0",
"@tauri-apps/plugin-opener": "^2.5.3",

View File

@@ -1,6 +1,5 @@
import {
Match,
Show,
Switch,
createEffect,
createMemo,
@@ -10,6 +9,8 @@ import {
untrack,
} from "solid-js";
import { useLocation, useNavigate } from "@solidjs/router";
import type { Agent, Provider } from "@opencode-ai/sdk/v2/client";
import { getVersion } from "@tauri-apps/api/app";
@@ -105,35 +106,73 @@ import {
export default function App() {
type ProviderAuthMethod = { type: "oauth" | "api"; label: string };
const initialView: View = (() => {
if (typeof window === "undefined") return "onboarding";
try {
return window.localStorage.getItem("openwork.onboardingComplete") === "1"
? "dashboard"
: "onboarding";
} catch {
return "onboarding";
}
})();
const location = useLocation();
const navigate = useNavigate();
const [view, _setView] = createSignal<View>(initialView);
const [creatingSession, setCreatingSession] = createSignal(false);
const [sessionViewLockUntil, setSessionViewLockUntil] = createSignal(0);
const setView = (next: View) => {
// Guard: Don't allow view to change to dashboard while creating session.
const currentView = createMemo<View>(() => {
const path = location.pathname.toLowerCase();
if (path.startsWith("/onboarding")) return "onboarding";
if (path.startsWith("/session")) return "session";
return "dashboard";
});
const [tab, setTabState] = createSignal<DashboardTab>("home");
const goToDashboard = (nextTab: DashboardTab, options?: { replace?: boolean }) => {
setTabState(nextTab);
navigate(`/dashboard/${nextTab}`, options);
};
const setTab = (nextTab: DashboardTab) => {
if (currentView() === "dashboard") {
goToDashboard(nextTab);
return;
}
setTabState(nextTab);
};
const setView = (next: View, sessionId?: string) => {
if (next === "dashboard" && creatingSession()) {
return;
}
if (next === "dashboard" && Date.now() < sessionViewLockUntil()) {
return;
}
_setView(next);
if (next === "onboarding") {
navigate("/onboarding");
return;
}
if (next === "session") {
if (sessionId) {
goToSession(sessionId);
return;
}
const fallback = activeSessionId();
if (fallback) {
goToSession(fallback);
return;
}
navigate("/session");
return;
}
goToDashboard(tab());
};
const goToSession = (sessionId: string, options?: { replace?: boolean }) => {
const trimmed = sessionId.trim();
if (!trimmed) {
navigate("/session", options);
return;
}
navigate(`/session/${trimmed}`, options);
};
const [mode, setMode] = createSignal<Mode | null>(null);
const [onboardingStep, setOnboardingStep] =
createSignal<OnboardingStep>("mode");
const [rememberModeChoice, setRememberModeChoice] = createSignal(false);
const [tab, setTab] = createSignal<DashboardTab>("home");
const [themeMode, setThemeMode] = createSignal<ThemeMode>(getInitialThemeMode());
const [engineSource, setEngineSource] = createSignal<"path" | "sidecar">(
@@ -900,7 +939,7 @@ export default function App() {
createEffect(() => {
// If we lose the client (disconnect / stop engine), don't strand the user
// in a session view that can't operate.
if (view() !== "session") return;
if (currentView() !== "session") return;
if (isDemoMode()) return;
if (creatingSession()) return;
if (client()) return;
@@ -1065,7 +1104,7 @@ export default function App() {
setDefaultModel(next);
setModelPickerOpen(false);
if (typeof window !== "undefined" && view() === "session") {
if (typeof window !== "undefined" && currentView() === "session") {
requestAnimationFrame(() => {
window.dispatchEvent(new CustomEvent("openwork:focusPrompt"));
});
@@ -1344,7 +1383,12 @@ export default function App() {
console.log("[DEBUG] current baseUrl:", baseUrl());
console.log("[DEBUG] engine info:", engine());
if (isDemoMode()) {
setView("session");
const demoId = activeSessionId();
if (demoId) {
goToSession(demoId);
} else {
setView("session");
}
return;
}
@@ -1452,7 +1496,7 @@ export default function App() {
// Now switch view AFTER session is selected
mark("view set to session");
// setSessionViewLockUntil(Date.now() + 1200);
setView("session");
goToSession(session.id);
} catch (e) {
mark("error caught", e);
const message = e instanceof Error ? e.message : t("app.unknown_error", currentLocale());
@@ -2034,8 +2078,8 @@ export default function App() {
workspaceStore.setEngineInstallLogs(notes || null);
},
onOpenSettings: () => {
setView("dashboard");
setTab("settings");
setView("dashboard");
},
themeMode: themeMode(),
setThemeMode,
@@ -2044,7 +2088,7 @@ export default function App() {
const dashboardProps = () => ({
tab: tab(),
setTab,
view: view(),
view: currentView(),
setView,
mode: mode(),
baseUrl: baseUrl(),
@@ -2186,87 +2230,174 @@ export default function App() {
setLanguage: setLocale,
});
const sessionProps = () => ({
selectedSessionId: activeSessionId(),
setView,
setTab,
activeWorkspaceDisplay: activeWorkspaceDisplay(),
setWorkspaceSearch: workspaceStore.setWorkspaceSearch,
setWorkspacePickerOpen: workspaceStore.setWorkspacePickerOpen,
headerStatus: headerStatus(),
busyHint: busyHint(),
selectedSessionModelLabel: selectedSessionModelLabel(),
openSessionModelPicker: openSessionModelPicker,
activePlugins: sidebarPluginList(),
activePluginStatus: sidebarPluginStatus(),
createSessionAndOpen: createSessionAndOpen,
sendPromptAsync: sendPrompt,
newTaskDisabled: newTaskDisabled(),
sessions: activeSessions().map((session) => ({
id: session.id,
title: session.title,
slug: session.slug,
})),
selectSession: isDemoMode() ? selectDemoSession : selectSession,
messages: activeMessages(),
todos: activeTodos(),
busyLabel: busyLabel(),
developerMode: developerMode(),
showThinking: showThinking(),
groupMessageParts,
summarizeStep,
expandedStepIds: expandedStepIds(),
setExpandedStepIds: setExpandedStepIds,
expandedSidebarSections: expandedSidebarSections(),
setExpandedSidebarSections: setExpandedSidebarSections,
artifacts: activeArtifacts(),
workingFiles: activeWorkingFiles(),
authorizedDirs: activeAuthorizedDirs(),
busy: busy(),
prompt: prompt(),
setPrompt: setPrompt,
sendPrompt: sendPrompt,
activePermission: activePermissionMemo(),
permissionReplyBusy: permissionReplyBusy(),
respondPermission: respondPermission,
respondPermissionAndRemember: respondPermissionAndRemember,
safeStringify: safeStringify,
showTryNotionPrompt: tryNotionPromptVisible() && notionIsActive(),
openConnect: openConnectFlow,
startProviderAuth: startProviderAuth,
openProviderAuthModal: openProviderAuthModal,
closeProviderAuthModal: closeProviderAuthModal,
providerAuthModalOpen: providerAuthModalOpen(),
providerAuthBusy: providerAuthBusy(),
providerAuthError: providerAuthError(),
providerAuthMethods: providerAuthMethods(),
providers: providers(),
providerConnectedIds: providerConnectedIds(),
listAgents: listAgents,
setSessionAgent: setSessionAgent,
saveSession: saveSessionExport,
sessionStatusById: activeSessionStatusById(),
onTryNotionPrompt: () => {
setPrompt("setup my crm");
setTryNotionPromptVisible(false);
setNotionSkillInstalled(true);
try {
window.localStorage.setItem("openwork.notionSkillInstalled", "1");
} catch {
// ignore
}
},
sessionStatus: selectedSessionStatus(),
renameSession: renameSessionTitle,
error: error(),
});
const dashboardTabs = new Set<DashboardTab>([
"home",
"sessions",
"templates",
"skills",
"plugins",
"mcp",
"settings",
]);
const resolveDashboardTab = (value?: string | null) => {
const normalized = value?.trim().toLowerCase() ?? "";
if (dashboardTabs.has(normalized as DashboardTab)) {
return normalized as DashboardTab;
}
return "home";
};
const initialRoute = () => {
if (typeof window === "undefined") return "/onboarding";
try {
return window.localStorage.getItem("openwork.onboardingComplete") === "1"
? "/dashboard/home"
: "/onboarding";
} catch {
return "/onboarding";
}
};
createEffect(() => {
const rawPath = location.pathname.trim();
const path = rawPath.toLowerCase();
if (path === "" || path === "/") {
navigate(initialRoute(), { replace: true });
return;
}
if (path.startsWith("/dashboard")) {
const [, , tabSegment] = path.split("/");
const resolvedTab = resolveDashboardTab(tabSegment);
if (resolvedTab !== tab()) {
setTabState(resolvedTab);
}
if (!tabSegment || tabSegment !== resolvedTab) {
goToDashboard(resolvedTab, { replace: true });
}
return;
}
if (path.startsWith("/session")) {
const [, , sessionSegment] = rawPath.split("/");
const id = (sessionSegment ?? "").trim();
if (!id) {
const fallback = activeSessionId();
if (fallback) {
goToSession(fallback, { replace: true });
} else {
goToDashboard("sessions", { replace: true });
}
return;
}
if (isDemoMode()) {
if (activeSessionId() !== id) {
selectDemoSession(id);
}
return;
}
if (selectedSessionId() !== id) {
void selectSession(id);
}
return;
}
if (path.startsWith("/onboarding")) {
return;
}
navigate("/dashboard/home", { replace: true });
});
return (
<>
<Switch>
<Match when={view() === "onboarding"}>
<Match when={currentView() === "onboarding"}>
<OnboardingView {...onboardingProps()} />
</Match>
<Match when={view() === "session"}>
<SessionView
selectedSessionId={activeSessionId()}
setView={setView}
setTab={setTab}
activeWorkspaceDisplay={activeWorkspaceDisplay()}
setWorkspaceSearch={workspaceStore.setWorkspaceSearch}
setWorkspacePickerOpen={workspaceStore.setWorkspacePickerOpen}
headerStatus={headerStatus()}
busyHint={busyHint()}
selectedSessionModelLabel={selectedSessionModelLabel()}
openSessionModelPicker={openSessionModelPicker}
activePlugins={sidebarPluginList()}
activePluginStatus={sidebarPluginStatus()}
createSessionAndOpen={createSessionAndOpen}
sendPromptAsync={sendPrompt}
newTaskDisabled={newTaskDisabled()}
sessions={activeSessions().map((session) => ({
id: session.id,
title: session.title,
slug: session.slug,
}))}
selectSession={isDemoMode() ? selectDemoSession : selectSession}
messages={activeMessages()}
todos={activeTodos()}
busyLabel={busyLabel()}
developerMode={developerMode()}
showThinking={showThinking()}
groupMessageParts={groupMessageParts}
summarizeStep={summarizeStep}
expandedStepIds={expandedStepIds()}
setExpandedStepIds={setExpandedStepIds}
expandedSidebarSections={expandedSidebarSections()}
setExpandedSidebarSections={setExpandedSidebarSections}
artifacts={activeArtifacts()}
workingFiles={activeWorkingFiles()}
authorizedDirs={activeAuthorizedDirs()}
busy={busy()}
prompt={prompt()}
setPrompt={setPrompt}
sendPrompt={sendPrompt}
activePermission={activePermissionMemo()}
permissionReplyBusy={permissionReplyBusy()}
respondPermission={respondPermission}
respondPermissionAndRemember={respondPermissionAndRemember}
safeStringify={safeStringify}
showTryNotionPrompt={tryNotionPromptVisible() && notionIsActive()}
openConnect={openConnectFlow}
startProviderAuth={startProviderAuth}
openProviderAuthModal={openProviderAuthModal}
closeProviderAuthModal={closeProviderAuthModal}
providerAuthModalOpen={providerAuthModalOpen()}
providerAuthBusy={providerAuthBusy()}
providerAuthError={providerAuthError()}
providerAuthMethods={providerAuthMethods()}
providers={providers()}
providerConnectedIds={providerConnectedIds()}
listAgents={listAgents}
setSessionAgent={setSessionAgent}
saveSession={saveSessionExport}
sessionStatusById={activeSessionStatusById()}
onTryNotionPrompt={() => {
setPrompt("setup my crm");
setTryNotionPromptVisible(false);
setNotionSkillInstalled(true);
try {
window.localStorage.setItem("openwork.notionSkillInstalled", "1");
} catch {
// ignore
}
}}
sessionStatus={selectedSessionStatus()}
renameSession={renameSessionTitle}
error={error()}
/>
<Match when={currentView() === "session"}>
<SessionView {...sessionProps()} />
</Match>
<Match when={true}>
<DashboardView {...dashboardProps()} />

View File

@@ -1,19 +1,9 @@
import { Show, createMemo } from "solid-js";
import { Dynamic } from "solid-js/web";
import { Folder, Globe, Zap } from "lucide-solid";
import { t, currentLocale } from "../../i18n";
import OpenWorkLogo from "./openwork-logo";
import type { WorkspaceInfo } from "../lib/tauri";
function iconForWorkspace(preset: string, workspaceType: string) {
if (workspaceType === "remote") return Globe;
if (preset === "starter") return Zap;
if (preset === "automation") return Folder;
if (preset === "minimal") return Globe;
return Folder;
}
export default function WorkspaceSwitchOverlay(props: {
open: boolean;
workspace: WorkspaceInfo | null;
@@ -58,10 +48,6 @@ export default function WorkspaceSwitchOverlay(props: {
return props.workspace.directory?.trim() ?? "";
});
const Icon = createMemo(() =>
iconForWorkspace(props.workspace?.preset ?? "starter", props.workspace?.workspaceType ?? "local")
);
return (
<Show when={props.open}>
<div class="fixed inset-0 z-[60] overflow-hidden bg-gray-1 text-gray-12 motion-safe:animate-in motion-safe:fade-in motion-safe:duration-300">
@@ -99,8 +85,8 @@ export default function WorkspaceSwitchOverlay(props: {
class="absolute -inset-1 rounded-full border border-gray-6/30 motion-safe:animate-spin motion-reduce:opacity-60"
style={{ "animation-duration": "9s", "animation-direction": "reverse" }}
/>
<div class="relative h-24 w-24 rounded-3xl bg-gray-2/80 border border-gray-6/70 shadow-2xl flex items-center justify-center text-gray-12">
<Dynamic component={Icon()} size={26} />
<div class="relative h-24 w-24 rounded-3xl bg-gray-1/90 border border-gray-5/60 shadow-2xl flex items-center justify-center">
<OpenWorkLogo size={44} class="drop-shadow-sm" />
</div>
</div>

View File

@@ -408,8 +408,8 @@ export function createWorkspaceStore(options: {
options.refreshSkills({ force: true }).catch(() => undefined);
if (!options.selectedSessionId()) {
options.setView("dashboard");
options.setTab("home");
options.setView("dashboard");
}
// If the user successfully connected, treat onboarding as complete so we
@@ -466,8 +466,8 @@ export function createWorkspaceStore(options: {
setWorkspacePickerOpen(false);
setCreateWorkspaceOpen(false);
options.setView("dashboard");
options.setTab("home");
options.setView("dashboard");
markOnboardingComplete();
} catch (e) {
const message = e instanceof Error ? e.message : safeStringify(e);
@@ -484,11 +484,6 @@ export function createWorkspaceStore(options: {
directory?: string | null;
displayName?: string | null;
}) {
if (!isTauriRuntime()) {
options.setError(t("app.error.tauri_required", currentLocale()));
return false;
}
const baseUrl = input.baseUrl.trim();
if (!baseUrl) {
options.setError(t("app.error.remote_base_url_required", currentLocale()));
@@ -518,14 +513,37 @@ export function createWorkspaceStore(options: {
options.setBusyLabel("status.creating_workspace");
options.setBusyStartedAt(Date.now());
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
const displayName = input.displayName?.trim() || null;
const workspaceId = `remote:${normalizedBaseUrl}:${resolvedDirectory}`;
try {
const ws = await workspaceCreateRemote({
baseUrl,
directory: resolvedDirectory ? resolvedDirectory : null,
displayName: input.displayName?.trim() ? input.displayName.trim() : null,
});
setWorkspaces(ws.workspaces);
syncActiveWorkspaceId(ws.activeId);
if (isTauriRuntime()) {
const ws = await workspaceCreateRemote({
baseUrl: normalizedBaseUrl,
directory: resolvedDirectory ? resolvedDirectory : null,
displayName,
});
setWorkspaces(ws.workspaces);
syncActiveWorkspaceId(ws.activeId);
} else {
const nextWorkspace: WorkspaceInfo = {
id: workspaceId,
name: displayName ?? normalizedBaseUrl,
path: "",
preset: "remote",
workspaceType: "remote",
baseUrl: normalizedBaseUrl,
directory: resolvedDirectory || null,
displayName,
};
setWorkspaces((prev) => {
const withoutMatch = prev.filter((workspace) => workspace.id !== workspaceId);
return [...withoutMatch, nextWorkspace];
});
syncActiveWorkspaceId(workspaceId);
}
setProjectDir(resolvedDirectory);
setWorkspaceConfig(null);

View File

@@ -34,7 +34,7 @@ export type DashboardViewProps = {
tab: DashboardTab;
setTab: (tab: DashboardTab) => void;
view: "dashboard" | "session" | "onboarding";
setView: (view: "dashboard" | "session" | "onboarding") => void;
setView: (view: "dashboard" | "session" | "onboarding", sessionId?: string) => void;
mode: "host" | "client" | null;
baseUrl: string;
clientConnected: boolean;
@@ -211,9 +211,9 @@ export default function DashboardView(props: DashboardViewProps) {
const openSessionFromList = (sessionId: string) => {
// Defer view switch to avoid click-through on the same event frame.
window.setTimeout(() => {
props.setView("session");
props.setTab("sessions");
void props.selectSession(sessionId);
props.setTab("sessions");
props.setView("session", sessionId);
}, 0);
};

View File

@@ -33,7 +33,7 @@ import FlyoutItem from "../components/flyout-item";
export type SessionViewProps = {
selectedSessionId: string | null;
setView: (view: View) => void;
setView: (view: View, sessionId?: string) => void;
setTab: (tab: DashboardTab) => void;
activeWorkspaceDisplay: WorkspaceDisplay;
setWorkspaceSearch: (value: string) => void;
@@ -655,8 +655,8 @@ export default function SessionView(props: SessionViewProps) {
<div class="text-lg font-medium">No session selected</div>
<Button
onClick={() => {
props.setView("dashboard");
props.setTab("sessions");
props.setView("dashboard");
}}
>
Back to dashboard
@@ -672,8 +672,8 @@ export default function SessionView(props: SessionViewProps) {
variant="ghost"
class="!p-2 rounded-full"
onClick={() => {
props.setView("dashboard");
props.setTab("sessions");
props.setView("dashboard");
}}
>
<ArrowRight class="rotate-180 w-5 h-5" />
@@ -713,11 +713,11 @@ export default function SessionView(props: SessionViewProps) {
}}
sessions={props.sessions}
selectedSessionId={props.selectedSessionId}
onSelectSession={async (id) => {
await props.selectSession(id);
props.setView("session");
props.setTab("sessions");
}}
onSelectSession={async (id) => {
await props.selectSession(id);
props.setView("session", id);
props.setTab("sessions");
}}
sessionStatusById={props.sessionStatusById}
onCreateSession={props.createSessionAndOpen}
newTaskDisabled={props.newTaskDisabled}

View File

@@ -17,7 +17,7 @@ export function createTemplateState(options: {
setSessionModelById: (value: Record<string, ModelRef> | ((current: Record<string, ModelRef>) => Record<string, ModelRef>)) => void;
defaultModel: Accessor<ModelRef>;
modelVariant: Accessor<string | null>;
setView: (view: "onboarding" | "dashboard" | "session") => void;
setView: (view: "onboarding" | "dashboard" | "session", sessionId?: string) => void;
isDemoMode: Accessor<boolean>;
activeWorkspaceRoot: Accessor<string>;
setBusy: (value: boolean) => void;

View File

@@ -1,5 +1,6 @@
/* @refresh reload */
import { render } from "solid-js/web";
import { HashRouter, Route, Router } from "@solidjs/router";
import { bootstrapTheme } from "./app/theme";
import "./app/index.css";
@@ -15,6 +16,8 @@ if (!root) {
throw new Error("Root element not found");
}
const RouterComponent = isTauriRuntime() ? HashRouter : Router;
const platform: Platform = {
platform: isTauriRuntime() ? "desktop" : "web",
openLink(url: string) {
@@ -79,7 +82,9 @@ const platform: Platform = {
render(
() => (
<PlatformProvider value={platform}>
<AppEntry />
<RouterComponent root={AppEntry}>
<Route path="*all" component={() => null} />
</RouterComponent>
</PlatformProvider>
),
root,

View File

@@ -0,0 +1,20 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 2acf9f2213ac91f71bf844c53bf63be28ce7d79d..49eac35140e3e004963ad5f9b2ffe315a740339f 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,4 +1,5 @@
export * from "./routers/index.js";
+export * from "./routers/index.js";
export * from "./components.jsx";
export * from "./lifecycle.js";
export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
diff --git a/dist/index.jsx b/dist/index.jsx
index c9079ba9cf5244de3c4a3fe938d33c67ab13fc00..305f052987ae0b2aec0edb163fec6d0dcf663581 100644
--- a/dist/index.jsx
+++ b/dist/index.jsx
@@ -1,4 +1,5 @@
export * from "./routers/index.js";
+export * from "./routers/index.js";
export * from "./components.jsx";
export * from "./lifecycle.js";
export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";

17
pnpm-lock.yaml generated
View File

@@ -4,6 +4,11 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
'@solidjs/router@0.15.4':
hash: 1db11a7c28fe4da76187d42efaffc6b9a70ad370462fffb794ff90e67744d770
path: patches/@solidjs__router@0.15.4.patch
importers:
.: {}
@@ -22,6 +27,9 @@ importers:
'@solid-primitives/storage':
specifier: ^4.3.3
version: 4.3.3(solid-js@1.9.10)
'@solidjs/router':
specifier: ^0.15.4
version: 0.15.4(patch_hash=1db11a7c28fe4da76187d42efaffc6b9a70ad370462fffb794ff90e67744d770)(solid-js@1.9.10)
'@tauri-apps/api':
specifier: ^2.0.0
version: 2.9.1
@@ -892,6 +900,11 @@ packages:
peerDependencies:
solid-js: ^1.6.12
'@solidjs/router@0.15.4':
resolution: {integrity: sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==}
peerDependencies:
solid-js: ^1.8.6
'@tailwindcss/node@4.1.18':
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
@@ -2353,6 +2366,10 @@ snapshots:
dependencies:
solid-js: 1.9.10
'@solidjs/router@0.15.4(patch_hash=1db11a7c28fe4da76187d42efaffc6b9a70ad370462fffb794ff90e67744d770)(solid-js@1.9.10)':
dependencies:
solid-js: 1.9.10
'@tailwindcss/node@4.1.18':
dependencies:
'@jridgewell/remapping': 2.3.5