@shipfox/client-integrations 14.0.0 → 16.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 (85) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/dist/application/complete-integration-callback.d.ts +2 -2
  4. package/dist/components/callback-status-shell.js +3 -3
  5. package/dist/components/callback-status-shell.js.map +1 -1
  6. package/dist/components/connection-picker.js +3 -3
  7. package/dist/components/connection-picker.js.map +1 -1
  8. package/dist/components/installed-integrations-section.js +9 -9
  9. package/dist/components/installed-integrations-section.js.map +1 -1
  10. package/dist/components/integration-delete-confirm-modal.js +1 -1
  11. package/dist/components/integration-delete-confirm-modal.js.map +1 -1
  12. package/dist/components/integration-gallery-for-workspace.js +3 -3
  13. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  14. package/dist/components/integration-gallery.stories.d.ts +1 -0
  15. package/dist/components/integration-gallery.stories.d.ts.map +1 -1
  16. package/dist/components/integration-gallery.stories.js +28 -0
  17. package/dist/components/integration-gallery.stories.js.map +1 -1
  18. package/dist/components/integration-usage-modal.d.ts +1 -1
  19. package/dist/components/integration-usage-modal.d.ts.map +1 -1
  20. package/dist/components/integration-usage-modal.js +8 -7
  21. package/dist/components/integration-usage-modal.js.map +1 -1
  22. package/dist/components/provider-grid.d.ts +1 -1
  23. package/dist/components/provider-grid.d.ts.map +1 -1
  24. package/dist/components/provider-grid.js +10 -10
  25. package/dist/components/provider-grid.js.map +1 -1
  26. package/dist/components/redirect-install-page.js +1 -1
  27. package/dist/components/redirect-install-page.js.map +1 -1
  28. package/dist/components/repository-picker.js +4 -4
  29. package/dist/components/repository-picker.js.map +1 -1
  30. package/dist/components/webhook/copyable-value.js +3 -3
  31. package/dist/components/webhook/copyable-value.js.map +1 -1
  32. package/dist/components/webhook/webhook-create-modal.js +1 -1
  33. package/dist/components/webhook/webhook-create-modal.js.map +1 -1
  34. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -1
  35. package/dist/components/webhook/webhook-usage-details.js +2 -2
  36. package/dist/components/webhook/webhook-usage-details.js.map +1 -1
  37. package/dist/feature.d.ts +8 -8
  38. package/dist/feature.d.ts.map +1 -1
  39. package/dist/hooks/api/integrations.d.ts +4 -4
  40. package/dist/hooks/api/integrations.d.ts.map +1 -1
  41. package/dist/hooks/api/webhook-connections.d.ts +1 -1
  42. package/dist/hooks/api/webhook-connections.d.ts.map +1 -1
  43. package/dist/pages/gitea-install-page.js +4 -4
  44. package/dist/pages/gitea-install-page.js.map +1 -1
  45. package/dist/pages/jira-callback-page.js +7 -7
  46. package/dist/pages/jira-callback-page.js.map +1 -1
  47. package/dist/pages/sentry-callback-page.js +7 -7
  48. package/dist/pages/sentry-callback-page.js.map +1 -1
  49. package/dist/pages/source-control-onboarding-page.js +2 -2
  50. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  51. package/dist/routes/gitea.d.ts.map +1 -1
  52. package/dist/routes/github-callback.d.ts.map +1 -1
  53. package/dist/routes/github.d.ts.map +1 -1
  54. package/dist/routes/integrations-settings.d.ts.map +1 -1
  55. package/dist/routes/integrations.d.ts.map +1 -1
  56. package/dist/routes/jira-callback.d.ts.map +1 -1
  57. package/dist/routes/jira.d.ts.map +1 -1
  58. package/dist/routes/linear-callback.d.ts.map +1 -1
  59. package/dist/routes/linear.d.ts.map +1 -1
  60. package/dist/routes/sentry-callback.d.ts.map +1 -1
  61. package/dist/routes/sentry.d.ts.map +1 -1
  62. package/dist/routes/slack-callback.d.ts.map +1 -1
  63. package/dist/routes/slack.d.ts.map +1 -1
  64. package/dist/tsconfig.test.tsbuildinfo +1 -1
  65. package/package.json +14 -14
  66. package/src/components/callback-status-shell.tsx +3 -3
  67. package/src/components/connection-picker.tsx +3 -3
  68. package/src/components/installed-integrations-section.tsx +9 -9
  69. package/src/components/integration-delete-confirm-modal.tsx +1 -1
  70. package/src/components/integration-gallery-for-workspace.tsx +3 -3
  71. package/src/components/integration-gallery.stories.tsx +20 -0
  72. package/src/components/integration-gallery.test.tsx +21 -0
  73. package/src/components/integration-usage-modal.tsx +9 -7
  74. package/src/components/provider-grid.tsx +10 -10
  75. package/src/components/redirect-install-page.tsx +1 -1
  76. package/src/components/repository-picker.test.tsx +2 -2
  77. package/src/components/repository-picker.tsx +6 -4
  78. package/src/components/webhook/copyable-value.tsx +3 -3
  79. package/src/components/webhook/webhook-create-modal.tsx +1 -1
  80. package/src/components/webhook/webhook-usage-details.tsx +2 -2
  81. package/src/pages/gitea-install-page.tsx +4 -4
  82. package/src/pages/jira-callback-page.tsx +7 -7
  83. package/src/pages/sentry-callback-page.tsx +7 -7
  84. package/src/pages/source-control-onboarding-page.tsx +2 -2
  85. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-integrations",
3
3
  "license": "MIT",
4
- "version": "14.0.0",
4
+ "version": "16.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,20 +27,20 @@
27
27
  "dependencies": {
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "@tanstack/react-form": "^1.32.0",
30
- "@shipfox/api-integration-core-dto": "12.0.0",
31
- "@shipfox/api-integration-gitea-dto": "12.0.0",
32
- "@shipfox/api-integration-github-dto": "12.0.0",
33
- "@shipfox/api-integration-jira-dto": "12.0.0",
34
- "@shipfox/api-integration-linear-dto": "12.0.0",
35
- "@shipfox/api-integration-slack-dto": "12.0.0",
36
- "@shipfox/api-integration-sentry-dto": "12.0.0",
37
- "@shipfox/api-integration-webhook-dto": "12.0.0",
30
+ "@shipfox/api-integration-core-dto": "12.2.0",
31
+ "@shipfox/api-integration-gitea-dto": "12.2.0",
32
+ "@shipfox/api-integration-github-dto": "12.2.0",
33
+ "@shipfox/api-integration-linear-dto": "12.2.0",
34
+ "@shipfox/api-integration-jira-dto": "12.2.0",
35
+ "@shipfox/api-integration-slack-dto": "12.2.0",
36
+ "@shipfox/api-integration-webhook-dto": "12.2.0",
37
+ "@shipfox/api-integration-sentry-dto": "12.2.0",
38
38
  "@shipfox/client-api": "6.0.1",
39
- "@shipfox/client-auth": "14.0.0",
40
- "@shipfox/client-shell": "14.0.0",
41
- "@shipfox/client-ui": "14.0.0",
42
- "@shipfox/integration-icons": "0.3.0",
43
- "@shipfox/react-ui": "0.5.0"
39
+ "@shipfox/client-auth": "16.0.0",
40
+ "@shipfox/client-shell": "16.0.0",
41
+ "@shipfox/client-ui": "16.0.0",
42
+ "@shipfox/react-ui": "1.1.0",
43
+ "@shipfox/integration-icons": "0.3.2"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@tanstack/react-query": "^5.101.0",
@@ -43,15 +43,15 @@ export function CallbackStatusShell({
43
43
  : undefined;
44
44
 
45
45
  return (
46
- <main className="flex min-h-screen bg-background-subtle-base px-16 py-32">
47
- <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20">
46
+ <main className="flex min-h-screen bg-background-subtle-base px-row py-frame">
47
+ <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-section">
48
48
  <h2 ref={headingRef} tabIndex={-1} className="text-24 font-semibold outline-none">
49
49
  {title}
50
50
  </h2>
51
51
  <Callout role={status === 'error' ? 'alert' : 'status'} type={status}>
52
52
  <Text size="sm">{message}</Text>
53
53
  </Callout>
54
- <div className="flex flex-col gap-8 sm:flex-row sm:items-center">
54
+ <div className="flex flex-col gap-inline sm:flex-row sm:items-center">
55
55
  {switchAccount ? (
56
56
  <ButtonLink asChild className="min-h-44 w-full sm:w-fit">
57
57
  <Link
@@ -19,7 +19,7 @@ export function ConnectionPicker({
19
19
  const labelId = useId();
20
20
 
21
21
  return (
22
- <div className="flex flex-col gap-10">
22
+ <div className="flex flex-col gap-inline">
23
23
  <Label id={labelId} className="sr-only">
24
24
  Source integration
25
25
  </Label>
@@ -27,7 +27,7 @@ export function ConnectionPicker({
27
27
  aria-labelledby={labelId}
28
28
  value={selectedConnectionId ?? ''}
29
29
  onValueChange={onSelect}
30
- className="grid grid-cols-2 gap-8 max-[760px]:grid-cols-1"
30
+ className="grid grid-cols-2 gap-inline max-[760px]:grid-cols-1"
31
31
  >
32
32
  {connections.map((connection) => (
33
33
  <RadioGroupItem key={connection.id} value={connection.id}>
@@ -43,7 +43,7 @@ function ConnectionOption({connection}: {connection: IntegrationConnection}) {
43
43
  const {ref: nameRef, isTruncated} = useIsTextTruncated<HTMLSpanElement>(connection.displayName);
44
44
 
45
45
  return (
46
- <span className="flex min-w-0 items-center gap-10">
46
+ <span className="flex min-w-0 items-center gap-inline">
47
47
  <Tooltip>
48
48
  <TooltipTrigger asChild>
49
49
  <span className="shrink-0">
@@ -48,8 +48,8 @@ export function InstalledIntegrationsSection({
48
48
  providerDisplayName,
49
49
  }: InstalledIntegrationsSectionProps) {
50
50
  return (
51
- <section className="flex flex-col gap-16" aria-label="Installed integrations">
52
- <div className="flex flex-col gap-4">
51
+ <section className="flex flex-col gap-group" aria-label="Installed integrations">
52
+ <div className="flex flex-col gap-tight">
53
53
  <Header variant="h3">Installed integrations</Header>
54
54
  <Text size="sm" className="text-foreground-neutral-muted">
55
55
  Provider accounts installed in this workspace.
@@ -59,7 +59,7 @@ export function InstalledIntegrationsSection({
59
59
  {isPending ? <InstalledSkeleton label="Loading integrations" /> : null}
60
60
 
61
61
  {error ? (
62
- <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
62
+ <div className={cn(INSTALLED_SURFACE_CLASS, 'px-row')}>
63
63
  <QueryLoadError
64
64
  query={{isError: true, isFetching, data: undefined, error, refetch: onRetry}}
65
65
  subject="integrations"
@@ -68,7 +68,7 @@ export function InstalledIntegrationsSection({
68
68
  ) : null}
69
69
 
70
70
  {!isPending && !error && connections.length === 0 ? (
71
- <div className={cn(INSTALLED_SURFACE_CLASS, 'px-16')}>
71
+ <div className={cn(INSTALLED_SURFACE_CLASS, 'px-row')}>
72
72
  <EmptyState
73
73
  icon="componentLine"
74
74
  title="No integrations installed yet"
@@ -120,7 +120,7 @@ function InstalledRow({
120
120
  const providerName = providerDisplayName(connection.provider);
121
121
 
122
122
  return (
123
- <li className="flex items-center gap-12 px-16 py-12 transition-colors hover:bg-background-components-hover">
123
+ <li className="flex items-center gap-cluster px-row py-row transition-colors hover:bg-background-components-hover">
124
124
  <IntegrationIcon
125
125
  source={connection.provider}
126
126
  aria-hidden
@@ -129,8 +129,8 @@ function InstalledRow({
129
129
  muted ? 'text-foreground-neutral-disabled' : 'text-foreground-neutral-base',
130
130
  )}
131
131
  />
132
- <div className="flex min-w-0 flex-1 flex-col gap-2">
133
- <div className="flex min-w-0 items-center gap-8">
132
+ <div className="flex min-w-0 flex-1 flex-col gap-tight">
133
+ <div className="flex min-w-0 items-center gap-inline">
134
134
  <Text
135
135
  size="md"
136
136
  bold
@@ -196,9 +196,9 @@ function InstalledSkeleton({label}: {label: string}) {
196
196
  className={cn('divide-y divide-border-neutral-base', INSTALLED_SURFACE_CLASS)}
197
197
  >
198
198
  {[0, 1, 2].map((row) => (
199
- <li key={row} className="flex items-center gap-12 px-16 py-12">
199
+ <li key={row} className="flex items-center gap-cluster px-row py-row">
200
200
  <Skeleton className="size-24 shrink-0" />
201
- <div className="flex min-w-0 flex-1 flex-col gap-2">
201
+ <div className="flex min-w-0 flex-1 flex-col gap-tight">
202
202
  <Skeleton className="h-16 w-120" />
203
203
  <Skeleton className="h-12 w-80" />
204
204
  </div>
@@ -36,7 +36,7 @@ export function IntegrationDeleteConfirmModal({
36
36
  <ModalContent aria-describedby={undefined}>
37
37
  <ModalTitle className="sr-only">Delete integration</ModalTitle>
38
38
  <ModalHeader title="Delete integration" showClose={!isPending} />
39
- <ModalBody className="gap-16">
39
+ <ModalBody className="gap-group">
40
40
  <Text size="sm">
41
41
  Are you sure you want to delete <strong className="font-medium">{name}</strong>? Once
42
42
  deleted, Shipfox will immediately stop processing events from this integration. This
@@ -123,7 +123,7 @@ export function IntegrationGalleryForWorkspace({
123
123
  }
124
124
 
125
125
  return (
126
- <div className="flex flex-col gap-24">
126
+ <div className="flex flex-col gap-section">
127
127
  <InstalledIntegrationsSection
128
128
  workspaceSlug={workspaceSlug}
129
129
  connections={sortedConnections}
@@ -145,8 +145,8 @@ export function IntegrationGalleryForWorkspace({
145
145
  providerDisplayName={providerDisplayName}
146
146
  />
147
147
 
148
- <section className="flex flex-col gap-16" aria-label="Available integrations">
149
- <div className="flex flex-col gap-4">
148
+ <section className="flex flex-col gap-group" aria-label="Available integrations">
149
+ <div className="flex flex-col gap-tight">
150
150
  <Header variant="h3">Available integrations</Header>
151
151
  <Text size="sm" className="text-foreground-neutral-muted">
152
152
  Providers available to install in this workspace.
@@ -17,6 +17,7 @@ import {
17
17
  } from '@tanstack/react-router';
18
18
  import {createStore, Provider as JotaiProvider} from 'jotai';
19
19
  import {useMemo} from 'react';
20
+ import {expect, screen, userEvent, within} from 'storybook/test';
20
21
  import {IntegrationGallery} from './integration-gallery.js';
21
22
 
22
23
  const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
@@ -148,6 +149,25 @@ export const LongNames: Story = {
148
149
  args: {scenario: 'long-names'},
149
150
  };
150
151
 
152
+ export const UsageModalCloseRestoresInteraction: Story = {
153
+ play: async ({canvasElement}) => {
154
+ const user = userEvent.setup();
155
+ const canvas = within(canvasElement);
156
+ const actionsButton = await canvas.findByRole('button', {
157
+ name: 'Open acme-corp integration actions',
158
+ });
159
+
160
+ await user.click(actionsButton);
161
+ await user.click(await screen.findByRole('menuitem', {name: 'Use this integration'}));
162
+ await screen.findByRole('dialog', {name: 'Use acme-corp'});
163
+ await user.click(await screen.findByRole('button', {name: 'Done'}));
164
+ expect(screen.queryByRole('dialog', {name: 'Use acme-corp'})).not.toBeInTheDocument();
165
+ expect(document.body.style.pointerEvents).not.toBe('none');
166
+ await user.click(actionsButton);
167
+ await screen.findByRole('menuitem', {name: 'Use this integration'});
168
+ },
169
+ };
170
+
151
171
  function fetchForScenario(scenario: Scenario): typeof fetch {
152
172
  return (input, init) => {
153
173
  const url = requestUrl(input);
@@ -367,6 +367,27 @@ describe('IntegrationGallery — installed section', () => {
367
367
  expect(screen.getByText(webhookConnectionDto.inbound_url)).toBeVisible();
368
368
  });
369
369
 
370
+ test('restores page interactivity after closing the usage modal', async () => {
371
+ renderGallery({}, {connections: [webhookConnection]});
372
+
373
+ await openActions('Open Stripe production integration actions');
374
+ expect(document.body.style.pointerEvents).toBe('none');
375
+ fireEvent.click(screen.getByRole('menuitem', {name: 'Use this integration'}));
376
+ expect(await screen.findByRole('dialog', {name: 'Use Stripe production'})).toBeVisible();
377
+ expect(document.body.style.pointerEvents).toBe('none');
378
+
379
+ const usageDialog = screen.getByRole('dialog', {name: 'Use Stripe production'});
380
+ fireEvent.click(within(usageDialog).getByRole('button', {name: 'Done'}));
381
+
382
+ await waitFor(() => {
383
+ expect(screen.queryByRole('dialog', {name: 'Use Stripe production'})).not.toBeInTheDocument();
384
+ expect(document.body).not.toHaveStyle({pointerEvents: 'none'});
385
+ });
386
+
387
+ await openActions('Open Stripe production integration actions');
388
+ expect(screen.getByRole('menuitem', {name: 'Use this integration'})).toBeVisible();
389
+ });
390
+
370
391
  test('opens the usage modal with a GitHub event selector', async () => {
371
392
  renderGallery({}, {connections: [githubConnection]});
372
393
 
@@ -71,13 +71,15 @@ export function IntegrationUsageModal({
71
71
  );
72
72
  const title = connection ? `Use ${connection.displayName}` : 'Use integration';
73
73
 
74
+ if (!open || connection === null) return null;
75
+
74
76
  return (
75
- <Modal open={open && connection !== null} onOpenChange={onOpenChange}>
77
+ <Modal open={open} onOpenChange={onOpenChange}>
76
78
  <ModalContent aria-describedby={undefined} className="max-h-[calc(100vh-32px)] max-w-[640px]">
77
79
  <ModalTitle className="sr-only">{title}</ModalTitle>
78
80
  <ModalHeader>
79
- <div className="flex min-w-0 flex-col gap-2">
80
- <div className="flex min-w-0 items-center gap-8">
81
+ <div className="flex min-w-0 flex-col gap-tight">
82
+ <div className="flex min-w-0 items-center gap-inline">
81
83
  <Text size="lg" aria-hidden="true" className="truncate">
82
84
  {title}
83
85
  </Text>
@@ -92,13 +94,13 @@ export function IntegrationUsageModal({
92
94
  </ModalHeader>
93
95
  {connection ? (
94
96
  <>
95
- <ModalBody className="min-h-0 flex-1 gap-24 overflow-y-auto overflow-x-clip scrollbar">
96
- <section className="flex w-full flex-col gap-12">
97
+ <ModalBody className="min-h-0 flex-1 gap-section overflow-y-auto overflow-x-clip scrollbar">
98
+ <section className="flex w-full flex-col gap-cluster">
97
99
  <Text size="md" bold>
98
100
  Usage
99
101
  </Text>
100
102
  {events.length > 1 ? (
101
- <div className="flex flex-col gap-6">
103
+ <div className="flex flex-col gap-inline">
102
104
  <label
103
105
  htmlFor="integration-usage-event"
104
106
  className="font-display text-sm font-medium text-foreground-neutral-base"
@@ -144,7 +146,7 @@ export function IntegrationUsageModal({
144
146
  </CodeBlock>
145
147
  </section>
146
148
  {children ? (
147
- <section className="flex w-full flex-col gap-12">
149
+ <section className="flex w-full flex-col gap-cluster">
148
150
  <Text size="md" bold>
149
151
  Details
150
152
  </Text>
@@ -23,7 +23,7 @@ export interface ProviderGridProps {
23
23
  onOpenProvider?: ((provider: string) => void) | undefined;
24
24
  }
25
25
 
26
- export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-12 max-[760px]:grid-cols-1';
26
+ export const PROVIDER_GRID_CLASS = 'grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1';
27
27
 
28
28
  export const PROVIDER_SURFACE_CLASS =
29
29
  'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
@@ -46,7 +46,7 @@ export function ProviderGrid({
46
46
 
47
47
  if (error) {
48
48
  return (
49
- <div className={cn(PROVIDER_SURFACE_CLASS, 'px-16')}>
49
+ <div className={cn(PROVIDER_SURFACE_CLASS, 'px-row')}>
50
50
  <QueryLoadError
51
51
  query={{
52
52
  isError: true,
@@ -63,7 +63,7 @@ export function ProviderGrid({
63
63
 
64
64
  if (installableProviders.length === 0) {
65
65
  return (
66
- <div className={cn(PROVIDER_SURFACE_CLASS, 'px-16')}>
66
+ <div className={cn(PROVIDER_SURFACE_CLASS, 'px-row')}>
67
67
  <EmptyState
68
68
  icon="componentLine"
69
69
  title="No integrations available"
@@ -106,7 +106,7 @@ function ProviderCard({
106
106
  type="button"
107
107
  aria-label={`Add ${provider.displayName}`}
108
108
  onClick={() => onOpenProvider?.(provider.provider)}
109
- className="group flex h-full w-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
109
+ className="group flex h-full w-full min-w-0 items-center justify-between gap-cluster rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact text-left transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
110
110
  >
111
111
  <ProviderCardContent provider={provider} action="Add" />
112
112
  </button>
@@ -118,7 +118,7 @@ function ProviderCard({
118
118
  to={catalog.setupPath}
119
119
  params={{workspaceSlug}}
120
120
  aria-label={`Install ${provider.displayName}`}
121
- className="group flex h-full min-w-0 items-center justify-between gap-12 rounded-8 border border-border-neutral-base bg-background-neutral-base p-16 transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
121
+ className="group flex h-full min-w-0 items-center justify-between gap-cluster rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
122
122
  >
123
123
  <ProviderCardContent provider={provider} action="Install" />
124
124
  </Link>
@@ -134,7 +134,7 @@ function ProviderCardContent({
134
134
  }) {
135
135
  return (
136
136
  <>
137
- <span className="flex min-w-0 items-center gap-12">
137
+ <span className="flex min-w-0 items-center gap-cluster">
138
138
  <IntegrationIcon
139
139
  source={provider.provider}
140
140
  aria-hidden
@@ -144,7 +144,7 @@ function ProviderCardContent({
144
144
  {provider.displayName}
145
145
  </Text>
146
146
  </span>
147
- <span className="flex shrink-0 items-center gap-4 text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
147
+ <span className="flex shrink-0 items-center gap-tight text-foreground-neutral-muted transition-colors group-hover:text-foreground-highlight-interactive">
148
148
  <Text as="span" size="sm">
149
149
  {action}
150
150
  </Text>
@@ -159,9 +159,9 @@ function ProviderGridSkeleton({label}: {label: string}) {
159
159
  <ul role="status" aria-label={label} className={PROVIDER_GRID_CLASS}>
160
160
  {[0, 1, 2, 3].map((tile) => (
161
161
  <li key={tile}>
162
- <Card className="h-full p-16">
163
- <div className="flex items-center justify-between gap-12">
164
- <div className="flex min-w-0 items-center gap-12">
162
+ <Card className="h-full p-panel-compact">
163
+ <div className="flex items-center justify-between gap-cluster">
164
+ <div className="flex min-w-0 items-center gap-cluster">
165
165
  <Skeleton className="size-24 shrink-0" />
166
166
  <Skeleton className="h-16 w-100" />
167
167
  </div>
@@ -54,7 +54,7 @@ export function RedirectInstallPage({
54
54
 
55
55
  if (errorMessage) {
56
56
  return (
57
- <div className="mx-auto flex w-full max-w-[480px] flex-col gap-16">
57
+ <div className="mx-auto flex w-full max-w-[480px] flex-col gap-group">
58
58
  <Callout role="alert" type="error">
59
59
  <Text size="sm">{errorMessage}</Text>
60
60
  </Callout>
@@ -75,7 +75,7 @@ describe('RepositoryPicker', () => {
75
75
  const loadingGrid = container.querySelector('[aria-hidden="true"]');
76
76
  if (!loadingGrid) throw new Error('Repository loading grid was not rendered');
77
77
 
78
- expect(loadingGrid).toHaveClass('grid', 'grid-cols-2', 'gap-8', 'max-[760px]:grid-cols-1');
78
+ expect(loadingGrid).toHaveClass('grid', 'grid-cols-2', 'gap-inline', 'max-[760px]:grid-cols-1');
79
79
  expect(loadingGrid.querySelectorAll('[data-slot="skeleton"]')).toHaveLength(4);
80
80
  for (const placeholder of loadingGrid.querySelectorAll(':scope > div')) {
81
81
  expect(placeholder).toHaveClass(
@@ -84,7 +84,7 @@ describe('RepositoryPicker', () => {
84
84
  'border',
85
85
  'border-border-neutral-base',
86
86
  'bg-background-neutral-base',
87
- 'p-14',
87
+ 'p-[14px]',
88
88
  );
89
89
  }
90
90
  });
@@ -9,7 +9,7 @@ import {Text} from '@shipfox/react-ui/typography';
9
9
  import {useId} from 'react';
10
10
  import type {Repository} from '#core/models.js';
11
11
 
12
- const REPOSITORY_GRID_CLASS_NAME = 'grid grid-cols-2 gap-8 max-[760px]:grid-cols-1';
12
+ const REPOSITORY_GRID_CLASS_NAME = 'grid grid-cols-2 gap-inline max-[760px]:grid-cols-1';
13
13
  const REPOSITORY_SKELETON_WIDTHS = ['w-64', 'w-96', 'w-80', 'w-112'] as const;
14
14
 
15
15
  export function RepositoryPicker({
@@ -41,7 +41,7 @@ export function RepositoryPicker({
41
41
  const showSearch = onSearchChange !== undefined;
42
42
 
43
43
  return (
44
- <div className="flex flex-col gap-10">
44
+ <div className="flex flex-col gap-inline">
45
45
  <Label id={labelId} className="sr-only">
46
46
  Repository
47
47
  </Label>
@@ -60,7 +60,7 @@ export function RepositoryPicker({
60
60
  {isLoading ? <RepositoryLoadingState /> : null}
61
61
 
62
62
  {!isLoading && repositories.length === 0 ? (
63
- <div className="rounded-8 border border-border-neutral-base bg-background-subtle-base p-14">
63
+ <div className="rounded-8 border border-border-neutral-base bg-background-subtle-base p-panel-compact">
64
64
  <Text size="sm">{emptyMessage}</Text>
65
65
  </div>
66
66
  ) : null}
@@ -119,10 +119,12 @@ function RepositoryLoadingState() {
119
119
  Loading repositories.
120
120
  </div>
121
121
  <div aria-hidden="true" className={REPOSITORY_GRID_CLASS_NAME}>
122
+ {/* The skeleton mirrors RadioGroupItem's own 14px padding contract so the
123
+ placeholder and the loaded card stay the same height. */}
122
124
  {REPOSITORY_SKELETON_WIDTHS.map((width) => (
123
125
  <div
124
126
  key={width}
125
- className="h-50 min-w-0 rounded-8 border border-border-neutral-base bg-background-neutral-base p-14"
127
+ className="h-50 min-w-0 rounded-8 border border-border-neutral-base bg-background-neutral-base p-[14px]"
126
128
  >
127
129
  <Skeleton className={`h-20 ${width}`} />
128
130
  </div>
@@ -40,12 +40,12 @@ export function CopyableValue({label, value, note, className}: CopyableValueProp
40
40
  }
41
41
 
42
42
  return (
43
- <div className={cn('flex w-full min-w-0 flex-col gap-8', className)}>
44
- <div className="flex min-w-0 items-center gap-8">
43
+ <div className={cn('flex w-full min-w-0 flex-col gap-inline', className)}>
44
+ <div className="flex min-w-0 items-center gap-inline">
45
45
  <Code
46
46
  as="span"
47
47
  variant="paragraph"
48
- className="min-w-0 flex-1 break-all rounded-4 border border-border-neutral-base bg-background-components-base px-8 py-6 text-foreground-neutral-base"
48
+ className="min-w-0 flex-1 break-all rounded-4 border border-border-neutral-base bg-background-components-base px-tight py-[6px] text-foreground-neutral-base"
49
49
  >
50
50
  {value}
51
51
  </Code>
@@ -97,7 +97,7 @@ export function WebhookCreateModal({
97
97
  void form.handleSubmit();
98
98
  }}
99
99
  >
100
- <ModalBody className="gap-16">
100
+ <ModalBody className="gap-group">
101
101
  <Text size="sm" className="text-foreground-neutral-muted">
102
102
  Create a named inbound webhook URL for this workspace.
103
103
  </Text>
@@ -29,8 +29,8 @@ export function WebhookUsageDetails({workspaceId, connectionId}: WebhookUsageDet
29
29
  if (!connection) return null;
30
30
 
31
31
  return (
32
- <div className="flex w-full flex-col gap-16">
33
- <div className="flex w-full min-w-0 flex-col gap-8">
32
+ <div className="flex w-full flex-col gap-group">
33
+ <div className="flex w-full min-w-0 flex-col gap-inline">
34
34
  <Text size="sm" bold>
35
35
  Inbound URL
36
36
  </Text>
@@ -40,8 +40,8 @@ export function GiteaInstallPage() {
40
40
  });
41
41
 
42
42
  return (
43
- <div className="mx-auto flex w-full max-w-[480px] flex-col gap-20">
44
- <header className="flex flex-col gap-8">
43
+ <div className="mx-auto flex w-full max-w-[480px] flex-col gap-section">
44
+ <header className="flex flex-col gap-inline">
45
45
  <Header variant="h1">Install Gitea</Header>
46
46
  <Text size="md" className="text-foreground-neutral-muted">
47
47
  Enter the Gitea organization to install in this workspace.
@@ -49,7 +49,7 @@ export function GiteaInstallPage() {
49
49
  </header>
50
50
 
51
51
  <form
52
- className="flex flex-col gap-18"
52
+ className="flex flex-col gap-group"
53
53
  noValidate
54
54
  onSubmit={(event) => {
55
55
  event.preventDefault();
@@ -83,7 +83,7 @@ export function GiteaInstallPage() {
83
83
  </FormField>
84
84
  )}
85
85
  </form.Field>
86
- <div className="flex items-center gap-12">
86
+ <div className="flex items-center gap-cluster">
87
87
  <Button type="submit" isLoading={connect.isPending}>
88
88
  Install
89
89
  </Button>
@@ -300,9 +300,9 @@ function JiraSiteSelectionPage({
300
300
  onSelect: (site: JiraSite) => void;
301
301
  }) {
302
302
  return (
303
- <main className="flex min-h-screen bg-background-subtle-base px-16 py-32">
304
- <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20">
305
- <header className="flex flex-col gap-8">
303
+ <main className="flex min-h-screen bg-background-subtle-base px-row py-frame">
304
+ <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-section">
305
+ <header className="flex flex-col gap-inline">
306
306
  <Header variant="h2">Choose a Jira site</Header>
307
307
  <Text size="sm" className="text-foreground-neutral-muted">
308
308
  Select the Jira site to connect to this Shipfox workspace.
@@ -315,10 +315,10 @@ function JiraSiteSelectionPage({
315
315
  </Callout>
316
316
  ) : null}
317
317
 
318
- <section className="flex flex-col gap-8" aria-label="Choose a Jira site">
318
+ <section className="flex flex-col gap-inline" aria-label="Choose a Jira site">
319
319
  {sites.map((site) => (
320
- <Card key={site.cloudId} className="p-16">
321
- <div className="flex items-center justify-between gap-12">
320
+ <Card key={site.cloudId} className="p-panel-compact">
321
+ <div className="flex items-center justify-between gap-cluster">
322
322
  <div className="min-w-0">
323
323
  <Text size="md" bold className="truncate">
324
324
  {site.name}
@@ -340,7 +340,7 @@ function JiraSiteSelectionPage({
340
340
  ))}
341
341
  </section>
342
342
 
343
- <div className="flex flex-col gap-8 sm:flex-row sm:items-center">
343
+ <div className="flex flex-col gap-inline sm:flex-row sm:items-center">
344
344
  {failure?.startOver && workspaceSlug ? (
345
345
  <ButtonLink asChild variant="muted" className="min-h-44 w-full sm:w-fit">
346
346
  <Link to="/w/$workspaceSlug/integrations/jira" params={{workspaceSlug}}>
@@ -170,7 +170,7 @@ export function SentryCallbackPage() {
170
170
 
171
171
  return (
172
172
  <CallbackColumn>
173
- <header className="flex flex-col gap-8">
173
+ <header className="flex flex-col gap-inline">
174
174
  <Header variant="h2">Install Sentry</Header>
175
175
  <Text size="sm" className="text-foreground-neutral-muted">
176
176
  {params.orgSlug
@@ -181,7 +181,7 @@ export function SentryCallbackPage() {
181
181
 
182
182
  {failure ? (
183
183
  <Callout role="alert" type="error">
184
- <div className="flex flex-col gap-8">
184
+ <div className="flex flex-col gap-inline">
185
185
  <Text size="sm">{failure.failure.message}</Text>
186
186
  {failure.failure.kind === 'retryable' ? (
187
187
  <Button
@@ -210,10 +210,10 @@ export function SentryCallbackPage() {
210
210
  </Callout>
211
211
  ) : null}
212
212
 
213
- <section className="flex flex-col gap-8" aria-label="Choose a workspace">
213
+ <section className="flex flex-col gap-inline" aria-label="Choose a workspace">
214
214
  {orderedWorkspaces.map((workspace) => (
215
- <Card key={workspace.id} className="p-16">
216
- <div className="flex items-center justify-between gap-12">
215
+ <Card key={workspace.id} className="p-panel-compact">
216
+ <div className="flex items-center justify-between gap-cluster">
217
217
  <Text size="md" bold className="truncate">
218
218
  {workspace.name}
219
219
  </Text>
@@ -246,8 +246,8 @@ export function SentryCallbackPage() {
246
246
 
247
247
  function CallbackColumn({children}: {children: React.ReactNode}) {
248
248
  return (
249
- <main className="flex min-h-screen bg-background-subtle-base px-16 py-32">
250
- <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20">
249
+ <main className="flex min-h-screen bg-background-subtle-base px-row py-frame">
250
+ <div className="mx-auto flex w-full max-w-[480px] flex-col justify-center gap-section">
251
251
  {children}
252
252
  </div>
253
253
  </main>
@@ -8,8 +8,8 @@ export function SourceControlOnboardingPage() {
8
8
  const providersQuery = useIntegrationProvidersQuery({capability: 'source_control'});
9
9
 
10
10
  return (
11
- <div className="mx-auto flex w-full max-w-[640px] flex-col gap-20">
12
- <header className="flex flex-col gap-8">
11
+ <div className="mx-auto flex w-full max-w-[640px] flex-col gap-section">
12
+ <header className="flex flex-col gap-inline">
13
13
  <Header variant="h1">Install source control</Header>
14
14
  <Text size="md" className="text-foreground-neutral-muted">
15
15
  Shipfox needs a source control integration to import your repositories.