@actuate-media/cms-admin 0.63.1 → 0.64.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 (108) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/design-system/README.md +53 -0
  3. package/design-system/tokens.css +114 -0
  4. package/design-system/tokens.json +149 -0
  5. package/design-system/tokens.scss +108 -0
  6. package/design-system/tokens.tailwind.cjs +80 -0
  7. package/dist/__tests__/layout/primitives.test.js +9 -2
  8. package/dist/__tests__/layout/primitives.test.js.map +1 -1
  9. package/dist/actuate-admin.css +1 -1
  10. package/dist/components/EnabledSwitch.d.ts +10 -0
  11. package/dist/components/EnabledSwitch.d.ts.map +1 -0
  12. package/dist/components/EnabledSwitch.js +8 -0
  13. package/dist/components/EnabledSwitch.js.map +1 -0
  14. package/dist/components/ThemeProvider.d.ts.map +1 -1
  15. package/dist/components/ThemeProvider.js +5 -3
  16. package/dist/components/ThemeProvider.js.map +1 -1
  17. package/dist/components/forms/FieldsPanel.js +2 -2
  18. package/dist/components/forms/FieldsPanel.js.map +1 -1
  19. package/dist/components/forms/NotificationsPanel.js +2 -2
  20. package/dist/components/forms/NotificationsPanel.js.map +1 -1
  21. package/dist/components/forms/WebhooksPanel.d.ts.map +1 -1
  22. package/dist/components/forms/WebhooksPanel.js +2 -2
  23. package/dist/components/forms/WebhooksPanel.js.map +1 -1
  24. package/dist/components/seo/primitives.d.ts.map +1 -1
  25. package/dist/components/seo/primitives.js +2 -1
  26. package/dist/components/seo/primitives.js.map +1 -1
  27. package/dist/components/ui/Button.d.ts +1 -1
  28. package/dist/components/ui/Button.js +1 -1
  29. package/dist/components/ui/Button.js.map +1 -1
  30. package/dist/components/ui/Card.d.ts +6 -5
  31. package/dist/components/ui/Card.d.ts.map +1 -1
  32. package/dist/components/ui/Card.js +20 -18
  33. package/dist/components/ui/Card.js.map +1 -1
  34. package/dist/components/ui/Input.d.ts +2 -1
  35. package/dist/components/ui/Input.d.ts.map +1 -1
  36. package/dist/components/ui/Input.js +9 -8
  37. package/dist/components/ui/Input.js.map +1 -1
  38. package/dist/components/ui/SearchInput.d.ts.map +1 -1
  39. package/dist/components/ui/SearchInput.js +2 -1
  40. package/dist/components/ui/SearchInput.js.map +1 -1
  41. package/dist/components/ui/Select.d.ts.map +1 -1
  42. package/dist/components/ui/Select.js +9 -10
  43. package/dist/components/ui/Select.js.map +1 -1
  44. package/dist/components/ui/Toggle.d.ts +31 -0
  45. package/dist/components/ui/Toggle.d.ts.map +1 -0
  46. package/dist/components/ui/Toggle.js +41 -0
  47. package/dist/components/ui/Toggle.js.map +1 -0
  48. package/dist/components/ui/form-control.d.ts +11 -0
  49. package/dist/components/ui/form-control.d.ts.map +1 -0
  50. package/dist/components/ui/form-control.js +11 -0
  51. package/dist/components/ui/form-control.js.map +1 -0
  52. package/dist/components/ui/index.d.ts +3 -0
  53. package/dist/components/ui/index.d.ts.map +1 -1
  54. package/dist/components/ui/index.js +2 -0
  55. package/dist/components/ui/index.js.map +1 -1
  56. package/dist/layout/Header.d.ts.map +1 -1
  57. package/dist/layout/Header.js +5 -2
  58. package/dist/layout/Header.js.map +1 -1
  59. package/dist/layout/Sidebar.js +1 -1
  60. package/dist/layout/Sidebar.js.map +1 -1
  61. package/dist/layout/primitives/tokens.d.ts +10 -4
  62. package/dist/layout/primitives/tokens.d.ts.map +1 -1
  63. package/dist/layout/primitives/tokens.js +10 -4
  64. package/dist/layout/primitives/tokens.js.map +1 -1
  65. package/dist/views/Forms.js +2 -2
  66. package/dist/views/Forms.js.map +1 -1
  67. package/dist/views/ScriptTagEditor.d.ts.map +1 -1
  68. package/dist/views/ScriptTagEditor.js +2 -1
  69. package/dist/views/ScriptTagEditor.js.map +1 -1
  70. package/dist/views/ScriptTags.d.ts.map +1 -1
  71. package/dist/views/ScriptTags.js +29 -8
  72. package/dist/views/ScriptTags.js.map +1 -1
  73. package/dist/views/page-builder/BottomBar.d.ts.map +1 -1
  74. package/dist/views/page-builder/BottomBar.js +2 -2
  75. package/dist/views/page-builder/BottomBar.js.map +1 -1
  76. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  77. package/dist/views/seo/TechnicalTab.js +2 -2
  78. package/dist/views/seo/TechnicalTab.js.map +1 -1
  79. package/dist/views/settings/components.d.ts.map +1 -1
  80. package/dist/views/settings/components.js +2 -1
  81. package/dist/views/settings/components.js.map +1 -1
  82. package/package.json +8 -3
  83. package/src/__tests__/layout/primitives.test.ts +10 -2
  84. package/src/components/EnabledSwitch.tsx +16 -0
  85. package/src/components/ThemeProvider.tsx +5 -3
  86. package/src/components/forms/FieldsPanel.tsx +4 -6
  87. package/src/components/forms/NotificationsPanel.tsx +2 -9
  88. package/src/components/forms/WebhooksPanel.tsx +4 -6
  89. package/src/components/seo/primitives.tsx +8 -7
  90. package/src/components/ui/Button.tsx +1 -1
  91. package/src/components/ui/Card.tsx +35 -18
  92. package/src/components/ui/Input.tsx +15 -17
  93. package/src/components/ui/SearchInput.tsx +9 -4
  94. package/src/components/ui/Select.tsx +15 -18
  95. package/src/components/ui/Toggle.tsx +85 -0
  96. package/src/components/ui/form-control.ts +12 -0
  97. package/src/components/ui/index.ts +3 -0
  98. package/src/layout/Header.tsx +6 -2
  99. package/src/layout/Sidebar.tsx +3 -3
  100. package/src/layout/primitives/tokens.ts +11 -4
  101. package/src/styles/actuate-tokens.css +97 -0
  102. package/src/styles/theme.css +88 -156
  103. package/src/views/Forms.tsx +4 -6
  104. package/src/views/ScriptTagEditor.tsx +6 -12
  105. package/src/views/ScriptTags.tsx +40 -19
  106. package/src/views/page-builder/BottomBar.tsx +3 -6
  107. package/src/views/seo/TechnicalTab.tsx +3 -6
  108. package/src/views/settings/components.tsx +11 -10
@@ -1,113 +1,74 @@
1
1
  @custom-variant dark (&:is(.dark *));
2
2
 
3
+ @import './actuate-tokens.css';
4
+
5
+ /*
6
+ * Semantic Tailwind tokens mapped from the ActuateCMS design system.
7
+ * Raw tokens live in actuate-tokens.css; Appearance accent overrides
8
+ * mutate --acc, --primary, --sidebar-primary, etc. at runtime.
9
+ */
3
10
  .actuate-admin {
4
- --font-size: 16px;
5
- --background: #ffffff;
6
- --foreground: oklch(0.145 0 0);
7
- --card: #ffffff;
8
- --card-foreground: oklch(0.145 0 0);
9
- --popover: oklch(1 0 0);
10
- --popover-foreground: oklch(0.145 0 0);
11
- --primary: #030213;
12
- --primary-foreground: oklch(1 0 0);
13
- --secondary: oklch(0.95 0.0058 264.53);
14
- --secondary-foreground: #030213;
15
- --muted: #ececf0;
16
- --muted-foreground: #717182;
17
- --accent: #e9ebef;
18
- --accent-foreground: #030213;
19
- --destructive: #d4183d;
11
+ --font-size: var(--fs-body);
12
+ --background: var(--bg);
13
+ --foreground: var(--txt);
14
+ /* --card surface token is defined in actuate-tokens.css */
15
+ --card-foreground: var(--txt);
16
+ --popover: var(--card);
17
+ --popover-foreground: var(--txt);
18
+ --primary: var(--acc);
19
+ --primary-foreground: #ffffff;
20
+ --secondary: var(--acc-l);
21
+ --secondary-foreground: var(--txt);
22
+ --muted: color-mix(in srgb, var(--bdr) 55%, var(--card));
23
+ --muted-foreground: var(--sub);
24
+ --accent: var(--acc-l);
25
+ --accent-foreground: var(--acc);
26
+ --destructive: var(--err);
20
27
  --destructive-foreground: #ffffff;
21
- /*
22
- * Semantic status + brand tokens.
23
- * ----------------------------------------------------------------
24
- * `destructive` already covers the "error/danger" state; these add
25
- * the remaining semantic states the admin needs (success / warning /
26
- * info) plus the violet brand accent used for links and primary
27
- * affordances. Values are tuned to read as TEXT on both the card
28
- * surface and their own `/10` tint (badge backgrounds), and as a
29
- * solid fill (dots, progress bars). The `*-foreground` variants are
30
- * for text placed on a SOLID fill. Light values match the previous
31
- * emerald/amber/blue/violet-600 palette so tokenizing is a no-op
32
- * visually; dark values track the *-300/*-400 tints used before.
33
- */
34
- --success: oklch(0.5 0.13 155);
28
+ --success: var(--suc);
35
29
  --success-foreground: #ffffff;
36
- --warning: oklch(0.52 0.13 73);
37
- --warning-foreground: oklch(0.205 0 0);
38
- --info: oklch(0.5 0.17 254);
30
+ --warning: var(--warn);
31
+ --warning-foreground: var(--txt);
32
+ --info: var(--info);
39
33
  --info-foreground: #ffffff;
40
- --brand: oklch(0.54 0.23 293);
34
+ --brand: var(--acc);
41
35
  --brand-foreground: #ffffff;
42
- --border: rgba(0, 0, 0, 0.1);
36
+ --border: var(--bdr);
43
37
  --input: transparent;
44
- --input-background: #f3f3f5;
45
- --switch-background: #cbced4;
38
+ --input-background: var(--card);
39
+ --switch-background: var(--bdr);
46
40
  --font-weight-medium: 500;
47
41
  --font-weight-normal: 400;
48
- --ring: oklch(0.708 0 0);
49
- --chart-1: oklch(0.646 0.222 41.116);
50
- --chart-2: oklch(0.6 0.118 184.704);
51
- --chart-3: oklch(0.398 0.07 227.392);
52
- --chart-4: oklch(0.828 0.189 84.429);
53
- --chart-5: oklch(0.769 0.188 70.08);
54
- --radius: 0.625rem;
55
- --sidebar: oklch(0.985 0 0);
56
- --sidebar-foreground: oklch(0.145 0 0);
57
- --sidebar-primary: #030213;
58
- --sidebar-primary-foreground: oklch(0.985 0 0);
59
- --sidebar-accent: oklch(0.97 0 0);
60
- --sidebar-accent-foreground: oklch(0.205 0 0);
61
- --sidebar-border: oklch(0.922 0 0);
62
- --sidebar-ring: oklch(0.708 0 0);
42
+ --ring: var(--acc-m);
43
+ --chart-1: var(--acc);
44
+ --chart-2: var(--info);
45
+ --chart-3: var(--suc);
46
+ --chart-4: var(--warn);
47
+ --chart-5: var(--err);
48
+ --radius: var(--r);
49
+ --sidebar: var(--sb);
50
+ --sidebar-foreground: var(--txt);
51
+ --sidebar-primary: var(--acc);
52
+ --sidebar-primary-foreground: #ffffff;
53
+ --sidebar-accent: var(--acc-l);
54
+ --sidebar-accent-foreground: var(--acc);
55
+ --sidebar-border: var(--bdr);
56
+ --sidebar-ring: var(--acc-m);
57
+ --shadow-card: var(--shd);
58
+ --shadow-elevated: var(--shd2);
63
59
  }
64
60
 
65
61
  .dark .actuate-admin,
66
62
  .actuate-admin.dark {
67
- --background: oklch(0.145 0 0);
68
- --foreground: oklch(0.985 0 0);
69
- --card: oklch(0.185 0 0);
70
- --card-foreground: oklch(0.985 0 0);
71
- --popover: oklch(0.185 0 0);
72
- --popover-foreground: oklch(0.985 0 0);
73
- --primary: oklch(0.985 0 0);
74
- --primary-foreground: oklch(0.205 0 0);
75
- --secondary: oklch(0.269 0 0);
76
- --secondary-foreground: oklch(0.985 0 0);
77
- --muted: oklch(0.269 0 0);
78
- --muted-foreground: oklch(0.708 0 0);
79
- --accent: oklch(0.269 0 0);
80
- --accent-foreground: oklch(0.985 0 0);
81
- --destructive: oklch(0.396 0.141 25.723);
82
- --destructive-foreground: oklch(0.637 0.237 25.331);
83
- --success: oklch(0.72 0.15 155);
84
- --success-foreground: oklch(0.205 0 0);
85
- --warning: oklch(0.8 0.13 80);
86
- --warning-foreground: oklch(0.205 0 0);
87
- --info: oklch(0.72 0.14 252);
88
- --info-foreground: oklch(0.205 0 0);
89
- --brand: oklch(0.72 0.18 293);
90
- --brand-foreground: oklch(0.205 0 0);
91
- --border: oklch(0.269 0 0);
92
- --input: oklch(0.269 0 0);
93
- --input-background: oklch(0.205 0 0);
94
- --switch-background: oklch(0.35 0 0);
95
- --ring: oklch(0.439 0 0);
96
- --font-weight-medium: 500;
97
- --font-weight-normal: 400;
98
- --chart-1: oklch(0.488 0.243 264.376);
99
- --chart-2: oklch(0.696 0.17 162.48);
100
- --chart-3: oklch(0.769 0.188 70.08);
101
- --chart-4: oklch(0.627 0.265 303.9);
102
- --chart-5: oklch(0.645 0.246 16.439);
103
- --sidebar: oklch(0.205 0 0);
104
- --sidebar-foreground: oklch(0.985 0 0);
105
- --sidebar-primary: oklch(0.488 0.243 264.376);
106
- --sidebar-primary-foreground: oklch(0.985 0 0);
107
- --sidebar-accent: oklch(0.269 0 0);
108
- --sidebar-accent-foreground: oklch(0.985 0 0);
109
- --sidebar-border: oklch(0.269 0 0);
110
- --sidebar-ring: oklch(0.439 0 0);
63
+ --primary-foreground: #0d0c18;
64
+ --secondary-foreground: var(--txt);
65
+ --warning-foreground: var(--txt);
66
+ --brand-foreground: #0d0c18;
67
+ --success-foreground: #0d0c18;
68
+ --info-foreground: #0d0c18;
69
+ --destructive-foreground: #0d0c18;
70
+ --sidebar-primary-foreground: #0d0c18;
71
+ --muted: color-mix(in srgb, var(--bdr) 40%, var(--card));
111
72
  }
112
73
 
113
74
  @theme inline {
@@ -145,10 +106,10 @@
145
106
  --color-chart-3: var(--chart-3);
146
107
  --color-chart-4: var(--chart-4);
147
108
  --color-chart-5: var(--chart-5);
148
- --radius-sm: calc(var(--radius) - 4px);
149
- --radius-md: calc(var(--radius) - 2px);
150
- --radius-lg: var(--radius);
151
- --radius-xl: calc(var(--radius) + 4px);
109
+ --radius-sm: var(--r-sm);
110
+ --radius-md: var(--r-control);
111
+ --radius-lg: var(--r);
112
+ --radius-xl: calc(var(--r) + 2px);
152
113
  --color-sidebar: var(--sidebar);
153
114
  --color-sidebar-foreground: var(--sidebar-foreground);
154
115
  --color-sidebar-primary: var(--sidebar-primary);
@@ -157,6 +118,12 @@
157
118
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
158
119
  --color-sidebar-border: var(--sidebar-border);
159
120
  --color-sidebar-ring: var(--sidebar-ring);
121
+ --text-2xl: var(--fs-page);
122
+ --text-xl: calc(var(--fs-card) + 4px);
123
+ --text-lg: var(--fs-card);
124
+ --text-base: var(--fs-body);
125
+ --text-sm: var(--fs-sub);
126
+ --text-xs: var(--fs-hint);
160
127
  }
161
128
 
162
129
  @layer base {
@@ -166,102 +133,67 @@
166
133
 
167
134
  .actuate-admin {
168
135
  @apply bg-background text-foreground;
169
- /*
170
- * Content font is var-driven so the Appearance tab can switch it at
171
- * runtime, but defaults to the system-ui stack. We do NOT load web fonts
172
- * here (no Google Fonts / @font-face) — the chosen family is applied as a
173
- * family string and falls back to system-ui when not installed, keeping
174
- * the admin dependency-free per the design system. This is the single
175
- * permitted `!important` (it must beat consumer global resets).
176
- */
177
- font-family: var(
178
- --actuate-admin-font,
179
- system-ui,
180
- -apple-system,
181
- BlinkMacSystemFont,
182
- 'Segoe UI',
183
- Roboto,
184
- sans-serif
185
- ) !important;
136
+ font-family: var(--actuate-admin-font, var(--font-sans)) !important;
186
137
  font-size: var(--font-size);
187
138
  line-height: 1.5;
188
139
  isolation: isolate;
189
- /*
190
- * Use `contain: style` only — NOT `contain: layout`.
191
- *
192
- * `contain: layout` would make every `.actuate-admin` element a
193
- * *containing block* for `position: fixed` descendants. That
194
- * silently breaks <AdminRoot>'s own fixed overlay (`position: fixed;
195
- * inset: 0`) whenever a consumer wraps AdminRoot in their own
196
- * `<div className="actuate-admin">` for tailwind isolation — the
197
- * inner fixed div resolves relative to the wrapper instead of the
198
- * viewport, and if the wrapper is 0-height (because all its
199
- * children are out-of-flow), every admin element renders offscreen
200
- * while still reporting non-zero computed dimensions. The visible
201
- * symptom is `<body> intercepts pointer events` on every click in
202
- * Playwright; the user-visible symptom is a blank-looking admin.
203
- *
204
- * `contain: style` keeps the style-scoping benefit (e.g. counters
205
- * don't leak out) without the positioning footgun.
206
- */
207
140
  contain: style;
208
141
  -webkit-font-smoothing: antialiased;
209
142
  -moz-osx-font-smoothing: grayscale;
210
143
  }
211
144
 
212
145
  .actuate-admin h1 {
213
- font-size: var(--text-2xl);
214
- font-weight: var(--font-weight-medium);
146
+ font-size: var(--fs-page);
147
+ font-weight: 700;
148
+ letter-spacing: -0.5px;
215
149
  line-height: 1.5;
216
150
  }
217
151
 
218
152
  .actuate-admin h2 {
219
- font-size: var(--text-xl);
220
- font-weight: var(--font-weight-medium);
153
+ font-size: var(--fs-card);
154
+ font-weight: 600;
221
155
  line-height: 1.5;
222
156
  }
223
157
 
224
158
  .actuate-admin h3 {
225
- font-size: var(--text-lg);
226
- font-weight: var(--font-weight-medium);
159
+ font-size: var(--fs-card);
160
+ font-weight: 600;
227
161
  line-height: 1.5;
228
162
  }
229
163
 
230
164
  .actuate-admin h4 {
231
- font-size: var(--text-base);
232
- font-weight: var(--font-weight-medium);
165
+ font-size: var(--fs-body);
166
+ font-weight: 600;
233
167
  line-height: 1.5;
234
168
  }
235
169
 
236
170
  .actuate-admin label {
237
- font-size: var(--text-base);
171
+ font-size: var(--fs-body);
238
172
  font-weight: var(--font-weight-medium);
239
173
  line-height: 1.5;
240
174
  }
241
175
 
242
176
  .actuate-admin button {
243
- font-size: var(--text-base);
177
+ font-size: var(--fs-body);
244
178
  font-weight: var(--font-weight-medium);
245
179
  line-height: 1.5;
246
180
  }
247
181
 
248
182
  .actuate-admin input {
249
- font-size: var(--text-base);
183
+ font-size: var(--fs-body);
250
184
  font-weight: var(--font-weight-normal);
251
185
  line-height: 1.5;
252
186
  }
187
+
188
+ .actuate-admin .actuate-section-label {
189
+ font-size: var(--fs-label);
190
+ font-weight: 600;
191
+ letter-spacing: 0.08em;
192
+ text-transform: uppercase;
193
+ color: var(--sub);
194
+ }
253
195
  }
254
196
 
255
- /*
256
- * Reduce-motion (admin-level Appearance toggle).
257
- *
258
- * Intentionally UNLAYERED: unlayered rules beat Tailwind's `@layer utilities`
259
- * transition/animation classes without needing `!important`, so the toggle can
260
- * neutralize utility-driven motion. We shorten (not delete) durations and never
261
- * touch outlines/focus rings, so state changes and focus visibility remain
262
- * intact. The OS `prefers-reduced-motion` is still respected app-wide via
263
- * `motion-safe:`/`motion-reduce:`; this class lets users opt in regardless.
264
- */
265
197
  .actuate-admin.reduce-motion,
266
198
  .actuate-admin.reduce-motion *,
267
199
  .actuate-admin.reduce-motion *::before,
@@ -8,7 +8,7 @@
8
8
  * opens the right-side `FormSchemaDrawer`.
9
9
  */
10
10
  import { ChevronRight, ClipboardList, Plus, Search, Settings2 } from 'lucide-react'
11
- import * as Switch from '@radix-ui/react-switch'
11
+ import { Toggle } from '../components/ui/Toggle.js'
12
12
  import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
13
13
  import { toast } from 'sonner'
14
14
  import { sortByRelevance } from '../lib/search.js'
@@ -382,14 +382,12 @@ function FormsTable({
382
382
  {formatRelative(form.lastEntryAt)}
383
383
  </td>
384
384
  <td className="px-4 py-3 text-center">
385
- <Switch.Root
385
+ <Toggle
386
386
  checked={form.notifyEnabled ?? false}
387
387
  onCheckedChange={(v) => onNotifyToggle(form, v)}
388
388
  aria-label={`Notifications for ${form.name}`}
389
- className="bg-destructive data-[state=checked]:bg-success focus-visible:ring-ring relative inline-flex h-[18px] w-8 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
390
- >
391
- <Switch.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[14px]" />
392
- </Switch.Root>
389
+ variant="enabled"
390
+ />
393
391
  </td>
394
392
  <td className="px-4 py-3 text-right">
395
393
  <button
@@ -3,6 +3,7 @@
3
3
  import { useState, useEffect } from 'react'
4
4
  import { ArrowLeft, Loader2, AlertTriangle, Trash2, X, Plus } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
+ import { EnabledSwitch } from '../components/EnabledSwitch.js'
6
7
  import { useApiData } from '../lib/useApiData.js'
7
8
  import { cmsApi } from '../lib/api.js'
8
9
 
@@ -284,18 +285,11 @@ export function ScriptTagEditor({ tagId, onNavigate }: ScriptTagEditorProps) {
284
285
  <div>
285
286
  <label className="text-foreground mb-1 block text-sm font-medium">Enabled</label>
286
287
  <div className="pt-2">
287
- <button
288
- type="button"
289
- onClick={() => setEnabled(!enabled)}
290
- className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${enabled ? 'bg-primary' : 'bg-switch-background'}`}
291
- aria-pressed={enabled}
292
- >
293
- <span
294
- className={`bg-card absolute top-0.5 block h-5 w-5 rounded-full transition-transform ${
295
- enabled ? 'translate-x-[22px]' : 'translate-x-0.5'
296
- }`}
297
- />
298
- </button>
288
+ <EnabledSwitch
289
+ checked={enabled}
290
+ onCheckedChange={setEnabled}
291
+ aria-label={enabled ? 'Disable script tag' : 'Enable script tag'}
292
+ />
299
293
  </div>
300
294
  </div>
301
295
  </div>
@@ -1,8 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useMemo, useState } from 'react'
4
4
  import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
+ import { EnabledSwitch } from '../components/EnabledSwitch.js'
6
7
  import { useApiData } from '../lib/useApiData.js'
7
8
  import { cmsApi } from '../lib/api.js'
8
9
 
@@ -35,6 +36,13 @@ const PLACEMENT_COLORS: Record<string, string> = {
35
36
  body_close: 'bg-success/15 text-success',
36
37
  }
37
38
 
39
+ function normalizeEnabled(value: unknown): boolean {
40
+ if (typeof value === 'boolean') return value
41
+ if (value === 'true' || value === 1) return true
42
+ if (value === 'false' || value === 0 || value === null) return false
43
+ return true
44
+ }
45
+
38
46
  function scopeLabel(tag: ScriptTag): string {
39
47
  const scope = String(tag.scope ?? '')
40
48
  const targetPaths = Array.isArray(tag.targetPaths) ? tag.targetPaths : []
@@ -53,21 +61,41 @@ function scopeLabel(tag: ScriptTag): string {
53
61
  export function ScriptTags({ onNavigate }: ScriptTagsProps) {
54
62
  const { data, loading, error, refetch } = useApiData<ScriptTag[]>('/script-tags')
55
63
  const [togglingId, setTogglingId] = useState<string | null>(null)
64
+ const [enabledOverrides, setEnabledOverrides] = useState<Record<string, boolean>>({})
56
65
 
57
- const tags = data ?? []
66
+ const tags = useMemo(
67
+ () =>
68
+ (data ?? []).map((tag) => ({
69
+ ...tag,
70
+ enabled: enabledOverrides[tag.id] ?? normalizeEnabled(tag.enabled),
71
+ })),
72
+ [data, enabledOverrides],
73
+ )
58
74
 
59
- const toggleEnabled = async (tag: ScriptTag) => {
75
+ const toggleEnabled = async (tag: ScriptTag, nextEnabled: boolean) => {
76
+ setEnabledOverrides((prev) => ({ ...prev, [tag.id]: nextEnabled }))
60
77
  setTogglingId(tag.id)
61
- const res = await cmsApi(`/script-tags/${tag.id}`, {
78
+
79
+ const res = await cmsApi<{ enabled?: boolean }>(`/script-tags/${tag.id}`, {
62
80
  method: 'PUT',
63
- body: JSON.stringify({ enabled: !tag.enabled }),
81
+ body: JSON.stringify({ enabled: nextEnabled }),
64
82
  })
83
+
65
84
  setTogglingId(null)
85
+
66
86
  if (res.error) {
87
+ setEnabledOverrides((prev) => {
88
+ const { [tag.id]: _, ...rest } = prev
89
+ return rest
90
+ })
67
91
  toast.error(res.error)
68
- } else {
69
- refetch()
92
+ return
70
93
  }
94
+
95
+ const saved = normalizeEnabled(res.data?.enabled ?? nextEnabled)
96
+ setEnabledOverrides((prev) => ({ ...prev, [tag.id]: saved }))
97
+ refetch()
98
+ toast.success(saved ? 'Tag enabled' : 'Tag disabled')
71
99
  }
72
100
 
73
101
  if (loading) {
@@ -167,19 +195,12 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
167
195
  {tag.priority}
168
196
  </td>
169
197
  <td className="px-4 py-3 text-center">
170
- <button
171
- type="button"
172
- onClick={() => toggleEnabled(tag)}
198
+ <EnabledSwitch
199
+ checked={tag.enabled}
173
200
  disabled={togglingId === tag.id}
174
- className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${tag.enabled ? 'bg-primary' : 'bg-switch-background'}`}
175
- aria-pressed={tag.enabled}
176
- >
177
- <span
178
- className={`bg-card absolute top-0.5 block h-5 w-5 rounded-full transition-transform ${
179
- tag.enabled ? 'translate-x-[22px]' : 'translate-x-0.5'
180
- }`}
181
- />
182
- </button>
201
+ onCheckedChange={(next) => void toggleEnabled(tag, next)}
202
+ aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
203
+ />
183
204
  </td>
184
205
  </tr>
185
206
  ))}
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { Plus, Grid3X3 } from 'lucide-react'
4
- import * as Switch from '@radix-ui/react-switch'
4
+ import { Toggle } from '../../components/ui/Toggle.js'
5
5
  import type { DeviceMode } from '../../hooks/useBuilderState.js'
6
6
 
7
7
  export interface BottomBarProps {
@@ -45,14 +45,11 @@ export function BottomBar({
45
45
  {/* Grid overlay toggle */}
46
46
  <label className="flex cursor-pointer items-center gap-2">
47
47
  <Grid3X3 size={14} className="text-muted-foreground" />
48
- <Switch.Root
48
+ <Toggle
49
49
  checked={showGridOverlay}
50
50
  onCheckedChange={onToggleGrid}
51
- className="bg-input-background data-[state=checked]:bg-primary relative h-[18px] w-8 rounded-full transition-colors"
52
51
  aria-label="Toggle grid overlay"
53
- >
54
- <Switch.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[14px]" />
55
- </Switch.Root>
52
+ />
56
53
  <span className="text-muted-foreground text-xs">Grid</span>
57
54
  </label>
58
55
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useId, useState } from 'react'
4
4
  import { RefreshCw, Map, AlertTriangle, CheckCircle2, ExternalLink, Bot } from 'lucide-react'
5
- import * as Switch from '@radix-ui/react-switch'
5
+ import { Toggle as UiToggle } from '../../components/ui/Toggle.js'
6
6
  import { toast } from 'sonner'
7
7
  import {
8
8
  fetchSitemapStatus,
@@ -67,16 +67,13 @@ function Toggle({
67
67
  {help}
68
68
  </span>
69
69
  </span>
70
- <Switch.Root
70
+ <UiToggle
71
71
  checked={checked}
72
72
  onCheckedChange={onChange}
73
73
  disabled={disabled}
74
74
  aria-labelledby={labelId}
75
75
  aria-describedby={helpId}
76
- className="bg-input-background data-[state=checked]:bg-primary focus-visible:ring-ring relative h-[18px] w-8 shrink-0 rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
77
- >
78
- <Switch.Thumb className="bg-background block h-3.5 w-3.5 translate-x-0.5 rounded-full shadow-sm transition-transform data-[state=checked]:translate-x-[14px]" />
79
- </Switch.Root>
76
+ />
80
77
  </div>
81
78
  )
82
79
  }
@@ -3,6 +3,7 @@
3
3
  import * as Dialog from '@radix-ui/react-dialog'
4
4
  import { AlertTriangle, Check, Loader2, Lock } from 'lucide-react'
5
5
  import { useId, type ReactNode } from 'react'
6
+ import { Card, CardContent, CardDescription, CardHeader } from '../../components/ui/Card.js'
6
7
  import { adminPortalContainer } from '../../lib/portal-container.js'
7
8
  import type { SettingsSourceMetadata, ValidationIssue } from './useGeneralSettings.js'
8
9
 
@@ -34,21 +35,21 @@ export function SettingsCard({
34
35
  }) {
35
36
  const headingId = useId()
36
37
  return (
37
- <section
38
- aria-labelledby={headingId}
39
- className={`border-border bg-card rounded-lg border p-6 ${className ?? ''}`}
40
- >
41
- <header className="mb-4 flex items-start justify-between gap-3">
38
+ <Card padding="none" aria-labelledby={headingId} role="region" className={className}>
39
+ <CardHeader className="flex items-start justify-between gap-3">
42
40
  <div className="min-w-0">
43
- <Heading id={headingId} className="text-foreground text-lg font-medium">
41
+ <Heading
42
+ id={headingId}
43
+ className="text-[length:var(--fs-card)] font-semibold text-[var(--txt)]"
44
+ >
44
45
  {title}
45
46
  </Heading>
46
- {description && <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>}
47
+ {description && <CardDescription>{description}</CardDescription>}
47
48
  </div>
48
49
  {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
49
- </header>
50
- {children}
51
- </section>
50
+ </CardHeader>
51
+ <CardContent className="pt-0 text-[var(--txt)]">{children}</CardContent>
52
+ </Card>
52
53
  )
53
54
  }
54
55