@shipfox/client-projects 17.0.0 → 22.0.0

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 (58) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +42 -0
  3. package/dist/components/source-strip.d.ts.map +1 -1
  4. package/dist/components/source-strip.js +49 -45
  5. package/dist/components/source-strip.js.map +1 -1
  6. package/dist/pages/create-project-page.d.ts.map +1 -1
  7. package/dist/pages/create-project-page.js +161 -175
  8. package/dist/pages/create-project-page.js.map +1 -1
  9. package/dist/pages/create-project-page.stories.js +1 -1
  10. package/dist/pages/create-project-page.stories.js.map +1 -1
  11. package/dist/pages/project-settings-page.d.ts.map +1 -1
  12. package/dist/pages/project-settings-page.js +82 -84
  13. package/dist/pages/project-settings-page.js.map +1 -1
  14. package/dist/pages/project-settings-page.stories.js +1 -1
  15. package/dist/pages/project-settings-page.stories.js.map +1 -1
  16. package/dist/pages/projects-hub-page.d.ts.map +1 -1
  17. package/dist/pages/projects-hub-page.js +160 -159
  18. package/dist/pages/projects-hub-page.js.map +1 -1
  19. package/dist/pages/projects-hub-page.stories.d.ts +22 -0
  20. package/dist/pages/projects-hub-page.stories.d.ts.map +1 -0
  21. package/dist/pages/projects-hub-page.stories.js +214 -0
  22. package/dist/pages/projects-hub-page.stories.js.map +1 -0
  23. package/dist/routes/create-project.d.ts +3 -0
  24. package/dist/routes/create-project.js +3 -0
  25. package/dist/routes/create-project.js.map +1 -1
  26. package/dist/routes/home.d.ts +3 -0
  27. package/dist/routes/home.js +3 -0
  28. package/dist/routes/home.js.map +1 -1
  29. package/dist/routes/project-index.d.ts +3 -0
  30. package/dist/routes/project-index.d.ts.map +1 -1
  31. package/dist/routes/project-index.js +3 -0
  32. package/dist/routes/project-index.js.map +1 -1
  33. package/dist/routes/project-settings-index.d.ts +3 -0
  34. package/dist/routes/project-settings-index.d.ts.map +1 -1
  35. package/dist/routes/project-settings-index.js +3 -0
  36. package/dist/routes/project-settings-index.js.map +1 -1
  37. package/dist/routes/project-settings.d.ts +3 -0
  38. package/dist/routes/project-settings.js +3 -0
  39. package/dist/routes/project-settings.js.map +1 -1
  40. package/dist/tsconfig.test.tsbuildinfo +1 -1
  41. package/package.json +7 -7
  42. package/src/components/source-strip.tsx +29 -26
  43. package/src/pages/create-project-page.stories.tsx +1 -1
  44. package/src/pages/create-project-page.test.tsx +85 -1
  45. package/src/pages/create-project-page.tsx +165 -153
  46. package/src/pages/home-router.test.tsx +2 -1
  47. package/src/pages/project-settings-page.stories.tsx +1 -1
  48. package/src/pages/project-settings-page.test.tsx +10 -1
  49. package/src/pages/project-settings-page.tsx +88 -76
  50. package/src/pages/projects-hub-page.stories.tsx +203 -0
  51. package/src/pages/projects-hub-page.test.tsx +90 -10
  52. package/src/pages/projects-hub-page.tsx +136 -125
  53. package/src/routes/create-project.tsx +4 -1
  54. package/src/routes/home.tsx +1 -0
  55. package/src/routes/project-index.tsx +1 -0
  56. package/src/routes/project-settings-index.tsx +1 -0
  57. package/src/routes/project-settings.tsx +4 -1
  58. package/tsconfig.build.tsbuildinfo +1 -1
@@ -8,12 +8,20 @@ import {
8
8
  import {QueryLoadError} from '@shipfox/client-ui';
9
9
  import {Button} from '@shipfox/react-ui/button';
10
10
  import {Callout} from '@shipfox/react-ui/callout';
11
- import {Card} from '@shipfox/react-ui/card';
12
11
  import {EmptyState} from '@shipfox/react-ui/empty-state';
13
12
  import {Icon} from '@shipfox/react-ui/icon';
14
13
  import {Input} from '@shipfox/react-ui/input';
14
+ import {
15
+ Panel,
16
+ PanelActions,
17
+ PanelBody,
18
+ PanelCell,
19
+ PanelCellAction,
20
+ PanelGrid,
21
+ PanelHeader,
22
+ } from '@shipfox/react-ui/panel';
15
23
  import {Skeleton} from '@shipfox/react-ui/skeleton';
16
- import {Header, Text} from '@shipfox/react-ui/typography';
24
+ import {Text} from '@shipfox/react-ui/typography';
17
25
  import {Link, useNavigate} from '@tanstack/react-router';
18
26
  import {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';
19
27
  import type {Project} from '#core/project.js';
@@ -41,114 +49,118 @@ export function ProjectsHubPage({search = ''}: {search?: string}) {
41
49
 
42
50
  return (
43
51
  <div className="flex w-full flex-col gap-section">
44
- <header className="flex flex-col gap-group">
45
- <div className="flex items-start justify-between gap-section max-[640px]:flex-col">
46
- <Header variant="h2">Projects</Header>
47
- </div>
48
- <div className="flex items-center gap-cluster max-[640px]:flex-col max-[640px]:items-stretch">
49
- <Input
50
- type="search"
51
- value={search}
52
- onChange={(event) => {
53
- const next = event.target.value.trim();
54
- navigate({search: (next ? {search: next} : {}) as never, replace: true});
55
- }}
56
- placeholder="Search projects…"
57
- aria-label="Search projects"
58
- className="flex-1"
59
- iconLeft={<Icon name="searchLine" className="size-16" />}
60
- iconRight={
61
- isSearching ? (
62
- <Icon
63
- name="spinner"
64
- size={16}
65
- className="text-foreground-neutral-muted"
66
- aria-hidden="true"
67
- />
68
- ) : undefined
69
- }
70
- />
71
- <Button asChild iconLeft="addLine" className="shrink-0 max-[640px]:w-full">
72
- <Link to="/w/$workspaceSlug/projects/new" params={{workspaceSlug: workspace.slug}}>
73
- New project
74
- </Link>
75
- </Button>
76
- </div>
77
- </header>
78
-
79
52
  <ModelProviderReminderBanner workspaceId={workspace.id} />
80
53
 
81
- {isInitialLoading || (search && hasNoData && query.isFetching) ? <ProjectsSkeleton /> : null}
54
+ <section aria-label="Projects">
55
+ <Panel>
56
+ <PanelHeader className="flex-wrap max-[640px]:items-stretch">
57
+ <div className="min-w-0 flex-1 max-[640px]:basis-full">
58
+ <Input
59
+ type="search"
60
+ value={search}
61
+ onChange={(event) => {
62
+ const next = event.target.value.trim();
63
+ navigate({search: (next ? {search: next} : {}) as never, replace: true});
64
+ }}
65
+ placeholder="Search projects…"
66
+ aria-label="Search projects"
67
+ iconLeft={<Icon name="searchLine" className="size-16" />}
68
+ iconRight={
69
+ isSearching ? (
70
+ <Icon
71
+ name="spinner"
72
+ size={16}
73
+ className="text-foreground-neutral-muted"
74
+ aria-hidden="true"
75
+ />
76
+ ) : undefined
77
+ }
78
+ />
79
+ </div>
80
+ <PanelActions className="max-[640px]:ml-0 max-[640px]:w-full">
81
+ <Button asChild iconLeft="addLine" className="max-[640px]:w-full">
82
+ <Link to="/w/$workspaceSlug/projects/new" params={{workspaceSlug: workspace.slug}}>
83
+ New project
84
+ </Link>
85
+ </Button>
86
+ </PanelActions>
87
+ </PanelHeader>
82
88
 
83
- {query.isError && hasNoData ? <QueryLoadError query={query} subject="projects" /> : null}
89
+ <PanelBody>
90
+ {isInitialLoading || (search && hasNoData && query.isFetching) ? (
91
+ <ProjectsSkeleton />
92
+ ) : null}
84
93
 
85
- {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (
86
- <EmptyProjects workspaceSlug={workspace.slug} />
87
- ) : null}
94
+ {query.isError && hasNoData ? (
95
+ <QueryLoadError query={query} subject="projects" variant="panel" />
96
+ ) : null}
88
97
 
89
- {!query.isFetching && !query.isError && projects.length === 0 && search ? (
90
- <NoSearchResults
91
- search={search}
92
- onClear={() => navigate({search: {} as never, replace: true})}
93
- />
94
- ) : null}
98
+ {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (
99
+ <EmptyProjects workspaceSlug={workspace.slug} />
100
+ ) : null}
95
101
 
96
- {projects.length > 0 ? (
97
- <section className="flex flex-col gap-group" aria-label="Projects list">
98
- <ul className="grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1">
99
- {projects.map((project) => (
100
- <ProjectCard
101
- project={project}
102
- connection={connectionsById.get(project.source.connectionId)}
103
- connectionsResolved={connectionsQuery.isSuccess}
104
- connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}
105
- key={project.id}
106
- workspaceSlug={workspace.slug}
102
+ {!query.isFetching && !query.isError && projects.length === 0 && search ? (
103
+ <NoSearchResults
104
+ search={search}
105
+ onClear={() => navigate({search: {} as never, replace: true})}
107
106
  />
108
- ))}
109
- </ul>
110
- {query.error && query.data ? (
111
- <Callout role="alert" type="error">
112
- <Text size="sm">
113
- Could not load the next page. Existing projects are still shown.
114
- </Text>
115
- </Callout>
116
- ) : null}
117
- {query.hasNextPage ? (
118
- <div className="flex justify-center">
119
- <Button
120
- variant="secondary"
121
- isLoading={query.isFetchingNextPage}
122
- onClick={() => query.fetchNextPage()}
123
- >
124
- Load more
125
- </Button>
126
- </div>
127
- ) : null}
128
- </section>
129
- ) : null}
107
+ ) : null}
108
+
109
+ {projects.length > 0 ? (
110
+ <>
111
+ <PanelGrid aria-label="Projects list">
112
+ {projects.map((project) => (
113
+ <ProjectCell
114
+ project={project}
115
+ connection={connectionsById.get(project.source.connectionId)}
116
+ connectionsResolved={connectionsQuery.isSuccess}
117
+ connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}
118
+ key={project.id}
119
+ workspaceSlug={workspace.slug}
120
+ />
121
+ ))}
122
+ </PanelGrid>
123
+ {query.error && query.data ? (
124
+ <div className="border-t border-border-neutral-base p-panel-compact">
125
+ <Callout role="alert" type="error">
126
+ <Text size="sm">
127
+ Could not load the next page. Existing projects are still shown.
128
+ </Text>
129
+ </Callout>
130
+ </div>
131
+ ) : null}
132
+ {query.hasNextPage ? (
133
+ <div className="flex justify-center border-t border-border-neutral-base p-panel-compact">
134
+ <Button
135
+ variant="secondary"
136
+ isLoading={query.isFetchingNextPage}
137
+ onClick={() => query.fetchNextPage()}
138
+ >
139
+ Load more
140
+ </Button>
141
+ </div>
142
+ ) : null}
143
+ </>
144
+ ) : null}
145
+ </PanelBody>
146
+ </Panel>
147
+ </section>
130
148
  </div>
131
149
  );
132
150
  }
133
151
 
134
152
  function ProjectsSkeleton() {
135
153
  return (
136
- <ul
137
- role="status"
138
- aria-label="Loading projects"
139
- className="grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1"
140
- >
154
+ <PanelGrid role="status" aria-label="Loading projects">
141
155
  {[0, 1, 2, 3, 4, 5].map((row) => (
142
- <li key={row}>
143
- <Card className="h-full p-panel-compact">
144
- <div className="flex items-center gap-cluster">
145
- <Skeleton className="size-24 shrink-0" />
146
- <Skeleton className="h-16 w-1/2" />
147
- </div>
148
- </Card>
149
- </li>
156
+ <PanelCell key={row}>
157
+ <div className="flex items-center gap-cluster px-row py-row">
158
+ <Skeleton className="size-24 shrink-0" />
159
+ <Skeleton className="h-16 w-1/2" />
160
+ </div>
161
+ </PanelCell>
150
162
  ))}
151
- </ul>
163
+ </PanelGrid>
152
164
  );
153
165
  }
154
166
 
@@ -158,6 +170,7 @@ function EmptyProjects({workspaceSlug}: {workspaceSlug: string}) {
158
170
  icon="folderLine"
159
171
  title="Create your first project"
160
172
  description="Connect a repository-backed project to start building workflows."
173
+ variant="panel"
161
174
  action={
162
175
  <Button asChild iconRight="chevronRight">
163
176
  <Link to="/w/$workspaceSlug/projects/new" params={{workspaceSlug}}>
@@ -175,6 +188,7 @@ function NoSearchResults({search, onClear}: {search: string; onClear: () => void
175
188
  icon="searchLine"
176
189
  title={`No projects match “${search}”`}
177
190
  description="Try a different search, or clear it to see all projects."
191
+ variant="panel"
178
192
  action={
179
193
  <Button size="sm" variant="secondary" onClick={onClear}>
180
194
  Clear search
@@ -184,7 +198,7 @@ function NoSearchResults({search, onClear}: {search: string; onClear: () => void
184
198
  );
185
199
  }
186
200
 
187
- function ProjectCard({
201
+ function ProjectCell({
188
202
  project,
189
203
  connection,
190
204
  connectionsResolved,
@@ -203,34 +217,31 @@ function ProjectCard({
203
217
  const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;
204
218
 
205
219
  return (
206
- <li>
207
- <Link
208
- to="/w/$workspaceSlug/p/$projectSlug"
209
- params={{workspaceSlug, projectSlug: project.slug}}
210
- className="block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
211
- >
212
- <Card className="h-full p-panel-compact transition-colors hover:bg-background-components-hover">
213
- <div className="flex min-w-0 items-center gap-cluster">
214
- {/* Settle on success or error: a failed fetch falls back to the
215
- neutral provider icon rather than spinning forever. */}
216
- {connectionsSettled ? (
217
- <IntegrationIcon
218
- source={connection?.provider}
219
- aria-hidden
220
- className="size-24 shrink-0 text-foreground-neutral-base"
221
- />
222
- ) : (
223
- <Skeleton className="size-24 shrink-0" />
224
- )}
225
- <div className="flex min-w-0 flex-1 items-center gap-inline">
226
- <Text size="md" bold className="truncate">
227
- {project.name}
228
- </Text>
229
- {status ? <ConnectionStatusBadge status={status} className="shrink-0" /> : null}
230
- </div>
231
- </div>
232
- </Card>
233
- </Link>
234
- </li>
220
+ <PanelCell>
221
+ <PanelCellAction asChild>
222
+ <Link
223
+ to="/w/$workspaceSlug/p/$projectSlug"
224
+ params={{workspaceSlug, projectSlug: project.slug}}
225
+ >
226
+ {/* Settle on success or error: a failed fetch falls back to the
227
+ neutral provider icon rather than spinning forever. */}
228
+ {connectionsSettled ? (
229
+ <IntegrationIcon
230
+ source={connection?.provider}
231
+ aria-hidden
232
+ className="size-24 shrink-0 text-foreground-neutral-base"
233
+ />
234
+ ) : (
235
+ <Skeleton className="size-24 shrink-0" />
236
+ )}
237
+ <span className="flex min-w-0 flex-1 items-center gap-inline">
238
+ <Text as="span" size="md" bold className="truncate">
239
+ {project.name}
240
+ </Text>
241
+ {status ? <ConnectionStatusBadge status={status} className="shrink-0" /> : null}
242
+ </span>
243
+ </Link>
244
+ </PanelCellAction>
245
+ </PanelCell>
235
246
  );
236
247
  }
@@ -1,4 +1,7 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {CreateProjectPage} from '#pages/create-project-page.js';
3
3
 
4
- export default defineRoute({component: CreateProjectPage});
4
+ export default defineRoute({
5
+ staticData: {frame: 'content'},
6
+ component: CreateProjectPage,
7
+ });
@@ -3,6 +3,7 @@ import {HomeRouter} from '#pages/home-router.js';
3
3
  import {validateProjectsSearch} from './search.js';
4
4
 
5
5
  export default defineRoute({
6
+ staticData: {frame: 'content'},
6
7
  validateSearch: validateProjectsSearch,
7
8
  component: () => <HomeRouter search={useRouteSearch(validateProjectsSearch)} />,
8
9
  });
@@ -2,6 +2,7 @@ import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {redirect} from '@tanstack/react-router';
3
3
 
4
4
  export default defineRoute({
5
+ staticData: {frame: 'content'},
5
6
  beforeLoad: ({params}: {params: {workspaceSlug: string; projectSlug: string}}) => {
6
7
  throw redirect({
7
8
  to: '/w/$workspaceSlug/p/$projectSlug/runs',
@@ -2,6 +2,7 @@ import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {redirect} from '@tanstack/react-router';
3
3
 
4
4
  export default defineRoute({
5
+ staticData: {frame: 'content'},
5
6
  beforeLoad: ({params}: {params: {workspaceSlug: string; projectSlug: string}}) => {
6
7
  throw redirect({
7
8
  to: '/w/$workspaceSlug/p/$projectSlug/settings/general',
@@ -1,4 +1,7 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {ProjectSettingsPage} from '#pages/project-settings-page.js';
3
3
 
4
- export default defineRoute({component: ProjectSettingsPage});
4
+ export default defineRoute({
5
+ staticData: {frame: 'content'},
6
+ component: ProjectSettingsPage,
7
+ });