@henryavila/mdprobe 0.1.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.
@@ -0,0 +1,99 @@
1
+ import { leftPanelOpen, files, currentFile, currentToc, openAnnotations, sections } from '../state/store.js'
2
+
3
+ export function LeftPanel({ onFileSelect }) {
4
+ const isCollapsed = !leftPanelOpen.value
5
+
6
+ // Count open annotations whose startLine falls within this section's range
7
+ function annotationsInSection(sectionIndex) {
8
+ const toc = currentToc.value
9
+ const section = toc[sectionIndex]
10
+ if (!section) return 0
11
+ const startLine = section.line
12
+ // End line is the start of the next same-or-higher level section, or Infinity
13
+ let endLine = Infinity
14
+ for (let i = sectionIndex + 1; i < toc.length; i++) {
15
+ if (toc[i].level <= section.level) {
16
+ endLine = toc[i].line
17
+ break
18
+ }
19
+ }
20
+ return openAnnotations.value.filter(a => {
21
+ const line = a.selectors?.position?.startLine
22
+ return line != null && line >= startLine && line < endLine
23
+ }).length
24
+ }
25
+
26
+ return (
27
+ <aside class={`left-panel ${isCollapsed ? 'collapsed' : ''}`}>
28
+ {isCollapsed ? (
29
+ <div class="panel-collapsed-indicator" onClick={() => leftPanelOpen.value = true}>
30
+ <span>&#9776;</span>
31
+ <span class="shortcut-key">[</span>
32
+ {openAnnotations.value.length > 0 && (
33
+ <span class="badge">{openAnnotations.value.length}</span>
34
+ )}
35
+ </div>
36
+ ) : (
37
+ <div class="panel-content">
38
+ <div class="panel-header" style="padding: 12px; display: flex; justify-content: space-between; align-items: center">
39
+ <span style="font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted)">
40
+ Files & TOC
41
+ </span>
42
+ <button class="btn btn-sm btn-ghost" onClick={() => leftPanelOpen.value = false}>&times;</button>
43
+ </div>
44
+
45
+ {/* Files section */}
46
+ {files.value.length > 1 && (
47
+ <div style="padding: 0 8px 8px">
48
+ <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); padding: 4px 4px 6px; font-weight: 600">Files</div>
49
+ {files.value.map(f => {
50
+ const path = f.path || f
51
+ const label = f.label || path.replace('.md', '')
52
+ const isActive = path === currentFile.value
53
+ return (
54
+ <div
55
+ key={path}
56
+ class={`file-item ${isActive ? 'active' : ''}`}
57
+ onClick={() => onFileSelect(path)}
58
+ >
59
+ <span class="icon">{'\uD83D\uDCC4'}</span>
60
+ <span>{label}</span>
61
+ </div>
62
+ )
63
+ })}
64
+ </div>
65
+ )}
66
+
67
+ {/* TOC section */}
68
+ <div style="padding: 0 8px">
69
+ <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); padding: 4px 4px 6px; font-weight: 600">Sections</div>
70
+ {currentToc.value.length === 0 ? (
71
+ <div style="padding: 8px; color: var(--text-muted); font-size: 13px">No sections</div>
72
+ ) : (
73
+ currentToc.value.map((entry, i) => {
74
+ const count = annotationsInSection(i)
75
+ const sec = sections.value.find(s => s.heading === entry.heading && s.level === entry.level)
76
+ const statusDot = sec?.status === 'approved' ? 'dot-approved'
77
+ : sec?.status === 'rejected' ? 'dot-rejected' : ''
78
+ return (
79
+ <div
80
+ key={i}
81
+ class={`toc-item level-${entry.level}`}
82
+ onClick={() => {
83
+ const el = document.querySelector(`[data-source-line="${entry.line}"]`)
84
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' })
85
+ }}
86
+ >
87
+ {statusDot && <span class={`toc-dot ${statusDot}`} />}
88
+ {entry.heading}
89
+ {count > 0 && <span class="badge" style="margin-left: 6px">{count}</span>}
90
+ </div>
91
+ )
92
+ })
93
+ )}
94
+ </div>
95
+ </div>
96
+ )}
97
+ </aside>
98
+ )
99
+ }
@@ -0,0 +1,94 @@
1
+ import { useState, useEffect, useRef } from 'preact/hooks'
2
+ import { AnnotationForm } from './AnnotationForm.jsx'
3
+
4
+ /**
5
+ * Draggable annotation popover — appears at the text selection position.
6
+ * No backdrop: uses strong shadow for visual separation.
7
+ * Drag-handle on the header lets users reposition to read content underneath.
8
+ */
9
+ export function Popover({ x, y, exact, selectors, onSave, onCancel }) {
10
+ const popoverRef = useRef(null)
11
+ const dragRef = useRef({ active: false, startX: 0, startY: 0 })
12
+
13
+ // Compute initial viewport-aware position
14
+ const popoverWidth = 520
15
+ const popoverHeight = 440
16
+ const viewportW = typeof window !== 'undefined' ? window.innerWidth : 800
17
+ const viewportH = typeof window !== 'undefined' ? window.innerHeight : 800
18
+ const spaceBelow = viewportH - y
19
+ const flipAbove = spaceBelow < popoverHeight
20
+
21
+ const initialLeft = Math.max(16, Math.min(x - popoverWidth / 2, viewportW - popoverWidth - 16))
22
+ const initialTop = flipAbove ? Math.max(8, y - popoverHeight - 8) : y
23
+
24
+ const [pos, setPos] = useState({ left: initialLeft, top: initialTop })
25
+
26
+ // Close on Escape
27
+ useEffect(() => {
28
+ function handleKeyDown(e) {
29
+ if (e.key === 'Escape') onCancel()
30
+ }
31
+ document.addEventListener('keydown', handleKeyDown)
32
+ return () => document.removeEventListener('keydown', handleKeyDown)
33
+ }, [onCancel])
34
+
35
+ // Drag: mousemove + mouseup on document for smooth tracking
36
+ useEffect(() => {
37
+ function onMouseMove(e) {
38
+ if (!dragRef.current.active) return
39
+ setPos({
40
+ left: e.clientX - dragRef.current.startX,
41
+ top: e.clientY - dragRef.current.startY,
42
+ })
43
+ }
44
+ function onMouseUp() {
45
+ dragRef.current.active = false
46
+ }
47
+ document.addEventListener('mousemove', onMouseMove)
48
+ document.addEventListener('mouseup', onMouseUp)
49
+ return () => {
50
+ document.removeEventListener('mousemove', onMouseMove)
51
+ document.removeEventListener('mouseup', onMouseUp)
52
+ }
53
+ }, [])
54
+
55
+ function handleDragStart(e) {
56
+ if (e.button !== 0) return
57
+ dragRef.current = {
58
+ active: true,
59
+ startX: e.clientX - pos.left,
60
+ startY: e.clientY - pos.top,
61
+ }
62
+ e.preventDefault()
63
+ }
64
+
65
+ const style = {
66
+ position: 'fixed',
67
+ left: `${pos.left}px`,
68
+ top: `${pos.top}px`,
69
+ width: `${popoverWidth}px`,
70
+ zIndex: 101,
71
+ }
72
+
73
+ return (
74
+ <div class="popover popover--enter" ref={popoverRef} style={style}>
75
+ <div class="popover__header" onMouseDown={handleDragStart}>
76
+ <span class="popover__title">New Annotation</span>
77
+ <button
78
+ type="button"
79
+ class="popover__close"
80
+ onClick={onCancel}
81
+ aria-label="Close"
82
+ >
83
+ ×
84
+ </button>
85
+ </div>
86
+ <AnnotationForm
87
+ exact={exact}
88
+ selectors={selectors}
89
+ onSave={onSave}
90
+ onCancel={onCancel}
91
+ />
92
+ </div>
93
+ )
94
+ }
@@ -0,0 +1,28 @@
1
+ export function ReplyThread({ replies }) {
2
+ if (!replies || replies.length === 0) return null
3
+
4
+ return (
5
+ <div style="margin-top: 8px">
6
+ {replies.map((reply, i) => (
7
+ <div key={i} class="reply">
8
+ <div class="author">{reply.author}</div>
9
+ <div class="text">{reply.comment}</div>
10
+ <div style="font-size: 10px; color: var(--text-muted); margin-top: 2px">
11
+ {formatTime(reply.created_at)}
12
+ </div>
13
+ </div>
14
+ ))}
15
+ </div>
16
+ )
17
+ }
18
+
19
+ function formatTime(isoString) {
20
+ if (!isoString) return ''
21
+ try {
22
+ const date = new Date(isoString)
23
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) +
24
+ ' ' + date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
25
+ } catch {
26
+ return isoString
27
+ }
28
+ }
@@ -0,0 +1,171 @@
1
+ import { useState } from 'preact/hooks'
2
+ import { rightPanelOpen, filteredAnnotations, selectedAnnotationId, showResolved,
3
+ filterTag, filterAuthor, uniqueTags, uniqueAuthors, openAnnotations } from '../state/store.js'
4
+ import { AnnotationForm } from './AnnotationForm.jsx'
5
+ import { ReplyThread } from './ReplyThread.jsx'
6
+
7
+ export function RightPanel({ annotationOps }) {
8
+ const isCollapsed = !rightPanelOpen.value
9
+ const [editingId, setEditingId] = useState(null)
10
+
11
+ function handleAnnotationClick(ann) {
12
+ selectedAnnotationId.value = ann.id
13
+ // Scroll to highlight in content
14
+ const highlight = document.querySelector(`[data-highlight-id="${ann.id}"]`)
15
+ highlight?.scrollIntoView({ behavior: 'smooth', block: 'center' })
16
+ }
17
+
18
+ return (
19
+ <aside class={`right-panel ${isCollapsed ? 'collapsed' : ''}`}>
20
+ {isCollapsed ? (
21
+ <div class="panel-collapsed-indicator" onClick={() => rightPanelOpen.value = true}>
22
+ <span>💬</span>
23
+ <span class="shortcut-key">]</span>
24
+ {openAnnotations.value.length > 0 && (
25
+ <span class="badge">{openAnnotations.value.length}</span>
26
+ )}
27
+ </div>
28
+ ) : (
29
+ <div class="panel-content">
30
+ {/* Header */}
31
+ <div class="panel-header" style="padding: 12px; display: flex; justify-content: space-between; align-items: center">
32
+ <span style="font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted)">
33
+ Annotations ({openAnnotations.value.length} open)
34
+ </span>
35
+ <button class="btn btn-sm btn-ghost" onClick={() => rightPanelOpen.value = false}>×</button>
36
+ </div>
37
+
38
+ {/* Filters */}
39
+ <div style="padding: 0 12px 8px; display: flex; gap: 6px; flex-wrap: wrap">
40
+ <select
41
+ class="filter-select"
42
+ value={filterTag.value || ''}
43
+ onChange={e => filterTag.value = e.target.value || null}
44
+ style="padding: 3px 6px; font-size: 11px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary)"
45
+ >
46
+ <option value="">All tags</option>
47
+ {uniqueTags.value.map(t => <option key={t} value={t}>{t}</option>)}
48
+ </select>
49
+
50
+ <select
51
+ value={filterAuthor.value || ''}
52
+ onChange={e => filterAuthor.value = e.target.value || null}
53
+ style="padding: 3px 6px; font-size: 11px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary)"
54
+ >
55
+ <option value="">All authors</option>
56
+ {uniqueAuthors.value.map(a => <option key={a} value={a}>{a}</option>)}
57
+ </select>
58
+
59
+ <label style="display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-muted); cursor: pointer">
60
+ <input type="checkbox" checked={showResolved.value} onChange={e => showResolved.value = e.target.checked} />
61
+ Show resolved
62
+ </label>
63
+ </div>
64
+
65
+ {/* Annotation list */}
66
+ <div style="overflow-y: auto; padding: 0 8px; flex: 1">
67
+ {filteredAnnotations.value.length === 0 ? (
68
+ <div style="padding: 16px; text-align: center; color: var(--text-muted); font-size: 13px">
69
+ No annotations
70
+ </div>
71
+ ) : (
72
+ filteredAnnotations.value.map(ann => (
73
+ <div
74
+ key={ann.id}
75
+ data-annotation-id={ann.id}
76
+ class={`annotation-card ${selectedAnnotationId.value === ann.id ? 'selected' : ''} ${ann.status === 'resolved' ? 'resolved' : ''}`}
77
+ onClick={() => handleAnnotationClick(ann)}
78
+ >
79
+ {/* Tag + Author + Time */}
80
+ <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 6px">
81
+ <span class={`tag tag-${ann.tag}`}>{ann.tag}</span>
82
+ <span style="font-size: 11px; color: var(--text-muted)">{ann.author}</span>
83
+ {ann.status === 'resolved' && <span style="font-size: 10px; color: var(--status-approved)">✓ resolved</span>}
84
+ </div>
85
+
86
+ {/* Quote */}
87
+ {ann.selectors?.quote?.exact && (
88
+ <div class="quote">{ann.selectors.quote.exact}</div>
89
+ )}
90
+
91
+ {/* Comment */}
92
+ <div style="font-size: 13px; margin-top: 4px">{ann.comment}</div>
93
+
94
+ {/* Actions (when selected) */}
95
+ {selectedAnnotationId.value === ann.id && (
96
+ <div style="margin-top: 8px; display: flex; gap: 6px; flex-wrap: wrap">
97
+ {ann.status === 'open' ? (
98
+ <button class="btn btn-sm" onClick={(e) => { e.stopPropagation(); annotationOps.resolveAnnotation(ann.id) }}>
99
+ Resolve
100
+ </button>
101
+ ) : (
102
+ <button class="btn btn-sm" onClick={(e) => { e.stopPropagation(); annotationOps.reopenAnnotation(ann.id) }}>
103
+ Reopen
104
+ </button>
105
+ )}
106
+ <button class="btn btn-sm" onClick={(e) => { e.stopPropagation(); setEditingId(ann.id) }}>
107
+ Edit
108
+ </button>
109
+ <button class="btn btn-sm btn-danger" onClick={(e) => {
110
+ e.stopPropagation()
111
+ if (confirm('Delete this annotation?')) annotationOps.deleteAnnotation(ann.id)
112
+ }}>
113
+ Delete
114
+ </button>
115
+ </div>
116
+ )}
117
+
118
+ {/* Edit form */}
119
+ {editingId === ann.id && (
120
+ <AnnotationForm
121
+ annotation={ann}
122
+ onSave={(data) => {
123
+ annotationOps.updateAnnotation(ann.id, data)
124
+ setEditingId(null)
125
+ }}
126
+ onCancel={() => setEditingId(null)}
127
+ />
128
+ )}
129
+
130
+ {/* Replies */}
131
+ {ann.replies?.length > 0 && (
132
+ <ReplyThread replies={ann.replies} />
133
+ )}
134
+
135
+ {/* Reply input (when selected) */}
136
+ {selectedAnnotationId.value === ann.id && (
137
+ <ReplyInput annotationId={ann.id} onReply={annotationOps.addReply} />
138
+ )}
139
+ </div>
140
+ ))
141
+ )}
142
+ </div>
143
+ </div>
144
+ )}
145
+ </aside>
146
+ )
147
+ }
148
+
149
+ function ReplyInput({ annotationId, onReply }) {
150
+ const [text, setText] = useState('')
151
+
152
+ function handleSubmit(e) {
153
+ e.preventDefault()
154
+ if (text.trim()) {
155
+ onReply(annotationId, text.trim())
156
+ setText('')
157
+ }
158
+ }
159
+
160
+ return (
161
+ <form class="reply-input" onSubmit={handleSubmit} onClick={e => e.stopPropagation()}>
162
+ <input
163
+ type="text"
164
+ value={text}
165
+ onInput={e => setText(e.target.value)}
166
+ placeholder="Reply..."
167
+ />
168
+ <button type="submit" class="btn btn-sm btn-primary" disabled={!text.trim()}>Reply</button>
169
+ </form>
170
+ )
171
+ }
@@ -0,0 +1,31 @@
1
+ import { sections } from '../state/store.js'
2
+
3
+ export function SectionApproval({ heading, annotationOps }) {
4
+ const section = sections.value.find(s => s.heading === heading)
5
+ if (!section) return null
6
+
7
+ const status = section.status
8
+ const computed = section.computed || status
9
+
10
+ return (
11
+ <div class="section-approval" style="display: inline-flex; gap: 4px; margin-left: 8px; vertical-align: middle">
12
+ <button
13
+ class={`btn btn-sm ${computed === 'approved' ? 'section-status approved' : computed === 'indeterminate' ? 'section-status indeterminate' : ''}`}
14
+ onClick={() => status === 'approved' ? annotationOps.resetSection(heading) : annotationOps.approveSection(heading)}
15
+ title={computed === 'indeterminate' ? 'Partially approved — click to approve all' : 'Approve section'}
16
+ >
17
+ {computed === 'indeterminate' ? '\u2500' : '\u2713'}
18
+ </button>
19
+ <button
20
+ class={`btn btn-sm ${computed === 'rejected' ? 'section-status rejected' : ''}`}
21
+ onClick={() => status === 'rejected' ? annotationOps.resetSection(heading) : annotationOps.rejectSection(heading)}
22
+ title="Reject section"
23
+ >
24
+ &cross;
25
+ </button>
26
+ <span class={`section-status ${computed}`} style="font-size: 10px">
27
+ {computed}
28
+ </span>
29
+ </div>
30
+ )
31
+ }
@@ -0,0 +1,18 @@
1
+ import { theme } from '../state/store.js'
2
+
3
+ export function ThemePicker({ themes, onSelect }) {
4
+ return (
5
+ <div class="theme-picker" title="Switch theme">
6
+ {themes.map(t => (
7
+ <button
8
+ key={t.id}
9
+ class={`theme-swatch ${theme.value === t.id ? 'active' : ''}`}
10
+ style={`background: ${t.color}`}
11
+ onClick={() => onSelect(t.id)}
12
+ title={t.label}
13
+ aria-label={`Switch to ${t.label} theme`}
14
+ />
15
+ ))}
16
+ </div>
17
+ )
18
+ }
@@ -0,0 +1,160 @@
1
+ import { annotations, sections, currentFile, author, driftWarning, sectionLevel } from '../state/store.js'
2
+
3
+ const API_BASE = '' // same origin
4
+
5
+ /**
6
+ * CRUD operations for annotations and section review status.
7
+ * All methods communicate with the mdprobe HTTP API and update the
8
+ * corresponding signals in the store on success.
9
+ *
10
+ * @returns {object} annotation and section action methods
11
+ */
12
+ export function useAnnotations() {
13
+ // ---------------------------------------------------------------------------
14
+ // Annotations
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Fetch all annotations (and sections) for the given file path. */
18
+ async function fetchAnnotations(filePath) {
19
+ const res = await fetch(
20
+ `${API_BASE}/api/annotations?path=${encodeURIComponent(filePath)}`,
21
+ )
22
+ if (!res.ok) throw new Error(`Failed to fetch annotations: ${res.status}`)
23
+ const data = await res.json()
24
+ annotations.value = data.annotations || []
25
+ sections.value = data.sections || []
26
+ if (data.sectionLevel != null) sectionLevel.value = data.sectionLevel
27
+ driftWarning.value = data.drift || false
28
+ return data
29
+ }
30
+
31
+ /** Create a new annotation on the current file. */
32
+ async function createAnnotation({ selectors, comment, tag }) {
33
+ const data = await postAnnotation('add', {
34
+ selectors,
35
+ comment,
36
+ tag,
37
+ author: author.value,
38
+ })
39
+ annotations.value = data.annotations || annotations.value
40
+ return data
41
+ }
42
+
43
+ /** Mark an annotation as resolved. */
44
+ async function resolveAnnotation(id) {
45
+ const data = await postAnnotation('resolve', { id })
46
+ annotations.value = data.annotations || annotations.value
47
+ }
48
+
49
+ /** Re-open a previously resolved annotation. */
50
+ async function reopenAnnotation(id) {
51
+ const data = await postAnnotation('reopen', { id })
52
+ annotations.value = data.annotations || annotations.value
53
+ }
54
+
55
+ /** Update the comment and/or tag of an existing annotation. */
56
+ async function updateAnnotation(id, { comment, tag }) {
57
+ const data = await postAnnotation('update', { id, comment, tag })
58
+ annotations.value = data.annotations || annotations.value
59
+ }
60
+
61
+ /** Delete an annotation by id. */
62
+ async function deleteAnnotation(id) {
63
+ const data = await postAnnotation('delete', { id })
64
+ annotations.value = data.annotations || annotations.value
65
+ }
66
+
67
+ /** Add a threaded reply to an annotation. */
68
+ async function addReply(annotationId, comment) {
69
+ const data = await postAnnotation('reply', {
70
+ id: annotationId,
71
+ author: author.value,
72
+ comment,
73
+ })
74
+ annotations.value = data.annotations || annotations.value
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Sections
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /** Approve a single section by heading text (cascades to children). */
82
+ async function approveSection(heading) {
83
+ const data = await postSection('approve', { heading })
84
+ sections.value = data.sections || sections.value
85
+ if (data.sectionLevel != null) sectionLevel.value = data.sectionLevel
86
+ }
87
+
88
+ /** Reject a single section by heading text (no cascade). */
89
+ async function rejectSection(heading) {
90
+ const data = await postSection('reject', { heading })
91
+ sections.value = data.sections || sections.value
92
+ if (data.sectionLevel != null) sectionLevel.value = data.sectionLevel
93
+ }
94
+
95
+ /** Approve all sections in the current file at once. */
96
+ async function approveAllSections() {
97
+ const data = await postSection('approveAll')
98
+ sections.value = data.sections || sections.value
99
+ }
100
+
101
+ /** Reset a single section to pending. */
102
+ async function resetSection(heading) {
103
+ const data = await postSection('reset', { heading })
104
+ sections.value = data.sections || sections.value
105
+ if (data.sectionLevel != null) sectionLevel.value = data.sectionLevel
106
+ }
107
+
108
+ /** Reset all section review statuses in the current file. */
109
+ async function clearAllSections() {
110
+ const data = await postSection('clearAll')
111
+ sections.value = data.sections || sections.value
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Helpers
116
+ // ---------------------------------------------------------------------------
117
+
118
+ async function postAnnotation(action, data) {
119
+ const res = await fetch(`${API_BASE}/api/annotations`, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({
123
+ file: currentFile.value,
124
+ action,
125
+ data,
126
+ }),
127
+ })
128
+ if (!res.ok) throw new Error(`Annotation ${action} failed: ${res.status}`)
129
+ return res.json()
130
+ }
131
+
132
+ async function postSection(action, extra = {}) {
133
+ const res = await fetch(`${API_BASE}/api/sections`, {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({
137
+ file: currentFile.value,
138
+ action,
139
+ ...extra,
140
+ }),
141
+ })
142
+ if (!res.ok) throw new Error(`Section ${action} failed: ${res.status}`)
143
+ return res.json()
144
+ }
145
+
146
+ return {
147
+ fetchAnnotations,
148
+ createAnnotation,
149
+ resolveAnnotation,
150
+ reopenAnnotation,
151
+ updateAnnotation,
152
+ deleteAnnotation,
153
+ addReply,
154
+ approveSection,
155
+ rejectSection,
156
+ resetSection,
157
+ approveAllSections,
158
+ clearAllSections,
159
+ }
160
+ }