@prenta/admin 0.97.5 → 0.98.1

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 (53) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/__tests__/components/seo-score-bands.test.js +0 -7
  3. package/dist/__tests__/components/seo-score-bands.test.js.map +1 -1
  4. package/dist/__tests__/lib/page-audit-input.test.js +77 -1
  5. package/dist/__tests__/lib/page-audit-input.test.js.map +1 -1
  6. package/dist/__tests__/lib/seo-service-errors.test.js +34 -3
  7. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
  8. package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts +2 -0
  9. package/dist/__tests__/views/page-builder-seo-panel.render.test.d.ts.map +1 -0
  10. package/dist/__tests__/views/page-builder-seo-panel.render.test.js +65 -0
  11. package/dist/__tests__/views/page-builder-seo-panel.render.test.js.map +1 -0
  12. package/dist/__tests__/views/seo-audit-tab.render.test.d.ts +2 -0
  13. package/dist/__tests__/views/seo-audit-tab.render.test.d.ts.map +1 -0
  14. package/dist/__tests__/views/seo-audit-tab.render.test.js +100 -0
  15. package/dist/__tests__/views/seo-audit-tab.render.test.js.map +1 -0
  16. package/dist/__tests__/views/seo-technical-tab.render.test.d.ts +2 -0
  17. package/dist/__tests__/views/seo-technical-tab.render.test.d.ts.map +1 -0
  18. package/dist/__tests__/views/seo-technical-tab.render.test.js +121 -0
  19. package/dist/__tests__/views/seo-technical-tab.render.test.js.map +1 -0
  20. package/dist/lib/page-audit-input.d.ts +14 -1
  21. package/dist/lib/page-audit-input.d.ts.map +1 -1
  22. package/dist/lib/page-audit-input.js +19 -0
  23. package/dist/lib/page-audit-input.js.map +1 -1
  24. package/dist/lib/seo-service.d.ts +30 -1
  25. package/dist/lib/seo-service.d.ts.map +1 -1
  26. package/dist/lib/seo-service.js.map +1 -1
  27. package/dist/prenta-admin.css +1 -1
  28. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
  29. package/dist/views/page-builder/ContextPanel.js +1 -1
  30. package/dist/views/page-builder/ContextPanel.js.map +1 -1
  31. package/dist/views/page-builder/SEOPanel.d.ts +1 -2
  32. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
  33. package/dist/views/page-builder/SEOPanel.js +10 -75
  34. package/dist/views/page-builder/SEOPanel.js.map +1 -1
  35. package/dist/views/seo/AuditTab.d.ts.map +1 -1
  36. package/dist/views/seo/AuditTab.js +3 -2
  37. package/dist/views/seo/AuditTab.js.map +1 -1
  38. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  39. package/dist/views/seo/TechnicalTab.js +5 -0
  40. package/dist/views/seo/TechnicalTab.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/__tests__/components/seo-score-bands.test.ts +0 -7
  43. package/src/__tests__/lib/page-audit-input.test.ts +91 -1
  44. package/src/__tests__/lib/seo-service-errors.test.ts +55 -1
  45. package/src/__tests__/views/page-builder-seo-panel.render.test.tsx +77 -0
  46. package/src/__tests__/views/seo-audit-tab.render.test.tsx +113 -0
  47. package/src/__tests__/views/seo-technical-tab.render.test.tsx +156 -0
  48. package/src/lib/page-audit-input.ts +36 -1
  49. package/src/lib/seo-service.ts +30 -2
  50. package/src/views/page-builder/ContextPanel.tsx +0 -1
  51. package/src/views/page-builder/SEOPanel.tsx +40 -282
  52. package/src/views/seo/AuditTab.tsx +60 -27
  53. package/src/views/seo/TechnicalTab.tsx +5 -0
@@ -1,127 +1,17 @@
1
1
  'use client'
2
2
 
3
- import { useState, useMemo } from 'react'
4
- import { analyzeSEO } from '@prenta/core/page-builder'
5
- import type {
6
- PageNode,
7
- BuilderSEOAnalysis,
8
- BuilderSEOCheck,
9
- BuilderReadabilityResult,
10
- } from '@prenta/core'
3
+ import { useMemo, useState } from 'react'
4
+ import type { PageNode } from '@prenta/core'
5
+ import { auditPage } from '@prenta/core/seo/page-audit'
6
+ import { ChevronDown, ChevronUp, Eye, FileText, Globe, Search } from 'lucide-react'
7
+ import { PageAuditPanel } from '../../components/seo/PageAuditPanel.js'
8
+ import { buildPageAuditInputFromBuilder } from '../../lib/page-audit-input.js'
11
9
  import type { PageSettings } from '../../hooks/useBuilderState.js'
12
- import {
13
- CheckCircle2,
14
- AlertCircle,
15
- XCircle,
16
- ChevronDown,
17
- ChevronUp,
18
- Search,
19
- Globe,
20
- Type,
21
- FileText,
22
- Clock,
23
- BarChart3,
24
- Target,
25
- Eye,
26
- } from 'lucide-react'
27
10
 
28
11
  export interface BuilderSEOPanelProps {
29
12
  tree: PageNode
30
13
  pageSettings: PageSettings
31
14
  onPageSettingsChange: (settings: Partial<PageSettings>) => void
32
- selectedNodeId: string | null
33
- }
34
-
35
- function getScoreColor(score: number): string {
36
- if (score >= 70) return 'text-success'
37
- if (score >= 40) return 'text-warning'
38
- return 'text-destructive'
39
- }
40
-
41
- function getRingStrokeColor(score: number): string {
42
- if (score >= 70) return 'rgb(34, 197, 94)'
43
- if (score >= 40) return 'rgb(245, 158, 11)'
44
- return 'rgb(239, 68, 68)'
45
- }
46
-
47
- function getScoreLabel(score: number): string {
48
- if (score >= 80) return 'Excellent'
49
- if (score >= 70) return 'Good'
50
- if (score >= 50) return 'Needs Work'
51
- return 'Poor'
52
- }
53
-
54
- function StatusIcon({ status }: { status: BuilderSEOCheck['status'] }) {
55
- switch (status) {
56
- case 'good':
57
- return <CheckCircle2 size={14} className="text-success shrink-0" />
58
- case 'warning':
59
- return <AlertCircle size={14} className="text-warning shrink-0" />
60
- case 'error':
61
- return <XCircle size={14} className="text-destructive shrink-0" />
62
- }
63
- }
64
-
65
- function ScoreRing({ score }: { score: number }) {
66
- const radius = 32
67
- const strokeWidth = 6
68
- const circumference = 2 * Math.PI * radius
69
- const progress = Math.min(score, 100) / 100
70
- const dashOffset = circumference * (1 - progress)
71
- const size = (radius + strokeWidth) * 2
72
-
73
- return (
74
- <div className="relative">
75
- <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90">
76
- <circle
77
- cx={size / 2}
78
- cy={size / 2}
79
- r={radius}
80
- fill="none"
81
- stroke="var(--border)"
82
- strokeWidth={strokeWidth}
83
- />
84
- <circle
85
- cx={size / 2}
86
- cy={size / 2}
87
- r={radius}
88
- fill="none"
89
- stroke={getRingStrokeColor(score)}
90
- strokeWidth={strokeWidth}
91
- strokeDasharray={circumference}
92
- strokeDashoffset={dashOffset}
93
- strokeLinecap="round"
94
- className="transition-all duration-700 ease-out"
95
- />
96
- </svg>
97
- <div className="absolute inset-0 flex items-center justify-center">
98
- <span className={`text-lg font-medium ${getScoreColor(score)}`}>{score}</span>
99
- </div>
100
- </div>
101
- )
102
- }
103
-
104
- function ScoreSummary({ checks }: { checks: BuilderSEOCheck[] }) {
105
- const passed = checks.filter((c) => c.status === 'good').length
106
- const warnings = checks.filter((c) => c.status === 'warning').length
107
- const errors = checks.filter((c) => c.status === 'error').length
108
-
109
- return (
110
- <div className="flex flex-col gap-1">
111
- <div className="flex items-center gap-1.5">
112
- <CheckCircle2 size={12} className="text-success" />
113
- <span className="text-muted-foreground text-xs">{passed} passed</span>
114
- </div>
115
- <div className="flex items-center gap-1.5">
116
- <AlertCircle size={12} className="text-warning" />
117
- <span className="text-muted-foreground text-xs">{warnings} warnings</span>
118
- </div>
119
- <div className="flex items-center gap-1.5">
120
- <XCircle size={12} className="text-destructive" />
121
- <span className="text-muted-foreground text-xs">{errors} issues</span>
122
- </div>
123
- </div>
124
- )
125
15
  }
126
16
 
127
17
  function CollapsibleSection({
@@ -160,95 +50,6 @@ function CollapsibleSection({
160
50
  )
161
51
  }
162
52
 
163
- function SEOChecksList({ checks }: { checks: BuilderSEOCheck[] }) {
164
- return (
165
- <div className="flex flex-col gap-2">
166
- {checks.map((check) => (
167
- <div key={check.id} className="flex items-start gap-2">
168
- <StatusIcon status={check.status} />
169
- <div className="flex flex-col">
170
- <span className="text-foreground text-xs font-medium">{check.label}</span>
171
- <span className="text-muted-foreground text-xs">{check.detail}</span>
172
- </div>
173
- </div>
174
- ))}
175
- </div>
176
- )
177
- }
178
-
179
- function ReadabilityGrid({ readability }: { readability: BuilderReadabilityResult }) {
180
- const fleschLabel =
181
- readability.fleschScore >= 60
182
- ? 'Easy'
183
- : readability.fleschScore >= 30
184
- ? 'Moderate'
185
- : 'Difficult'
186
-
187
- return (
188
- <div className="grid grid-cols-2 gap-3">
189
- <div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
190
- <div className="flex items-center gap-1.5">
191
- <BarChart3 size={12} className="text-muted-foreground" />
192
- <span className="text-muted-foreground text-xs">Flesch Score</span>
193
- </div>
194
- <span className={`text-sm font-medium ${getScoreColor(readability.fleschScore)}`}>
195
- {readability.fleschScore}
196
- </span>
197
- <span className="text-muted-foreground text-xs">{fleschLabel}</span>
198
- </div>
199
-
200
- <div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
201
- <div className="flex items-center gap-1.5">
202
- <Type size={12} className="text-muted-foreground" />
203
- <span className="text-muted-foreground text-xs">Word Count</span>
204
- </div>
205
- <span className="text-foreground text-sm font-medium">{readability.wordCount}</span>
206
- <span className="text-muted-foreground text-xs">
207
- {readability.wordCount >= 300 ? 'Good length' : 'Short'}
208
- </span>
209
- </div>
210
-
211
- <div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
212
- <div className="flex items-center gap-1.5">
213
- <FileText size={12} className="text-muted-foreground" />
214
- <span className="text-muted-foreground text-xs">Avg Sentence</span>
215
- </div>
216
- <span className="text-foreground text-sm font-medium">
217
- {readability.avgSentenceLength} words
218
- </span>
219
- <span className="text-muted-foreground text-xs">
220
- {readability.avgSentenceLength <= 20 ? 'Good' : 'Long'}
221
- </span>
222
- </div>
223
-
224
- <div className="bg-muted/50 flex flex-col gap-1 rounded-md p-3">
225
- <div className="flex items-center gap-1.5">
226
- <Clock size={12} className="text-muted-foreground" />
227
- <span className="text-muted-foreground text-xs">Reading Time</span>
228
- </div>
229
- <span className="text-foreground text-sm font-medium">{readability.readingTime} min</span>
230
- <span className="text-muted-foreground text-xs">~{readability.wordCount} words</span>
231
- </div>
232
- </div>
233
- )
234
- }
235
-
236
- function PerBlockHints({ hints }: { hints: BuilderSEOCheck[] }) {
237
- return (
238
- <div className="flex flex-col gap-2">
239
- {hints.map((hint, index) => (
240
- <div key={`${hint.id}-${index}`} className="flex items-start gap-2">
241
- <StatusIcon status={hint.status} />
242
- <div className="flex flex-col">
243
- <span className="text-foreground text-xs font-medium">{hint.label}</span>
244
- <span className="text-muted-foreground text-xs">{hint.detail}</span>
245
- </div>
246
- </div>
247
- ))}
248
- </div>
249
- )
250
- }
251
-
252
53
  function BasicSEOFields({
253
54
  pageSettings,
254
55
  onPageSettingsChange,
@@ -263,7 +64,9 @@ function BasicSEOFields({
263
64
  <div className="flex flex-col gap-4">
264
65
  <div className="flex flex-col gap-1.5">
265
66
  <div className="flex items-center justify-between">
266
- <label className="text-foreground text-xs font-medium">Meta Title</label>
67
+ <label htmlFor="builder-meta-title" className="text-foreground text-xs font-medium">
68
+ Meta Title
69
+ </label>
267
70
  <span
268
71
  className={`text-xs ${metaTitleLength > 60 ? 'text-destructive' : metaTitleLength > 0 ? 'text-muted-foreground' : 'text-muted-foreground'}`}
269
72
  >
@@ -271,6 +74,7 @@ function BasicSEOFields({
271
74
  </span>
272
75
  </div>
273
76
  <input
77
+ id="builder-meta-title"
274
78
  type="text"
275
79
  value={pageSettings.metaTitle || ''}
276
80
  onChange={(e) => onPageSettingsChange({ metaTitle: e.target.value })}
@@ -358,14 +162,12 @@ export function BuilderSEOPanel({
358
162
  tree,
359
163
  pageSettings,
360
164
  onPageSettingsChange,
361
- selectedNodeId,
362
165
  }: BuilderSEOPanelProps) {
363
- const analysis = useMemo<BuilderSEOAnalysis>(
364
- () => analyzeSEO(tree, pageSettings as any),
166
+ const audit = useMemo(
167
+ () => auditPage(buildPageAuditInputFromBuilder({ tree, pageSettings })),
365
168
  [tree, pageSettings],
366
169
  )
367
-
368
- const [expandedSections, setExpandedSections] = useState<string[]>(['checks'])
170
+ const [expandedSections, setExpandedSections] = useState<string[]>(['basicSeo'])
369
171
 
370
172
  const toggleSection = (section: string) => {
371
173
  setExpandedSections((prev) =>
@@ -373,11 +175,6 @@ export function BuilderSEOPanel({
373
175
  )
374
176
  }
375
177
 
376
- const blockHints = useMemo(() => {
377
- if (!selectedNodeId) return []
378
- return analysis.perBlockHints.get(selectedNodeId) || []
379
- }, [analysis.perBlockHints, selectedNodeId])
380
-
381
178
  if (!tree.children || tree.children.length === 0) {
382
179
  return (
383
180
  <div className="flex min-h-[200px] flex-col items-center justify-center p-6 text-center">
@@ -392,73 +189,34 @@ export function BuilderSEOPanel({
392
189
 
393
190
  return (
394
191
  <div className="flex flex-col">
395
- <div className="border-border flex items-center gap-4 border-b px-4 py-5">
396
- <ScoreRing score={analysis.score} />
397
- <div className="flex flex-col gap-1">
398
- <span className="text-foreground text-xs font-medium">
399
- {getScoreLabel(analysis.score)}
400
- </span>
401
- <ScoreSummary checks={analysis.checks} />
402
- </div>
403
- </div>
404
-
405
- <div className="flex flex-col">
406
- <CollapsibleSection
407
- title="SEO Checks"
408
- icon={Search}
409
- expanded={expandedSections.includes('checks')}
410
- onToggle={() => toggleSection('checks')}
411
- >
412
- <SEOChecksList checks={analysis.checks} />
413
- </CollapsibleSection>
414
-
415
- <CollapsibleSection
416
- title="Readability"
417
- icon={BarChart3}
418
- expanded={expandedSections.includes('readability')}
419
- onToggle={() => toggleSection('readability')}
420
- >
421
- <ReadabilityGrid readability={analysis.readability} />
422
- </CollapsibleSection>
192
+ <PageAuditPanel audit={audit} />
423
193
 
424
- {blockHints.length > 0 && (
425
- <CollapsibleSection
426
- title="Block Hints"
427
- icon={Target}
428
- expanded={expandedSections.includes('blockHints')}
429
- onToggle={() => toggleSection('blockHints')}
430
- >
431
- <PerBlockHints hints={blockHints} />
432
- </CollapsibleSection>
433
- )}
434
-
435
- <CollapsibleSection
436
- title="Basic SEO"
437
- icon={FileText}
438
- expanded={expandedSections.includes('basicSeo')}
439
- onToggle={() => toggleSection('basicSeo')}
440
- >
441
- <BasicSEOFields pageSettings={pageSettings} onPageSettingsChange={onPageSettingsChange} />
442
- </CollapsibleSection>
443
-
444
- <CollapsibleSection
445
- title="Search Preview"
446
- icon={Eye}
447
- expanded={expandedSections.includes('searchPreview')}
448
- onToggle={() => toggleSection('searchPreview')}
449
- >
450
- <SearchPreview pageSettings={pageSettings} />
451
- </CollapsibleSection>
452
-
453
- <CollapsibleSection
454
- title="Social Preview"
455
- icon={Globe}
456
- expanded={expandedSections.includes('socialPreview')}
457
- onToggle={() => toggleSection('socialPreview')}
458
- >
459
- <SocialPreview pageSettings={pageSettings} />
460
- </CollapsibleSection>
461
- </div>
194
+ <CollapsibleSection
195
+ title="Basic SEO"
196
+ icon={FileText}
197
+ expanded={expandedSections.includes('basicSeo')}
198
+ onToggle={() => toggleSection('basicSeo')}
199
+ >
200
+ <BasicSEOFields pageSettings={pageSettings} onPageSettingsChange={onPageSettingsChange} />
201
+ </CollapsibleSection>
202
+
203
+ <CollapsibleSection
204
+ title="Search Preview"
205
+ icon={Eye}
206
+ expanded={expandedSections.includes('searchPreview')}
207
+ onToggle={() => toggleSection('searchPreview')}
208
+ >
209
+ <SearchPreview pageSettings={pageSettings} />
210
+ </CollapsibleSection>
211
+
212
+ <CollapsibleSection
213
+ title="Social Preview"
214
+ icon={Globe}
215
+ expanded={expandedSections.includes('socialPreview')}
216
+ onToggle={() => toggleSection('socialPreview')}
217
+ >
218
+ <SocialPreview pageSettings={pageSettings} />
219
+ </CollapsibleSection>
462
220
  </div>
463
221
  )
464
222
  }
@@ -1,7 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import { Fragment, useMemo, useState } from 'react'
4
- import { ClipboardCheck, Clock, CheckCircle2, FileDown, Sparkles } from 'lucide-react'
4
+ import {
5
+ AlertTriangle,
6
+ ClipboardCheck,
7
+ Clock,
8
+ CheckCircle2,
9
+ FileDown,
10
+ Sparkles,
11
+ } from 'lucide-react'
5
12
  import { toast } from 'sonner'
6
13
  import {
7
14
  fetchSeoIssues,
@@ -61,34 +68,60 @@ function issueStatusTone(status: SeoIssueStatus) {
61
68
  function AuditSummaryCards({ refetchKey }: { refetchKey: number }) {
62
69
  const { data, loading } = useSeoResource(fetchSeoAuditRuns, [refetchKey])
63
70
  const last = (data ?? [])[0]
71
+ const coverage = last?.metadata?.renderedAudit
64
72
  return (
65
- <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
66
- <div className="border-border bg-card rounded-lg border p-4">
67
- <p className="text-muted-foreground flex items-center gap-1.5 text-sm">
68
- <Clock className="h-3.5 w-3.5" aria-hidden /> Last audit
69
- </p>
70
- <p className="text-foreground mt-1 text-sm font-medium">
71
- {loading ? '…' : last ? new Date(last.startedAt).toLocaleString() : 'Never'}
72
- </p>
73
- </div>
74
- <div className="border-border bg-card rounded-lg border p-4">
75
- <p className="text-muted-foreground text-sm">Pages scanned</p>
76
- <p className="text-foreground mt-1 text-2xl font-medium tabular-nums">
77
- {last?.pagesScanned ?? 0}
78
- </p>
79
- </div>
80
- <div className="border-border bg-card rounded-lg border p-4">
81
- <p className="text-muted-foreground text-sm">Issues found</p>
82
- <p className="text-foreground mt-1 text-2xl font-medium tabular-nums">
83
- {last?.issuesFound ?? 0}
84
- </p>
85
- </div>
86
- <div className="border-border bg-card rounded-lg border p-4">
87
- <p className="text-muted-foreground text-sm">Site score</p>
88
- <p className="text-foreground mt-1 text-2xl font-medium tabular-nums">
89
- {last?.siteScoreAfter ?? '—'}
90
- </p>
73
+ <div className="space-y-4">
74
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
75
+ <div className="border-border bg-card rounded-lg border p-4">
76
+ <p className="text-muted-foreground flex items-center gap-1.5 text-sm">
77
+ <Clock className="h-3.5 w-3.5" aria-hidden /> Last audit
78
+ </p>
79
+ <p className="text-foreground mt-1 text-sm font-medium">
80
+ {loading ? '…' : last ? new Date(last.startedAt).toLocaleString() : 'Never'}
81
+ </p>
82
+ </div>
83
+ <div className="border-border bg-card rounded-lg border p-4">
84
+ <p className="text-muted-foreground text-sm">Pages scanned</p>
85
+ <p className="text-foreground mt-1 text-2xl font-medium tabular-nums">
86
+ {last?.pagesScanned ?? 0}
87
+ </p>
88
+ {coverage ? (
89
+ <p className="text-muted-foreground mt-1 flex flex-wrap gap-x-3 text-sm">
90
+ <span>{coverage.verified} verified</span>
91
+ <span>{coverage.unverified} unverified</span>
92
+ </p>
93
+ ) : null}
94
+ </div>
95
+ <div className="border-border bg-card rounded-lg border p-4">
96
+ <p className="text-muted-foreground text-sm">Issues found</p>
97
+ <p className="text-foreground mt-1 text-2xl font-medium tabular-nums">
98
+ {last?.issuesFound ?? 0}
99
+ </p>
100
+ </div>
101
+ <div className="border-border bg-card rounded-lg border p-4">
102
+ <p className="text-muted-foreground text-sm">Site score</p>
103
+ <p className="text-foreground mt-1 text-2xl font-medium tabular-nums">
104
+ {last?.siteScoreAfter ?? '—'}
105
+ </p>
106
+ </div>
91
107
  </div>
108
+ {coverage?.partial ? (
109
+ <div
110
+ role="status"
111
+ className="border-warning/30 bg-warning/10 text-foreground rounded-lg border p-4"
112
+ >
113
+ <div className="flex items-start gap-3">
114
+ <AlertTriangle size={20} className="text-warning shrink-0" aria-hidden="true" />
115
+ <div>
116
+ <p className="font-medium">Partial rendered audit</p>
117
+ <p className="text-muted-foreground mt-1 text-sm">
118
+ The score covers {coverage.verified} verified pages. Evidence was unavailable for{' '}
119
+ {coverage.unverified}; their prior rendered-dependent issues were preserved.
120
+ </p>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ ) : null}
92
125
  </div>
93
126
  )
94
127
  }
@@ -55,6 +55,11 @@ const CRAWL_TOGGLES: { key: keyof CrawlSettings; label: string; help: string }[]
55
55
  label: 'Ping search engines on publish',
56
56
  help: 'Notify Google and Bing when the sitemap changes (production only).',
57
57
  },
58
+ {
59
+ key: 'auditRenderedHtml',
60
+ label: 'Audit rendered public HTML',
61
+ help: 'Fetch public page output so audits grade crawler-visible headings and content.',
62
+ },
58
63
  ]
59
64
 
60
65
  function Toggle({