nucleus-core-ts 0.9.800 → 0.9.802

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.
@@ -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
  import { describedBy } from './formSupport';
6
7
  import { Field } from './Primitives';
@@ -9,52 +10,57 @@ import { Field } from './Primitives';
9
10
  * decide whether it is used and how carefully.
10
11
  */ const theme = integrationsPageTheme;
11
12
  const CHECKBOX = 'h-4 w-4 shrink-0 accent-sky-600 dark:accent-sky-500';
12
- const BASE_URL_HINT = 'The scheme is free. https:// and plain http:// are both accepted and neither is rewritten — calling a service inside the same cluster over http, for example http://hr-service:8080, is a legitimate setup.';
13
13
  export function ConnectionBasicFields({ draft, errors, disabled, onChange }) {
14
+ const labels = useLabels();
14
15
  return /*#__PURE__*/ _jsxs("section", {
15
- "aria-label": "Connection details",
16
- className: "flex flex-col gap-3",
16
+ "aria-label": labels.connectionForm.sectionLabel,
17
+ className: "flex flex-col gap-4",
17
18
  children: [
18
- /*#__PURE__*/ _jsx(Field, {
19
- error: errors.name,
20
- htmlFor: "connection-name",
21
- label: "Name",
22
- children: /*#__PURE__*/ _jsx("input", {
23
- "aria-describedby": describedBy('connection-name', false, Boolean(errors.name)),
24
- className: theme.field.input,
25
- disabled: disabled,
26
- id: "connection-name",
27
- onChange: (event)=>onChange({
28
- name: event.target.value
29
- }),
30
- placeholder: "Directory service",
31
- type: "text",
32
- value: draft.name
33
- })
34
- }),
35
- /*#__PURE__*/ _jsx(Field, {
36
- error: errors.slug,
37
- hint: "Used to refer to this connection elsewhere. It does not change once things point at it.",
38
- htmlFor: "connection-slug",
39
- label: "Slug",
40
- children: /*#__PURE__*/ _jsx("input", {
41
- "aria-describedby": describedBy('connection-slug', true, Boolean(errors.slug)),
42
- className: theme.field.input,
43
- disabled: disabled,
44
- id: "connection-slug",
45
- onChange: (event)=>onChange({
46
- slug: event.target.value
47
- }),
48
- placeholder: "hr-service",
49
- type: "text",
50
- value: draft.slug
51
- })
19
+ /*#__PURE__*/ _jsxs("div", {
20
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
21
+ children: [
22
+ /*#__PURE__*/ _jsx(Field, {
23
+ error: errors.name,
24
+ htmlFor: "connection-name",
25
+ label: labels.connectionForm.name,
26
+ children: /*#__PURE__*/ _jsx("input", {
27
+ "aria-describedby": describedBy('connection-name', false, Boolean(errors.name)),
28
+ className: theme.field.input,
29
+ disabled: disabled,
30
+ id: "connection-name",
31
+ onChange: (event)=>onChange({
32
+ name: event.target.value
33
+ }),
34
+ placeholder: labels.connectionForm.namePlaceholder,
35
+ type: "text",
36
+ value: draft.name
37
+ })
38
+ }),
39
+ /*#__PURE__*/ _jsx(Field, {
40
+ error: errors.slug,
41
+ hint: labels.connectionForm.slugHint,
42
+ htmlFor: "connection-slug",
43
+ label: labels.connectionForm.slug,
44
+ children: /*#__PURE__*/ _jsx("input", {
45
+ "aria-describedby": describedBy('connection-slug', true, Boolean(errors.slug)),
46
+ className: theme.field.input,
47
+ disabled: disabled,
48
+ id: "connection-slug",
49
+ onChange: (event)=>onChange({
50
+ slug: event.target.value
51
+ }),
52
+ placeholder: "hr-service",
53
+ type: "text",
54
+ value: draft.slug
55
+ })
56
+ })
57
+ ]
52
58
  }),
53
59
  /*#__PURE__*/ _jsx(Field, {
54
60
  error: errors.baseUrl,
55
- hint: BASE_URL_HINT,
61
+ hint: labels.connectionForm.baseUrlHint,
56
62
  htmlFor: "connection-base-url",
57
- label: "Base URL",
63
+ label: labels.connectionForm.baseUrl,
58
64
  children: /*#__PURE__*/ _jsx("input", {
59
65
  "aria-describedby": describedBy('connection-base-url', true, Boolean(errors.baseUrl)),
60
66
  className: theme.field.input,
@@ -69,9 +75,9 @@ export function ConnectionBasicFields({ draft, errors, disabled, onChange }) {
69
75
  })
70
76
  }),
71
77
  /*#__PURE__*/ _jsx(Field, {
72
- hint: "For whoever has to work out what this connection is, months from now.",
78
+ hint: labels.connectionForm.descriptionHint,
73
79
  htmlFor: "connection-description",
74
- label: "Description",
80
+ label: labels.connectionForm.description,
75
81
  children: /*#__PURE__*/ _jsx("textarea", {
76
82
  "aria-describedby": describedBy('connection-description', true, false),
77
83
  className: cn(theme.field.input, 'h-20 py-2 sm:h-20'),
@@ -80,71 +86,76 @@ export function ConnectionBasicFields({ draft, errors, disabled, onChange }) {
80
86
  onChange: (event)=>onChange({
81
87
  description: event.target.value
82
88
  }),
83
- placeholder: "Read-only employee directory used by the nightly import.",
89
+ placeholder: labels.connectionForm.descriptionPlaceholder,
84
90
  value: draft.description
85
91
  })
86
92
  }),
87
93
  /*#__PURE__*/ _jsxs("div", {
88
- className: theme.field.wrapper,
94
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
89
95
  children: [
90
- /*#__PURE__*/ _jsxs("label", {
91
- className: cn(theme.field.label, 'flex items-center gap-2'),
92
- htmlFor: "connection-enabled",
96
+ /*#__PURE__*/ _jsxs("div", {
97
+ className: cn(theme.field.wrapper, 'rounded-xl border border-slate-200 p-3 dark:border-slate-800'),
93
98
  children: [
94
- /*#__PURE__*/ _jsx("input", {
95
- "aria-describedby": "connection-enabled-hint",
96
- checked: draft.enabled,
97
- className: CHECKBOX,
98
- disabled: disabled,
99
- id: "connection-enabled",
100
- onChange: (event)=>onChange({
101
- enabled: event.target.checked
99
+ /*#__PURE__*/ _jsxs("label", {
100
+ className: cn(theme.field.label, 'flex items-center gap-2 normal-case tracking-normal text-slate-700 text-sm dark:text-slate-200'),
101
+ htmlFor: "connection-enabled",
102
+ children: [
103
+ /*#__PURE__*/ _jsx("input", {
104
+ "aria-describedby": "connection-enabled-hint",
105
+ checked: draft.enabled,
106
+ className: CHECKBOX,
107
+ disabled: disabled,
108
+ id: "connection-enabled",
109
+ onChange: (event)=>onChange({
110
+ enabled: event.target.checked
111
+ }),
112
+ type: "checkbox"
102
113
  }),
103
- type: "checkbox"
114
+ labels.connectionForm.enabled
115
+ ]
104
116
  }),
105
- "Enabled"
117
+ /*#__PURE__*/ _jsx("span", {
118
+ className: theme.field.hint,
119
+ id: "connection-enabled-hint",
120
+ children: labels.connectionForm.enabledHint
121
+ })
106
122
  ]
107
123
  }),
108
- /*#__PURE__*/ _jsx("span", {
109
- className: theme.field.hint,
110
- id: "connection-enabled-hint",
111
- children: "Turning this off stops every endpoint and import that goes through this connection. The settings are kept."
112
- })
113
- ]
114
- }),
115
- /*#__PURE__*/ _jsxs("div", {
116
- className: theme.field.wrapper,
117
- children: [
118
- /*#__PURE__*/ _jsxs("label", {
119
- className: cn(theme.field.label, 'flex items-center gap-2'),
120
- htmlFor: "connection-tls-verify",
124
+ /*#__PURE__*/ _jsxs("div", {
125
+ className: cn(theme.field.wrapper, 'rounded-xl border border-slate-200 p-3 dark:border-slate-800'),
121
126
  children: [
122
- /*#__PURE__*/ _jsx("input", {
123
- "aria-describedby": "connection-tls-verify-hint",
124
- checked: draft.tlsVerify,
125
- className: CHECKBOX,
126
- disabled: disabled,
127
- id: "connection-tls-verify",
128
- onChange: (event)=>onChange({
129
- tlsVerify: event.target.checked
127
+ /*#__PURE__*/ _jsxs("label", {
128
+ className: cn(theme.field.label, 'flex items-center gap-2 normal-case tracking-normal text-slate-700 text-sm dark:text-slate-200'),
129
+ htmlFor: "connection-tls-verify",
130
+ children: [
131
+ /*#__PURE__*/ _jsx("input", {
132
+ "aria-describedby": "connection-tls-verify-hint",
133
+ checked: draft.tlsVerify,
134
+ className: CHECKBOX,
135
+ disabled: disabled,
136
+ id: "connection-tls-verify",
137
+ onChange: (event)=>onChange({
138
+ tlsVerify: event.target.checked
139
+ }),
140
+ type: "checkbox"
130
141
  }),
131
- type: "checkbox"
142
+ labels.connectionForm.tlsVerify
143
+ ]
132
144
  }),
133
- "Verify TLS certificate"
145
+ /*#__PURE__*/ _jsx("span", {
146
+ className: theme.field.hint,
147
+ id: "connection-tls-verify-hint",
148
+ children: draft.tlsVerify ? labels.connectionForm.tlsVerifyOnHint : labels.connectionForm.tlsVerifyOffHint
149
+ })
134
150
  ]
135
- }),
136
- /*#__PURE__*/ _jsx("span", {
137
- className: theme.field.hint,
138
- id: "connection-tls-verify-hint",
139
- children: draft.tlsVerify ? 'On. Unchecking it applies to requests made through THIS connection only — no other connection and nothing else in the app is affected.' : 'Off. Certificates are not checked for requests made through THIS connection only. Every other connection still verifies. Use it for an internal service with its own certificate authority.'
140
151
  })
141
152
  ]
142
153
  }),
143
154
  /*#__PURE__*/ _jsx(Field, {
144
155
  error: errors.timeoutMs,
145
- hint: "How long one request may take before it is given up on. Empty means the server default.",
156
+ hint: labels.connectionForm.timeoutHint,
146
157
  htmlFor: "connection-timeout",
147
- label: "Timeout (ms)",
158
+ label: labels.connectionForm.timeout,
148
159
  children: /*#__PURE__*/ _jsx("input", {
149
160
  "aria-describedby": describedBy('connection-timeout', true, Boolean(errors.timeoutMs)),
150
161
  className: theme.field.input,
@@ -45,6 +45,35 @@ export function IntegrationsPage({ title = 'Integrations', subtitle = 'Connect a
45
45
  const handleSourceSaved = ()=>{
46
46
  setSourcesReloadToken((token)=>token + 1);
47
47
  };
48
+ /**
49
+ * How far this connection has got, asked for directly.
50
+ *
51
+ * The step strip promises to say where you are, so it cannot depend on having
52
+ * VISITED the step that counts — on a fresh load every total was zero and the
53
+ * strip told an operator with five calls, three mappings and two finished
54
+ * imports to "finish the previous step first". Three counting requests, one
55
+ * row each, are the price of a strip that is telling the truth.
56
+ */ const loadProgress = useEffectEvent((sourceId)=>{
57
+ const one = {
58
+ page: 1,
59
+ limit: 1
60
+ };
61
+ const total = (result)=>result.meta?.totalItems ?? 0;
62
+ actions.listEndpoints(sourceId, one).then((r)=>store.setEndpointsTotal(total(r))).catch(()=>{});
63
+ actions.listMappings(sourceId, one).then((r)=>store.setMappingsTotal(total(r))).catch(()=>{});
64
+ // Runs are deliberately NOT counted here. A run records the mapping it came
65
+ // from and nothing about the connection, so the only count available is
66
+ // every run of every connection — which on the second connection would be
67
+ // a number that looks like this one's and is not. The Runs tab sets the
68
+ // real figure for the mapping it is showing; until then the step says
69
+ // nothing rather than something wrong.
70
+ });
71
+ useEffect(()=>{
72
+ if (store.selectedSourceId) loadProgress(store.selectedSourceId);
73
+ }, [
74
+ store.selectedSourceId,
75
+ sourcesReloadToken
76
+ ]);
48
77
  // One measured entrance. Anything longer makes an operator wait to read counts
49
78
  // they came here to check.
50
79
  useGSAP(()=>{
@@ -49,6 +49,9 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
49
49
  store.endpointsTotal = total;
50
50
  store.endpointsLoading = false;
51
51
  }),
52
+ setEndpointsTotal: (store)=>(total)=>{
53
+ store.endpointsTotal = total;
54
+ },
52
55
  setEndpointsPage: (store)=>(page)=>{
53
56
  store.endpointsPage = page;
54
57
  },
@@ -70,6 +73,9 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
70
73
  store.mappingsTotal = total;
71
74
  store.mappingsLoading = false;
72
75
  }),
76
+ setMappingsTotal: (store)=>(total)=>{
77
+ store.mappingsTotal = total;
78
+ },
73
79
  setMappingsPage: (store)=>(page)=>{
74
80
  store.mappingsPage = page;
75
81
  },
@@ -97,6 +103,9 @@ export const { useStore: useIntegrationsStore } = createStore(initial, {
97
103
  store.runsTotal = total;
98
104
  store.runsLoading = false;
99
105
  }),
106
+ setRunsTotal: (store)=>(total)=>{
107
+ store.runsTotal = total;
108
+ },
100
109
  setRunsPage: (store)=>(page)=>{
101
110
  store.runsPage = page;
102
111
  },
@@ -57,16 +57,25 @@ export type Actions = {
57
57
  selectSource: (id: string | null) => void;
58
58
  setTab: (tab: ConnectionTab) => void;
59
59
  setEndpoints: (items: IntegrationEndpoint[], total: number) => void;
60
+ /**
61
+ * Just the count, without the rows.
62
+ *
63
+ * The step strip reports how far a connection has got, and it cannot wait for
64
+ * the tab that lists the rows to have been opened — so it counts directly.
65
+ */
66
+ setEndpointsTotal: (total: number) => void;
60
67
  setEndpointsPage: (page: number) => void;
61
68
  setEndpointsSearch: (search: string) => void;
62
69
  setEndpointsLoading: (loading: boolean) => void;
63
70
  selectEndpoint: (id: string | null) => void;
64
71
  setMappings: (items: IntegrationMapping[], total: number) => void;
72
+ setMappingsTotal: (total: number) => void;
65
73
  setMappingsPage: (page: number) => void;
66
74
  setMappingsSearch: (search: string) => void;
67
75
  setMappingsLoading: (loading: boolean) => void;
68
76
  selectMapping: (id: string | null) => void;
69
77
  setRuns: (items: IntegrationRun[], total: number) => void;
78
+ setRunsTotal: (total: number) => void;
70
79
  setRunsPage: (page: number) => void;
71
80
  setRunsLoading: (loading: boolean) => void;
72
81
  setSample: (sample: EndpointSampleValue | null) => void;
@@ -80,6 +80,26 @@ export type IntegrationsLabels = {
80
80
  skipped: string;
81
81
  dedupKey: string;
82
82
  };
83
+ /** The connection form — the first screen anyone meets. */
84
+ connectionForm: {
85
+ sectionLabel: string;
86
+ name: string;
87
+ namePlaceholder: string;
88
+ slug: string;
89
+ slugHint: string;
90
+ baseUrl: string;
91
+ baseUrlHint: string;
92
+ description: string;
93
+ descriptionHint: string;
94
+ descriptionPlaceholder: string;
95
+ enabled: string;
96
+ enabledHint: string;
97
+ tlsVerify: string;
98
+ tlsVerifyOnHint: string;
99
+ tlsVerifyOffHint: string;
100
+ timeout: string;
101
+ timeoutHint: string;
102
+ };
83
103
  writeMode: {
84
104
  upsert: string;
85
105
  append: string;
@@ -74,6 +74,25 @@
74
74
  skipped: 'Skipped',
75
75
  dedupKey: 'Dedup key'
76
76
  },
77
+ connectionForm: {
78
+ sectionLabel: 'Connection details',
79
+ name: 'Name',
80
+ namePlaceholder: 'Directory service',
81
+ slug: 'Slug',
82
+ slugHint: 'Used to refer to this connection elsewhere. It does not change once things point at it.',
83
+ baseUrl: 'Base URL',
84
+ baseUrlHint: 'The scheme is free. https:// and plain http:// are both accepted and neither is rewritten — calling a service inside the same cluster over http, for example http://hr-service:8080, is a legitimate setup.',
85
+ description: 'Description',
86
+ descriptionHint: 'For whoever has to work out what this connection is, months from now.',
87
+ descriptionPlaceholder: 'Read-only employee directory used by the nightly import.',
88
+ enabled: 'Enabled',
89
+ enabledHint: 'Turning this off stops every endpoint and import that goes through this connection. The settings are kept.',
90
+ tlsVerify: 'Verify TLS certificate',
91
+ tlsVerifyOnHint: 'On. Unchecking it applies to requests made through THIS connection only — no other connection and nothing else in the app is affected.',
92
+ tlsVerifyOffHint: 'Off. Certificates are not checked for requests made through THIS connection only. Every other connection still verifies. Use it for an internal service with its own certificate authority.',
93
+ timeout: 'Timeout (ms)',
94
+ timeoutHint: 'How long one request may take before it is given up on. Empty means the server default.'
95
+ },
77
96
  writeMode: {
78
97
  upsert: 'update what matches, insert the rest',
79
98
  append: 'always insert, never match',
@@ -7,106 +7,120 @@
7
7
  *
8
8
  * An app can replace any slice of this with {@link extendIntegrationsTheme}
9
9
  * without forking the components.
10
- */ /** Shared so a control and its skeleton cannot drift apart in height. */ const CONTROL = 'h-10 rounded-lg text-sm sm:h-9';
10
+ */ /** Shared so a control and its skeleton cannot drift apart in height. */ const CONTROL = 'h-11 rounded-lg text-sm sm:h-10';
11
+ /**
12
+ * Micro-caps for field labels.
13
+ *
14
+ * A form of twenty identical `text-sm` labels reads as a wall. Making the label
15
+ * smaller and wider-tracked than its value gives every row an obvious top and
16
+ * bottom, which is what lets an eye skip the ones it does not need.
17
+ */ const LABEL = 'font-semibold text-[11px] uppercase tracking-wider';
11
18
  export const integrationsPageTheme = {
12
- container: 'flex flex-col gap-4 text-slate-900 sm:gap-5 dark:text-slate-100',
19
+ container: 'flex flex-col gap-5 text-slate-900 sm:gap-6 dark:text-slate-100',
13
20
  header: {
14
- wrapper: 'flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between',
15
- title: 'font-semibold text-lg tracking-tight sm:text-xl',
16
- subtitle: 'text-slate-600 text-sm dark:text-slate-400',
21
+ wrapper: 'flex flex-col gap-2 border-slate-200 border-b pb-4 sm:flex-row sm:items-end sm:justify-between dark:border-slate-800',
22
+ title: 'font-bold text-slate-900 text-xl tracking-tight sm:text-2xl dark:text-slate-50',
23
+ subtitle: 'max-w-2xl text-slate-600 text-sm leading-relaxed dark:text-slate-400',
17
24
  actions: 'flex flex-wrap items-center gap-2'
18
25
  },
19
- // One column on a phone; the connection list becomes a rail from lg upward,
20
- // where there is finally room for both without either being cramped.
26
+ // The rail is a chooser, not the work. It gets the narrower column and the
27
+ // detail gets the rest before this they were near enough equal, so a form
28
+ // of twenty fields was squeezed beside a list of one.
21
29
  layout: {
22
- split: 'grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)] lg:gap-5',
30
+ split: 'grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,17rem)_minmax(0,1fr)] lg:gap-6',
23
31
  aside: 'flex min-w-0 flex-col gap-3',
24
- main: 'flex min-w-0 flex-col gap-4'
32
+ main: 'flex min-w-0 flex-col gap-4 sm:gap-5'
25
33
  },
34
+ // Cards sit on a tinted page, so a white surface separates on its own instead
35
+ // of relying on a shadow that light mode barely renders.
26
36
  card: {
27
- wrapper: 'rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900',
28
- header: 'flex flex-col gap-2 border-slate-200 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800',
29
- title: 'font-medium text-slate-900 text-sm dark:text-slate-100',
30
- body: 'flex flex-col gap-3 p-4',
31
- footer: 'flex flex-wrap items-center justify-end gap-2 border-slate-200 border-t px-4 py-3 dark:border-slate-800'
37
+ wrapper: 'overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-slate-200/50 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:shadow-none',
38
+ header: 'flex flex-col gap-2 border-slate-200/80 border-b bg-slate-50/80 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5 dark:border-slate-800 dark:bg-slate-950/40',
39
+ title: 'font-semibold text-slate-900 text-sm dark:text-slate-100',
40
+ body: 'flex flex-col gap-4 p-4 sm:gap-5 sm:p-5',
41
+ footer: 'flex flex-wrap items-center justify-end gap-2 border-slate-200/80 border-t bg-slate-50/80 px-4 py-3 sm:px-5 dark:border-slate-800 dark:bg-slate-950/40'
32
42
  },
33
43
  list: {
34
44
  wrapper: 'flex flex-col gap-1.5',
35
- item: 'flex w-full flex-col gap-0.5 rounded-lg border border-transparent px-3 py-2.5 text-left transition-colors hover:bg-slate-100 dark:hover:bg-slate-800',
36
- itemSelected: 'border-sky-300 bg-sky-50 hover:bg-sky-50 dark:border-sky-700 dark:bg-sky-950 dark:hover:bg-sky-950',
37
- itemTitle: 'truncate font-medium text-slate-900 text-sm dark:text-slate-100',
45
+ item: 'flex w-full flex-col gap-0.5 rounded-xl border border-transparent px-3 py-2.5 text-left transition-all hover:border-slate-200 hover:bg-white dark:hover:border-slate-700 dark:hover:bg-slate-900',
46
+ // A selected row is the only thing on the rail that carries the accent, so
47
+ // it is legible at a glance from across the screen.
48
+ itemSelected: 'border-sky-400 bg-white shadow-sky-100 shadow-sm hover:border-sky-400 hover:bg-white dark:border-sky-600 dark:bg-slate-900 dark:shadow-none',
49
+ itemTitle: 'truncate font-semibold text-slate-900 text-sm dark:text-slate-100',
38
50
  itemMeta: 'truncate text-slate-500 text-xs dark:text-slate-400',
39
- empty: 'rounded-lg border border-slate-200 border-dashed px-4 py-8 text-center text-slate-500 text-sm dark:border-slate-700 dark:text-slate-400',
51
+ empty: 'rounded-xl border border-slate-300 border-dashed px-4 py-10 text-center text-slate-500 text-sm leading-relaxed dark:border-slate-700 dark:text-slate-400',
40
52
  // Watched by the infinite scroller; the next page is requested when it appears.
41
53
  sentinel: 'h-8 w-full shrink-0'
42
54
  },
43
55
  tabs: {
44
- wrapper: 'flex gap-1 overflow-x-auto rounded-lg border border-slate-200 bg-slate-50 p-1 dark:border-slate-800 dark:bg-slate-900',
45
- button: 'shrink-0 rounded-md px-3 py-1.5 font-medium text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-500 focus-visible:outline-offset-2',
56
+ wrapper: 'flex gap-1 overflow-x-auto rounded-xl border border-slate-200 bg-slate-50 p-1 dark:border-slate-800 dark:bg-slate-900',
57
+ button: 'shrink-0 rounded-lg px-3 py-1.5 font-medium text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-sky-500 focus-visible:outline-offset-2',
46
58
  active: 'bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-slate-100',
47
59
  inactive: 'text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100'
48
60
  },
49
61
  // A step is a card, not a tab: it has to carry a name, a sentence saying what
50
62
  // 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.
63
+ // not fit on a phone side by side, so they stack and become a row at lg.
52
64
  steps: {
53
65
  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',
66
+ wrapper: 'grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-4',
67
+ item: 'flex min-w-0 items-start gap-3 rounded-2xl border border-slate-200/80 bg-white px-3.5 py-3 text-left shadow-slate-200/40 shadow-sm transition-all hover:border-slate-300 hover:shadow dark:border-slate-800 dark:bg-slate-900 dark:shadow-none dark:hover:border-slate-700',
68
+ itemActive: 'border-sky-400 bg-sky-50/60 ring-1 ring-sky-400 hover:border-sky-400 dark:border-sky-600 dark:bg-sky-950/40 dark:ring-sky-600',
69
+ itemDone: 'border-emerald-200 dark:border-emerald-900/70',
70
+ itemLocked: 'opacity-60',
71
+ marker: 'flex size-7 shrink-0 items-center justify-center rounded-full border font-bold text-[11px] tabular-nums',
72
+ markerActive: 'border-sky-500 bg-sky-500 text-white shadow-sky-200 shadow-sm dark:border-sky-500 dark:bg-sky-500 dark:shadow-none',
61
73
  markerDone: 'border-emerald-300 bg-emerald-100 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-400',
62
74
  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'
75
+ name: 'font-semibold text-slate-900 text-sm tracking-tight dark:text-slate-100',
76
+ purpose: 'mt-1 text-slate-600 text-xs leading-relaxed dark:text-slate-400',
77
+ evidence: 'mt-1.5 font-semibold text-[11px] text-emerald-700 dark:text-emerald-400'
66
78
  },
67
79
  field: {
68
80
  wrapper: 'flex flex-col gap-1.5',
69
- label: 'font-medium text-slate-700 text-xs dark:text-slate-300',
70
- input: `w-full border border-slate-300 bg-white px-3 text-slate-900 outline-none transition-colors placeholder:text-slate-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500/20 disabled:opacity-60 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:placeholder:text-slate-500 ${CONTROL}`,
71
- hint: 'text-slate-500 text-xs dark:text-slate-400',
72
- error: 'text-rose-600 text-xs dark:text-rose-400'
81
+ label: `${LABEL} text-slate-500 dark:text-slate-400`,
82
+ input: `w-full border border-slate-300 bg-white px-3 text-slate-900 outline-none transition-all placeholder:text-slate-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-500/15 disabled:opacity-60 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:placeholder:text-slate-500 ${CONTROL}`,
83
+ hint: 'text-slate-500 text-xs leading-relaxed dark:text-slate-400',
84
+ error: 'font-medium text-rose-600 text-xs dark:text-rose-400'
73
85
  },
74
86
  button: {
75
- primary: `inline-flex items-center justify-center gap-1.5 bg-sky-600 px-3.5 font-medium text-white transition-colors hover:bg-sky-700 disabled:opacity-50 dark:bg-sky-500 dark:hover:bg-sky-400 dark:text-slate-950 ${CONTROL}`,
76
- secondary: `inline-flex items-center justify-center gap-1.5 border border-slate-300 bg-white px-3.5 font-medium text-slate-700 transition-colors hover:bg-slate-50 disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 ${CONTROL}`,
77
- ghost: `inline-flex items-center justify-center gap-1.5 px-2.5 font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 disabled:opacity-50 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100 ${CONTROL}`,
78
- danger: `inline-flex items-center justify-center gap-1.5 bg-rose-600 px-3.5 font-medium text-white transition-colors hover:bg-rose-700 disabled:opacity-50 dark:bg-rose-600 dark:hover:bg-rose-500 ${CONTROL}`
87
+ primary: `inline-flex items-center justify-center gap-1.5 rounded-lg bg-sky-600 px-4 font-semibold text-white shadow-sky-200 shadow-sm transition-all hover:bg-sky-700 hover:shadow disabled:opacity-50 disabled:shadow-none dark:bg-sky-500 dark:text-slate-950 dark:shadow-none dark:hover:bg-sky-400 ${CONTROL}`,
88
+ secondary: `inline-flex items-center justify-center gap-1.5 rounded-lg border border-slate-300 bg-white px-4 font-semibold text-slate-700 transition-colors hover:border-slate-400 hover:bg-slate-50 disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 ${CONTROL}`,
89
+ ghost: `inline-flex items-center justify-center gap-1.5 rounded-lg px-3 font-semibold text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 disabled:opacity-50 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100 ${CONTROL}`,
90
+ danger: `inline-flex items-center justify-center gap-1.5 rounded-lg bg-rose-600 px-4 font-semibold text-white shadow-rose-200 shadow-sm transition-all hover:bg-rose-700 disabled:opacity-50 dark:bg-rose-600 dark:shadow-none dark:hover:bg-rose-500 ${CONTROL}`
79
91
  },
80
92
  badge: {
81
- base: 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 font-medium text-xs',
82
- neutral: 'border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300',
93
+ base: 'inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 font-semibold text-[11px]',
94
+ neutral: 'border-slate-200 bg-slate-100 text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300',
83
95
  success: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-400',
84
96
  warning: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400',
85
97
  danger: 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-400',
86
98
  info: 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-800 dark:bg-sky-950 dark:text-sky-400'
87
99
  },
100
+ // The counts are the point of the preview, so they are the largest type on
101
+ // the screen — before this they were the same size as the labels beneath them.
88
102
  stat: {
89
- grid: 'grid grid-cols-2 gap-2 xs:grid-cols-3 sm:grid-cols-4 lg:grid-cols-6',
90
- cell: 'flex flex-col items-center gap-0.5 rounded-lg border border-slate-200 bg-white px-2 py-3 text-center dark:border-slate-800 dark:bg-slate-900',
91
- value: 'font-semibold text-lg tabular-nums',
92
- label: 'text-[11px] text-slate-500 leading-tight dark:text-slate-400'
103
+ grid: 'grid grid-cols-2 gap-2.5 xs:grid-cols-3 sm:grid-cols-4 lg:grid-cols-6',
104
+ cell: 'flex flex-col items-center gap-1 rounded-xl border border-slate-200/80 bg-white px-2 py-3.5 text-center shadow-slate-200/40 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:shadow-none',
105
+ value: 'font-bold text-2xl tabular-nums tracking-tight',
106
+ label: `${LABEL} text-slate-500 leading-tight dark:text-slate-400`
93
107
  },
94
108
  notice: {
95
- info: 'rounded-lg border border-sky-200 bg-sky-50 p-3 dark:border-sky-900 dark:bg-sky-950',
96
- warning: 'rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950',
97
- danger: 'rounded-lg border border-rose-200 bg-rose-50 p-3 dark:border-rose-900 dark:bg-rose-950',
98
- title: 'font-medium text-sm',
99
- body: 'mt-0.5 text-sm leading-relaxed opacity-90'
109
+ info: 'rounded-xl border border-sky-200 bg-sky-50 p-3.5 text-sky-900 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-100',
110
+ warning: 'rounded-xl border border-amber-200 bg-amber-50 p-3.5 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100',
111
+ danger: 'rounded-xl border border-rose-200 bg-rose-50 p-3.5 text-rose-900 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-100',
112
+ title: 'font-semibold text-sm',
113
+ body: 'mt-1 text-sm leading-relaxed opacity-90'
100
114
  },
101
- code: 'block overflow-x-auto rounded-lg border border-slate-200 bg-slate-50 p-3 font-mono text-[11px] text-slate-700 leading-relaxed sm:text-xs dark:border-slate-800 dark:bg-slate-950 dark:text-slate-300',
115
+ code: 'block max-h-80 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3.5 font-mono text-[11px] text-slate-700 leading-relaxed sm:text-xs dark:border-slate-800 dark:bg-slate-950 dark:text-slate-300',
102
116
  skeleton: {
103
- line: 'h-3 animate-pulse rounded bg-slate-200 dark:bg-slate-800',
104
- block: 'animate-pulse rounded-lg bg-slate-200 dark:bg-slate-800'
117
+ line: 'h-3 animate-pulse rounded-full bg-slate-200 dark:bg-slate-800',
118
+ block: 'animate-pulse rounded-xl bg-slate-200 dark:bg-slate-800'
105
119
  },
106
120
  progress: {
107
- track: 'h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800',
121
+ track: 'h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800',
108
122
  bar: 'h-full rounded-full bg-sky-600 transition-[width] duration-300 ease-out dark:bg-sky-500',
109
- label: 'text-slate-600 text-xs tabular-nums dark:text-slate-400'
123
+ label: 'font-medium text-slate-600 text-xs tabular-nums dark:text-slate-400'
110
124
  }
111
125
  };
112
126
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.800",
3
+ "version": "0.9.802",
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",