@shipfox/client-projects 13.0.0 → 14.0.1

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.
@@ -30,20 +30,20 @@ export function ProjectsHubPage({ search = '' }) {
30
30
  const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;
31
31
  const hasNoData = !query.data;
32
32
  return /*#__PURE__*/ _jsxs("div", {
33
- className: "flex w-full flex-col gap-24",
33
+ className: "flex w-full flex-col gap-section",
34
34
  children: [
35
35
  /*#__PURE__*/ _jsxs("header", {
36
- className: "flex flex-col gap-16",
36
+ className: "flex flex-col gap-group",
37
37
  children: [
38
38
  /*#__PURE__*/ _jsx("div", {
39
- className: "flex items-start justify-between gap-24 max-[640px]:flex-col",
39
+ className: "flex items-start justify-between gap-section max-[640px]:flex-col",
40
40
  children: /*#__PURE__*/ _jsx(Header, {
41
41
  variant: "h2",
42
42
  children: "Projects"
43
43
  })
44
44
  }),
45
45
  /*#__PURE__*/ _jsxs("div", {
46
- className: "flex items-center gap-12 max-[640px]:flex-col max-[640px]:items-stretch",
46
+ className: "flex items-center gap-cluster max-[640px]:flex-col max-[640px]:items-stretch",
47
47
  children: [
48
48
  /*#__PURE__*/ _jsx(Input, {
49
49
  type: "search",
@@ -106,10 +106,11 @@ export function ProjectsHubPage({ search = '' }) {
106
106
  })
107
107
  }) : null,
108
108
  projects.length > 0 ? /*#__PURE__*/ _jsxs("section", {
109
+ className: "flex flex-col gap-group",
109
110
  "aria-label": "Projects list",
110
111
  children: [
111
112
  /*#__PURE__*/ _jsx("ul", {
112
- className: "grid grid-cols-2 gap-12 max-[760px]:grid-cols-1",
113
+ className: "grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1",
113
114
  children: projects.map((project)=>/*#__PURE__*/ _jsx(ProjectCard, {
114
115
  project: project,
115
116
  connection: connectionsById.get(project.source.connectionId),
@@ -121,14 +122,13 @@ export function ProjectsHubPage({ search = '' }) {
121
122
  query.error && query.data ? /*#__PURE__*/ _jsx(Callout, {
122
123
  role: "alert",
123
124
  type: "error",
124
- className: "mt-16",
125
125
  children: /*#__PURE__*/ _jsx(Text, {
126
126
  size: "sm",
127
127
  children: "Could not load the next page. Existing projects are still shown."
128
128
  })
129
129
  }) : null,
130
130
  query.hasNextPage ? /*#__PURE__*/ _jsx("div", {
131
- className: "mt-16 flex justify-center",
131
+ className: "flex justify-center",
132
132
  children: /*#__PURE__*/ _jsx(Button, {
133
133
  variant: "secondary",
134
134
  isLoading: query.isFetchingNextPage,
@@ -145,7 +145,7 @@ function ProjectsSkeleton() {
145
145
  return /*#__PURE__*/ _jsx("ul", {
146
146
  role: "status",
147
147
  "aria-label": "Loading projects",
148
- className: "grid grid-cols-2 gap-12 max-[760px]:grid-cols-1",
148
+ className: "grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1",
149
149
  children: [
150
150
  0,
151
151
  1,
@@ -155,9 +155,9 @@ function ProjectsSkeleton() {
155
155
  5
156
156
  ].map((row)=>/*#__PURE__*/ _jsx("li", {
157
157
  children: /*#__PURE__*/ _jsx(Card, {
158
- className: "h-full p-16",
158
+ className: "h-full p-panel-compact",
159
159
  children: /*#__PURE__*/ _jsxs("div", {
160
- className: "flex items-center gap-12",
160
+ className: "flex items-center gap-cluster",
161
161
  children: [
162
162
  /*#__PURE__*/ _jsx(Skeleton, {
163
163
  className: "size-24 shrink-0"
@@ -216,9 +216,9 @@ function ProjectCard({ project, connection, connectionsResolved, connectionsSett
216
216
  },
217
217
  className: "block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none",
218
218
  children: /*#__PURE__*/ _jsx(Card, {
219
- className: "h-full p-16 transition-colors hover:bg-background-components-hover",
219
+ className: "h-full p-panel-compact transition-colors hover:bg-background-components-hover",
220
220
  children: /*#__PURE__*/ _jsxs("div", {
221
- className: "flex min-w-0 items-center gap-12",
221
+ className: "flex min-w-0 items-center gap-cluster",
222
222
  children: [
223
223
  connectionsSettled ? /*#__PURE__*/ _jsx(IntegrationIcon, {
224
224
  source: connection?.provider,
@@ -228,7 +228,7 @@ function ProjectCard({ project, connection, connectionsResolved, connectionsSett
228
228
  className: "size-24 shrink-0"
229
229
  }),
230
230
  /*#__PURE__*/ _jsxs("div", {
231
- className: "flex min-w-0 flex-1 items-center gap-8",
231
+ className: "flex min-w-0 flex-1 items-center gap-inline",
232
232
  children: [
233
233
  /*#__PURE__*/ _jsx(Text, {
234
234
  size: "md",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/projects-hub-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionStatusBadge,\n type IntegrationConnection,\n IntegrationIcon,\n useIntegrationConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {QueryLoadError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Input} from '@shipfox/react-ui/input';\nimport {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectsInfiniteQuery} from '#hooks/api/projects.js';\n\nexport function ProjectsHubPage({search = ''}: {search?: string}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const query = useProjectsInfiniteQuery(workspace.id, search || undefined);\n const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];\n\n // The provider logo and connection health live on the integration connection,\n // not the project, so resolve them once for the whole list and index by id.\n // Skip the fetch when there are no cards to annotate.\n const connectionsQuery = useIntegrationConnectionsQuery(\n projects.length > 0 ? workspace.id : undefined,\n );\n const connectionsById = new Map(\n (connectionsQuery.data ?? []).map((connection) => [connection.id, connection]),\n );\n\n const isInitialLoading = query.isPending;\n const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;\n const hasNoData = !query.data;\n\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-16\">\n <div className=\"flex items-start justify-between gap-24 max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-12 max-[640px]:flex-col max-[640px]:items-stretch\">\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => {\n const next = event.target.value.trim();\n navigate({search: (next ? {search: next} : {}) as never, replace: true});\n }}\n placeholder=\"Search projects…\"\n aria-label=\"Search projects\"\n className=\"flex-1\"\n iconLeft={<Icon name=\"searchLine\" className=\"size-16\" />}\n iconRight={\n isSearching ? (\n <Icon\n name=\"spinner\"\n size={16}\n className=\"text-foreground-neutral-muted\"\n aria-hidden=\"true\"\n />\n ) : undefined\n }\n />\n <Button asChild iconLeft=\"addLine\" className=\"shrink-0 max-[640px]:w-full\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug: workspace.slug}}>\n New project\n </Link>\n </Button>\n </div>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {isInitialLoading || (search && hasNoData && query.isFetching) ? <ProjectsSkeleton /> : null}\n\n {query.isError && hasNoData ? <QueryLoadError query={query} subject=\"projects\" /> : null}\n\n {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (\n <EmptyProjects workspaceSlug={workspace.slug} />\n ) : null}\n\n {!query.isFetching && !query.isError && projects.length === 0 && search ? (\n <NoSearchResults\n search={search}\n onClear={() => navigate({search: {} as never, replace: true})}\n />\n ) : null}\n\n {projects.length > 0 ? (\n <section aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-12 max-[760px]:grid-cols-1\">\n {projects.map((project) => (\n <ProjectCard\n project={project}\n connection={connectionsById.get(project.source.connectionId)}\n connectionsResolved={connectionsQuery.isSuccess}\n connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}\n key={project.id}\n workspaceSlug={workspace.slug}\n />\n ))}\n </ul>\n {query.error && query.data ? (\n <Callout role=\"alert\" type=\"error\" className=\"mt-16\">\n <Text size=\"sm\">\n Could not load the next page. Existing projects are still shown.\n </Text>\n </Callout>\n ) : null}\n {query.hasNextPage ? (\n <div className=\"mt-16 flex justify-center\">\n <Button\n variant=\"secondary\"\n isLoading={query.isFetchingNextPage}\n onClick={() => query.fetchNextPage()}\n >\n Load more\n </Button>\n </div>\n ) : null}\n </section>\n ) : null}\n </div>\n );\n}\n\nfunction ProjectsSkeleton() {\n return (\n <ul\n role=\"status\"\n aria-label=\"Loading projects\"\n className=\"grid grid-cols-2 gap-12 max-[760px]:grid-cols-1\"\n >\n {[0, 1, 2, 3, 4, 5].map((row) => (\n <li key={row}>\n <Card className=\"h-full p-16\">\n <div className=\"flex items-center gap-12\">\n <Skeleton className=\"size-24 shrink-0\" />\n <Skeleton className=\"h-16 w-1/2\" />\n </div>\n </Card>\n </li>\n ))}\n </ul>\n );\n}\n\nfunction EmptyProjects({workspaceSlug}: {workspaceSlug: string}) {\n return (\n <EmptyState\n icon=\"folderLine\"\n title=\"Create your first project\"\n description=\"Connect a repository-backed project to start building workflows.\"\n action={\n <Button asChild iconRight=\"chevronRight\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug}}>\n Create project\n </Link>\n </Button>\n }\n />\n );\n}\n\nfunction NoSearchResults({search, onClear}: {search: string; onClear: () => void}) {\n return (\n <EmptyState\n icon=\"searchLine\"\n title={`No projects match “${search}”`}\n description=\"Try a different search, or clear it to see all projects.\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={onClear}>\n Clear search\n </Button>\n }\n />\n );\n}\n\nfunction ProjectCard({\n project,\n connection,\n connectionsResolved,\n connectionsSettled,\n workspaceSlug,\n}: {\n project: Project;\n connection: IntegrationConnection | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceSlug: string;\n}) {\n // On a resolved fetch, `active` carries no badge while a missing connection\n // reads as an error so a broken source is still flagged. An unresolved or\n // failed fetch shows nothing, so a fetch failure never flags every card.\n const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;\n\n return (\n <li>\n <Link\n to=\"/w/$workspaceSlug/p/$projectSlug\"\n params={{workspaceSlug, projectSlug: project.slug}}\n className=\"block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none\"\n >\n <Card className=\"h-full p-16 transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-12\">\n {/* Settle on success or error: a failed fetch falls back to the\n neutral provider icon rather than spinning forever. */}\n {connectionsSettled ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-24 shrink-0 text-foreground-neutral-base\"\n />\n ) : (\n <Skeleton className=\"size-24 shrink-0\" />\n )}\n <div className=\"flex min-w-0 flex-1 items-center gap-8\">\n <Text size=\"md\" bold className=\"truncate\">\n {project.name}\n </Text>\n {status ? <ConnectionStatusBadge status={status} className=\"shrink-0\" /> : null}\n </div>\n </div>\n </Card>\n </Link>\n </li>\n );\n}\n"],"names":["useActiveWorkspace","ConnectionStatusBadge","IntegrationIcon","useIntegrationConnectionsQuery","QueryLoadError","Button","Callout","Card","EmptyState","Icon","Input","Skeleton","Header","Text","Link","useNavigate","ModelProviderReminderBanner","useProjectsInfiniteQuery","ProjectsHubPage","search","workspace","navigate","query","id","undefined","projects","data","pages","flatMap","page","connectionsQuery","length","connectionsById","Map","map","connection","isInitialLoading","isPending","isSearching","Boolean","isFetching","isFetchingNextPage","hasNoData","div","className","header","variant","type","value","onChange","event","next","target","trim","replace","placeholder","aria-label","iconLeft","name","iconRight","size","aria-hidden","asChild","to","params","workspaceSlug","slug","workspaceId","ProjectsSkeleton","isError","subject","EmptyProjects","NoSearchResults","onClear","section","ul","project","ProjectCard","get","source","connectionId","connectionsResolved","isSuccess","connectionsSettled","error","role","hasNextPage","isLoading","onClick","fetchNextPage","row","li","icon","title","description","action","status","lifecycleStatus","projectSlug","provider","bold"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/pages/projects-hub-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionStatusBadge,\n type IntegrationConnection,\n IntegrationIcon,\n useIntegrationConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {QueryLoadError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Input} from '@shipfox/react-ui/input';\nimport {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectsInfiniteQuery} from '#hooks/api/projects.js';\n\nexport function ProjectsHubPage({search = ''}: {search?: string}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const query = useProjectsInfiniteQuery(workspace.id, search || undefined);\n const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];\n\n // The provider logo and connection health live on the integration connection,\n // not the project, so resolve them once for the whole list and index by id.\n // Skip the fetch when there are no cards to annotate.\n const connectionsQuery = useIntegrationConnectionsQuery(\n projects.length > 0 ? workspace.id : undefined,\n );\n const connectionsById = new Map(\n (connectionsQuery.data ?? []).map((connection) => [connection.id, connection]),\n );\n\n const isInitialLoading = query.isPending;\n const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;\n const hasNoData = !query.data;\n\n return (\n <div className=\"flex w-full flex-col gap-section\">\n <header className=\"flex flex-col gap-group\">\n <div className=\"flex items-start justify-between gap-section max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-cluster max-[640px]:flex-col max-[640px]:items-stretch\">\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => {\n const next = event.target.value.trim();\n navigate({search: (next ? {search: next} : {}) as never, replace: true});\n }}\n placeholder=\"Search projects…\"\n aria-label=\"Search projects\"\n className=\"flex-1\"\n iconLeft={<Icon name=\"searchLine\" className=\"size-16\" />}\n iconRight={\n isSearching ? (\n <Icon\n name=\"spinner\"\n size={16}\n className=\"text-foreground-neutral-muted\"\n aria-hidden=\"true\"\n />\n ) : undefined\n }\n />\n <Button asChild iconLeft=\"addLine\" className=\"shrink-0 max-[640px]:w-full\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug: workspace.slug}}>\n New project\n </Link>\n </Button>\n </div>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {isInitialLoading || (search && hasNoData && query.isFetching) ? <ProjectsSkeleton /> : null}\n\n {query.isError && hasNoData ? <QueryLoadError query={query} subject=\"projects\" /> : null}\n\n {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (\n <EmptyProjects workspaceSlug={workspace.slug} />\n ) : null}\n\n {!query.isFetching && !query.isError && projects.length === 0 && search ? (\n <NoSearchResults\n search={search}\n onClear={() => navigate({search: {} as never, replace: true})}\n />\n ) : null}\n\n {projects.length > 0 ? (\n <section className=\"flex flex-col gap-group\" aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1\">\n {projects.map((project) => (\n <ProjectCard\n project={project}\n connection={connectionsById.get(project.source.connectionId)}\n connectionsResolved={connectionsQuery.isSuccess}\n connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}\n key={project.id}\n workspaceSlug={workspace.slug}\n />\n ))}\n </ul>\n {query.error && query.data ? (\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n Could not load the next page. Existing projects are still shown.\n </Text>\n </Callout>\n ) : null}\n {query.hasNextPage ? (\n <div className=\"flex justify-center\">\n <Button\n variant=\"secondary\"\n isLoading={query.isFetchingNextPage}\n onClick={() => query.fetchNextPage()}\n >\n Load more\n </Button>\n </div>\n ) : null}\n </section>\n ) : null}\n </div>\n );\n}\n\nfunction ProjectsSkeleton() {\n return (\n <ul\n role=\"status\"\n aria-label=\"Loading projects\"\n className=\"grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1\"\n >\n {[0, 1, 2, 3, 4, 5].map((row) => (\n <li key={row}>\n <Card className=\"h-full p-panel-compact\">\n <div className=\"flex items-center gap-cluster\">\n <Skeleton className=\"size-24 shrink-0\" />\n <Skeleton className=\"h-16 w-1/2\" />\n </div>\n </Card>\n </li>\n ))}\n </ul>\n );\n}\n\nfunction EmptyProjects({workspaceSlug}: {workspaceSlug: string}) {\n return (\n <EmptyState\n icon=\"folderLine\"\n title=\"Create your first project\"\n description=\"Connect a repository-backed project to start building workflows.\"\n action={\n <Button asChild iconRight=\"chevronRight\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug}}>\n Create project\n </Link>\n </Button>\n }\n />\n );\n}\n\nfunction NoSearchResults({search, onClear}: {search: string; onClear: () => void}) {\n return (\n <EmptyState\n icon=\"searchLine\"\n title={`No projects match “${search}”`}\n description=\"Try a different search, or clear it to see all projects.\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={onClear}>\n Clear search\n </Button>\n }\n />\n );\n}\n\nfunction ProjectCard({\n project,\n connection,\n connectionsResolved,\n connectionsSettled,\n workspaceSlug,\n}: {\n project: Project;\n connection: IntegrationConnection | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceSlug: string;\n}) {\n // On a resolved fetch, `active` carries no badge while a missing connection\n // reads as an error so a broken source is still flagged. An unresolved or\n // failed fetch shows nothing, so a fetch failure never flags every card.\n const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;\n\n return (\n <li>\n <Link\n to=\"/w/$workspaceSlug/p/$projectSlug\"\n params={{workspaceSlug, projectSlug: project.slug}}\n className=\"block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none\"\n >\n <Card className=\"h-full p-panel-compact transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-cluster\">\n {/* Settle on success or error: a failed fetch falls back to the\n neutral provider icon rather than spinning forever. */}\n {connectionsSettled ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-24 shrink-0 text-foreground-neutral-base\"\n />\n ) : (\n <Skeleton className=\"size-24 shrink-0\" />\n )}\n <div className=\"flex min-w-0 flex-1 items-center gap-inline\">\n <Text size=\"md\" bold className=\"truncate\">\n {project.name}\n </Text>\n {status ? <ConnectionStatusBadge status={status} className=\"shrink-0\" /> : null}\n </div>\n </div>\n </Card>\n </Link>\n </li>\n );\n}\n"],"names":["useActiveWorkspace","ConnectionStatusBadge","IntegrationIcon","useIntegrationConnectionsQuery","QueryLoadError","Button","Callout","Card","EmptyState","Icon","Input","Skeleton","Header","Text","Link","useNavigate","ModelProviderReminderBanner","useProjectsInfiniteQuery","ProjectsHubPage","search","workspace","navigate","query","id","undefined","projects","data","pages","flatMap","page","connectionsQuery","length","connectionsById","Map","map","connection","isInitialLoading","isPending","isSearching","Boolean","isFetching","isFetchingNextPage","hasNoData","div","className","header","variant","type","value","onChange","event","next","target","trim","replace","placeholder","aria-label","iconLeft","name","iconRight","size","aria-hidden","asChild","to","params","workspaceSlug","slug","workspaceId","ProjectsSkeleton","isError","subject","EmptyProjects","NoSearchResults","onClear","section","ul","project","ProjectCard","get","source","connectionId","connectionsResolved","isSuccess","connectionsSettled","error","role","hasNextPage","isLoading","onClick","fetchNextPage","row","li","icon","title","description","action","status","lifecycleStatus","projectSlug","provider","bold"],"mappings":"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"}