@prenta/admin 0.93.0 → 0.94.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 +111 -0
- package/dist/__tests__/lib/version-diff.test.d.ts +2 -0
- package/dist/__tests__/lib/version-diff.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-diff.test.js +230 -0
- package/dist/__tests__/lib/version-diff.test.js.map +1 -0
- package/dist/__tests__/lib/version-history.test.d.ts +2 -0
- package/dist/__tests__/lib/version-history.test.d.ts.map +1 -0
- package/dist/__tests__/lib/version-history.test.js +125 -0
- package/dist/__tests__/lib/version-history.test.js.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts +2 -0
- package/dist/__tests__/styles/responsive-layer.test.d.ts.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.js +127 -0
- package/dist/__tests__/styles/responsive-layer.test.js.map +1 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts +2 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js +70 -0
- package/dist/__tests__/views/canvas-review-mode.render.test.js.map +1 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js +29 -0
- package/dist/__tests__/views/editor-top-bar.render.test.js.map +1 -1
- package/dist/__tests__/views/version-history-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/version-history-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/version-history-panel.render.test.js +153 -0
- package/dist/__tests__/views/version-history-panel.render.test.js.map +1 -0
- package/dist/components/EditorRail.js +3 -3
- package/dist/components/EditorRail.js.map +1 -1
- package/dist/components/page-tab/DirectiveSwitch.d.ts.map +1 -1
- package/dist/components/page-tab/DirectiveSwitch.js +5 -1
- package/dist/components/page-tab/DirectiveSwitch.js.map +1 -1
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
- package/dist/layout/primitives/AdminShell.js +2 -2
- package/dist/layout/primitives/AdminShell.js.map +1 -1
- package/dist/lib/version-diff.d.ts +50 -0
- package/dist/lib/version-diff.d.ts.map +1 -0
- package/dist/lib/version-diff.js +294 -0
- package/dist/lib/version-diff.js.map +1 -0
- package/dist/lib/version-history.d.ts +73 -0
- package/dist/lib/version-history.d.ts.map +1 -0
- package/dist/lib/version-history.js +126 -0
- package/dist/lib/version-history.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/EditorCanvas.d.ts +14 -1
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
- package/dist/views/page-editor/EditorCanvas.js +3 -3
- package/dist/views/page-editor/EditorCanvas.js.map +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +8 -2
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +8 -6
- 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 +115 -32
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/ReviewBar.d.ts +21 -0
- package/dist/views/page-editor/ReviewBar.d.ts.map +1 -0
- package/dist/views/page-editor/ReviewBar.js +22 -0
- package/dist/views/page-editor/ReviewBar.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -1
- package/dist/views/page-editor/SectionInspector.js +1 -1
- package/dist/views/page-editor/SectionInspector.js.map +1 -1
- package/dist/views/page-editor/SectionsPanel.js +1 -1
- package/dist/views/page-editor/SectionsPanel.js.map +1 -1
- package/dist/views/page-editor/VersionHistoryPanel.d.ts +29 -0
- package/dist/views/page-editor/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/views/page-editor/VersionHistoryPanel.js +81 -0
- package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +13 -1
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -1
- package/dist/views/page-editor/sections/SectionRenderer.js +23 -8
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/lib/version-diff.test.ts +278 -0
- package/src/__tests__/lib/version-history.test.ts +166 -0
- package/src/__tests__/styles/responsive-layer.test.ts +152 -0
- package/src/__tests__/views/canvas-review-mode.render.test.tsx +94 -0
- package/src/__tests__/views/editor-top-bar.render.test.tsx +42 -0
- package/src/__tests__/views/version-history-panel.render.test.tsx +195 -0
- package/src/components/EditorRail.tsx +5 -5
- package/src/components/page-tab/DirectiveSwitch.tsx +4 -1
- package/src/layout/primitives/AdminShell.tsx +13 -1
- package/src/lib/version-diff.ts +376 -0
- package/src/lib/version-history.ts +192 -0
- package/src/styles/responsive.css +281 -0
- package/src/styles/theme.css +8 -0
- package/src/views/page-editor/EditorCanvas.tsx +34 -7
- package/src/views/page-editor/EditorTopBar.tsx +20 -8
- package/src/views/page-editor/PageSectionEditor.tsx +162 -43
- package/src/views/page-editor/ReviewBar.tsx +76 -0
- package/src/views/page-editor/SectionInspector.tsx +2 -1
- package/src/views/page-editor/SectionsPanel.tsx +2 -2
- package/src/views/page-editor/VersionHistoryPanel.tsx +302 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +53 -12
|
@@ -109,3 +109,45 @@ describe('EditorTopBar view-on-site vs view-live', () => {
|
|
|
109
109
|
expect(screen.queryByRole('button', { name: /View (live|draft)/ })).toBeNull()
|
|
110
110
|
})
|
|
111
111
|
})
|
|
112
|
+
|
|
113
|
+
describe('EditorTopBar history button', () => {
|
|
114
|
+
it('reports whether history is showing, since it takes over the rail rather than opening a dialog', () => {
|
|
115
|
+
const { unmount } = render(<EditorTopBar {...baseProps} onHistory={vi.fn()} />)
|
|
116
|
+
expect(
|
|
117
|
+
screen.getByRole('button', { name: 'Version history' }).getAttribute('aria-pressed'),
|
|
118
|
+
).toBe('false')
|
|
119
|
+
unmount()
|
|
120
|
+
|
|
121
|
+
render(<EditorTopBar {...baseProps} onHistory={vi.fn()} historyOpen />)
|
|
122
|
+
const open = screen.getByRole('button', { name: 'Version history' })
|
|
123
|
+
expect(open.getAttribute('aria-pressed')).toBe('true')
|
|
124
|
+
expect(open.getAttribute('title')).toBe('Close version history')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('gives every icon-only control a touch target', () => {
|
|
128
|
+
// These buttons are 32px square, which is a fine mouse target and an
|
|
129
|
+
// unusable thumb one. `prenta-touch-target` is what grows them under a
|
|
130
|
+
// coarse pointer — and it is applied per control, so a button written with
|
|
131
|
+
// a conditional className is easy to leave out. Exactly that happened to
|
|
132
|
+
// the history button, whose pressed state made it the odd one out.
|
|
133
|
+
render(
|
|
134
|
+
<EditorTopBar
|
|
135
|
+
{...baseProps}
|
|
136
|
+
onHistory={vi.fn()}
|
|
137
|
+
onViewOnSite={vi.fn()}
|
|
138
|
+
onToggleSitePreview={vi.fn()}
|
|
139
|
+
/>,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
for (const name of [
|
|
143
|
+
'Page settings',
|
|
144
|
+
'Version history',
|
|
145
|
+
'Preview page',
|
|
146
|
+
'Inline site preview',
|
|
147
|
+
'View draft on site',
|
|
148
|
+
]) {
|
|
149
|
+
const button = screen.getByRole('button', { name })
|
|
150
|
+
expect(button.className, `${name} has no touch target`).toContain('prenta-touch-target')
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
|
|
4
|
+
import { VersionHistoryPanel } from '../../views/page-editor/VersionHistoryPanel.js'
|
|
5
|
+
import type { PageVersion } from '../../lib/version-history.js'
|
|
6
|
+
import type { PageSection } from '../../lib/page-editor-service.js'
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup)
|
|
9
|
+
|
|
10
|
+
function section(id: string, content: Record<string, unknown>): PageSection {
|
|
11
|
+
return { id, sectionType: 'hero', name: 'Hero Banner', visible: true, content, settings: {} }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const NOW = new Date()
|
|
15
|
+
const iso = (hoursAgo: number) => new Date(NOW.getTime() - hoursAgo * 3_600_000).toISOString()
|
|
16
|
+
|
|
17
|
+
const VERSIONS: PageVersion[] = [
|
|
18
|
+
{
|
|
19
|
+
id: 'v3',
|
|
20
|
+
changeType: 'PUBLISH',
|
|
21
|
+
createdAt: iso(1),
|
|
22
|
+
author: { id: 'u1', name: 'Sarah Mills' },
|
|
23
|
+
sections: [section('s1', { heading: 'Third headline' })],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'v2',
|
|
27
|
+
changeType: 'UPDATE',
|
|
28
|
+
createdAt: iso(2),
|
|
29
|
+
author: { id: 'u2', name: 'Dan Ito' },
|
|
30
|
+
sections: [section('s1', { heading: 'Second headline' })],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'v1',
|
|
34
|
+
changeType: 'CREATE',
|
|
35
|
+
createdAt: iso(30),
|
|
36
|
+
author: { id: 'u1', name: 'Sarah Mills' },
|
|
37
|
+
sections: [section('s1', { heading: 'First headline' })],
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
function renderPanel(props: Partial<React.ComponentProps<typeof VersionHistoryPanel>> = {}) {
|
|
42
|
+
const onSelect = vi.fn()
|
|
43
|
+
const onRestore = vi.fn()
|
|
44
|
+
const onClose = vi.fn()
|
|
45
|
+
render(
|
|
46
|
+
<VersionHistoryPanel
|
|
47
|
+
versions={VERSIONS}
|
|
48
|
+
loading={false}
|
|
49
|
+
error={null}
|
|
50
|
+
selectedIndex={0}
|
|
51
|
+
onSelect={onSelect}
|
|
52
|
+
onClose={onClose}
|
|
53
|
+
onRestore={onRestore}
|
|
54
|
+
restoring={false}
|
|
55
|
+
canRestore
|
|
56
|
+
{...props}
|
|
57
|
+
/>,
|
|
58
|
+
)
|
|
59
|
+
return { onSelect, onRestore, onClose }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('VersionHistoryPanel', () => {
|
|
63
|
+
it('groups entries under sticky day headings', () => {
|
|
64
|
+
renderPanel()
|
|
65
|
+
expect(screen.getByRole('heading', { name: 'Today' })).toBeTruthy()
|
|
66
|
+
// v1 is 30 hours back, so it lands on its own day.
|
|
67
|
+
expect(screen.getAllByRole('heading', { level: 3 }).length).toBeGreaterThan(1)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('marks the newest entry as current and shows the published pill', () => {
|
|
71
|
+
renderPanel()
|
|
72
|
+
expect(screen.getByText('Current')).toBeTruthy()
|
|
73
|
+
expect(screen.getByText('Published')).toBeTruthy()
|
|
74
|
+
// No pill claims "Draft" — nothing in this codebase records one.
|
|
75
|
+
expect(screen.queryByText('Draft')).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('summarises each entry by what that save changed', () => {
|
|
79
|
+
renderPanel()
|
|
80
|
+
// v3 changed one field against v2.
|
|
81
|
+
expect(screen.getByText(/Sarah Mills · 1 change$/)).toBeTruthy()
|
|
82
|
+
// v1 has no predecessor in the list, so it reports no computed changes
|
|
83
|
+
// rather than claiming it created everything.
|
|
84
|
+
expect(screen.getByText(/Sarah Mills · no content changes/)).toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('shows the computed field diff, old struck through and new after it', () => {
|
|
88
|
+
renderPanel()
|
|
89
|
+
// The newest changed entry auto-expands.
|
|
90
|
+
expect(screen.getByText('Heading')).toBeTruthy()
|
|
91
|
+
const oldValue = screen.getByText('Second headline')
|
|
92
|
+
const newValue = screen.getByText('Third headline')
|
|
93
|
+
expect(oldValue.className).toContain('line-through')
|
|
94
|
+
expect(newValue.className).not.toContain('line-through')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('selects the version when its row is activated, which is what drives the canvas', () => {
|
|
98
|
+
const { onSelect } = renderPanel()
|
|
99
|
+
fireEvent.click(screen.getByText(/Dan Ito/))
|
|
100
|
+
expect(onSelect).toHaveBeenCalledWith(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('offers Restore only on the selected older row', () => {
|
|
104
|
+
renderPanel({ selectedIndex: 0 })
|
|
105
|
+
// Selected row is the current one — restoring it is a no-op.
|
|
106
|
+
expect(screen.queryByRole('button', { name: /Restore this version/ })).toBeNull()
|
|
107
|
+
|
|
108
|
+
cleanup()
|
|
109
|
+
renderPanel({ selectedIndex: 1 })
|
|
110
|
+
const restore = screen.getByRole('button', { name: /Restore this version/ })
|
|
111
|
+
expect(restore).toBeTruthy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('shows the diff for whichever version is selected, including one selected from outside', () => {
|
|
115
|
+
// The review bar and the canvas both move the selection without the panel
|
|
116
|
+
// being clicked. When disclosure was tracked separately from selection,
|
|
117
|
+
// the diff described one version while the canvas rendered another — and
|
|
118
|
+
// Restore sat on a row that was not open.
|
|
119
|
+
renderPanel({ selectedIndex: 1 })
|
|
120
|
+
expect(screen.getByText('First headline')).toBeTruthy()
|
|
121
|
+
expect(screen.getByText('Second headline')).toBeTruthy()
|
|
122
|
+
expect(screen.queryByText('Third headline')).toBeNull()
|
|
123
|
+
expect(screen.getByRole('button', { name: /Restore this version/ })).toBeTruthy()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('collapses the row it is already on rather than reselecting it', () => {
|
|
127
|
+
const { onSelect } = renderPanel({ selectedIndex: 0 })
|
|
128
|
+
expect(screen.getByText('Heading')).toBeTruthy()
|
|
129
|
+
|
|
130
|
+
fireEvent.click(screen.getByText(/Sarah Mills · 1 change/))
|
|
131
|
+
|
|
132
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
133
|
+
expect(screen.queryByText('Heading')).toBeNull()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('can restore the oldest version, which by definition has no diff to show', () => {
|
|
137
|
+
// Restore used to live inside the diff block, so a version with no
|
|
138
|
+
// computed changes — the oldest one held, or a metadata-only save — was
|
|
139
|
+
// reviewable and not restorable. That is the wrong way round: the oldest
|
|
140
|
+
// version is the one a rollback most often wants.
|
|
141
|
+
renderPanel({ selectedIndex: 2 })
|
|
142
|
+
|
|
143
|
+
expect(screen.getByText(/No section changes to show/)).toBeTruthy()
|
|
144
|
+
expect(screen.getByRole('button', { name: /Restore this version/ })).toBeTruthy()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('hides Restore from a user who cannot edit', () => {
|
|
148
|
+
renderPanel({ selectedIndex: 1, canRestore: false })
|
|
149
|
+
expect(screen.queryByRole('button', { name: /Restore this version/ })).toBeNull()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('restores the version whose row was clicked', () => {
|
|
153
|
+
const { onRestore } = renderPanel({ selectedIndex: 1 })
|
|
154
|
+
fireEvent.click(screen.getByRole('button', { name: /Restore this version/ }))
|
|
155
|
+
expect(onRestore).toHaveBeenCalledWith(expect.objectContaining({ id: 'v2' }))
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('renders two non-adjacent groups sharing a day label without colliding', () => {
|
|
159
|
+
// `groupVersionsByDay` walks CONSECUTIVE runs, so an out-of-order page
|
|
160
|
+
// legitimately yields ['Today', 'Yesterday', 'Today'] — its own test
|
|
161
|
+
// asserts that. Keyed on the label, the second "Today" collides with the
|
|
162
|
+
// first and React mis-associates the two sections' contents.
|
|
163
|
+
const outOfOrder: PageVersion[] = [
|
|
164
|
+
{ ...VERSIONS[0]!, id: 'a', createdAt: iso(1) },
|
|
165
|
+
{ ...VERSIONS[1]!, id: 'b', createdAt: iso(30) },
|
|
166
|
+
{ ...VERSIONS[2]!, id: 'c', createdAt: iso(2) },
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
170
|
+
renderPanel({ versions: outOfOrder })
|
|
171
|
+
|
|
172
|
+
expect(screen.getAllByRole('heading', { name: 'Today' })).toHaveLength(2)
|
|
173
|
+
expect(
|
|
174
|
+
spy.mock.calls.some((call) => String(call[0]).includes('same key')),
|
|
175
|
+
'React reported duplicate keys',
|
|
176
|
+
).toBe(false)
|
|
177
|
+
spy.mockRestore()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('reports an empty history rather than an empty box', () => {
|
|
181
|
+
renderPanel({ versions: [] })
|
|
182
|
+
expect(screen.getByText(/No versions yet/)).toBeTruthy()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('surfaces a load failure', () => {
|
|
186
|
+
renderPanel({ versions: [], error: 'Network unreachable' })
|
|
187
|
+
expect(screen.getByText('Network unreachable')).toBeTruthy()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('announces itself as a named region so it is reachable without sight of the overlay', () => {
|
|
191
|
+
renderPanel()
|
|
192
|
+
const region = screen.getByRole('region', { name: 'Version history' })
|
|
193
|
+
expect(within(region).getByRole('button', { name: 'Close version history' })).toBeTruthy()
|
|
194
|
+
})
|
|
195
|
+
})
|
|
@@ -96,7 +96,7 @@ export function EditorRail({ collapsedLabel, children }: EditorRailProps) {
|
|
|
96
96
|
if (!open) {
|
|
97
97
|
return (
|
|
98
98
|
<div
|
|
99
|
-
className="bg-card border-border flex shrink-0 flex-col items-center gap-3 border-r py-3"
|
|
99
|
+
className="prenta-editor-rail-collapsed bg-card border-border flex shrink-0 flex-col items-center gap-3 border-r py-3"
|
|
100
100
|
style={{ width: RAIL_COLLAPSED_WIDTH }}
|
|
101
101
|
>
|
|
102
102
|
<button
|
|
@@ -104,14 +104,14 @@ export function EditorRail({ collapsedLabel, children }: EditorRailProps) {
|
|
|
104
104
|
onClick={() => setOpen(true)}
|
|
105
105
|
aria-label={`Expand ${collapsedLabel} panel`}
|
|
106
106
|
title={`Expand ${collapsedLabel} panel`}
|
|
107
|
-
className="text-muted-foreground hover:bg-accent hover:text-accent-foreground flex h-6.5 w-6.5 items-center justify-center rounded-md transition-colors"
|
|
107
|
+
className="prenta-touch-target text-muted-foreground hover:bg-accent hover:text-accent-foreground flex h-6.5 w-6.5 items-center justify-center rounded-md transition-colors"
|
|
108
108
|
>
|
|
109
109
|
<PanelLeftOpen className="h-4 w-4" aria-hidden />
|
|
110
110
|
</button>
|
|
111
111
|
{/* Vertical label so the strip still says which panel is hidden. The
|
|
112
112
|
button's accessible name already carries it, hence aria-hidden. */}
|
|
113
113
|
<span
|
|
114
|
-
className="text-muted-foreground text-[10px] font-bold tracking-[0.8px] whitespace-nowrap uppercase [writing-mode:vertical-rl]"
|
|
114
|
+
className="prenta-rail-label text-muted-foreground text-[10px] font-bold tracking-[0.8px] whitespace-nowrap uppercase [writing-mode:vertical-rl]"
|
|
115
115
|
aria-hidden
|
|
116
116
|
>
|
|
117
117
|
{collapsedLabel}
|
|
@@ -123,7 +123,7 @@ export function EditorRail({ collapsedLabel, children }: EditorRailProps) {
|
|
|
123
123
|
return (
|
|
124
124
|
<div
|
|
125
125
|
ref={railRef}
|
|
126
|
-
className={`bg-card border-border relative flex shrink-0 flex-col border-r ${
|
|
126
|
+
className={`prenta-editor-rail bg-card border-border relative flex shrink-0 flex-col border-r ${
|
|
127
127
|
// A width transition during a drag fights the pointer: the panel
|
|
128
128
|
// chases the cursor a frame behind and the resize feels broken.
|
|
129
129
|
dragging ? '' : 'motion-safe:transition-[width] motion-safe:duration-(--mo-slow)'
|
|
@@ -209,7 +209,7 @@ export function RailHeader({
|
|
|
209
209
|
onClick={onCollapse}
|
|
210
210
|
aria-label={`Collapse ${title} panel`}
|
|
211
211
|
title={`Collapse ${title} panel`}
|
|
212
|
-
className="text-muted-foreground hover:bg-accent hover:text-accent-foreground flex h-6.5 w-6.5 items-center justify-center rounded-md transition-colors"
|
|
212
|
+
className="prenta-touch-target text-muted-foreground hover:bg-accent hover:text-accent-foreground flex h-6.5 w-6.5 items-center justify-center rounded-md transition-colors"
|
|
213
213
|
>
|
|
214
214
|
<PanelLeftClose className="h-4 w-4" aria-hidden />
|
|
215
215
|
</button>
|
|
@@ -74,7 +74,10 @@ export function DirectiveSwitch({
|
|
|
74
74
|
onCheckedChange={onChange}
|
|
75
75
|
disabled={disabled}
|
|
76
76
|
aria-label={`${label} — currently ${token}`}
|
|
77
|
-
|
|
77
|
+
// `prenta-touch-hit`, not `prenta-touch-target`: growing the track to
|
|
78
|
+
// 44px would strand the thumb, which is positioned against it. The
|
|
79
|
+
// hit area grows; the switch keeps its measured 34×19.
|
|
80
|
+
className={`prenta-touch-hit focus-visible:ring-ring relative h-[19px] w-[34px] shrink-0 rounded-full transition-colors duration-(--mo-base) focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50 ${
|
|
78
81
|
checked ? 'bg-success' : 'bg-destructive/55'
|
|
79
82
|
}`}
|
|
80
83
|
>
|
|
@@ -71,6 +71,16 @@ export function AdminShell({
|
|
|
71
71
|
: { display: 'block', position: 'relative' }
|
|
72
72
|
}
|
|
73
73
|
>
|
|
74
|
+
{/*
|
|
75
|
+
* First focusable thing in the admin, so a keyboard user's very first
|
|
76
|
+
* Tab offers a jump past the sidebar's ~20 nav items. Off-screen until
|
|
77
|
+
* focused; styling lives in `responsive.css` because it has to survive
|
|
78
|
+
* a consumer whose Tailwind build does not scan this file.
|
|
79
|
+
*/}
|
|
80
|
+
<a className="prenta-skip-link" href="#prenta-main">
|
|
81
|
+
Skip to content
|
|
82
|
+
</a>
|
|
83
|
+
|
|
74
84
|
{!isDesktop && mobileSidebarOpen && (
|
|
75
85
|
<div
|
|
76
86
|
aria-hidden
|
|
@@ -111,7 +121,9 @@ export function AdminShell({
|
|
|
111
121
|
>
|
|
112
122
|
{header}
|
|
113
123
|
{breadcrumbs}
|
|
114
|
-
<main className="min-h-0 flex-1 overflow-y-auto">
|
|
124
|
+
<main id="prenta-main" className="min-h-0 flex-1 overflow-y-auto">
|
|
125
|
+
{children}
|
|
126
|
+
</main>
|
|
115
127
|
</div>
|
|
116
128
|
</div>
|
|
117
129
|
)
|