nucleus-core-ts 0.9.798 → 0.9.800

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 (25) hide show
  1. package/dist/fe/components/IntegrationsPage/components/ConnectionList.js +11 -9
  2. package/dist/fe/components/IntegrationsPage/components/ConnectionTabs.d.ts +11 -3
  3. package/dist/fe/components/IntegrationsPage/components/ConnectionTabs.js +93 -59
  4. package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.d.ts +1 -1
  5. package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.js +143 -109
  6. package/dist/fe/components/IntegrationsPage/components/RunHistory.js +3 -1
  7. package/dist/fe/components/IntegrationsPage/components/RunPanel.js +4 -2
  8. package/dist/fe/components/IntegrationsPage/components/RunPlanIssues.js +6 -4
  9. package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.js +8 -6
  10. package/dist/fe/components/IntegrationsPage/components/RunsTab.js +3 -1
  11. package/dist/fe/components/IntegrationsPage/index.d.ts +2 -0
  12. package/dist/fe/components/IntegrationsPage/index.js +1 -0
  13. package/dist/fe/components/IntegrationsPage/store/index.js +6 -0
  14. package/dist/fe/components/IntegrationsPage/text/context.d.ts +7 -0
  15. package/dist/fe/components/IntegrationsPage/text/context.js +20 -0
  16. package/dist/fe/components/IntegrationsPage/text/index.d.ts +117 -0
  17. package/dist/fe/components/IntegrationsPage/text/index.js +127 -0
  18. package/dist/fe/components/IntegrationsPage/text/text.test.d.ts +1 -0
  19. package/dist/fe/components/IntegrationsPage/text/text.test.js +67 -0
  20. package/dist/fe/components/IntegrationsPage/theme/index.d.ts +16 -0
  21. package/dist/fe/components/IntegrationsPage/theme/index.js +18 -0
  22. package/dist/fe/components/IntegrationsPage/types/actions.d.ts +9 -0
  23. package/dist/fe/index.d.ts +2 -1
  24. package/dist/fe/index.js +1 -1
  25. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent, useState } from 'react';
4
4
  import { cn } from '../../../utils/cn';
5
5
  import { useIntegrationsStore } from '../store';
6
+ import { useLabels } from '../text/context';
6
7
  import { integrationsPageTheme } from '../theme';
7
8
  import { Badge, Field, Notice } from './Primitives';
8
9
  import { ListSkeleton } from './Skeletons';
@@ -19,6 +20,7 @@ const PAGE_SIZE = 20;
19
20
  const SEARCH_DEBOUNCE_MS = 300;
20
21
  export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
21
22
  const store = useIntegrationsStore();
23
+ const labels = useLabels();
22
24
  const search = store.sourcesSearch;
23
25
  const selectedId = store.selectedSourceId;
24
26
  const [term, setTerm] = useState(search);
@@ -84,11 +86,11 @@ export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
84
86
  children: [
85
87
  /*#__PURE__*/ _jsx("h2", {
86
88
  className: theme.header.title,
87
- children: "Connections"
89
+ children: labels.connections.heading
88
90
  }),
89
91
  /*#__PURE__*/ _jsx("p", {
90
92
  className: theme.header.subtitle,
91
- children: list.total === 1 ? '1 connection' : `${list.total} connections`
93
+ children: list.total === 1 ? labels.connections.countOne : labels.connections.countMany(list.total)
92
94
  })
93
95
  ]
94
96
  }),
@@ -98,19 +100,19 @@ export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
98
100
  className: theme.button.primary,
99
101
  onClick: handleNew,
100
102
  type: "button",
101
- children: "New connection"
103
+ children: labels.connections.create
102
104
  })
103
105
  })
104
106
  ]
105
107
  }),
106
108
  /*#__PURE__*/ _jsx(Field, {
107
109
  htmlFor: "connection-search",
108
- label: "Search",
110
+ label: labels.connections.searchLabel,
109
111
  children: /*#__PURE__*/ _jsx("input", {
110
112
  className: theme.field.input,
111
113
  id: "connection-search",
112
114
  onChange: (event)=>setTerm(event.target.value),
113
- placeholder: "Name or slug",
115
+ placeholder: labels.connections.searchPlaceholder,
114
116
  type: "search",
115
117
  value: term
116
118
  })
@@ -119,7 +121,7 @@ export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
119
121
  rows: 6
120
122
  }) : null,
121
123
  list.error ? /*#__PURE__*/ _jsxs(Notice, {
122
- title: "This list could not be loaded",
124
+ title: labels.connections.loadFailed,
123
125
  tone: "danger",
124
126
  children: [
125
127
  list.error,
@@ -128,13 +130,13 @@ export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
128
130
  className: theme.button.ghost,
129
131
  onClick: list.reload,
130
132
  type: "button",
131
- children: "Try again"
133
+ children: labels.connections.retry
132
134
  })
133
135
  ]
134
136
  }) : null,
135
137
  !firstLoad && list.items.length === 0 && !list.error ? /*#__PURE__*/ _jsx("p", {
136
138
  className: theme.list.empty,
137
- children: search.length > 0 ? `No connection matches “${search}”. Clear the search to see them all.` : 'No connections yet. A connection is one service this app calls — its base URL, how it authenticates and how long it may take. Start with New connection.'
139
+ children: search.length > 0 ? labels.connections.emptySearch(search) : labels.connections.empty
138
140
  }) : null,
139
141
  list.items.length > 0 ? /*#__PURE__*/ _jsx("ul", {
140
142
  className: theme.list.wrapper,
@@ -154,7 +156,7 @@ export function ConnectionList({ actions, onNewConnection, reloadToken = 0 }) {
154
156
  }),
155
157
  source.enabled === false ? /*#__PURE__*/ _jsx(Badge, {
156
158
  tone: "neutral",
157
- children: "Off"
159
+ children: labels.connections.off
158
160
  }) : null
159
161
  ]
160
162
  }),
@@ -2,11 +2,19 @@ import type { ConnectionTab } from '../store/state';
2
2
  /** Shared with the panel so `aria-controls` and `aria-labelledby` line up. */
3
3
  export declare function connectionTabId(tab: ConnectionTab): string;
4
4
  export declare function connectionTabPanelId(tab: ConnectionTab): string;
5
+ /** What a step has to show about itself beyond its name. */
6
+ export type ConnectionStepState = {
7
+ /** Shown in place of the number, and in the step's own words. */
8
+ done?: boolean;
9
+ /** The step before it is unfinished; still reachable, but said so. */
10
+ locked?: boolean;
11
+ /** One short phrase proving the step is done — "3 calls, all sampled". */
12
+ evidence?: string;
13
+ };
5
14
  export type ConnectionTabsProps = {
6
15
  value: ConnectionTab;
7
16
  onChange: (tab: ConnectionTab) => void;
8
- /** Server-reported totals. A zero is left off rather than shown as "0". */
9
- counts?: Partial<Record<ConnectionTab, number>>;
17
+ steps?: Partial<Record<ConnectionTab, ConnectionStepState>>;
10
18
  label?: string;
11
19
  };
12
- export declare function ConnectionTabs({ value, onChange, counts, label, }: ConnectionTabsProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function ConnectionTabs({ value, onChange, steps, label, }: ConnectionTabsProps): import("react/jsx-runtime").JSX.Element;
@@ -2,32 +2,29 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef } from 'react';
4
4
  import { cn } from '../../../utils/cn';
5
+ import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
6
7
  /**
7
- * The strip that switches between a connection's own settings, its endpoints,
8
- * its mappings and its runs.
8
+ * The four phases of an import, in the order they happen.
9
9
  *
10
- * It is a real ARIA tablist: one tab is in the tab order and the arrow keys move
11
- * between the rest. A row of separately tabbable buttons would make a keyboard
12
- * user press Tab three times to leave a control they were never inside.
10
+ * These were four flat tabs Connection, Endpoints, Mappings, Runs sitting
11
+ * side by side with nothing to say that they are a SEQUENCE, that a mapping is
12
+ * meaningless before a call has been tried, or which of them the operator had
13
+ * already finished. People opened the middle one first and met a dead end.
14
+ *
15
+ * So each is a step: a number, its name, one sentence saying what it is for, and
16
+ * a line of evidence once it is done. What is on screen now answers "where am I,
17
+ * what is this for, and what have I already got".
18
+ *
19
+ * It remains a real ARIA tablist — one stop in the tab order, arrow keys between
20
+ * the steps. A row of separately tabbable cards would make a keyboard user press
21
+ * Tab four times to leave a strip they were never inside.
13
22
  */ const theme = integrationsPageTheme;
14
- const TABS = [
15
- {
16
- value: 'connection',
17
- label: 'Connection'
18
- },
19
- {
20
- value: 'endpoints',
21
- label: 'Endpoints'
22
- },
23
- {
24
- value: 'mappings',
25
- label: 'Mappings'
26
- },
27
- {
28
- value: 'runs',
29
- label: 'Runs'
30
- }
23
+ const ORDER = [
24
+ 'connection',
25
+ 'endpoints',
26
+ 'mappings',
27
+ 'runs'
31
28
  ];
32
29
  /** Shared with the panel so `aria-controls` and `aria-labelledby` line up. */ export function connectionTabId(tab) {
33
30
  return `integrations-tab-${tab}`;
@@ -35,18 +32,25 @@ const TABS = [
35
32
  export function connectionTabPanelId(tab) {
36
33
  return `integrations-panel-${tab}`;
37
34
  }
38
- export function ConnectionTabs({ value, onChange, counts, label = 'Connection sections' }) {
35
+ export function ConnectionTabs({ value, onChange, steps, label = 'Import steps' }) {
36
+ const labels = useLabels();
39
37
  const buttonsRef = useRef({});
38
+ const copy = {
39
+ connection: labels.journey.connection,
40
+ endpoints: labels.journey.endpoints,
41
+ mappings: labels.journey.mappings,
42
+ runs: labels.journey.runs
43
+ };
40
44
  const select = (index)=>{
41
- const next = TABS[(index % TABS.length + TABS.length) % TABS.length];
45
+ const next = ORDER[(index % ORDER.length + ORDER.length) % ORDER.length];
42
46
  if (!next) return;
43
- onChange(next.value);
47
+ onChange(next);
44
48
  // Selection follows focus, so the focus has to follow the selection too —
45
- // otherwise the next arrow press starts from the tab the user just left.
46
- buttonsRef.current[next.value]?.focus();
49
+ // otherwise the next arrow press starts from the step the user just left.
50
+ buttonsRef.current[next]?.focus();
47
51
  };
48
52
  const handleKeyDown = (event, tab)=>{
49
- const index = TABS.findIndex((entry)=>entry.value === tab);
53
+ const index = ORDER.indexOf(tab);
50
54
  if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
51
55
  event.preventDefault();
52
56
  select(index + 1);
@@ -58,38 +62,68 @@ export function ConnectionTabs({ value, onChange, counts, label = 'Connection se
58
62
  select(0);
59
63
  } else if (event.key === 'End') {
60
64
  event.preventDefault();
61
- select(TABS.length - 1);
65
+ select(ORDER.length - 1);
62
66
  }
63
67
  };
64
- return /*#__PURE__*/ _jsx("div", {
65
- "aria-label": label,
66
- className: theme.tabs.wrapper,
67
- role: "tablist",
68
- children: TABS.map((tab)=>{
69
- const selected = tab.value === value;
70
- const count = counts?.[tab.value];
71
- return /*#__PURE__*/ _jsxs("button", {
72
- "aria-controls": connectionTabPanelId(tab.value),
73
- "aria-selected": selected,
74
- className: cn(theme.tabs.button, selected ? theme.tabs.active : theme.tabs.inactive),
75
- id: connectionTabId(tab.value),
76
- onClick: ()=>onChange(tab.value),
77
- onKeyDown: (event)=>handleKeyDown(event, tab.value),
78
- ref: (node)=>{
79
- buttonsRef.current[tab.value] = node;
80
- },
81
- role: "tab",
82
- // Roving tabindex: the tablist is ONE stop in the tab order.
83
- tabIndex: selected ? 0 : -1,
84
- type: "button",
85
- children: [
86
- tab.label,
87
- typeof count === 'number' && count > 0 ? /*#__PURE__*/ _jsx("span", {
88
- className: cn(theme.progress.label, 'ml-1.5'),
89
- children: count
90
- }) : null
91
- ]
92
- }, tab.value);
93
- })
68
+ return /*#__PURE__*/ _jsxs("div", {
69
+ className: "flex flex-col gap-2.5",
70
+ children: [
71
+ /*#__PURE__*/ _jsx("p", {
72
+ className: theme.steps.intro,
73
+ children: labels.journey.intro
74
+ }),
75
+ /*#__PURE__*/ _jsx("div", {
76
+ "aria-label": label,
77
+ className: theme.steps.wrapper,
78
+ role: "tablist",
79
+ children: ORDER.map((tab, index)=>{
80
+ const selected = tab === value;
81
+ const state = steps?.[tab] ?? {};
82
+ const { name, purpose } = copy[tab];
83
+ return /*#__PURE__*/ _jsxs("button", {
84
+ "aria-controls": connectionTabPanelId(tab),
85
+ "aria-selected": selected,
86
+ className: cn(theme.steps.item, state.done && !selected && theme.steps.itemDone, state.locked && !selected && theme.steps.itemLocked, selected && theme.steps.itemActive),
87
+ id: connectionTabId(tab),
88
+ onClick: ()=>onChange(tab),
89
+ onKeyDown: (event)=>handleKeyDown(event, tab),
90
+ ref: (node)=>{
91
+ buttonsRef.current[tab] = node;
92
+ },
93
+ role: "tab",
94
+ // Roving tabindex: the whole strip is ONE stop in the tab order.
95
+ tabIndex: selected ? 0 : -1,
96
+ type: "button",
97
+ children: [
98
+ /*#__PURE__*/ _jsx("span", {
99
+ "aria-hidden": "true",
100
+ className: cn(theme.steps.marker, selected ? theme.steps.markerActive : state.done ? theme.steps.markerDone : state.locked ? theme.steps.markerLocked : theme.steps.markerLocked),
101
+ children: state.done && !selected ? '✓' : index + 1
102
+ }),
103
+ /*#__PURE__*/ _jsxs("span", {
104
+ className: "min-w-0 flex-1",
105
+ children: [
106
+ /*#__PURE__*/ _jsx("span", {
107
+ className: cn(theme.steps.name, 'block'),
108
+ children: name
109
+ }),
110
+ /*#__PURE__*/ _jsx("span", {
111
+ className: cn(theme.steps.purpose, 'block'),
112
+ children: purpose
113
+ }),
114
+ state.evidence ? /*#__PURE__*/ _jsx("span", {
115
+ className: cn(theme.steps.evidence, 'block'),
116
+ children: state.evidence
117
+ }) : state.locked ? /*#__PURE__*/ _jsx("span", {
118
+ className: cn(theme.field.hint, 'mt-1 block'),
119
+ children: labels.journey.lockedHint
120
+ }) : null
121
+ ]
122
+ })
123
+ ]
124
+ }, tab);
125
+ })
126
+ })
127
+ ]
94
128
  });
95
129
  }
@@ -1,2 +1,2 @@
1
1
  import type { IntegrationsPageProps } from '../types';
2
- export declare function IntegrationsPage({ title, subtitle, className, actions, onSubscribeProgress, }: IntegrationsPageProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function IntegrationsPage({ title, subtitle, className, actions, labels: labelsOverride, onSubscribeProgress, }: IntegrationsPageProps): import("react/jsx-runtime").JSX.Element;
@@ -5,6 +5,8 @@ import gsap from 'gsap';
5
5
  import { useEffect, useEffectEvent, useRef, useState } from 'react';
6
6
  import { cn } from '../../../utils/cn';
7
7
  import { useIntegrationsStore } from '../store';
8
+ import { resolveIntegrationsLabels } from '../text';
9
+ import { IntegrationsLabelsProvider } from '../text/context';
8
10
  import { integrationsPageTheme } from '../theme';
9
11
  import { ConnectionList } from './ConnectionList';
10
12
  import { ConnectionSettingsTab } from './ConnectionSettingsTab';
@@ -22,8 +24,12 @@ import { RunsTab } from './RunsTab';
22
24
  * page from the server rather than being handed a list to slice.
23
25
  */ gsap.registerPlugin(useGSAP);
24
26
  const theme = integrationsPageTheme;
25
- export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect an external service, describe the data you want and import it on your terms.', className, actions, onSubscribeProgress }) {
27
+ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect an external service, describe the data you want and import it on your terms.', className, actions, labels: labelsOverride, onSubscribeProgress }) {
26
28
  const store = useIntegrationsStore();
29
+ // Resolved here rather than read from the context, because this component is
30
+ // the one that PROVIDES it — and passed down so a control four levels deep
31
+ // does not need a `labels` prop of its own.
32
+ const labels = resolveIntegrationsLabels(labelsOverride);
27
33
  const containerRef = useRef(null);
28
34
  const contentRef = useRef(null);
29
35
  /**
@@ -74,118 +80,146 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
74
80
  };
75
81
  const tab = store.tab;
76
82
  const hasConnection = store.selectedSourceId !== null;
77
- return /*#__PURE__*/ _jsxs("div", {
78
- className: cn(theme.container, className),
79
- ref: containerRef,
80
- children: [
81
- /*#__PURE__*/ _jsx("header", {
82
- className: theme.header.wrapper,
83
- children: /*#__PURE__*/ _jsxs("div", {
84
- className: "flex min-w-0 flex-col gap-1",
85
- children: [
86
- /*#__PURE__*/ _jsx("h1", {
87
- className: theme.header.title,
88
- children: title
89
- }),
90
- subtitle ? /*#__PURE__*/ _jsx("p", {
91
- className: theme.header.subtitle,
92
- children: subtitle
93
- }) : null
94
- ]
95
- })
96
- }),
97
- /*#__PURE__*/ _jsx("div", {
98
- "aria-live": "assertive",
99
- className: "contents",
100
- role: "alert",
101
- children: store.error ? /*#__PURE__*/ _jsxs("div", {
102
- className: "relative",
83
+ /**
84
+ * What each step can say about itself.
85
+ *
86
+ * Evidence rather than a bare count: "3 calls, all tried" answers a question a
87
+ * number does not — whether the thing was only declared or actually run. A
88
+ * step whose prerequisite is missing is marked locked rather than hidden,
89
+ * because a step you cannot see is one you cannot understand the order of.
90
+ */ const selectedSource = store.sources.find((s)=>s.id === store.selectedSourceId);
91
+ const sampled = store.endpoints.filter((e)=>(e.sampleResponse?.records?.length ?? 0) > 0).length;
92
+ const steps = {
93
+ connection: {
94
+ done: Boolean(selectedSource),
95
+ evidence: selectedSource ? labels.journey.evidence.connection(selectedSource.name) : undefined
96
+ },
97
+ endpoints: {
98
+ done: store.endpointsTotal > 0,
99
+ evidence: store.endpointsTotal > 0 ? labels.journey.evidence.endpoints(store.endpointsTotal, sampled) : undefined
100
+ },
101
+ mappings: {
102
+ done: store.mappingsTotal > 0,
103
+ locked: store.endpointsTotal === 0,
104
+ evidence: store.mappingsTotal > 0 ? labels.journey.evidence.mappings(store.mappingsTotal) : undefined
105
+ },
106
+ runs: {
107
+ done: store.runsTotal > 0,
108
+ locked: store.mappingsTotal === 0,
109
+ evidence: store.runsTotal > 0 ? labels.journey.evidence.runs(store.runsTotal) : undefined
110
+ }
111
+ };
112
+ return /*#__PURE__*/ _jsx(IntegrationsLabelsProvider, {
113
+ labels: labels,
114
+ children: /*#__PURE__*/ _jsxs("div", {
115
+ className: cn(theme.container, className),
116
+ ref: containerRef,
117
+ children: [
118
+ /*#__PURE__*/ _jsx("header", {
119
+ className: theme.header.wrapper,
120
+ children: /*#__PURE__*/ _jsxs("div", {
121
+ className: "flex min-w-0 flex-col gap-1",
122
+ children: [
123
+ /*#__PURE__*/ _jsx("h1", {
124
+ className: theme.header.title,
125
+ children: title
126
+ }),
127
+ subtitle ? /*#__PURE__*/ _jsx("p", {
128
+ className: theme.header.subtitle,
129
+ children: subtitle
130
+ }) : null
131
+ ]
132
+ })
133
+ }),
134
+ /*#__PURE__*/ _jsx("div", {
135
+ "aria-live": "assertive",
136
+ className: "contents",
137
+ role: "alert",
138
+ children: store.error ? /*#__PURE__*/ _jsxs("div", {
139
+ className: "relative",
140
+ children: [
141
+ /*#__PURE__*/ _jsx(Notice, {
142
+ title: labels.common.somethingWentWrong,
143
+ tone: "danger",
144
+ children: store.error
145
+ }),
146
+ /*#__PURE__*/ _jsx("button", {
147
+ "aria-label": labels.common.dismiss,
148
+ className: cn(theme.button.ghost, 'absolute top-1.5 right-1.5 h-8 w-8 px-0 sm:h-8'),
149
+ onClick: dismissError,
150
+ type: "button",
151
+ children: /*#__PURE__*/ _jsx("span", {
152
+ "aria-hidden": "true",
153
+ children: "✕"
154
+ })
155
+ })
156
+ ]
157
+ }) : null
158
+ }),
159
+ /*#__PURE__*/ _jsxs("div", {
160
+ className: theme.layout.split,
161
+ ref: contentRef,
103
162
  children: [
104
- /*#__PURE__*/ _jsx(Notice, {
105
- title: "Something went wrong",
106
- tone: "danger",
107
- children: store.error
163
+ /*#__PURE__*/ _jsx("aside", {
164
+ "aria-label": labels.connections.heading,
165
+ className: theme.layout.aside,
166
+ children: /*#__PURE__*/ _jsx(ConnectionList, {
167
+ actions: actions,
168
+ reloadToken: sourcesReloadToken
169
+ })
108
170
  }),
109
- /*#__PURE__*/ _jsx("button", {
110
- "aria-label": "Dismiss error",
111
- className: cn(theme.button.ghost, 'absolute top-1.5 right-1.5 h-8 w-8 px-0 sm:h-8'),
112
- onClick: dismissError,
113
- type: "button",
114
- children: /*#__PURE__*/ _jsx("span", {
115
- "aria-hidden": "true",
116
- children: "✕"
171
+ /*#__PURE__*/ _jsx("section", {
172
+ "aria-label": "Connection detail",
173
+ className: theme.layout.main,
174
+ children: hasConnection ? /*#__PURE__*/ _jsxs(_Fragment, {
175
+ children: [
176
+ /*#__PURE__*/ _jsx(ConnectionTabs, {
177
+ onChange: changeTab,
178
+ steps: steps,
179
+ value: tab
180
+ }),
181
+ /*#__PURE__*/ _jsxs("section", {
182
+ "aria-labelledby": connectionTabId(tab),
183
+ className: theme.layout.main,
184
+ id: connectionTabPanelId(tab),
185
+ role: "tabpanel",
186
+ children: [
187
+ tab === 'connection' ? /*#__PURE__*/ _jsx(ConnectionSettingsTab, {
188
+ actions: actions,
189
+ onSaved: handleSourceSaved
190
+ }) : null,
191
+ tab === 'endpoints' ? /*#__PURE__*/ _jsx(EndpointsTab, {
192
+ actions: actions
193
+ }) : null,
194
+ tab === 'mappings' ? /*#__PURE__*/ _jsx(MappingsTab, {
195
+ actions: actions
196
+ }) : null,
197
+ tab === 'runs' ? /*#__PURE__*/ _jsx(RunsTab, {
198
+ actions: actions
199
+ }) : null
200
+ ]
201
+ })
202
+ ]
203
+ }) : // Not a dead end. With nothing selected the only useful thing on this
204
+ // screen is creating a connection, so that is what it shows — the
205
+ // previous message told an operator to select from a list that is
206
+ // empty on a fresh install, with no way to add the first one.
207
+ /*#__PURE__*/ _jsxs(_Fragment, {
208
+ children: [
209
+ /*#__PURE__*/ _jsx("p", {
210
+ className: theme.list.empty,
211
+ children: "Select a connection on the left, or set up a new one here."
212
+ }),
213
+ /*#__PURE__*/ _jsx(ConnectionSettingsTab, {
214
+ actions: actions,
215
+ onSaved: handleSourceSaved
216
+ })
217
+ ]
117
218
  })
118
219
  })
119
220
  ]
120
- }) : null
121
- }),
122
- /*#__PURE__*/ _jsxs("div", {
123
- className: theme.layout.split,
124
- ref: contentRef,
125
- children: [
126
- /*#__PURE__*/ _jsx("aside", {
127
- "aria-label": "Connections",
128
- className: theme.layout.aside,
129
- children: /*#__PURE__*/ _jsx(ConnectionList, {
130
- actions: actions,
131
- reloadToken: sourcesReloadToken
132
- })
133
- }),
134
- /*#__PURE__*/ _jsx("section", {
135
- "aria-label": "Connection detail",
136
- className: theme.layout.main,
137
- children: hasConnection ? /*#__PURE__*/ _jsxs(_Fragment, {
138
- children: [
139
- /*#__PURE__*/ _jsx(ConnectionTabs, {
140
- counts: {
141
- endpoints: store.endpointsTotal,
142
- mappings: store.mappingsTotal,
143
- runs: store.runsTotal
144
- },
145
- onChange: changeTab,
146
- value: tab
147
- }),
148
- /*#__PURE__*/ _jsxs("section", {
149
- "aria-labelledby": connectionTabId(tab),
150
- className: theme.layout.main,
151
- id: connectionTabPanelId(tab),
152
- role: "tabpanel",
153
- children: [
154
- tab === 'connection' ? /*#__PURE__*/ _jsx(ConnectionSettingsTab, {
155
- actions: actions,
156
- onSaved: handleSourceSaved
157
- }) : null,
158
- tab === 'endpoints' ? /*#__PURE__*/ _jsx(EndpointsTab, {
159
- actions: actions
160
- }) : null,
161
- tab === 'mappings' ? /*#__PURE__*/ _jsx(MappingsTab, {
162
- actions: actions
163
- }) : null,
164
- tab === 'runs' ? /*#__PURE__*/ _jsx(RunsTab, {
165
- actions: actions
166
- }) : null
167
- ]
168
- })
169
- ]
170
- }) : // Not a dead end. With nothing selected the only useful thing on this
171
- // screen is creating a connection, so that is what it shows — the
172
- // previous message told an operator to select from a list that is
173
- // empty on a fresh install, with no way to add the first one.
174
- /*#__PURE__*/ _jsxs(_Fragment, {
175
- children: [
176
- /*#__PURE__*/ _jsx("p", {
177
- className: theme.list.empty,
178
- children: "Select a connection on the left, or set up a new one here."
179
- }),
180
- /*#__PURE__*/ _jsx(ConnectionSettingsTab, {
181
- actions: actions,
182
- onSaved: handleSourceSaved
183
- })
184
- ]
185
- })
186
- })
187
- ]
188
- })
189
- ]
221
+ })
222
+ ]
223
+ })
190
224
  });
191
225
  }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent } from 'react';
4
4
  import { useIntegrationsStore } from '../store';
5
+ import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
6
7
  import { Notice } from './Primitives';
7
8
  import { RunHistoryRow } from './RunHistoryRow';
@@ -17,6 +18,7 @@ import { useInfiniteList } from './useInfiniteList';
17
18
  const PAGE_SIZE = 20;
18
19
  export function RunHistory({ actions, mappingId, refreshKey, onRefresh }) {
19
20
  const store = useIntegrationsStore();
21
+ const labels = useLabels();
20
22
  const list = useInfiniteList({
21
23
  load: (page)=>actions.listRuns({
22
24
  page,
@@ -69,7 +71,7 @@ export function RunHistory({ actions, mappingId, refreshKey, onRefresh }) {
69
71
  children: [
70
72
  /*#__PURE__*/ _jsx("h3", {
71
73
  className: theme.card.title,
72
- children: "History"
74
+ children: labels.runs.historyHeading
73
75
  }),
74
76
  /*#__PURE__*/ _jsx("span", {
75
77
  className: theme.field.hint,
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent, useState } from 'react';
4
4
  import { useIntegrationsStore } from '../store';
5
+ import { useLabels } from '../text/context';
5
6
  import { integrationsPageTheme } from '../theme';
6
7
  import { Notice } from './Primitives';
7
8
  import { RunConfirmStep } from './RunConfirmStep';
@@ -19,6 +20,7 @@ import { StatGridSkeleton } from './Skeletons';
19
20
  */ const theme = integrationsPageTheme;
20
21
  export function RunPanel({ mapping, actions, onSubscribeProgress, onRunSettled }) {
21
22
  const store = useIntegrationsStore();
23
+ const labels = useLabels();
22
24
  const [confirming, setConfirming] = useState(false);
23
25
  const [starting, setStarting] = useState(false);
24
26
  const [cancelling, setCancelling] = useState(false);
@@ -103,7 +105,7 @@ export function RunPanel({ mapping, actions, onSubscribeProgress, onRunSettled }
103
105
  }),
104
106
  /*#__PURE__*/ _jsx("p", {
105
107
  className: theme.field.hint,
106
- children: `Writes into ${mapping.targetEntity} · ${mapping.writeMode} · missing rows: ${mapping.missingRecordPolicy}`
108
+ children: labels.runs.summary(mapping.targetEntity, mapping.writeMode, mapping.missingRecordPolicy)
107
109
  })
108
110
  ]
109
111
  }),
@@ -150,7 +152,7 @@ export function RunPanel({ mapping, actions, onSubscribeProgress, onRunSettled }
150
152
  }) : null,
151
153
  !(store.planning || plan || activeRunId) ? /*#__PURE__*/ _jsx(Notice, {
152
154
  tone: "info",
153
- title: "Preview before you run",
155
+ title: labels.runs.previewExplainTitle,
154
156
  children: "A preview reads the source without writing anything, and reports how many rows would be created, updated, skipped, and how many rows in the target the source no longer returns."
155
157
  }) : null
156
158
  ]
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from '../../../utils/cn';
4
+ import { useLabels } from '../text/context';
4
5
  import { integrationsPageTheme } from '../theme';
5
6
  /**
6
7
  * The records the planner could not use, folded away.
@@ -18,13 +19,14 @@ import { integrationsPageTheme } from '../theme';
18
19
  }).join(', ');
19
20
  }
20
21
  export function RunPlanIssues({ issues }) {
22
+ const labels = useLabels();
21
23
  if (issues.length === 0) return null;
22
24
  return /*#__PURE__*/ _jsxs("details", {
23
25
  className: cn(theme.card.wrapper, 'px-3 py-2.5 sm:px-4 sm:py-3'),
24
26
  children: [
25
27
  /*#__PURE__*/ _jsx("summary", {
26
28
  className: cn(theme.card.title, 'cursor-pointer'),
27
- children: `${issues.length} record(s) the plan could not use`
29
+ children: labels.runs.unusable(issues.length)
28
30
  }),
29
31
  /*#__PURE__*/ _jsx("ul", {
30
32
  className: "mt-2 flex flex-col gap-2",
@@ -41,15 +43,15 @@ export function RunPlanIssues({ issues }) {
41
43
  }),
42
44
  issue.dedupKey ? /*#__PURE__*/ _jsx("span", {
43
45
  className: theme.field.hint,
44
- children: `Dedup key: ${issue.dedupKey}`
46
+ children: `${labels.runs.dedupKey}: ${issue.dedupKey}`
45
47
  }) : null,
46
48
  issue.keptRow ? /*#__PURE__*/ _jsx("span", {
47
49
  className: theme.field.hint,
48
- children: `Kept — ${describeRow(issue.keptRow)}`
50
+ children: `${labels.runs.kept} — ${describeRow(issue.keptRow)}`
49
51
  }) : null,
50
52
  issue.row ? /*#__PURE__*/ _jsx("span", {
51
53
  className: theme.field.hint,
52
- children: issue.keptRow ? `Skipped — ${describeRow(issue.row)}` : describeRow(issue.row)
54
+ children: issue.keptRow ? `${labels.runs.skipped} — ${describeRow(issue.row)}` : describeRow(issue.row)
53
55
  }) : null
54
56
  ]
55
57
  }, `${issue.sourceIndex}-${issue.kind}-${index}`))
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLabels } from '../text/context';
3
4
  import { integrationsPageTheme } from '../theme';
4
5
  import { Badge, Notice, Stat } from './Primitives';
5
6
  import { RunPlanIssues } from './RunPlanIssues';
@@ -17,6 +18,7 @@ import { RunPlanIssues } from './RunPlanIssues';
17
18
  return undefined;
18
19
  }
19
20
  export function RunPlanSummary({ plan }) {
21
+ const labels = useLabels();
20
22
  // A source that reports a total and hands over a different number of records
21
23
  // is the loudest symptom of a wrong pagination profile, and the run that
22
24
  // follows treats everything it never saw as missing.
@@ -54,28 +56,28 @@ export function RunPlanSummary({ plan }) {
54
56
  className: theme.stat.grid,
55
57
  children: [
56
58
  /*#__PURE__*/ _jsx(Stat, {
57
- label: "To create",
59
+ label: labels.runs.toCreate,
58
60
  value: plan.toCreate
59
61
  }),
60
62
  /*#__PURE__*/ _jsx(Stat, {
61
- label: "To update",
63
+ label: labels.runs.toUpdate,
62
64
  value: plan.toUpdate
63
65
  }),
64
66
  /*#__PURE__*/ _jsx(Stat, {
65
- label: "To skip",
67
+ label: labels.runs.toSkip,
66
68
  value: plan.toSkip
67
69
  }),
68
70
  /*#__PURE__*/ _jsx(Stat, {
69
- label: "Missing",
71
+ label: labels.runs.missing,
70
72
  tone: missingTone(plan),
71
73
  value: plan.missing
72
74
  }),
73
75
  /*#__PURE__*/ _jsx(Stat, {
74
- label: "Already there",
76
+ label: labels.runs.alreadyThere,
75
77
  value: plan.existingCount
76
78
  }),
77
79
  /*#__PURE__*/ _jsx(Stat, {
78
- label: "Fetched",
80
+ label: labels.runs.fetched,
79
81
  value: plan.totalFetched
80
82
  })
81
83
  ]
@@ -2,11 +2,13 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { useIntegrationsStore } from '../store';
5
+ import { useLabels } from '../text/context';
5
6
  import { Notice } from './Primitives';
6
7
  import { RunHistory } from './RunHistory';
7
8
  import { RunPanel } from './RunPanel';
8
9
  export function RunsTab({ actions, onSubscribeProgress }) {
9
10
  const store = useIntegrationsStore();
11
+ const labels = useLabels();
10
12
  // A lookup by id, not a list operation: the selected mapping is on the page the
11
13
  // server already returned, and nothing here filters, sorts or pages it.
12
14
  const selectedId = store.selectedMappingId;
@@ -25,7 +27,7 @@ export function RunsTab({ actions, onSubscribeProgress }) {
25
27
  onSubscribeProgress: onSubscribeProgress
26
28
  }) : /*#__PURE__*/ _jsx(Notice, {
27
29
  tone: "info",
28
- title: "Choose a mapping to preview or run an import",
30
+ title: labels.runs.chooseMappingTitle,
29
31
  children: "A mapping is what says which endpoint feeds which target entity, and how missing rows are treated. Pick one on the Mappings tab; the history below still lists every run of this connection."
30
32
  }),
31
33
  /*#__PURE__*/ _jsx(RunHistory, {
@@ -13,6 +13,8 @@ export type { InfiniteListPage, UseInfiniteListOptions, UseInfiniteListResult, }
13
13
  export { useInfiniteList } from './components/useInfiniteList';
14
14
  export { useIntegrationsStore } from './store';
15
15
  export type { ConnectionTab } from './store/state';
16
+ export type { IntegrationsLabels, IntegrationsLabelsOverride } from './text';
17
+ export { defaultIntegrationsLabels, resolveIntegrationsLabels } from './text';
16
18
  export type { IntegrationsPageTheme } from './theme';
17
19
  export { extendIntegrationsTheme, integrationsPageTheme } from './theme';
18
20
  export type * from './types';
@@ -8,4 +8,5 @@ export { RunsTab } from './components/RunsTab';
8
8
  export { CardSkeleton, ListSkeleton, StatGridSkeleton } from './components/Skeletons';
9
9
  export { useInfiniteList } from './components/useInfiniteList';
10
10
  export { useIntegrationsStore } from './store';
11
+ export { defaultIntegrationsLabels, resolveIntegrationsLabels } from './text';
11
12
  export { extendIntegrationsTheme, integrationsPageTheme } from './theme';
@@ -85,6 +85,12 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
85
85
  // A plan belongs to the mapping it was computed for. Showing it against a
86
86
  // different one would have an operator approve counts from another import.
87
87
  store.plan = null;
88
+ // And so does a RUN, which says something stronger than a plan does: that
89
+ // this import happened. Seen live — the Ünvanlar run stayed on screen
90
+ // under a header reading "Writes into locations", for a mapping that had
91
+ // never been run at all.
92
+ store.activeRunId = null;
93
+ store.progress = null;
88
94
  }),
89
95
  setRuns: (store)=>(items, total)=>batch(()=>{
90
96
  store.runs = items;
@@ -0,0 +1,7 @@
1
+ import { type IntegrationsLabels } from './index';
2
+ export declare function IntegrationsLabelsProvider({ labels, children, }: {
3
+ labels: IntegrationsLabels;
4
+ children: React.ReactNode;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ /** The resolved labels. Falls back to English when no provider is above. */
7
+ export declare function useLabels(): IntegrationsLabels;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ import { defaultIntegrationsLabels } from './index';
5
+ /**
6
+ * The panel's words, reachable from any depth without prop-drilling.
7
+ *
8
+ * A context rather than a prop because the strings are needed by controls four
9
+ * levels down — a field row, a badge, a stat cell — and threading them through
10
+ * would put a `labels` prop on components that otherwise take none.
11
+ */ const LabelsContext = /*#__PURE__*/ createContext(defaultIntegrationsLabels);
12
+ export function IntegrationsLabelsProvider({ labels, children }) {
13
+ return /*#__PURE__*/ _jsx(LabelsContext.Provider, {
14
+ value: labels,
15
+ children: children
16
+ });
17
+ }
18
+ /** The resolved labels. Falls back to English when no provider is above. */ export function useLabels() {
19
+ return useContext(LabelsContext);
20
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Every word the panel says, in one place — and overridable by the app.
3
+ *
4
+ * The theme could already be replaced slice by slice; the text could not, so a
5
+ * Turkish intranet rendered a Turkish page header above an entirely English
6
+ * panel. From the operator's side that does not read as "in another language",
7
+ * it reads as no explanation at all: `upsert`, `Deduplication column`,
8
+ * `missing rows: ignore` mean nothing to the person whose job this screen is.
9
+ *
10
+ * English stays the default so an app that says nothing still gets sentences,
11
+ * and every group is overridable on its own — an app translating the steps does
12
+ * not have to restate the buttons.
13
+ */
14
+ export type IntegrationsLabels = {
15
+ /** The four phases of the work, in the order they happen. */
16
+ journey: {
17
+ heading: string;
18
+ /** Shown once, above the steps: what this screen is for. */
19
+ intro: string;
20
+ connection: {
21
+ name: string;
22
+ purpose: string;
23
+ };
24
+ endpoints: {
25
+ name: string;
26
+ purpose: string;
27
+ };
28
+ mappings: {
29
+ name: string;
30
+ purpose: string;
31
+ };
32
+ runs: {
33
+ name: string;
34
+ purpose: string;
35
+ };
36
+ /** Marks a step whose prerequisite is not done yet. */
37
+ lockedHint: string;
38
+ doneHint: string;
39
+ /** One short phrase per step proving it is done. */
40
+ evidence: {
41
+ connection: (name: string) => string;
42
+ endpoints: (total: number, sampled: number) => string;
43
+ mappings: (total: number) => string;
44
+ runs: (total: number) => string;
45
+ };
46
+ };
47
+ connections: {
48
+ heading: string;
49
+ countOne: string;
50
+ countMany: (n: number) => string;
51
+ searchLabel: string;
52
+ searchPlaceholder: string;
53
+ create: string;
54
+ empty: string;
55
+ emptySearch: (term: string) => string;
56
+ loadFailed: string;
57
+ retry: string;
58
+ off: string;
59
+ };
60
+ runs: {
61
+ /** The one-line summary above the buttons. */
62
+ summary: (target: string, writeMode: string, missing: string) => string;
63
+ preview: string;
64
+ start: string;
65
+ cancel: string;
66
+ previewExplainTitle: string;
67
+ previewExplainBody: string;
68
+ historyHeading: string;
69
+ historyEmpty: string;
70
+ chooseMappingTitle: string;
71
+ chooseMappingBody: string;
72
+ toCreate: string;
73
+ toUpdate: string;
74
+ toSkip: string;
75
+ missing: string;
76
+ alreadyThere: string;
77
+ fetched: string;
78
+ unusable: (n: number) => string;
79
+ kept: string;
80
+ skipped: string;
81
+ dedupKey: string;
82
+ };
83
+ writeMode: {
84
+ upsert: string;
85
+ append: string;
86
+ replace: string;
87
+ };
88
+ missingPolicy: {
89
+ ignore: string;
90
+ deactivate: string;
91
+ delete: string;
92
+ };
93
+ common: {
94
+ save: string;
95
+ cancel: string;
96
+ delete: string;
97
+ deleting: string;
98
+ saving: string;
99
+ somethingWentWrong: string;
100
+ dismiss: string;
101
+ };
102
+ };
103
+ /** What every app gets before it says otherwise. */
104
+ export declare const defaultIntegrationsLabels: IntegrationsLabels;
105
+ type DeepPartial<T> = {
106
+ [K in keyof T]?: T[K] extends (...args: never[]) => unknown ? T[K] : T[K] extends object ? DeepPartial<T[K]> : T[K];
107
+ };
108
+ export type IntegrationsLabelsOverride = DeepPartial<IntegrationsLabels>;
109
+ /**
110
+ * Fills the gaps in an app's labels from the defaults.
111
+ *
112
+ * Two levels deep is everything this shape has, so a group is merged rather
113
+ * than replaced — an app naming the four steps does not lose the sentences that
114
+ * explain them.
115
+ */
116
+ export declare function resolveIntegrationsLabels(overrides?: IntegrationsLabelsOverride): IntegrationsLabels;
117
+ export {};
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Every word the panel says, in one place — and overridable by the app.
3
+ *
4
+ * The theme could already be replaced slice by slice; the text could not, so a
5
+ * Turkish intranet rendered a Turkish page header above an entirely English
6
+ * panel. From the operator's side that does not read as "in another language",
7
+ * it reads as no explanation at all: `upsert`, `Deduplication column`,
8
+ * `missing rows: ignore` mean nothing to the person whose job this screen is.
9
+ *
10
+ * English stays the default so an app that says nothing still gets sentences,
11
+ * and every group is overridable on its own — an app translating the steps does
12
+ * not have to restate the buttons.
13
+ */ /** What every app gets before it says otherwise. */ export const defaultIntegrationsLabels = {
14
+ journey: {
15
+ heading: 'How an import works',
16
+ intro: 'Data is brought in from another service and written into one of your own tables. Four steps, in this order — each one is only useful once the one before it is done.',
17
+ connection: {
18
+ name: 'Connection',
19
+ purpose: 'Which service, at which address, and how we prove who we are.'
20
+ },
21
+ endpoints: {
22
+ name: 'Calls',
23
+ purpose: 'Which call to make, and what it really returns — try it and see.'
24
+ },
25
+ mappings: {
26
+ name: 'Mapping',
27
+ purpose: 'Which field from the answer lands in which column of your table.'
28
+ },
29
+ runs: {
30
+ name: 'Import',
31
+ purpose: 'See what would happen, then let it happen.'
32
+ },
33
+ lockedHint: 'Finish the step before this one first.',
34
+ doneHint: 'Done',
35
+ evidence: {
36
+ connection: (name)=>`Connected to ${name}`,
37
+ endpoints: (total, sampled)=>sampled === total ? `${total} call(s), all tried` : `${total} call(s), ${sampled} tried`,
38
+ mappings: (total)=>`${total} mapping(s)`,
39
+ runs: (total)=>`${total} import(s) run`
40
+ }
41
+ },
42
+ connections: {
43
+ heading: 'Connections',
44
+ countOne: '1 connection',
45
+ countMany: (n)=>`${n} connections`,
46
+ searchLabel: 'Search',
47
+ searchPlaceholder: 'Name or slug',
48
+ create: 'New connection',
49
+ empty: 'No connections yet. A connection is one service this app calls — its base URL, how it authenticates and how long it may take. Start with New connection.',
50
+ emptySearch: (term)=>`No connection matches “${term}”. Clear the search to see them all.`,
51
+ loadFailed: 'This list could not be loaded',
52
+ retry: 'Try again',
53
+ off: 'Off'
54
+ },
55
+ runs: {
56
+ summary: (target, writeMode, missing)=>`Writes into ${target} · ${writeMode} · missing rows: ${missing}`,
57
+ preview: 'Preview',
58
+ start: 'Run import',
59
+ cancel: 'Cancel',
60
+ previewExplainTitle: 'Preview before you run',
61
+ previewExplainBody: 'A preview reads the source without writing anything, and reports how many rows would be created, updated, skipped, and how many rows in the target the source no longer returns.',
62
+ historyHeading: 'History',
63
+ historyEmpty: 'Nothing has run yet. A preview writes nothing; the first entry appears here once an import is started.',
64
+ chooseMappingTitle: 'Choose a mapping to preview or run an import',
65
+ chooseMappingBody: 'A mapping is what says which endpoint feeds which target entity, and how missing rows are treated.',
66
+ toCreate: 'To create',
67
+ toUpdate: 'To update',
68
+ toSkip: 'To skip',
69
+ missing: 'Missing',
70
+ alreadyThere: 'Already there',
71
+ fetched: 'Fetched',
72
+ unusable: (n)=>`${n} record(s) the plan could not use`,
73
+ kept: 'Kept',
74
+ skipped: 'Skipped',
75
+ dedupKey: 'Dedup key'
76
+ },
77
+ writeMode: {
78
+ upsert: 'update what matches, insert the rest',
79
+ append: 'always insert, never match',
80
+ replace: 'empty the table, then insert'
81
+ },
82
+ missingPolicy: {
83
+ ignore: 'leave them exactly as they are',
84
+ deactivate: 'mark them inactive',
85
+ delete: 'delete them'
86
+ },
87
+ common: {
88
+ save: 'Save',
89
+ cancel: 'Cancel',
90
+ delete: 'Delete',
91
+ deleting: 'Deleting…',
92
+ saving: 'Saving…',
93
+ somethingWentWrong: 'Something went wrong',
94
+ dismiss: 'Dismiss error'
95
+ }
96
+ };
97
+ /**
98
+ * Fills the gaps in an app's labels from the defaults.
99
+ *
100
+ * Two levels deep is everything this shape has, so a group is merged rather
101
+ * than replaced — an app naming the four steps does not lose the sentences that
102
+ * explain them.
103
+ */ export function resolveIntegrationsLabels(overrides) {
104
+ if (!overrides) return defaultIntegrationsLabels;
105
+ const merged = {
106
+ ...defaultIntegrationsLabels
107
+ };
108
+ for (const [group, value] of Object.entries(overrides)){
109
+ const base = defaultIntegrationsLabels[group];
110
+ if (!value || typeof value !== 'object' || !base || typeof base !== 'object') {
111
+ merged[group] = value ?? base;
112
+ continue;
113
+ }
114
+ const groupMerged = {
115
+ ...base
116
+ };
117
+ for (const [key, inner] of Object.entries(value)){
118
+ const innerBase = groupMerged[key];
119
+ groupMerged[key] = inner && typeof inner === 'object' && innerBase && typeof innerBase === 'object' ? {
120
+ ...innerBase,
121
+ ...inner
122
+ } : inner ?? innerBase;
123
+ }
124
+ merged[group] = groupMerged;
125
+ }
126
+ return merged;
127
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { defaultIntegrationsLabels, resolveIntegrationsLabels } from './index';
3
+ /**
4
+ * The panel shipped English inside a Turkish app, which does not read as
5
+ * "another language" to the person using it — it reads as no explanation at
6
+ * all. Apps can now say it their own way, and the point of merging rather than
7
+ * replacing is that translating one group never blanks the rest.
8
+ */ describe('resolveIntegrationsLabels', ()=>{
9
+ test('saying nothing gets the English defaults, not empty strings', ()=>{
10
+ expect(resolveIntegrationsLabels()).toBe(defaultIntegrationsLabels);
11
+ expect(resolveIntegrationsLabels(undefined).connections.heading).toBe('Connections');
12
+ });
13
+ test('overriding one key inside a group keeps that group’s other keys', ()=>{
14
+ const labels = resolveIntegrationsLabels({
15
+ connections: {
16
+ heading: 'Bağlantılar'
17
+ }
18
+ });
19
+ expect(labels.connections.heading).toBe('Bağlantılar');
20
+ expect(labels.connections.searchLabel).toBe(defaultIntegrationsLabels.connections.searchLabel);
21
+ expect(labels.runs.preview).toBe(defaultIntegrationsLabels.runs.preview);
22
+ });
23
+ test('a nested group merges too — naming a step keeps its explanation', ()=>{
24
+ const labels = resolveIntegrationsLabels({
25
+ journey: {
26
+ connection: {
27
+ name: 'Bağlantı'
28
+ }
29
+ }
30
+ });
31
+ expect(labels.journey.connection.name).toBe('Bağlantı');
32
+ expect(labels.journey.connection.purpose).toBe(defaultIntegrationsLabels.journey.connection.purpose);
33
+ expect(labels.journey.endpoints.name).toBe(defaultIntegrationsLabels.journey.endpoints.name);
34
+ });
35
+ test('a function label can be replaced, and is called with the same arguments', ()=>{
36
+ const labels = resolveIntegrationsLabels({
37
+ connections: {
38
+ countMany: (n)=>`${n} bağlantı`
39
+ },
40
+ runs: {
41
+ summary: (t)=>`${t} tablosuna yazar`
42
+ }
43
+ });
44
+ expect(labels.connections.countMany(3)).toBe('3 bağlantı');
45
+ expect(labels.runs.summary('departments', 'upsert', 'ignore')).toBe('departments tablosuna yazar');
46
+ });
47
+ test('the defaults are never mutated by an override', ()=>{
48
+ resolveIntegrationsLabels({
49
+ connections: {
50
+ heading: 'Bağlantılar'
51
+ }
52
+ });
53
+ expect(defaultIntegrationsLabels.connections.heading).toBe('Connections');
54
+ });
55
+ test('every step has both a name and a sentence saying what it is for', ()=>{
56
+ const { journey } = defaultIntegrationsLabels;
57
+ for (const step of [
58
+ journey.connection,
59
+ journey.endpoints,
60
+ journey.mappings,
61
+ journey.runs
62
+ ]){
63
+ expect(step.name.length).toBeGreaterThan(0);
64
+ expect(step.purpose.length).toBeGreaterThan(10);
65
+ }
66
+ });
67
+ });
@@ -43,6 +43,22 @@ export type IntegrationsPageTheme = {
43
43
  active: string;
44
44
  inactive: string;
45
45
  };
46
+ /** The four phases of an import, as a strip of steps rather than flat tabs. */
47
+ steps: {
48
+ intro: string;
49
+ wrapper: string;
50
+ item: string;
51
+ itemActive: string;
52
+ itemDone: string;
53
+ itemLocked: string;
54
+ marker: string;
55
+ markerActive: string;
56
+ markerDone: string;
57
+ markerLocked: string;
58
+ name: string;
59
+ purpose: string;
60
+ evidence: string;
61
+ };
46
62
  field: {
47
63
  wrapper: string;
48
64
  label: string;
@@ -46,6 +46,24 @@ export const integrationsPageTheme = {
46
46
  active: 'bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-slate-100',
47
47
  inactive: 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100'
48
48
  },
49
+ // A step is a card, not a tab: it has to carry a name, a sentence saying what
50
+ // the step is for, and a line of evidence that it is done. Four of those do
51
+ // not fit on a phone side by side, so they stack and become a row at sm.
52
+ steps: {
53
+ intro: 'text-slate-600 text-sm leading-relaxed sm:max-w-3xl dark:text-slate-400',
54
+ wrapper: 'grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4',
55
+ item: 'flex min-w-0 items-start gap-2.5 rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-left transition-colors hover:border-slate-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-500 focus-visible:outline-offset-2 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700',
56
+ itemActive: 'border-sky-400 bg-sky-50 ring-1 ring-sky-400 hover:border-sky-400 dark:border-sky-600 dark:bg-sky-950 dark:ring-sky-600',
57
+ itemDone: 'border-emerald-200 dark:border-emerald-900',
58
+ itemLocked: 'opacity-70',
59
+ marker: 'flex size-6 shrink-0 items-center justify-center rounded-full border font-semibold text-[11px] tabular-nums',
60
+ markerActive: 'border-sky-500 bg-sky-500 text-white dark:border-sky-500 dark:bg-sky-500',
61
+ markerDone: 'border-emerald-300 bg-emerald-100 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-400',
62
+ markerLocked: 'border-slate-200 bg-slate-100 text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-500',
63
+ name: 'font-semibold text-slate-900 text-sm dark:text-slate-100',
64
+ purpose: 'mt-0.5 text-slate-600 text-xs leading-snug dark:text-slate-400',
65
+ evidence: 'mt-1 font-medium text-[11px] text-emerald-700 dark:text-emerald-400'
66
+ },
49
67
  field: {
50
68
  wrapper: 'flex flex-col gap-1.5',
51
69
  label: 'font-medium text-slate-700 text-xs dark:text-slate-300',
@@ -1,4 +1,5 @@
1
1
  import type { FilterCondition, SortCondition } from '../../../types';
2
+ import type { IntegrationsLabelsOverride } from '../text';
2
3
  import type { IntegrationEndpoint, IntegrationMapping, IntegrationRun, IntegrationSource } from './records';
3
4
  import type { ConnectionTestValue, EndpointSampleValue, RunPlanValue, RunProgressValue } from './results';
4
5
  /**
@@ -84,6 +85,14 @@ export type IntegrationsPageProps = {
84
85
  subtitle?: string;
85
86
  className?: string;
86
87
  actions: IntegrationsActions;
88
+ /**
89
+ * The panel's own words, in the app's language.
90
+ *
91
+ * Anything left out falls back to English. Without this the panel was English
92
+ * inside apps that are not — which does not read as "another language" to the
93
+ * person using it, it reads as no explanation at all.
94
+ */
95
+ labels?: IntegrationsLabelsOverride;
87
96
  /** Realtime run progress, when the app wires the pub/sub topic. */
88
97
  onSubscribeProgress?: (handler: (progress: RunProgressValue) => void) => () => void;
89
98
  };
@@ -21,8 +21,9 @@ export type { ForgotPasswordAction, ForgotPasswordFormProps, ForgotPasswordHeade
21
21
  export { ForgotPasswordPage, useForgotPasswordStore, } from './components/ForgotPasswordPage';
22
22
  export type { FormBuilderProps, FormFieldConfig, FormFieldProps, FormFieldRenderProps, FormFooterRenderProps, FormLayout, FormMode, FormSize, FormState, UseFormBuilderOptions, UseFormBuilderReturn, } from './components/FormBuilder';
23
23
  export { FormBuilder, FormField, formBuilderTheme, useFormBuilder, } from './components/FormBuilder';
24
- export { extendIntegrationsTheme, IntegrationsPage, integrationsPageTheme, useIntegrationsStore, } from './components/IntegrationsPage';
24
+ export { defaultIntegrationsLabels, extendIntegrationsTheme, IntegrationsPage, integrationsPageTheme, resolveIntegrationsLabels, useIntegrationsStore, } from './components/IntegrationsPage';
25
25
  export type { ConnectionTab } from './components/IntegrationsPage/store/state';
26
+ export type { IntegrationsLabels, IntegrationsLabelsOverride, } from './components/IntegrationsPage/text';
26
27
  export type { IntegrationsPageTheme } from './components/IntegrationsPage/theme';
27
28
  export type * from './components/IntegrationsPage/types';
28
29
  export type { LoginAction, LoginFeatureConfig, LoginFormData, LoginFormProps, LoginHeaderProps, LoginPageConfig, LoginPageProps, LoginPageVariant, LogoutAction, MeAction, } from './components/LoginPage';
package/dist/fe/index.js CHANGED
@@ -15,7 +15,7 @@ export { ComponentCanvas, ComponentSidebar, createDefaultRegistry, DesignSystemP
15
15
  export { DeviceCard, DevicesHeader, DevicesPage, useDevicesStore } from './components/DevicesPage';
16
16
  export { ForgotPasswordPage, useForgotPasswordStore } from './components/ForgotPasswordPage';
17
17
  export { FormBuilder, FormField, formBuilderTheme, useFormBuilder } from './components/FormBuilder';
18
- export { extendIntegrationsTheme, IntegrationsPage, integrationsPageTheme, useIntegrationsStore } from './components/IntegrationsPage';
18
+ export { defaultIntegrationsLabels, extendIntegrationsTheme, IntegrationsPage, integrationsPageTheme, resolveIntegrationsLabels, useIntegrationsStore } from './components/IntegrationsPage';
19
19
  export { LoginPage, useLoginStore } from './components/LoginPage';
20
20
  export { MagicLinkVerifyPage, magicLinkVerifyPageTheme, useMagicLinkVerifyStore } from './components/MagicLinkVerifyPage';
21
21
  export { extendNotificationCenterTheme, NotificationBell, NotificationCenter, notificationCenterTheme, useNotificationCenterStore } from './components/NotificationCenter';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.798",
3
+ "version": "0.9.800",
4
4
  "description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
5
5
  "author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
6
6
  "license": "SEE LICENSE IN LICENSE",