@actuate-media/cms-admin 0.72.3 → 0.74.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.
- package/CHANGELOG.md +22 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +3 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/LoginRoot.d.ts.map +1 -1
- package/dist/LoginRoot.js +2 -1
- package/dist/LoginRoot.js.map +1 -1
- package/dist/__tests__/components/resizable-aside.test.d.ts +2 -0
- package/dist/__tests__/components/resizable-aside.test.d.ts.map +1 -0
- package/dist/__tests__/components/resizable-aside.test.js +77 -0
- package/dist/__tests__/components/resizable-aside.test.js.map +1 -0
- package/dist/__tests__/components/theme-provider-accent.test.d.ts +2 -0
- package/dist/__tests__/components/theme-provider-accent.test.d.ts.map +1 -0
- package/dist/__tests__/components/theme-provider-accent.test.js +64 -0
- package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -0
- package/dist/__tests__/views/portal-container.test.js +13 -0
- package/dist/__tests__/views/portal-container.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.js +6 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.js +13 -4
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/CoauthorResultPopover.js +1 -1
- package/dist/components/EditorSeoAside.d.ts +20 -0
- package/dist/components/EditorSeoAside.d.ts.map +1 -0
- package/dist/components/EditorSeoAside.js +30 -0
- package/dist/components/EditorSeoAside.js.map +1 -0
- package/dist/components/MediaPickerModal.js +1 -1
- package/dist/components/SEOConfigPanel.js +1 -1
- package/dist/components/SchedulePublishDialog.js +1 -1
- package/dist/components/SharePreviewLinkDialog.js +1 -1
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js +2 -1
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/VersionHistory.js +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +28 -8
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +13 -3
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/components/seo/primitives.d.ts.map +1 -1
- package/dist/components/seo/primitives.js +3 -3
- package/dist/components/seo/primitives.js.map +1 -1
- package/dist/components/ui/Card.js +5 -5
- package/dist/components/ui/Card.js.map +1 -1
- package/dist/components/ui/InspectorPane.js +3 -3
- package/dist/components/ui/InspectorPane.js.map +1 -1
- package/dist/components/ui/PanelSection.d.ts +16 -0
- package/dist/components/ui/PanelSection.d.ts.map +1 -0
- package/dist/components/ui/PanelSection.js +15 -0
- package/dist/components/ui/PanelSection.js.map +1 -0
- package/dist/components/ui/ResizableAside.d.ts +38 -0
- package/dist/components/ui/ResizableAside.d.ts.map +1 -0
- package/dist/components/ui/ResizableAside.js +209 -0
- package/dist/components/ui/ResizableAside.js.map +1 -0
- package/dist/lib/admin-root-element.d.ts +9 -0
- package/dist/lib/admin-root-element.d.ts.map +1 -0
- package/dist/lib/admin-root-element.js +14 -0
- package/dist/lib/admin-root-element.js.map +1 -0
- package/dist/lib/portal-container.d.ts.map +1 -1
- package/dist/lib/portal-container.js +2 -3
- package/dist/lib/portal-container.js.map +1 -1
- package/dist/lib/seo-service.d.ts +89 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +50 -2
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/ApiKeys.js +2 -2
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +12 -14
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +35 -6
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/PageEditor.js +1 -1
- package/dist/views/Posts/PostTypeEditor.js +3 -3
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +4 -2
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTags.js +1 -1
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +4 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +5 -3
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +24 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +7 -2
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +1 -1
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -1
- package/dist/views/post-editor/PostHeaderPanel.js +2 -1
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +24 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js +1 -1
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -1
- package/dist/views/seo/LinksTab.d.ts +4 -0
- package/dist/views/seo/LinksTab.d.ts.map +1 -0
- package/dist/views/seo/LinksTab.js +111 -0
- package/dist/views/seo/LinksTab.js.map +1 -0
- package/dist/views/seo/OverviewTab.js +1 -1
- package/dist/views/seo/OverviewTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +39 -6
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/seo/TechnicalTab.js +2 -2
- package/dist/views/settings/AISettingsTab.js +1 -1
- package/dist/views/settings/AiFeaturesCard.js +1 -1
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/AppearanceTab.js +1 -1
- package/dist/views/settings/BrandVoiceCard.js +1 -1
- package/dist/views/settings/EmailSettingsTab.js +2 -2
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/SecurityStatusCard.js +1 -1
- package/dist/views/settings/SeoSettingsTab.js +4 -4
- package/dist/views/settings/UpdatesTab.js +2 -2
- package/dist/views/settings/components.js +3 -3
- package/dist/views/settings/components.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +3 -2
- package/src/LoginRoot.tsx +2 -1
- package/src/__tests__/components/resizable-aside.test.tsx +130 -0
- package/src/__tests__/components/theme-provider-accent.test.tsx +84 -0
- package/src/__tests__/views/portal-container.test.tsx +14 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +6 -0
- package/src/__tests__/views/seo-content-pane.render.test.tsx +14 -4
- package/src/components/CoauthorResultPopover.tsx +1 -1
- package/src/components/EditorSeoAside.tsx +149 -0
- package/src/components/MediaPickerModal.tsx +1 -1
- package/src/components/SEOConfigPanel.tsx +2 -2
- package/src/components/SchedulePublishDialog.tsx +1 -1
- package/src/components/SharePreviewLinkDialog.tsx +1 -1
- package/src/components/ThemeProvider.tsx +3 -1
- package/src/components/VersionHistory.tsx +1 -1
- package/src/components/seo/SeoEditorPane.tsx +41 -12
- package/src/components/seo/SeoIssueFixPanel.tsx +16 -4
- package/src/components/seo/primitives.tsx +7 -3
- package/src/components/ui/Card.tsx +5 -5
- package/src/components/ui/InspectorPane.tsx +3 -3
- package/src/components/ui/PanelSection.tsx +53 -0
- package/src/components/ui/ResizableAside.tsx +317 -0
- package/src/lib/admin-root-element.ts +15 -0
- package/src/lib/portal-container.ts +3 -2
- package/src/lib/seo-service.ts +143 -2
- package/src/views/ApiKeys.tsx +2 -2
- package/src/views/Dashboard.tsx +40 -53
- package/src/views/MediaBrowser.tsx +62 -12
- package/src/views/PageEditor.tsx +1 -1
- package/src/views/Posts/PostTypeEditor.tsx +3 -3
- package/src/views/SEO.tsx +6 -0
- package/src/views/ScriptTags.tsx +2 -2
- package/src/views/Users.tsx +1 -1
- package/src/views/page-editor/EditorTopBar.tsx +24 -0
- package/src/views/page-editor/PageSectionEditor.tsx +44 -4
- package/src/views/page-editor/SectionInspector.tsx +33 -27
- package/src/views/page-editor/SectionsPanel.tsx +1 -1
- package/src/views/post-editor/PostHeaderPanel.tsx +16 -12
- package/src/views/post-editor/PostSectionEditor.tsx +44 -4
- package/src/views/post-editor/PostTemplateEditor.tsx +1 -1
- package/src/views/seo/LinksTab.tsx +462 -0
- package/src/views/seo/OverviewTab.tsx +1 -1
- package/src/views/seo/RedirectsTab.tsx +97 -4
- package/src/views/seo/TechnicalTab.tsx +2 -2
- package/src/views/settings/AISettingsTab.tsx +3 -3
- package/src/views/settings/AiFeaturesCard.tsx +1 -1
- package/src/views/settings/AppearanceTab.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +2 -2
- package/src/views/settings/EmailSettingsTab.tsx +2 -2
- package/src/views/settings/GeneralSettingsTab.tsx +3 -3
- package/src/views/settings/SecurityStatusCard.tsx +1 -1
- package/src/views/settings/SeoSettingsTab.tsx +6 -6
- package/src/views/settings/UpdatesTab.tsx +3 -3
- package/src/views/settings/components.tsx +3 -3
|
@@ -7,10 +7,19 @@
|
|
|
7
7
|
* assists, and `updateSeoFields` save path as the drawer; only the
|
|
8
8
|
* presentation pattern changed.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cloneElement,
|
|
12
|
+
isValidElement,
|
|
13
|
+
useEffect,
|
|
14
|
+
useId,
|
|
15
|
+
useMemo,
|
|
16
|
+
useState,
|
|
17
|
+
type ReactElement,
|
|
18
|
+
} from 'react'
|
|
11
19
|
import { Sparkles, Check, Loader2, Wand2 } from 'lucide-react'
|
|
12
20
|
import { toast } from 'sonner'
|
|
13
21
|
import type { BrandAlignmentScoreResult } from '@actuate-media/cms-core/brand-voice'
|
|
22
|
+
import { resolveRenderedMetaTitle } from '@actuate-media/cms-core/client'
|
|
14
23
|
import {
|
|
15
24
|
fetchSeoContentRecord,
|
|
16
25
|
updateSeoFields,
|
|
@@ -148,7 +157,7 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
148
157
|
}
|
|
149
158
|
setRecord(rec)
|
|
150
159
|
setForm({
|
|
151
|
-
metaTitle: rec.
|
|
160
|
+
metaTitle: rec.metaTitlePage ?? '',
|
|
152
161
|
metaDescription: rec.metaDescription ?? '',
|
|
153
162
|
canonicalUrl: rec.canonicalUrl ?? '',
|
|
154
163
|
focusKeyword: rec.focusKeyword ?? '',
|
|
@@ -182,11 +191,20 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
182
191
|
return
|
|
183
192
|
}
|
|
184
193
|
if (result.text) {
|
|
185
|
-
|
|
194
|
+
const targetField =
|
|
195
|
+
field === 'ogTitle' ? 'metaTitle' : field === 'ogDescription' ? 'metaDescription' : field
|
|
196
|
+
const patch: Partial<SeoFieldPayload> = { [targetField]: result.text }
|
|
197
|
+
if (field === 'ogTitle') patch.ogTitle = ''
|
|
198
|
+
if (field === 'ogDescription') patch.ogDescription = ''
|
|
199
|
+
update(patch)
|
|
186
200
|
if (result.brandAlignment) {
|
|
187
|
-
setAiAlignments((prev) => ({ ...prev, [
|
|
201
|
+
setAiAlignments((prev) => ({ ...prev, [targetField]: result.brandAlignment }))
|
|
188
202
|
}
|
|
189
|
-
toast.success(
|
|
203
|
+
toast.success(
|
|
204
|
+
field === 'ogTitle' || field === 'ogDescription'
|
|
205
|
+
? 'AI suggestion applied to meta tags — OG inherits from meta unless you override.'
|
|
206
|
+
: 'AI suggestion applied — review before saving.',
|
|
207
|
+
)
|
|
190
208
|
} else {
|
|
191
209
|
toast.error('No suggestion was returned.')
|
|
192
210
|
}
|
|
@@ -256,7 +274,18 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
256
274
|
onSaved?.()
|
|
257
275
|
}
|
|
258
276
|
|
|
259
|
-
const
|
|
277
|
+
const renderedTitle = useMemo(() => {
|
|
278
|
+
if (!record) return form.metaTitle?.trim() || 'Untitled'
|
|
279
|
+
return resolveRenderedMetaTitle({
|
|
280
|
+
metaTitlePage: form.metaTitle,
|
|
281
|
+
documentTitle: record.title,
|
|
282
|
+
site: {
|
|
283
|
+
siteName: record.siteName ?? undefined,
|
|
284
|
+
titleTemplate: record.titleTemplate ?? undefined,
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
}, [form.metaTitle, record])
|
|
288
|
+
|
|
260
289
|
const desc = form.metaDescription || 'No meta description set. Search engines may generate one.'
|
|
261
290
|
const url = record?.url ?? ''
|
|
262
291
|
|
|
@@ -285,11 +314,11 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
285
314
|
|
|
286
315
|
<InspectorPaneBody>
|
|
287
316
|
{loading ? (
|
|
288
|
-
<div className="px-
|
|
317
|
+
<div className="px-5 py-4">
|
|
289
318
|
<SeoLoading />
|
|
290
319
|
</div>
|
|
291
320
|
) : error ? (
|
|
292
|
-
<div className="px-
|
|
321
|
+
<div className="px-5 py-4">
|
|
293
322
|
<SeoErrorState message={error} />
|
|
294
323
|
</div>
|
|
295
324
|
) : record ? (
|
|
@@ -297,7 +326,7 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
297
326
|
<InspectorPaneSection title="Google Preview">
|
|
298
327
|
<div className="rounded-[7px] border border-[var(--bdr)] bg-[var(--card)] p-3">
|
|
299
328
|
<p className="truncate text-[11.5px] text-[var(--suc)]">{url}</p>
|
|
300
|
-
<p className="text-[15px] leading-snug text-[var(--acc)]">{
|
|
329
|
+
<p className="text-[15px] leading-snug text-[var(--acc)]">{renderedTitle}</p>
|
|
301
330
|
<p className="line-clamp-2 text-[12px] text-[var(--sub)]">{desc}</p>
|
|
302
331
|
</div>
|
|
303
332
|
</InspectorPaneSection>
|
|
@@ -316,7 +345,7 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
316
345
|
className={inputCls}
|
|
317
346
|
value={form.metaTitle ?? ''}
|
|
318
347
|
onChange={(e) => update({ metaTitle: e.target.value })}
|
|
319
|
-
placeholder="Page title
|
|
348
|
+
placeholder="Page-specific title (%page% in your site template)"
|
|
320
349
|
/>
|
|
321
350
|
</PaneField>
|
|
322
351
|
<PaneField
|
|
@@ -401,10 +430,10 @@ export function SeoEditorPane({ entityType, entityId, onClose, onSaved }: SeoEdi
|
|
|
401
430
|
{url.replace(/^https?:\/\//, '').split('/')[0]}
|
|
402
431
|
</p>
|
|
403
432
|
<p className="truncate text-[12.5px] font-medium text-[var(--txt)]">
|
|
404
|
-
{form.ogTitle ||
|
|
433
|
+
{form.ogTitle || renderedTitle}
|
|
405
434
|
</p>
|
|
406
435
|
<p className="line-clamp-1 text-[12px] text-[var(--sub)]">
|
|
407
|
-
{form.ogDescription || desc}
|
|
436
|
+
{form.ogDescription || form.metaDescription || desc}
|
|
408
437
|
</p>
|
|
409
438
|
</div>
|
|
410
439
|
</div>
|
|
@@ -321,7 +321,11 @@ export function SeoIssueFixPanel({
|
|
|
321
321
|
toast.error(res.error)
|
|
322
322
|
return
|
|
323
323
|
}
|
|
324
|
-
toast.success(
|
|
324
|
+
toast.success(
|
|
325
|
+
suggestion?.fixStrategy === 'fix-link'
|
|
326
|
+
? 'Link updated in content and issue resolved.'
|
|
327
|
+
: 'Redirect created and issue resolved.',
|
|
328
|
+
)
|
|
325
329
|
onChanged?.()
|
|
326
330
|
}
|
|
327
331
|
|
|
@@ -408,11 +412,15 @@ export function SeoIssueFixPanel({
|
|
|
408
412
|
{suggestion.source === 'ai' && !suggestion.aiUnavailable ? (
|
|
409
413
|
<span className="bg-primary/10 text-primary inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium">
|
|
410
414
|
<Sparkles className="h-3 w-3" aria-hidden />
|
|
411
|
-
|
|
415
|
+
{suggestion.fixStrategy === 'fix-link'
|
|
416
|
+
? 'Fix link in content'
|
|
417
|
+
: 'AI suggestion'}
|
|
412
418
|
</span>
|
|
413
419
|
) : suggestion.source === 'deterministic' ? (
|
|
414
420
|
<span className="bg-muted text-muted-foreground rounded-full px-2.5 py-0.5 text-xs font-medium">
|
|
415
|
-
|
|
421
|
+
{suggestion.fixStrategy === 'fix-link'
|
|
422
|
+
? 'Content link fix'
|
|
423
|
+
: 'Rule-based fix'}
|
|
416
424
|
</span>
|
|
417
425
|
) : null}
|
|
418
426
|
</div>
|
|
@@ -483,7 +491,11 @@ export function SeoIssueFixPanel({
|
|
|
483
491
|
disabled={busy}
|
|
484
492
|
>
|
|
485
493
|
<Check className="h-4 w-4" aria-hidden />
|
|
486
|
-
{busy
|
|
494
|
+
{busy
|
|
495
|
+
? 'Applying…'
|
|
496
|
+
: suggestion.fixStrategy === 'fix-link'
|
|
497
|
+
? 'Fix link in content'
|
|
498
|
+
: 'Approve redirect'}
|
|
487
499
|
</button>
|
|
488
500
|
) : inlineFix && suggestion?.autoFixable && !inlineApplyEnabled ? (
|
|
489
501
|
<PlanUpgradeCallout feature="seo.inlineApply" upgradeUrl={planInfo?.upgradeUrl} />
|
|
@@ -153,14 +153,18 @@ export function SectionCard({
|
|
|
153
153
|
{action}
|
|
154
154
|
</CardHeader>
|
|
155
155
|
)}
|
|
156
|
-
<CardContent className={title || action ? '
|
|
156
|
+
<CardContent className={title || action ? '' : 'pt-6!'}>{children}</CardContent>
|
|
157
157
|
</Card>
|
|
158
158
|
)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export function SeoLoading({ label = 'Loading…' }: { label?: string }) {
|
|
162
162
|
return (
|
|
163
|
-
<div
|
|
163
|
+
<div
|
|
164
|
+
className="flex min-h-32 items-center justify-center py-12"
|
|
165
|
+
role="status"
|
|
166
|
+
aria-live="polite"
|
|
167
|
+
>
|
|
164
168
|
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
|
|
165
169
|
<span className="sr-only">{label}</span>
|
|
166
170
|
</div>
|
|
@@ -195,7 +199,7 @@ export function SeoEmptyState({
|
|
|
195
199
|
export function SeoErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
|
196
200
|
return (
|
|
197
201
|
<div
|
|
198
|
-
className="border-destructive/30 bg-destructive/5 flex items-center gap-3 rounded-lg border p-
|
|
202
|
+
className="border-destructive/30 bg-destructive/5 flex items-center gap-3 rounded-lg border p-4"
|
|
199
203
|
role="alert"
|
|
200
204
|
>
|
|
201
205
|
<AlertTriangle className="text-destructive h-5 w-5 shrink-0" aria-hidden />
|
|
@@ -10,8 +10,8 @@ const card = cv(
|
|
|
10
10
|
/** Stat / compact surfaces (`.dstat`). */
|
|
11
11
|
sm: 'px-4 py-3.5',
|
|
12
12
|
/** Simple padded panel (`.dcard-b`). */
|
|
13
|
-
md: 'px-
|
|
14
|
-
lg: 'px-
|
|
13
|
+
md: 'px-6 py-4',
|
|
14
|
+
lg: 'px-6 py-6',
|
|
15
15
|
},
|
|
16
16
|
elevation: {
|
|
17
17
|
flat: 'shadow-none',
|
|
@@ -55,7 +55,7 @@ export function CardHeader({
|
|
|
55
55
|
}: HTMLAttributes<HTMLDivElement> & { bordered?: boolean }) {
|
|
56
56
|
return (
|
|
57
57
|
<div
|
|
58
|
-
className={[bordered ? 'border-b border-[var(--bdr)] px-
|
|
58
|
+
className={[bordered ? 'border-b border-[var(--bdr)] px-6 pt-6 pb-4' : '', className]
|
|
59
59
|
.filter(Boolean)
|
|
60
60
|
.join(' ')}
|
|
61
61
|
{...rest}
|
|
@@ -96,7 +96,7 @@ export function CardDescription({
|
|
|
96
96
|
export function CardContent({ className = '', children, ...rest }: HTMLAttributes<HTMLDivElement>) {
|
|
97
97
|
return (
|
|
98
98
|
<div
|
|
99
|
-
className={`px-
|
|
99
|
+
className={`px-6 pt-4 pb-6 text-[length:var(--fs-body)] leading-[1.55] text-[var(--sub)] ${className}`}
|
|
100
100
|
{...rest}
|
|
101
101
|
>
|
|
102
102
|
{children}
|
|
@@ -107,7 +107,7 @@ export function CardContent({ className = '', children, ...rest }: HTMLAttribute
|
|
|
107
107
|
export function CardFooter({ className = '', children, ...rest }: HTMLAttributes<HTMLDivElement>) {
|
|
108
108
|
return (
|
|
109
109
|
<div
|
|
110
|
-
className={`flex items-center justify-end gap-2 border-t border-[var(--bdr)] px-
|
|
110
|
+
className={`flex items-center justify-end gap-2 border-t border-[var(--bdr)] px-6 py-4 ${className}`}
|
|
111
111
|
{...rest}
|
|
112
112
|
>
|
|
113
113
|
{children}
|
|
@@ -86,7 +86,7 @@ export function InspectorPaneHeader({
|
|
|
86
86
|
closeLabel?: string
|
|
87
87
|
}) {
|
|
88
88
|
return (
|
|
89
|
-
<div className="flex items-start gap-2 border-b border-[var(--bdr)] px-
|
|
89
|
+
<div className="flex items-start gap-2 border-b border-[var(--bdr)] px-5 py-4">
|
|
90
90
|
<div className="min-w-0 flex-1">{children}</div>
|
|
91
91
|
<button
|
|
92
92
|
type="button"
|
|
@@ -115,7 +115,7 @@ export function InspectorPaneSection({
|
|
|
115
115
|
children: ReactNode
|
|
116
116
|
}) {
|
|
117
117
|
return (
|
|
118
|
-
<div className="border-b border-[var(--bdr)] px-
|
|
118
|
+
<div className="border-b border-[var(--bdr)] px-5 py-4 last:border-b-0">
|
|
119
119
|
{title && (
|
|
120
120
|
<div className="mb-2.5 flex items-baseline gap-2">
|
|
121
121
|
<span className="text-[10.5px] font-semibold tracking-[0.6px] text-[var(--muted)] uppercase">
|
|
@@ -130,5 +130,5 @@ export function InspectorPaneSection({
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
export function InspectorPaneFooter({ children }: { children: ReactNode }) {
|
|
133
|
-
return <div className="border-t border-[var(--bdr)] px-
|
|
133
|
+
return <div className="border-t border-[var(--bdr)] px-5 py-4">{children}</div>
|
|
134
134
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bordered panel with a header divider — Dashboard, modals, and list-heavy views.
|
|
7
|
+
* Matches Settings/Section card rhythm: `px-6` gutters, `py-4` header, body flush
|
|
8
|
+
* below the divider with its own padding.
|
|
9
|
+
*/
|
|
10
|
+
export function PanelSection({
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
action,
|
|
14
|
+
children,
|
|
15
|
+
className = '',
|
|
16
|
+
}: {
|
|
17
|
+
title?: ReactNode
|
|
18
|
+
description?: ReactNode
|
|
19
|
+
action?: ReactNode
|
|
20
|
+
children: ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<section
|
|
25
|
+
className={`bg-card border-border overflow-hidden rounded-xl border shadow-sm ${className}`}
|
|
26
|
+
>
|
|
27
|
+
{(title || action) && (
|
|
28
|
+
<header className="border-border flex items-center justify-between gap-3 border-b px-6 py-4">
|
|
29
|
+
<div className="min-w-0">
|
|
30
|
+
{title &&
|
|
31
|
+
(typeof title === 'string' ? (
|
|
32
|
+
<h2 className="text-foreground text-sm font-medium">{title}</h2>
|
|
33
|
+
) : (
|
|
34
|
+
title
|
|
35
|
+
))}
|
|
36
|
+
{description &&
|
|
37
|
+
(typeof description === 'string' ? (
|
|
38
|
+
<p className="text-muted-foreground mt-0.5 text-xs">{description}</p>
|
|
39
|
+
) : (
|
|
40
|
+
description
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
{action}
|
|
44
|
+
</header>
|
|
45
|
+
)}
|
|
46
|
+
{children}
|
|
47
|
+
</section>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Standard inset for list rows / table bodies inside a {@link PanelSection}. */
|
|
52
|
+
export const PANEL_LIST_ROW_CLASS =
|
|
53
|
+
'hover:bg-accent/50 focus-visible:bg-accent/50 w-full px-6 py-4 text-left transition-colors'
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ComponentPropsWithoutRef,
|
|
11
|
+
type KeyboardEvent,
|
|
12
|
+
type PointerEvent,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react'
|
|
15
|
+
import { Maximize2, Minimize2 } from 'lucide-react'
|
|
16
|
+
|
|
17
|
+
export const SECTION_INSPECTOR_WIDTH_KEY = 'actuate:section-inspector-width'
|
|
18
|
+
export const SECTION_INSPECTOR_DEFAULT_WIDTH = 320
|
|
19
|
+
export const SECTION_INSPECTOR_MIN_WIDTH = 280
|
|
20
|
+
export const SECTION_INSPECTOR_MAX_WIDTH = 640
|
|
21
|
+
export const SECTION_INSPECTOR_MIN_CANVAS_WIDTH = 96
|
|
22
|
+
/** Below this editor-row width the expand action uses an overlay panel. */
|
|
23
|
+
export const SECTION_INSPECTOR_OVERLAY_BREAKPOINT = 768
|
|
24
|
+
export const SECTION_INSPECTOR_OVERLAY_MAX_WIDTH = 480
|
|
25
|
+
|
|
26
|
+
function clampWidth(width: number, max = SECTION_INSPECTOR_MAX_WIDTH): number {
|
|
27
|
+
return Math.min(max, Math.max(SECTION_INSPECTOR_MIN_WIDTH, width))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Max inline width given the surrounding editor row. */
|
|
31
|
+
export function computeInspectorMaxWidth(containerWidth: number): number {
|
|
32
|
+
if (!Number.isFinite(containerWidth) || containerWidth <= 0) {
|
|
33
|
+
return SECTION_INSPECTOR_MAX_WIDTH
|
|
34
|
+
}
|
|
35
|
+
const containerCap = Math.max(
|
|
36
|
+
SECTION_INSPECTOR_MIN_WIDTH,
|
|
37
|
+
containerWidth - SECTION_INSPECTOR_MIN_CANVAS_WIDTH,
|
|
38
|
+
)
|
|
39
|
+
return Math.min(SECTION_INSPECTOR_MAX_WIDTH, containerCap)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Width when expanded as an overlay on tablet/mobile. */
|
|
43
|
+
export function computeInspectorOverlayWidth(containerWidth: number): number {
|
|
44
|
+
if (!Number.isFinite(containerWidth) || containerWidth <= 0) {
|
|
45
|
+
return SECTION_INSPECTOR_OVERLAY_MAX_WIDTH
|
|
46
|
+
}
|
|
47
|
+
return Math.min(
|
|
48
|
+
SECTION_INSPECTOR_OVERLAY_MAX_WIDTH,
|
|
49
|
+
Math.max(SECTION_INSPECTOR_MIN_WIDTH, containerWidth - 32),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function readSectionInspectorWidth(): number {
|
|
54
|
+
if (typeof window === 'undefined') return SECTION_INSPECTOR_DEFAULT_WIDTH
|
|
55
|
+
try {
|
|
56
|
+
const raw = localStorage.getItem(SECTION_INSPECTOR_WIDTH_KEY)
|
|
57
|
+
if (!raw) return SECTION_INSPECTOR_DEFAULT_WIDTH
|
|
58
|
+
const parsed = Number.parseInt(raw, 10)
|
|
59
|
+
if (!Number.isFinite(parsed)) return SECTION_INSPECTOR_DEFAULT_WIDTH
|
|
60
|
+
return clampWidth(parsed)
|
|
61
|
+
} catch {
|
|
62
|
+
return SECTION_INSPECTOR_DEFAULT_WIDTH
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function persistSectionInspectorWidth(width: number, max = SECTION_INSPECTOR_MAX_WIDTH): void {
|
|
67
|
+
try {
|
|
68
|
+
localStorage.setItem(SECTION_INSPECTOR_WIDTH_KEY, String(clampWidth(width, max)))
|
|
69
|
+
} catch {
|
|
70
|
+
// Storage may be unavailable in private mode.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface InspectorResizeContextValue {
|
|
75
|
+
width: number
|
|
76
|
+
maxWidth: number
|
|
77
|
+
isExpanded: boolean
|
|
78
|
+
canExpand: boolean
|
|
79
|
+
toggleExpanded: () => void
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const InspectorResizeContext = createContext<InspectorResizeContextValue | null>(null)
|
|
83
|
+
|
|
84
|
+
export function useInspectorResize(): InspectorResizeContextValue | null {
|
|
85
|
+
return useContext(InspectorResizeContext)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ResizableAsideProps extends ComponentPropsWithoutRef<'aside'> {
|
|
89
|
+
children: ReactNode
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Right-hand editor shell with a draggable left edge. Used by the page/post
|
|
94
|
+
* section inspector and post header panel (default 320px, persisted in
|
|
95
|
+
* localStorage). On tablet/mobile the handle uses a 44px touch target and
|
|
96
|
+
* the header expand button can grow the panel to an overlay width.
|
|
97
|
+
*/
|
|
98
|
+
export function ResizableAside({ children, className, style, ...props }: ResizableAsideProps) {
|
|
99
|
+
const asideRef = useRef<HTMLElement>(null)
|
|
100
|
+
const [width, setWidth] = useState(SECTION_INSPECTOR_DEFAULT_WIDTH)
|
|
101
|
+
const [containerWidth, setContainerWidth] = useState(0)
|
|
102
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
103
|
+
const [dragging, setDragging] = useState(false)
|
|
104
|
+
const widthRef = useRef(width)
|
|
105
|
+
const compactWidthRef = useRef(SECTION_INSPECTOR_DEFAULT_WIDTH)
|
|
106
|
+
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null)
|
|
107
|
+
|
|
108
|
+
const inlineMax = computeInspectorMaxWidth(containerWidth)
|
|
109
|
+
const overlayMax = computeInspectorOverlayWidth(containerWidth)
|
|
110
|
+
const useOverlay =
|
|
111
|
+
containerWidth > 0 && containerWidth < SECTION_INSPECTOR_OVERLAY_BREAKPOINT && isExpanded
|
|
112
|
+
const effectiveMax = useOverlay ? overlayMax : inlineMax
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setWidth(readSectionInspectorWidth())
|
|
116
|
+
}, [])
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
widthRef.current = width
|
|
120
|
+
}, [width])
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const parent = asideRef.current?.parentElement
|
|
124
|
+
if (!parent) return
|
|
125
|
+
|
|
126
|
+
const update = (nextWidth: number) => {
|
|
127
|
+
setContainerWidth(nextWidth)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
update(parent.getBoundingClientRect().width)
|
|
131
|
+
const observer = new ResizeObserver((entries) => {
|
|
132
|
+
const entry = entries[0]
|
|
133
|
+
if (!entry) return
|
|
134
|
+
update(entry.contentRect.width)
|
|
135
|
+
})
|
|
136
|
+
observer.observe(parent)
|
|
137
|
+
return () => observer.disconnect()
|
|
138
|
+
}, [])
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setWidth((current) => clampWidth(current, effectiveMax))
|
|
142
|
+
}, [effectiveMax])
|
|
143
|
+
|
|
144
|
+
const endDrag = useCallback(() => {
|
|
145
|
+
dragRef.current = null
|
|
146
|
+
setDragging(false)
|
|
147
|
+
document.body.style.cursor = ''
|
|
148
|
+
document.body.style.userSelect = ''
|
|
149
|
+
persistSectionInspectorWidth(widthRef.current, inlineMax)
|
|
150
|
+
if (widthRef.current < effectiveMax - 24) {
|
|
151
|
+
setIsExpanded(false)
|
|
152
|
+
}
|
|
153
|
+
}, [effectiveMax, inlineMax])
|
|
154
|
+
|
|
155
|
+
const onPointerDown = useCallback((event: PointerEvent<HTMLDivElement>) => {
|
|
156
|
+
if (event.button !== 0) return
|
|
157
|
+
dragRef.current = { startX: event.clientX, startWidth: widthRef.current }
|
|
158
|
+
setDragging(true)
|
|
159
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
160
|
+
document.body.style.cursor = 'col-resize'
|
|
161
|
+
document.body.style.userSelect = 'none'
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
const onPointerMove = useCallback(
|
|
165
|
+
(event: PointerEvent<HTMLDivElement>) => {
|
|
166
|
+
if (!dragRef.current) return
|
|
167
|
+
const delta = dragRef.current.startX - event.clientX
|
|
168
|
+
setWidth(clampWidth(dragRef.current.startWidth + delta, effectiveMax))
|
|
169
|
+
},
|
|
170
|
+
[effectiveMax],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const onPointerUp = useCallback(
|
|
174
|
+
(event: PointerEvent<HTMLDivElement>) => {
|
|
175
|
+
if (!dragRef.current) return
|
|
176
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
177
|
+
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
178
|
+
}
|
|
179
|
+
endDrag()
|
|
180
|
+
},
|
|
181
|
+
[endDrag],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const nudgeWidth = useCallback(
|
|
185
|
+
(delta: number) => {
|
|
186
|
+
setWidth((current) => {
|
|
187
|
+
const next = clampWidth(current + delta, effectiveMax)
|
|
188
|
+
persistSectionInspectorWidth(next, inlineMax)
|
|
189
|
+
if (next < effectiveMax - 24) setIsExpanded(false)
|
|
190
|
+
return next
|
|
191
|
+
})
|
|
192
|
+
},
|
|
193
|
+
[effectiveMax, inlineMax],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const onKeyDown = useCallback(
|
|
197
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
198
|
+
if (event.key === 'ArrowLeft') {
|
|
199
|
+
event.preventDefault()
|
|
200
|
+
nudgeWidth(16)
|
|
201
|
+
} else if (event.key === 'ArrowRight') {
|
|
202
|
+
event.preventDefault()
|
|
203
|
+
nudgeWidth(-16)
|
|
204
|
+
} else if (event.key === 'Home') {
|
|
205
|
+
event.preventDefault()
|
|
206
|
+
setIsExpanded(false)
|
|
207
|
+
setWidth(SECTION_INSPECTOR_MIN_WIDTH)
|
|
208
|
+
persistSectionInspectorWidth(SECTION_INSPECTOR_MIN_WIDTH, inlineMax)
|
|
209
|
+
} else if (event.key === 'End') {
|
|
210
|
+
event.preventDefault()
|
|
211
|
+
setWidth(effectiveMax)
|
|
212
|
+
persistSectionInspectorWidth(effectiveMax, inlineMax)
|
|
213
|
+
setIsExpanded(effectiveMax >= inlineMax - 8)
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
[effectiveMax, inlineMax, nudgeWidth],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const toggleExpanded = useCallback(() => {
|
|
220
|
+
if (isExpanded) {
|
|
221
|
+
const restored = clampWidth(compactWidthRef.current, inlineMax)
|
|
222
|
+
setIsExpanded(false)
|
|
223
|
+
setWidth(restored)
|
|
224
|
+
persistSectionInspectorWidth(restored, inlineMax)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
compactWidthRef.current = widthRef.current
|
|
229
|
+
const target =
|
|
230
|
+
containerWidth > 0 && containerWidth < SECTION_INSPECTOR_OVERLAY_BREAKPOINT
|
|
231
|
+
? overlayMax
|
|
232
|
+
: inlineMax
|
|
233
|
+
setIsExpanded(true)
|
|
234
|
+
setWidth(target)
|
|
235
|
+
persistSectionInspectorWidth(target, inlineMax)
|
|
236
|
+
}, [containerWidth, inlineMax, isExpanded, overlayMax])
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
return () => {
|
|
240
|
+
if (dragRef.current) endDrag()
|
|
241
|
+
}
|
|
242
|
+
}, [endDrag])
|
|
243
|
+
|
|
244
|
+
const canExpand =
|
|
245
|
+
!isExpanded &&
|
|
246
|
+
(containerWidth > 0 && containerWidth < SECTION_INSPECTOR_OVERLAY_BREAKPOINT
|
|
247
|
+
? overlayMax > width + 8
|
|
248
|
+
: inlineMax > width + 8)
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<InspectorResizeContext.Provider
|
|
252
|
+
value={{ width, maxWidth: effectiveMax, isExpanded, canExpand, toggleExpanded }}
|
|
253
|
+
>
|
|
254
|
+
<aside
|
|
255
|
+
{...props}
|
|
256
|
+
ref={asideRef}
|
|
257
|
+
data-overlay={useOverlay ? '' : undefined}
|
|
258
|
+
className={`relative shrink-0 ${useOverlay ? 'absolute top-0 right-0 bottom-0 z-20 shadow-lg' : ''} ${className ?? ''}`}
|
|
259
|
+
style={{ width, ...style }}
|
|
260
|
+
>
|
|
261
|
+
<div
|
|
262
|
+
role="separator"
|
|
263
|
+
aria-orientation="vertical"
|
|
264
|
+
aria-label="Resize inspector panel"
|
|
265
|
+
aria-valuemin={SECTION_INSPECTOR_MIN_WIDTH}
|
|
266
|
+
aria-valuemax={effectiveMax}
|
|
267
|
+
aria-valuenow={width}
|
|
268
|
+
tabIndex={0}
|
|
269
|
+
data-dragging={dragging ? '' : undefined}
|
|
270
|
+
onPointerDown={onPointerDown}
|
|
271
|
+
onPointerMove={onPointerMove}
|
|
272
|
+
onPointerUp={onPointerUp}
|
|
273
|
+
onPointerCancel={onPointerUp}
|
|
274
|
+
onKeyDown={onKeyDown}
|
|
275
|
+
className="group absolute inset-y-0 -left-5 z-10 flex w-11 -translate-x-1/2 cursor-col-resize touch-none focus-visible:outline-none md:-left-1 md:w-3 md:translate-x-0"
|
|
276
|
+
>
|
|
277
|
+
<div
|
|
278
|
+
className={`mx-auto h-full w-px motion-safe:transition-colors md:mx-0 ${
|
|
279
|
+
dragging
|
|
280
|
+
? 'bg-primary'
|
|
281
|
+
: 'bg-border group-hover:bg-primary/50 group-focus-visible:bg-primary/50'
|
|
282
|
+
}`}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
{children}
|
|
286
|
+
</aside>
|
|
287
|
+
</InspectorResizeContext.Provider>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const EXPAND_BUTTON_CLASS =
|
|
292
|
+
'text-muted-foreground hover:text-foreground hover:bg-accent flex h-9 w-9 items-center justify-center rounded-md transition-colors focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none md:h-7 md:w-7'
|
|
293
|
+
|
|
294
|
+
/** One-tap expand/collapse for tablet and mobile inspector panels. */
|
|
295
|
+
export function InspectorExpandButton({ className }: { className?: string }) {
|
|
296
|
+
const ctx = useInspectorResize()
|
|
297
|
+
if (!ctx) return null
|
|
298
|
+
|
|
299
|
+
const { isExpanded, canExpand, toggleExpanded } = ctx
|
|
300
|
+
if (!canExpand && !isExpanded) return null
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
onClick={toggleExpanded}
|
|
306
|
+
aria-label={isExpanded ? 'Collapse inspector panel' : 'Expand inspector panel'}
|
|
307
|
+
aria-pressed={isExpanded}
|
|
308
|
+
className={className ?? EXPAND_BUTTON_CLASS}
|
|
309
|
+
>
|
|
310
|
+
{isExpanded ? (
|
|
311
|
+
<Minimize2 className="h-4 w-4" aria-hidden />
|
|
312
|
+
) : (
|
|
313
|
+
<Maximize2 className="h-4 w-4" aria-hidden />
|
|
314
|
+
)}
|
|
315
|
+
</button>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Marks the canonical admin shell root (AdminRoot / LoginRoot). */
|
|
2
|
+
export const ADMIN_ROOT_ATTR = 'data-actuate-admin-root'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the element that owns admin theme tokens (accent, color mode, density).
|
|
6
|
+
* Prefer the marked shell root so a consumer wrapper `.actuate-admin` does not
|
|
7
|
+
* steal runtime overrides from the inner AdminRoot.
|
|
8
|
+
*/
|
|
9
|
+
export function getAdminRootElement(): HTMLElement | null {
|
|
10
|
+
if (typeof document === 'undefined') return null
|
|
11
|
+
return (
|
|
12
|
+
document.querySelector<HTMLElement>(`[${ADMIN_ROOT_ATTR}]`) ??
|
|
13
|
+
document.querySelector<HTMLElement>('.actuate-admin')
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getAdminRootElement } from './admin-root-element.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Admin theme tokens are scoped under `.actuate-admin` (styles/theme.css),
|
|
3
5
|
* and dark mode / accent / density are applied as classes-attributes on that
|
|
@@ -9,6 +11,5 @@
|
|
|
9
11
|
* Returns `undefined` during SSR (portals only materialize client-side).
|
|
10
12
|
*/
|
|
11
13
|
export function adminPortalContainer(): HTMLElement | undefined {
|
|
12
|
-
|
|
13
|
-
return document.querySelector<HTMLElement>('.actuate-admin') ?? undefined
|
|
14
|
+
return getAdminRootElement() ?? undefined
|
|
14
15
|
}
|