ikanban-web 0.2.10 → 0.2.11

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.
package/dist/index.html CHANGED
@@ -27,8 +27,8 @@
27
27
  <meta property="og:image" content="/ikanban/social-share.png" />
28
28
  <meta property="twitter:image" content="/social-share.png" />
29
29
  <script id="oc-theme-preload-script" src="/ikanban/oc-theme-preload.js"></script>
30
- <script type="module" crossorigin src="/ikanban/assets/index-n2BMSLCz.js"></script>
31
- <link rel="stylesheet" crossorigin href="/ikanban/assets/index-DbuHW0gF.css">
30
+ <script type="module" crossorigin src="/ikanban/assets/index-CE2ZqGRp.js"></script>
31
+ <link rel="stylesheet" crossorigin href="/ikanban/assets/index-Z4u1EVXA.css">
32
32
  </head>
33
33
  <body class="antialiased overscroll-none text-12-regular overflow-hidden">
34
34
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikanban-web",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "iKanban web app",
5
5
  "private": false,
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -1,43 +1,54 @@
1
- import "@/index.css"
2
- import { File } from "ikanban-ui/file"
3
- import { I18nProvider } from "ikanban-ui/context"
4
- import { DialogProvider } from "ikanban-ui/context/dialog"
5
- import { FileComponentProvider } from "ikanban-ui/context/file"
6
- import { MarkedProvider } from "ikanban-ui/context/marked"
7
- import { Font } from "ikanban-ui/font"
8
- import { ThemeProvider } from "ikanban-ui/theme"
9
- import { MetaProvider } from "@solidjs/meta"
10
- import { Navigate, Route, Router } from "@solidjs/router"
11
- import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
12
- import { CommandProvider } from "@/context/command"
13
- import { CommentsProvider } from "@/context/comments"
14
- import { FileProvider } from "@/context/file"
15
- import { GlobalSDKProvider } from "@/context/global-sdk"
16
- import { GlobalSyncProvider } from "@/context/global-sync"
17
- import { HighlightsProvider } from "@/context/highlights"
18
- import { LanguageProvider, useLanguage } from "@/context/language"
19
- import { LayoutProvider } from "@/context/layout"
20
- import { ModelsProvider } from "@/context/models"
21
- import { NotificationProvider } from "@/context/notification"
22
- import { PermissionProvider } from "@/context/permission"
23
- import { usePlatform } from "@/context/platform"
24
- import { PromptProvider } from "@/context/prompt"
25
- import { type ServerConnection, ServerProvider, useServer } from "@/context/server"
26
- import { SettingsProvider } from "@/context/settings"
27
- import { TerminalProvider } from "@/context/terminal"
28
- import DirectoryLayout from "@/pages/directory-layout"
29
- import Layout from "@/pages/layout"
30
- import { ErrorPage } from "./pages/error"
1
+ import "@/index.css";
2
+ import { File } from "ikanban-ui/file";
3
+ import { I18nProvider } from "ikanban-ui/context";
4
+ import { DialogProvider } from "ikanban-ui/context/dialog";
5
+ import { FileComponentProvider } from "ikanban-ui/context/file";
6
+ import { MarkedProvider } from "ikanban-ui/context/marked";
7
+ import { Font } from "ikanban-ui/font";
8
+ import { ThemeProvider } from "ikanban-ui/theme";
9
+ import { MetaProvider } from "@solidjs/meta";
10
+ import { Route, Router } from "@solidjs/router";
11
+ import {
12
+ ErrorBoundary,
13
+ type JSX,
14
+ lazy,
15
+ type ParentProps,
16
+ Show,
17
+ Suspense,
18
+ } from "solid-js";
19
+ import { CommandProvider } from "@/context/command";
20
+ import { CommentsProvider } from "@/context/comments";
21
+ import { FileProvider } from "@/context/file";
22
+ import { GlobalSDKProvider } from "@/context/global-sdk";
23
+ import { GlobalSyncProvider } from "@/context/global-sync";
24
+ import { HighlightsProvider } from "@/context/highlights";
25
+ import { LanguageProvider, useLanguage } from "@/context/language";
26
+ import { LayoutProvider } from "@/context/layout";
27
+ import { ModelsProvider } from "@/context/models";
28
+ import { NotificationProvider } from "@/context/notification";
29
+ import { PermissionProvider } from "@/context/permission";
30
+ import { usePlatform } from "@/context/platform";
31
+ import { PromptProvider } from "@/context/prompt";
32
+ import {
33
+ type ServerConnection,
34
+ ServerProvider,
35
+ useServer,
36
+ } from "@/context/server";
37
+ import { SettingsProvider } from "@/context/settings";
38
+ import { TerminalProvider } from "@/context/terminal";
39
+ import DirectoryLayout from "@/pages/directory-layout";
40
+ import Layout from "@/pages/layout";
41
+ import { ErrorPage } from "./pages/error";
31
42
 
32
- const Home = lazy(() => import("@/pages/home"))
33
- const Session = lazy(() => import("@/pages/session"))
34
- const Loading = () => <div class="size-full" />
43
+ const Home = lazy(() => import("@/pages/home"));
44
+ const Session = lazy(() => import("@/pages/session"));
45
+ const Loading = () => <div class="size-full" />;
35
46
 
36
47
  const HomeRoute = () => (
37
48
  <Suspense fallback={<Loading />}>
38
49
  <Home />
39
50
  </Suspense>
40
- )
51
+ );
41
52
 
42
53
  const SessionRoute = () => (
43
54
  <SessionProviders>
@@ -45,32 +56,36 @@ const SessionRoute = () => (
45
56
  <Session />
46
57
  </Suspense>
47
58
  </SessionProviders>
48
- )
59
+ );
49
60
 
50
- const SessionIndexRoute = () => (
51
- <Suspense fallback={<Loading />}>
52
- <Home />
53
- </Suspense>
54
- )
61
+ const SessionIndexRoute = () => <SessionRoute />;
55
62
 
56
63
  function UiI18nBridge(props: ParentProps) {
57
- const language = useLanguage()
58
- return <I18nProvider value={{ locale: language.locale, t: language.t }}>{props.children}</I18nProvider>
64
+ const language = useLanguage();
65
+ return (
66
+ <I18nProvider value={{ locale: language.locale, t: language.t }}>
67
+ {props.children}
68
+ </I18nProvider>
69
+ );
59
70
  }
60
71
 
61
72
  declare global {
62
73
  interface Window {
63
74
  __OPENCODE__?: {
64
- updaterEnabled?: boolean
65
- deepLinks?: string[]
66
- wsl?: boolean
67
- }
75
+ updaterEnabled?: boolean;
76
+ deepLinks?: string[];
77
+ wsl?: boolean;
78
+ };
68
79
  }
69
80
  }
70
81
 
71
82
  function MarkedProviderWithNativeParser(props: ParentProps) {
72
- const platform = usePlatform()
73
- return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
83
+ const platform = usePlatform();
84
+ return (
85
+ <MarkedProvider nativeParser={platform.parseMarkdown}>
86
+ {props.children}
87
+ </MarkedProvider>
88
+ );
74
89
  }
75
90
 
76
91
  function AppShellProviders(props: ParentProps) {
@@ -90,7 +105,7 @@ function AppShellProviders(props: ParentProps) {
90
105
  </LayoutProvider>
91
106
  </PermissionProvider>
92
107
  </SettingsProvider>
93
- )
108
+ );
94
109
  }
95
110
 
96
111
  function SessionProviders(props: ParentProps) {
@@ -102,7 +117,7 @@ function SessionProviders(props: ParentProps) {
102
117
  </PromptProvider>
103
118
  </FileProvider>
104
119
  </TerminalProvider>
105
- )
120
+ );
106
121
  }
107
122
 
108
123
  function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
@@ -111,7 +126,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
111
126
  {props.appChildren}
112
127
  {props.children}
113
128
  </AppShellProviders>
114
- )
129
+ );
115
130
  }
116
131
 
117
132
  export function AppBaseProviders(props: ParentProps) {
@@ -124,7 +139,9 @@ export function AppBaseProviders(props: ParentProps) {
124
139
  <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
125
140
  <DialogProvider>
126
141
  <MarkedProviderWithNativeParser>
127
- <FileComponentProvider component={File}>{props.children}</FileComponentProvider>
142
+ <FileComponentProvider component={File}>
143
+ {props.children}
144
+ </FileComponentProvider>
128
145
  </MarkedProviderWithNativeParser>
129
146
  </DialogProvider>
130
147
  </ErrorBoundary>
@@ -132,22 +149,22 @@ export function AppBaseProviders(props: ParentProps) {
132
149
  </LanguageProvider>
133
150
  </ThemeProvider>
134
151
  </MetaProvider>
135
- )
152
+ );
136
153
  }
137
154
 
138
155
  function ServerKey(props: ParentProps) {
139
- const server = useServer()
156
+ const server = useServer();
140
157
  return (
141
158
  <Show when={server.key} keyed>
142
159
  {props.children}
143
160
  </Show>
144
- )
161
+ );
145
162
  }
146
163
 
147
164
  export function AppInterface(props: {
148
- children?: JSX.Element
149
- defaultServer: ServerConnection.Key
150
- servers?: Array<ServerConnection.Any>
165
+ children?: JSX.Element;
166
+ defaultServer: ServerConnection.Key;
167
+ servers?: Array<ServerConnection.Any>;
151
168
  }) {
152
169
  return (
153
170
  <ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
@@ -156,17 +173,21 @@ export function AppInterface(props: {
156
173
  <GlobalSyncProvider>
157
174
  <Router
158
175
  base={(import.meta.env.BASE_URL ?? "/").replace(/\/+$/, "")}
159
- root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
176
+ root={(routerProps) => (
177
+ <RouterRoot appChildren={props.children}>
178
+ {routerProps.children}
179
+ </RouterRoot>
180
+ )}
160
181
  >
161
182
  <Route path="/" component={HomeRoute} />
162
183
  <Route path="/:dir" component={DirectoryLayout}>
163
184
  <Route path="/" component={SessionIndexRoute} />
164
- <Route path="/session/:id?" component={SessionRoute} />
185
+ <Route path="/:id?" component={SessionRoute} />
165
186
  </Route>
166
187
  </Router>
167
188
  </GlobalSyncProvider>
168
189
  </GlobalSDKProvider>
169
190
  </ServerKey>
170
191
  </ServerProvider>
171
- )
192
+ );
172
193
  }
@@ -0,0 +1,70 @@
1
+ import { useDialog } from "ikanban-ui/context/dialog"
2
+ import { Dialog } from "ikanban-ui/dialog"
3
+ import { Keybind } from "ikanban-ui/keybind"
4
+ import { List } from "ikanban-ui/list"
5
+ import { createMemo, onCleanup, Show } from "solid-js"
6
+ import { type CommandOption, formatKeybind, useCommand } from "@/context/command"
7
+ import { useLanguage } from "@/context/language"
8
+
9
+ export function DialogCommandPalette() {
10
+ const command = useCommand()
11
+ const dialog = useDialog()
12
+ const language = useLanguage()
13
+ const state = {
14
+ cleanup: undefined as (() => void) | void,
15
+ committed: false,
16
+ }
17
+
18
+ const items = createMemo(() =>
19
+ command.options.filter((option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open"),
20
+ )
21
+
22
+ const handleMove = (item: CommandOption | undefined) => {
23
+ state.cleanup?.()
24
+ state.cleanup = item?.onHighlight?.()
25
+ }
26
+
27
+ onCleanup(() => {
28
+ if (state.committed) return
29
+ state.cleanup?.()
30
+ })
31
+
32
+ return (
33
+ <Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
34
+ <List
35
+ search={{
36
+ placeholder: language.t("palette.search.placeholder"),
37
+ autofocus: true,
38
+ hideIcon: true,
39
+ }}
40
+ emptyMessage={language.t("palette.empty")}
41
+ items={items}
42
+ key={(item) => item.id}
43
+ filterKeys={["title", "description", "category"]}
44
+ groupBy={(item) => item.category ?? language.t("palette.group.commands")}
45
+ onMove={handleMove}
46
+ onSelect={(item) => {
47
+ if (!item) return
48
+ state.committed = true
49
+ state.cleanup = undefined
50
+ dialog.close()
51
+ item.onSelect?.("palette")
52
+ }}
53
+ >
54
+ {(item) => (
55
+ <div class="w-full flex items-center justify-between gap-4">
56
+ <div class="flex items-center gap-2 min-w-0">
57
+ <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
58
+ <Show when={item.description}>
59
+ <span class="text-14-regular text-text-weak truncate">{item.description}</span>
60
+ </Show>
61
+ </div>
62
+ <Show when={item.keybind}>
63
+ <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
64
+ </Show>
65
+ </div>
66
+ )}
67
+ </List>
68
+ </Dialog>
69
+ )
70
+ }
@@ -75,7 +75,7 @@ export const DialogFork: Component = () => {
75
75
  return
76
76
  }
77
77
  dialog.close()
78
- navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
78
+ navigate(`/${base64Encode(sdk.directory)}/${forked.data.id}`)
79
79
  requestAnimationFrame(() => {
80
80
  prompt.set(restored)
81
81
  })
@@ -365,7 +365,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
365
365
 
366
366
  if (item.type === "session") {
367
367
  if (!item.directory || !item.sessionID) return
368
- navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
368
+ navigate(`/${base64Encode(item.directory)}/${item.sessionID}`)
369
369
  return
370
370
  }
371
371
 
@@ -203,7 +203,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
203
203
  if (session) {
204
204
  if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
205
205
  layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
206
- navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
206
+ navigate(`/${base64Encode(sessionDirectory)}/${session.id}`)
207
207
  }
208
208
  }
209
209
  if (!session) {
@@ -192,6 +192,17 @@ export function Titlebar() {
192
192
  </div>
193
193
  </Show>
194
194
  <div class="flex items-center gap-1 shrink-0">
195
+ <Show when={params.dir}>
196
+ <Tooltip placement="bottom" value={language.t("home.title")} openDelay={2000}>
197
+ <Button
198
+ variant="ghost"
199
+ icon="arrow-left"
200
+ class="titlebar-icon w-8 h-6 p-0 box-border"
201
+ onClick={() => navigate("/")}
202
+ aria-label={language.t("home.title")}
203
+ />
204
+ </Tooltip>
205
+ </Show>
195
206
  <TooltipKeybind
196
207
  class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
197
208
  placement="bottom"
@@ -234,7 +245,7 @@ export function Titlebar() {
234
245
  class="titlebar-icon w-8 h-6 p-0 box-border"
235
246
  onClick={() => {
236
247
  if (!params.dir) return
237
- navigate(`/${params.dir}/session`)
248
+ navigate(`/${params.dir}`)
238
249
  }}
239
250
  aria-label={language.t("command.session.new")}
240
251
  />
@@ -245,7 +245,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
245
245
  session: sessionID,
246
246
  })
247
247
 
248
- const href = `/${base64Encode(directory)}/session/${sessionID}`
248
+ const href = `/${base64Encode(directory)}/${sessionID}`
249
249
  if (settings.notifications.agent()) {
250
250
  void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href)
251
251
  }
@@ -278,7 +278,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
278
278
  const description =
279
279
  session?.title ??
280
280
  (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
281
- const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
281
+ const href = sessionID ? `/${base64Encode(directory)}/${sessionID}` : `/${base64Encode(directory)}`
282
282
  if (settings.notifications.errors()) {
283
283
  void platform.notify(language.t("notification.session.error.title"), description, href)
284
284
  }
@@ -19,8 +19,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
19
19
  <DataProvider
20
20
  data={sync.data}
21
21
  directory={props.directory}
22
- onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)}
23
- onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`}
22
+ onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/${sessionID}`)}
23
+ onSessionHref={(sessionID: string) => `/${params.dir}/${sessionID}`}
24
24
  >
25
25
  <LocalProvider>{props.children}</LocalProvider>
26
26
  </DataProvider>
@@ -1,4 +1,4 @@
1
- import { createMemo, createEffect, For, Match, Switch } from "solid-js";
1
+ import { createMemo, createEffect, createResource, For, Match, Switch } from "solid-js";
2
2
  import { Button } from "ikanban-ui/button";
3
3
  import { Logo } from "ikanban-ui/logo";
4
4
  import { useLayout } from "@/context/layout";
@@ -15,7 +15,6 @@ import { useServer } from "@/context/server";
15
15
  import { useGlobalSync } from "@/context/global-sync";
16
16
  import { useLanguage } from "@/context/language";
17
17
  import { IconButton } from "ikanban-ui/icon-button";
18
- import { sortedRootSessions } from "@/pages/layout/helpers";
19
18
  import type { Session } from "@opencode-ai/sdk/v2/client";
20
19
 
21
20
  type BoardColumn = "progress" | "idle";
@@ -26,6 +25,34 @@ type BoardCard = {
26
25
  updatedAt: number;
27
26
  };
28
27
 
28
+ const homeStyles = {
29
+ border: { border: "1px solid var(--border-weak-base)" },
30
+ heroIcon: {
31
+ border: "1px solid var(--border-weak-base)",
32
+ background:
33
+ "linear-gradient(135deg, color-mix(in srgb, var(--text-interactive-base) 12%, var(--background-stronger)), var(--background-stronger))",
34
+ },
35
+ emptyPanel: {
36
+ "border-color": "var(--border-weak-base)",
37
+ background:
38
+ "linear-gradient(180deg, color-mix(in srgb, var(--surface-base-hover) 35%, var(--background-stronger)), var(--background-stronger))",
39
+ },
40
+ emptyIcon: {
41
+ border: "1px solid var(--border-weak-base)",
42
+ background:
43
+ "linear-gradient(135deg, color-mix(in srgb, var(--text-interactive-base) 10%, var(--surface-inset-base)), var(--surface-inset-base))",
44
+ },
45
+ card: {
46
+ border: "1px solid var(--border-weak-base)",
47
+ background: "var(--background-base)",
48
+ },
49
+ emptyState: {
50
+ "border-color": "var(--border-weak-base)",
51
+ background:
52
+ "linear-gradient(180deg, color-mix(in srgb, var(--surface-base-hover) 30%, var(--surface-inset-base)), var(--surface-inset-base))",
53
+ },
54
+ } as const;
55
+
29
56
  export default function Home() {
30
57
  const sync = useGlobalSync();
31
58
  const layout = useLayout();
@@ -35,26 +62,49 @@ export default function Home() {
35
62
  const globalSDK = useGlobalSDK();
36
63
  const server = useServer();
37
64
  const language = useLanguage();
38
- const recent = createMemo(() => {
65
+ const projects = createMemo(() => {
39
66
  return sync.data.project
40
67
  .slice()
41
68
  .sort(
42
69
  (a, b) =>
43
70
  (b.time.updated ?? b.time.created) -
44
71
  (a.time.updated ?? a.time.created),
45
- )
46
- .slice(0, 5);
72
+ );
47
73
  });
74
+
75
+ const [rootSessions, { mutate: mutateRootSessions }] = createResource(
76
+ () => projects().map((project) => project.worktree),
77
+ async (directories) => {
78
+ const entries = await Promise.all(
79
+ directories.map(async (directory) => {
80
+ const result = await globalSDK.client.session.list({
81
+ directory,
82
+ roots: true,
83
+ });
84
+
85
+ const sessions = (result.data ?? []).filter(
86
+ (session): session is Session => !!session?.id && !session.time?.archived,
87
+ );
88
+
89
+ return [directory, sessions] as const;
90
+ }),
91
+ );
92
+
93
+ return Object.fromEntries(entries) as Record<string, Session[]>;
94
+ },
95
+ );
96
+
48
97
  const boardColumns = createMemo<Record<BoardColumn, BoardCard[]>>(() => {
49
98
  const now = Date.now();
50
99
  const columns: Record<BoardColumn, BoardCard[]> = {
51
100
  progress: [],
52
101
  idle: [],
53
102
  };
103
+ const sessionsByProject = rootSessions() ?? {};
54
104
 
55
- for (const project of recent()) {
105
+ for (const project of projects()) {
56
106
  const [store] = sync.child(project.worktree);
57
- const sessions = sortedRootSessions(store, now);
107
+ const sessions = sessionsByProject[project.worktree] ?? [];
58
108
 
59
109
  for (const session of sessions) {
60
110
  const status = store.session_status[session.id];
@@ -82,7 +132,7 @@ export default function Home() {
82
132
  });
83
133
 
84
134
  createEffect(() => {
85
- for (const project of recent()) {
135
+ for (const project of projects()) {
86
136
  sync.child(project.worktree);
87
137
  }
88
138
  });
@@ -97,13 +147,13 @@ export default function Home() {
97
147
  function openProject(directory: string) {
98
148
  layout.projects.open(directory);
99
149
  server.projects.touch(directory);
100
- navigate(`/${base64Encode(directory)}/session`);
150
+ navigate(`/${base64Encode(directory)}`);
101
151
  }
102
152
 
103
153
  function openSession(directory: string, sessionID: string) {
104
154
  layout.projects.open(directory);
105
155
  server.projects.touch(directory);
106
- navigate(`/${base64Encode(directory)}/session/${sessionID}`);
156
+ navigate(`/${base64Encode(directory)}/${sessionID}`);
107
157
  }
108
158
 
109
159
  async function archiveSession(directory: string, sessionID: string) {
@@ -112,6 +162,13 @@ export default function Home() {
112
162
  sessionID,
113
163
  time: { archived: Date.now() },
114
164
  });
165
+ mutateRootSessions((current) => {
166
+ if (!current?.[directory]) return current;
167
+ return {
168
+ ...current,
169
+ [directory]: current[directory].filter((session) => session.id !== sessionID),
170
+ };
171
+ });
115
172
  setStore(
116
173
  "session",
117
174
  (sessions) => sessions.filter((session) => session.id !== sessionID),
@@ -148,7 +205,10 @@ export default function Home() {
148
205
  <div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
149
206
  <div class="min-w-0">
150
207
  <div class="flex items-center gap-3">
151
- <div class="flex size-10 items-center justify-center rounded-2xl border border-border-weak bg-background-base/80">
208
+ <div
209
+ class="flex size-10 items-center justify-center rounded-2xl border shadow-xs-border-base"
210
+ style={homeStyles.heroIcon}
211
+ >
152
212
  <Logo class="w-6 opacity-90" />
153
213
  </div>
154
214
  <div>
@@ -189,7 +249,7 @@ export default function Home() {
189
249
  </div>
190
250
 
191
251
  <Switch>
192
- <Match when={sync.data.project.length > 0}>
252
+ <Match when={projects().length > 0}>
193
253
  <div class="mt-6 flex w-full flex-col gap-6">
194
254
  <section class="flex flex-col gap-4">
195
255
  <div class="grid gap-3 lg:grid-cols-2">
@@ -216,9 +276,15 @@ export default function Home() {
216
276
  </div>
217
277
  </Match>
218
278
  <Match when={true}>
219
- <div class="mt-6 rounded-2xl border border-dashed border-border-weak bg-background-base/60 px-6 py-10">
279
+ <div
280
+ class="mt-6 rounded-2xl border border-dashed px-6 py-10 shadow-xs-border-base"
281
+ style={homeStyles.emptyPanel}
282
+ >
220
283
  <div class="mx-auto flex max-w-md flex-col items-center gap-3 text-center">
221
- <div class="flex size-12 items-center justify-center rounded-2xl border border-border-weak bg-background-base/80">
284
+ <div
285
+ class="flex size-12 items-center justify-center rounded-2xl border shadow-xs-border-base"
286
+ style={homeStyles.emptyIcon}
287
+ >
222
288
  <Icon name="folder-add-left" size="large" />
223
289
  </div>
224
290
  <div class="flex flex-col gap-1 items-center justify-center">
@@ -251,12 +317,25 @@ function BoardColumnView(props: {
251
317
  }) {
252
318
  const toneClass = () => {
253
319
  if (props.tone === "progress")
254
- return "bg-emerald-500/12 text-emerald-300 border-emerald-500/20";
255
- return "bg-sky-500/12 text-sky-300 border-sky-500/20";
320
+ return "bg-surface-base-hover text-icon-success-base border-border-weak-base";
321
+ return "bg-surface-inset-base text-icon-base border-border-weak-base";
256
322
  };
257
323
 
324
+ const sectionStyle = () =>
325
+ props.tone === "progress"
326
+ ? {
327
+ ...homeStyles.border,
328
+ background:
329
+ "linear-gradient(180deg, color-mix(in srgb, var(--text-interactive-base) 8%, var(--background-stronger)), var(--background-stronger))",
330
+ }
331
+ : {
332
+ ...homeStyles.border,
333
+ background:
334
+ "linear-gradient(180deg, color-mix(in srgb, var(--surface-inset-base) 55%, var(--background-stronger)), var(--background-stronger))",
335
+ };
336
+
258
337
  return (
259
- <section class="rounded-2xl border border-border-weak bg-background-base/80 p-3 md:p-4 min-h-72">
338
+ <section class="rounded-2xl p-3 md:p-4 min-h-72 shadow-xs-border-base" style={sectionStyle()}>
260
339
  <div class="flex items-center justify-between gap-3">
261
340
  <div class="flex items-center gap-2 min-w-0">
262
341
  <div
@@ -274,7 +353,10 @@ function BoardColumnView(props: {
274
353
  <Match when={props.cards.length > 0}>
275
354
  <For each={props.cards}>
276
355
  {(card) => (
277
- <div class="overflow-hidden rounded-xl border border-border-weak bg-background-base/70 hover:border-border-strong">
356
+ <div
357
+ class="overflow-hidden rounded-xl transition-colors"
358
+ style={homeStyles.card}
359
+ >
278
360
  <div class="flex items-start justify-between gap-3 px-3 pt-3">
279
361
  <div class="min-w-0 flex-1 pt-1">
280
362
  <div class="truncate text-14-medium leading-tight text-text-strong">
@@ -320,7 +402,10 @@ function BoardColumnView(props: {
320
402
  </For>
321
403
  </Match>
322
404
  <Match when={true}>
323
- <div class="rounded-xl border border-dashed border-border-weak px-3 py-8 text-center text-12-regular text-text-weak">
405
+ <div
406
+ class="rounded-xl border border-dashed px-3 py-8 text-center text-12-regular text-text-weak"
407
+ style={homeStyles.emptyState}
408
+ >
324
409
  {props.empty}
325
410
  </div>
326
411
  </Match>