@puredesktop/puredesktop-ui-bridge 2.1.6 → 2.1.8
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/package.json +55 -1
- package/src/bridge/agentTypes.ts +21 -1
- package/src/bridge/collectionImagePaste.ts +19 -1
- package/src/bridge/context.mjs +32 -0
- package/src/bridge/documents.d.mts +76 -0
- package/src/bridge/documents.mjs +40 -0
- package/src/bridge/methods.d.mts +23 -0
- package/src/bridge/methods.mjs +23 -0
- package/src/bridge/pureRender/base.css +44 -0
- package/src/bridge/pureRender/document.ts +29 -2
- package/src/bridge/pureRender/index.ts +3 -0
- package/src/bridge/pureRender/normalize.ts +167 -0
- package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
- package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
- package/src/bridge/pureRender/styles.ts +125 -5
- package/src/bridge/pureRender/theme.test.ts +259 -0
- package/src/bridge/pureRender/theme.ts +19 -1
- package/src/bridge/pureRender/types.ts +33 -0
- package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
- package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
- package/src/bridge/react/useDocumentHotkeys.ts +41 -0
- package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
- package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
- package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
- package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
- package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
- package/src/bridge/storage.d.mts +3 -1
- package/src/bridge/storage.test.ts +2 -2
- package/src/bridge/types.ts +7 -0
- package/src/components/agents/AgentDrawerPanel.tsx +2 -0
- package/src/components/agents/AgentMessageList.tsx +6 -1
- package/src/components/agents/AgentToolCallCard.tsx +20 -20
- package/src/components/agents/ContextPicker.tsx +239 -0
- package/src/components/agents/agentPanelStyles.ts +3 -4
- package/src/components/agents/agentTypes.ts +2 -0
- package/src/components/chrome/WorkspaceTabStrip.tsx +18 -1
- package/src/components/chrome/workspaceTabTypes.ts +2 -0
- package/src/components/common/containers/AppFrame.test.tsx +4 -4
- package/src/components/common/containers/AppFrame.tsx +10 -1
- package/src/components/common/containers/AppHeader.tsx +22 -0
- package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
- package/src/components/common/documents/DocumentSwitcher.tsx +1176 -0
- package/src/components/common/documents/index.ts +8 -0
- package/src/components/common/research/EvidenceDossier.tsx +1232 -150
- package/src/components/common/research/index.ts +2 -0
- package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
- package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
- package/src/components/print-design/index.ts +2 -0
- package/src/components/print-design/previewContent.test.ts +32 -0
- package/src/components/print-design/previewContent.ts +31 -0
- package/src/context/buildContextDocument.test.ts +240 -0
- package/src/context/buildContextDocument.ts +309 -0
- package/src/context/contextAccess.ts +66 -0
- package/src/context/contextConfig.ts +72 -0
- package/src/context/contextDocument.test.ts +155 -0
- package/src/context/contextDocument.ts +300 -0
- package/src/context/contextSections.test.ts +88 -0
- package/src/context/contextSections.ts +84 -0
- package/src/context/htmlToContextMarkdown.test.ts +134 -0
- package/src/context/htmlToContextMarkdown.ts +369 -0
- package/src/theme/appAccents.test.ts +36 -35
- package/src/theme/appAccents.ts +87 -104
- package/src/theme/appIdentityCss.mjs +169 -231
- package/src/theme/appIdentityCss.ts +213 -233
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { styled } from 'styled-components'
|
|
10
|
+
import {
|
|
11
|
+
Boxes,
|
|
12
|
+
Clock,
|
|
13
|
+
FileStack,
|
|
14
|
+
FileUp,
|
|
15
|
+
MonitorUp,
|
|
16
|
+
Plus,
|
|
17
|
+
Search,
|
|
18
|
+
X,
|
|
19
|
+
} from 'lucide-react'
|
|
20
|
+
import {
|
|
21
|
+
listPlatformDocumentsByType,
|
|
22
|
+
listPlatformRecentDocuments,
|
|
23
|
+
type PlatformDocumentListEntry,
|
|
24
|
+
type PlatformRecentDocumentEntry,
|
|
25
|
+
} from '../../../bridge/documents.mjs'
|
|
26
|
+
import { bridge } from '../../../bridge/client.mjs'
|
|
27
|
+
import { PLATFORM_BRIDGE_METHODS } from '../../../bridge/methods.mjs'
|
|
28
|
+
|
|
29
|
+
/** A "Bring in" route beyond the two defaults (e.g. Research's source URL). */
|
|
30
|
+
export interface DocumentSwitcherSource {
|
|
31
|
+
id: string
|
|
32
|
+
label: string
|
|
33
|
+
icon?: ReactNode
|
|
34
|
+
onSelect: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type DocumentPreviewStyle =
|
|
38
|
+
| 'document'
|
|
39
|
+
| 'structured'
|
|
40
|
+
| 'grid'
|
|
41
|
+
| 'none'
|
|
42
|
+
|
|
43
|
+
export interface DocumentSwitcherProps {
|
|
44
|
+
appSlug: string
|
|
45
|
+
/** Document suffixes this app opens, e.g. ['.research']. */
|
|
46
|
+
suffixes: string[]
|
|
47
|
+
/** 'modal' renders full-screen over a scrim (⌘O); 'landing' renders inline
|
|
48
|
+
* as the empty-app home surface. */
|
|
49
|
+
variant?: 'modal' | 'landing'
|
|
50
|
+
open?: boolean
|
|
51
|
+
onClose?: () => void
|
|
52
|
+
onOpenDocument: (path: string) => void
|
|
53
|
+
onCreateNew: () => void
|
|
54
|
+
newLabel?: string
|
|
55
|
+
/** Main-pane title; defaults to `Open a <noun>`. */
|
|
56
|
+
title?: string
|
|
57
|
+
/** Singular item noun, e.g. 'board', 'dossier', 'report'. */
|
|
58
|
+
itemNoun?: string
|
|
59
|
+
/** Plural noun; defaults to `<noun>s`. */
|
|
60
|
+
itemNounPlural?: string
|
|
61
|
+
/** Wordmark name after "pure"; defaults to `appSlug`. */
|
|
62
|
+
wordmarkName?: string
|
|
63
|
+
/** Reading/authoring apps set true — serif item names + text previews. */
|
|
64
|
+
nameSerif?: boolean
|
|
65
|
+
/** Thumbnail style; defaults from `nameSerif` (document vs structured). */
|
|
66
|
+
previewStyle?: DocumentPreviewStyle
|
|
67
|
+
/** Extension shown in the storage footer; defaults to `suffixes[0]`. */
|
|
68
|
+
extensionLabel?: string
|
|
69
|
+
/** Folder hint shown in the storage footer, e.g. './Documents/Boards'. */
|
|
70
|
+
storageLabel?: string
|
|
71
|
+
/** In-app import of a compatible file (⌘I). Hidden when omitted. */
|
|
72
|
+
onImportFile?: () => void
|
|
73
|
+
/** Native OS file dialog. Defaults to a built-in open-file dialog. */
|
|
74
|
+
onChooseFromSystem?: () => void
|
|
75
|
+
/** Extra "Bring in" routes (bespoke per app). */
|
|
76
|
+
extraSources?: DocumentSwitcherSource[]
|
|
77
|
+
footer?: ReactNode
|
|
78
|
+
className?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface DocItem {
|
|
82
|
+
path: string
|
|
83
|
+
name: string
|
|
84
|
+
isDraft: boolean
|
|
85
|
+
mtimeMs: number
|
|
86
|
+
kind: 'package' | 'file'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type BrowseSource = 'all' | 'recents' | 'drafts'
|
|
90
|
+
type SortKey = 'recent' | 'name'
|
|
91
|
+
|
|
92
|
+
function relativeDay(iso: string | number): string {
|
|
93
|
+
const time = typeof iso === 'number' ? iso : Date.parse(iso)
|
|
94
|
+
if (!Number.isFinite(time)) return ''
|
|
95
|
+
const days = Math.floor((Date.now() - time) / 86_400_000)
|
|
96
|
+
if (days <= 0) return 'today'
|
|
97
|
+
if (days === 1) return 'yesterday'
|
|
98
|
+
if (days < 7) return `${days}d ago`
|
|
99
|
+
return new Date(time).toLocaleDateString([], {
|
|
100
|
+
month: 'short',
|
|
101
|
+
day: 'numeric',
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function chooseFromSystem(): Promise<string | null> {
|
|
106
|
+
try {
|
|
107
|
+
const result = await bridge.call<{ path?: string | null }>(
|
|
108
|
+
PLATFORM_BRIDGE_METHODS.DIALOG_OPEN_FILE,
|
|
109
|
+
)
|
|
110
|
+
const path = result?.path
|
|
111
|
+
return typeof path === 'string' && path.trim() ? path : null
|
|
112
|
+
} catch {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The Pure file picker: a full-screen, per-app "Open" surface. One shared
|
|
119
|
+
* shell re-skinned by each app via identity tokens (`--app-*` from `data-app`),
|
|
120
|
+
* document nouns, item-name font, and a preview thumbnail. Open · New · Import ·
|
|
121
|
+
* Choose-from-system, with a recents strip and a sortable browse table. Also
|
|
122
|
+
* serves as the empty-app landing state via `variant: 'landing'`.
|
|
123
|
+
*/
|
|
124
|
+
export function DocumentSwitcher({
|
|
125
|
+
appSlug,
|
|
126
|
+
suffixes,
|
|
127
|
+
variant = 'modal',
|
|
128
|
+
open = true,
|
|
129
|
+
onClose,
|
|
130
|
+
onOpenDocument,
|
|
131
|
+
onCreateNew,
|
|
132
|
+
newLabel,
|
|
133
|
+
title,
|
|
134
|
+
itemNoun = 'document',
|
|
135
|
+
itemNounPlural,
|
|
136
|
+
wordmarkName,
|
|
137
|
+
nameSerif = false,
|
|
138
|
+
previewStyle,
|
|
139
|
+
extensionLabel,
|
|
140
|
+
storageLabel,
|
|
141
|
+
onImportFile,
|
|
142
|
+
onChooseFromSystem,
|
|
143
|
+
extraSources,
|
|
144
|
+
footer,
|
|
145
|
+
className,
|
|
146
|
+
}: DocumentSwitcherProps): ReactElement | null {
|
|
147
|
+
const noun = itemNoun
|
|
148
|
+
const plural = itemNounPlural ?? `${noun}s`
|
|
149
|
+
const preview: DocumentPreviewStyle =
|
|
150
|
+
previewStyle ?? (nameSerif ? 'document' : 'structured')
|
|
151
|
+
const heading = title ?? `Open a ${noun}`
|
|
152
|
+
const ext = extensionLabel ?? suffixes[0] ?? ''
|
|
153
|
+
|
|
154
|
+
const [recents, setRecents] = useState<PlatformRecentDocumentEntry[]>([])
|
|
155
|
+
const [items, setItems] = useState<DocItem[] | null>(null)
|
|
156
|
+
const [query, setQuery] = useState('')
|
|
157
|
+
const [source, setSource] = useState<BrowseSource>('all')
|
|
158
|
+
const [sort, setSort] = useState<SortKey>('recent')
|
|
159
|
+
const [selected, setSelected] = useState(0)
|
|
160
|
+
const filterRef = useRef<HTMLInputElement>(null)
|
|
161
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!open) return
|
|
165
|
+
let cancelled = false
|
|
166
|
+
void (async () => {
|
|
167
|
+
const [recentEntries, all] = await Promise.all([
|
|
168
|
+
listPlatformRecentDocuments({ appSlug, limit: 8 }).catch(() => []),
|
|
169
|
+
listPlatformDocumentsByType({ suffixes }).catch(() => []),
|
|
170
|
+
])
|
|
171
|
+
if (cancelled) return
|
|
172
|
+
setRecents(recentEntries)
|
|
173
|
+
setItems(
|
|
174
|
+
all.map((entry: PlatformDocumentListEntry) => ({
|
|
175
|
+
path: entry.path,
|
|
176
|
+
name: entry.name,
|
|
177
|
+
isDraft: entry.isDraft,
|
|
178
|
+
mtimeMs: entry.mtimeMs,
|
|
179
|
+
kind: entry.kind,
|
|
180
|
+
})),
|
|
181
|
+
)
|
|
182
|
+
})()
|
|
183
|
+
return () => {
|
|
184
|
+
cancelled = true
|
|
185
|
+
}
|
|
186
|
+
}, [appSlug, open, suffixes])
|
|
187
|
+
|
|
188
|
+
// Focus the filter when the picker opens.
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (open) {
|
|
191
|
+
const timer = window.setTimeout(() => filterRef.current?.focus(), 20)
|
|
192
|
+
return () => window.clearTimeout(timer)
|
|
193
|
+
}
|
|
194
|
+
return undefined
|
|
195
|
+
}, [open])
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
setSelected(0)
|
|
199
|
+
}, [source, sort, query])
|
|
200
|
+
|
|
201
|
+
const recentPaths = useMemo(
|
|
202
|
+
() => new Set(recents.map(entry => entry.path)),
|
|
203
|
+
[recents],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const filter = query.trim().toLowerCase()
|
|
207
|
+
const rows = useMemo(() => {
|
|
208
|
+
if (!items) return []
|
|
209
|
+
let scoped =
|
|
210
|
+
source === 'drafts'
|
|
211
|
+
? items.filter(item => item.isDraft)
|
|
212
|
+
: source === 'recents'
|
|
213
|
+
? items.filter(item => recentPaths.has(item.path))
|
|
214
|
+
: items
|
|
215
|
+
if (filter) {
|
|
216
|
+
scoped = scoped.filter(item => item.name.toLowerCase().includes(filter))
|
|
217
|
+
}
|
|
218
|
+
const sorted = [...scoped]
|
|
219
|
+
if (sort === 'name') {
|
|
220
|
+
sorted.sort((a, b) => a.name.localeCompare(b.name))
|
|
221
|
+
} else {
|
|
222
|
+
sorted.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
223
|
+
}
|
|
224
|
+
return sorted
|
|
225
|
+
}, [filter, items, recentPaths, sort, source])
|
|
226
|
+
|
|
227
|
+
// Recent preview cards — up to 3, newest first (recents list, else newest).
|
|
228
|
+
const recentCards = useMemo(() => {
|
|
229
|
+
if (!items) return []
|
|
230
|
+
const byPath = new Map(items.map(item => [item.path, item]))
|
|
231
|
+
const fromRecents = recents
|
|
232
|
+
.map(entry => byPath.get(entry.path))
|
|
233
|
+
.filter((item): item is DocItem => Boolean(item))
|
|
234
|
+
const source =
|
|
235
|
+
fromRecents.length > 0
|
|
236
|
+
? fromRecents
|
|
237
|
+
: [...items].sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
238
|
+
return source.slice(0, 3)
|
|
239
|
+
}, [items, recents])
|
|
240
|
+
|
|
241
|
+
const draftCount = items?.filter(item => item.isDraft).length ?? 0
|
|
242
|
+
|
|
243
|
+
const handleChoose = onChooseFromSystem
|
|
244
|
+
? onChooseFromSystem
|
|
245
|
+
: () => {
|
|
246
|
+
void chooseFromSystem().then(path => {
|
|
247
|
+
if (path) onOpenDocument(path)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!open) return
|
|
253
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
254
|
+
const meta = event.metaKey || event.ctrlKey
|
|
255
|
+
if (event.key === 'Escape') {
|
|
256
|
+
event.preventDefault()
|
|
257
|
+
onClose?.()
|
|
258
|
+
} else if (meta && event.key.toLowerCase() === 'n') {
|
|
259
|
+
event.preventDefault()
|
|
260
|
+
onCreateNew()
|
|
261
|
+
} else if (meta && event.key.toLowerCase() === 'i' && onImportFile) {
|
|
262
|
+
event.preventDefault()
|
|
263
|
+
onImportFile()
|
|
264
|
+
} else if (!meta && event.key === 'ArrowDown') {
|
|
265
|
+
event.preventDefault()
|
|
266
|
+
setSelected(index => Math.min(index + 1, Math.max(rows.length - 1, 0)))
|
|
267
|
+
} else if (!meta && event.key === 'ArrowUp') {
|
|
268
|
+
event.preventDefault()
|
|
269
|
+
setSelected(index => Math.max(index - 1, 0))
|
|
270
|
+
} else if (event.key === 'Enter' && rows.length > 0) {
|
|
271
|
+
const active = document.activeElement
|
|
272
|
+
// Let Enter inside the New tile / buttons act normally.
|
|
273
|
+
if (active instanceof HTMLButtonElement) return
|
|
274
|
+
event.preventDefault()
|
|
275
|
+
const item = rows[Math.min(selected, rows.length - 1)]
|
|
276
|
+
if (item) onOpenDocument(item.path)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
window.addEventListener('keydown', onKeyDown)
|
|
280
|
+
return () => window.removeEventListener('keydown', onKeyDown)
|
|
281
|
+
}, [
|
|
282
|
+
onClose,
|
|
283
|
+
onCreateNew,
|
|
284
|
+
onImportFile,
|
|
285
|
+
onOpenDocument,
|
|
286
|
+
open,
|
|
287
|
+
rows,
|
|
288
|
+
selected,
|
|
289
|
+
])
|
|
290
|
+
|
|
291
|
+
if (!open) return null
|
|
292
|
+
|
|
293
|
+
const substats =
|
|
294
|
+
items === null
|
|
295
|
+
? 'LOADING…'
|
|
296
|
+
: `${items.length} ${plural.toUpperCase()}${
|
|
297
|
+
draftCount ? ` · ${draftCount} DRAFT${draftCount === 1 ? '' : 'S'}` : ''
|
|
298
|
+
}`
|
|
299
|
+
|
|
300
|
+
const sheet = (
|
|
301
|
+
<Sheet
|
|
302
|
+
data-app={appSlug}
|
|
303
|
+
className={className}
|
|
304
|
+
$variant={variant}
|
|
305
|
+
role={variant === 'modal' ? 'dialog' : undefined}
|
|
306
|
+
aria-modal={variant === 'modal' ? true : undefined}
|
|
307
|
+
aria-label={heading}
|
|
308
|
+
>
|
|
309
|
+
{/* LEFT RAIL */}
|
|
310
|
+
<Rail>
|
|
311
|
+
<Wordmark>
|
|
312
|
+
<span>pure</span>
|
|
313
|
+
<strong>{wordmarkName ?? appSlug}</strong>
|
|
314
|
+
</Wordmark>
|
|
315
|
+
|
|
316
|
+
<RailKicker>Browse</RailKicker>
|
|
317
|
+
<RailItem
|
|
318
|
+
type="button"
|
|
319
|
+
$active={source === 'all'}
|
|
320
|
+
onClick={() => setSource('all')}
|
|
321
|
+
>
|
|
322
|
+
<Boxes size={16} strokeWidth={2} aria-hidden />
|
|
323
|
+
All {plural}
|
|
324
|
+
<RailCount>{items?.length ?? 0}</RailCount>
|
|
325
|
+
</RailItem>
|
|
326
|
+
<RailItem
|
|
327
|
+
type="button"
|
|
328
|
+
$active={source === 'recents'}
|
|
329
|
+
onClick={() => setSource('recents')}
|
|
330
|
+
>
|
|
331
|
+
<Clock size={16} strokeWidth={2} aria-hidden />
|
|
332
|
+
Recents
|
|
333
|
+
</RailItem>
|
|
334
|
+
<RailItem
|
|
335
|
+
type="button"
|
|
336
|
+
$active={source === 'drafts'}
|
|
337
|
+
onClick={() => setSource('drafts')}
|
|
338
|
+
>
|
|
339
|
+
<FileStack size={16} strokeWidth={2} aria-hidden />
|
|
340
|
+
Drafts
|
|
341
|
+
{draftCount ? <RailCount>{draftCount}</RailCount> : null}
|
|
342
|
+
</RailItem>
|
|
343
|
+
|
|
344
|
+
<RailKicker style={{ marginTop: 24 }}>Bring in</RailKicker>
|
|
345
|
+
{onImportFile ? (
|
|
346
|
+
<RailItem type="button" onClick={onImportFile}>
|
|
347
|
+
<FileUp size={16} strokeWidth={2} aria-hidden />
|
|
348
|
+
Import a file…
|
|
349
|
+
</RailItem>
|
|
350
|
+
) : null}
|
|
351
|
+
<RailItem type="button" onClick={handleChoose}>
|
|
352
|
+
<MonitorUp size={16} strokeWidth={2} aria-hidden />
|
|
353
|
+
Choose from system…
|
|
354
|
+
</RailItem>
|
|
355
|
+
{extraSources?.map(entry => (
|
|
356
|
+
<RailItem key={entry.id} type="button" onClick={entry.onSelect}>
|
|
357
|
+
{entry.icon ?? <FileUp size={16} strokeWidth={2} aria-hidden />}
|
|
358
|
+
{entry.label}
|
|
359
|
+
</RailItem>
|
|
360
|
+
))}
|
|
361
|
+
|
|
362
|
+
<RailFooter>
|
|
363
|
+
<RailKicker>Storage</RailKicker>
|
|
364
|
+
<StorageText>
|
|
365
|
+
Saved to <strong>PureFiles</strong>
|
|
366
|
+
<StorageMeta>
|
|
367
|
+
{storageLabel ? `${storageLabel} · ` : ''}
|
|
368
|
+
{ext}
|
|
369
|
+
</StorageMeta>
|
|
370
|
+
</StorageText>
|
|
371
|
+
</RailFooter>
|
|
372
|
+
</Rail>
|
|
373
|
+
|
|
374
|
+
{/* MAIN */}
|
|
375
|
+
<Main>
|
|
376
|
+
<MainHead>
|
|
377
|
+
<div>
|
|
378
|
+
<Heading>{heading}</Heading>
|
|
379
|
+
<SubStats>{substats}</SubStats>
|
|
380
|
+
</div>
|
|
381
|
+
<HeadControls>
|
|
382
|
+
<FilterWrap>
|
|
383
|
+
<Search size={16} strokeWidth={2} aria-hidden />
|
|
384
|
+
<FilterInput
|
|
385
|
+
ref={filterRef}
|
|
386
|
+
value={query}
|
|
387
|
+
onChange={event => setQuery(event.target.value)}
|
|
388
|
+
placeholder={`Filter ${plural}…`}
|
|
389
|
+
aria-label={`Filter ${plural}`}
|
|
390
|
+
/>
|
|
391
|
+
</FilterWrap>
|
|
392
|
+
{variant === 'modal' ? (
|
|
393
|
+
<CloseButton
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={() => onClose?.()}
|
|
396
|
+
aria-label="Close"
|
|
397
|
+
>
|
|
398
|
+
<X size={17} strokeWidth={2} aria-hidden />
|
|
399
|
+
</CloseButton>
|
|
400
|
+
) : null}
|
|
401
|
+
</HeadControls>
|
|
402
|
+
</MainHead>
|
|
403
|
+
|
|
404
|
+
<Body>
|
|
405
|
+
<CardRow>
|
|
406
|
+
<NewCard type="button" onClick={onCreateNew}>
|
|
407
|
+
<NewIcon>
|
|
408
|
+
<Plus size={22} strokeWidth={2.2} aria-hidden />
|
|
409
|
+
</NewIcon>
|
|
410
|
+
<NewLabel>{newLabel ?? `New ${noun}`}</NewLabel>
|
|
411
|
+
<NewHint>⌘N</NewHint>
|
|
412
|
+
</NewCard>
|
|
413
|
+
{recentCards.map(item => (
|
|
414
|
+
<PreviewCard
|
|
415
|
+
key={item.path}
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={() => onOpenDocument(item.path)}
|
|
418
|
+
>
|
|
419
|
+
<PreviewThumb>
|
|
420
|
+
<Thumbnail $preview={preview} $serif={nameSerif} name={item.name} />
|
|
421
|
+
</PreviewThumb>
|
|
422
|
+
<PreviewMeta>
|
|
423
|
+
<PreviewName $serif={nameSerif}>{item.name}</PreviewName>
|
|
424
|
+
<PreviewWhen>{relativeDay(item.mtimeMs)}</PreviewWhen>
|
|
425
|
+
</PreviewMeta>
|
|
426
|
+
</PreviewCard>
|
|
427
|
+
))}
|
|
428
|
+
</CardRow>
|
|
429
|
+
|
|
430
|
+
<ListHead>
|
|
431
|
+
<ListKicker>All {plural}</ListKicker>
|
|
432
|
+
<SortControls>
|
|
433
|
+
<SortButton
|
|
434
|
+
type="button"
|
|
435
|
+
$active={sort === 'recent'}
|
|
436
|
+
onClick={() => setSort('recent')}
|
|
437
|
+
>
|
|
438
|
+
Recent
|
|
439
|
+
</SortButton>
|
|
440
|
+
<SortButton
|
|
441
|
+
type="button"
|
|
442
|
+
$active={sort === 'name'}
|
|
443
|
+
onClick={() => setSort('name')}
|
|
444
|
+
>
|
|
445
|
+
Name
|
|
446
|
+
</SortButton>
|
|
447
|
+
</SortControls>
|
|
448
|
+
</ListHead>
|
|
449
|
+
|
|
450
|
+
<ListBox ref={listRef}>
|
|
451
|
+
{items === null ? (
|
|
452
|
+
<EmptyNote>Loading…</EmptyNote>
|
|
453
|
+
) : rows.length === 0 ? (
|
|
454
|
+
<EmptyNote>
|
|
455
|
+
{filter
|
|
456
|
+
? 'Nothing matches that filter.'
|
|
457
|
+
: source === 'drafts'
|
|
458
|
+
? 'No drafts yet.'
|
|
459
|
+
: `No ${plural} yet — create the first, import one, or choose from your system.`}
|
|
460
|
+
</EmptyNote>
|
|
461
|
+
) : (
|
|
462
|
+
rows.map((item, index) => (
|
|
463
|
+
<Row
|
|
464
|
+
key={item.path}
|
|
465
|
+
type="button"
|
|
466
|
+
$selected={index === selected}
|
|
467
|
+
onMouseEnter={() => setSelected(index)}
|
|
468
|
+
onClick={() => onOpenDocument(item.path)}
|
|
469
|
+
>
|
|
470
|
+
<RowIcon>
|
|
471
|
+
<Boxes size={17} strokeWidth={2} aria-hidden />
|
|
472
|
+
</RowIcon>
|
|
473
|
+
<RowMain>
|
|
474
|
+
<RowName $serif={nameSerif}>{item.name}</RowName>
|
|
475
|
+
<RowMeta>
|
|
476
|
+
{item.kind === 'package' ? 'Package' : 'File'} · {ext}
|
|
477
|
+
</RowMeta>
|
|
478
|
+
</RowMain>
|
|
479
|
+
<RowWhen>{relativeDay(item.mtimeMs)}</RowWhen>
|
|
480
|
+
<RowBadge $draft={item.isDraft}>
|
|
481
|
+
{item.isDraft ? 'Draft' : capitalize(noun)}
|
|
482
|
+
</RowBadge>
|
|
483
|
+
</Row>
|
|
484
|
+
))
|
|
485
|
+
)}
|
|
486
|
+
</ListBox>
|
|
487
|
+
{footer}
|
|
488
|
+
</Body>
|
|
489
|
+
|
|
490
|
+
<FooterBar>
|
|
491
|
+
<FooterHints>↑↓ to navigate · ⏎ to open{onImportFile ? ' · ⌘I import' : ''}</FooterHints>
|
|
492
|
+
<FooterActions>
|
|
493
|
+
{onImportFile ? (
|
|
494
|
+
<SecondaryButton type="button" onClick={onImportFile}>
|
|
495
|
+
Import a file…
|
|
496
|
+
</SecondaryButton>
|
|
497
|
+
) : (
|
|
498
|
+
<SecondaryButton type="button" onClick={handleChoose}>
|
|
499
|
+
Choose from system…
|
|
500
|
+
</SecondaryButton>
|
|
501
|
+
)}
|
|
502
|
+
<PrimaryButton
|
|
503
|
+
type="button"
|
|
504
|
+
disabled={rows.length === 0}
|
|
505
|
+
onClick={() => {
|
|
506
|
+
const item = rows[Math.min(selected, rows.length - 1)]
|
|
507
|
+
if (item) onOpenDocument(item.path)
|
|
508
|
+
}}
|
|
509
|
+
>
|
|
510
|
+
Open {noun}
|
|
511
|
+
</PrimaryButton>
|
|
512
|
+
</FooterActions>
|
|
513
|
+
</FooterBar>
|
|
514
|
+
</Main>
|
|
515
|
+
</Sheet>
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if (variant === 'landing') return sheet
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<Scrim onMouseDown={() => onClose?.()}>
|
|
522
|
+
<SheetHolder onMouseDown={event => event.stopPropagation()}>
|
|
523
|
+
{sheet}
|
|
524
|
+
</SheetHolder>
|
|
525
|
+
</Scrim>
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function capitalize(value: string): string {
|
|
530
|
+
return value ? value[0]!.toUpperCase() + value.slice(1) : value
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** A lightweight preview thumbnail derived from the doc name + app style. */
|
|
534
|
+
function Thumbnail({
|
|
535
|
+
$preview,
|
|
536
|
+
$serif,
|
|
537
|
+
name,
|
|
538
|
+
}: {
|
|
539
|
+
$preview: DocumentPreviewStyle
|
|
540
|
+
$serif: boolean
|
|
541
|
+
name: string
|
|
542
|
+
}): ReactElement | null {
|
|
543
|
+
if ($preview === 'none') {
|
|
544
|
+
return (
|
|
545
|
+
<ThumbFallback>
|
|
546
|
+
<Boxes size={26} strokeWidth={1.6} aria-hidden />
|
|
547
|
+
</ThumbFallback>
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
if ($preview === 'document') {
|
|
551
|
+
return (
|
|
552
|
+
<ThumbDoc>
|
|
553
|
+
<ThumbDocTitle $serif={$serif}>{name}</ThumbDocTitle>
|
|
554
|
+
<ThumbLine style={{ width: '92%' }} />
|
|
555
|
+
<ThumbLine style={{ width: '80%' }} />
|
|
556
|
+
<ThumbLine style={{ width: '88%' }} />
|
|
557
|
+
<ThumbPills>
|
|
558
|
+
<ThumbPill $accent style={{ width: 22 }} />
|
|
559
|
+
<ThumbPill style={{ width: 14 }} />
|
|
560
|
+
<ThumbPill style={{ width: 18 }} />
|
|
561
|
+
</ThumbPills>
|
|
562
|
+
</ThumbDoc>
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
if ($preview === 'grid') {
|
|
566
|
+
return (
|
|
567
|
+
<ThumbGrid>
|
|
568
|
+
{Array.from({ length: 4 }).map((_, index) => (
|
|
569
|
+
<ThumbCell key={index} $accent={index === 0} />
|
|
570
|
+
))}
|
|
571
|
+
</ThumbGrid>
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
// structured — columns of blocks (kanban-ish)
|
|
575
|
+
return (
|
|
576
|
+
<ThumbCols>
|
|
577
|
+
<ThumbCol>
|
|
578
|
+
<ThumbColHead style={{ width: '60%' }} />
|
|
579
|
+
<ThumbBlock $accent />
|
|
580
|
+
<ThumbBlock />
|
|
581
|
+
</ThumbCol>
|
|
582
|
+
<ThumbCol>
|
|
583
|
+
<ThumbColHead style={{ width: '55%' }} />
|
|
584
|
+
<ThumbBlock />
|
|
585
|
+
</ThumbCol>
|
|
586
|
+
<ThumbCol>
|
|
587
|
+
<ThumbColHead style={{ width: '50%' }} />
|
|
588
|
+
</ThumbCol>
|
|
589
|
+
</ThumbCols>
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
//#region styled-components
|
|
594
|
+
|
|
595
|
+
const serifStack =
|
|
596
|
+
"var(--platform-typography-font-family-serif, 'Source Serif 4', Georgia, serif)"
|
|
597
|
+
const sansStack = 'var(--platform-typography-font-family, Archivo, system-ui, sans-serif)'
|
|
598
|
+
const monoStack =
|
|
599
|
+
"var(--platform-typography-font-family-mono, 'JetBrains Mono', ui-monospace, monospace)"
|
|
600
|
+
const ink = 'var(--platform-colors-text, #1b1b1e)'
|
|
601
|
+
const faint = 'var(--platform-colors-text-muted, #9a9aa0)'
|
|
602
|
+
const line = 'var(--platform-colors-border, #ece7ee)'
|
|
603
|
+
const surface = 'var(--platform-colors-surface, #faf8fb)'
|
|
604
|
+
const elevated = 'var(--platform-colors-elevated, #ffffff)'
|
|
605
|
+
|
|
606
|
+
const Scrim = styled.div`
|
|
607
|
+
position: fixed;
|
|
608
|
+
inset: 0;
|
|
609
|
+
z-index: 60;
|
|
610
|
+
display: flex;
|
|
611
|
+
background: rgba(20, 18, 22, 0.34);
|
|
612
|
+
`
|
|
613
|
+
|
|
614
|
+
const SheetHolder = styled.div`
|
|
615
|
+
position: absolute;
|
|
616
|
+
inset: 28px;
|
|
617
|
+
display: flex;
|
|
618
|
+
`
|
|
619
|
+
|
|
620
|
+
const Sheet = styled.div<{ $variant: 'modal' | 'landing' }>`
|
|
621
|
+
display: grid;
|
|
622
|
+
grid-template-columns: 296px 1fr;
|
|
623
|
+
width: 100%;
|
|
624
|
+
height: 100%;
|
|
625
|
+
min-height: 0;
|
|
626
|
+
overflow: hidden;
|
|
627
|
+
color: ${ink};
|
|
628
|
+
background: ${elevated};
|
|
629
|
+
border-radius: ${({ $variant }) => ($variant === 'modal' ? '10px' : '0')};
|
|
630
|
+
box-shadow: ${({ $variant }) =>
|
|
631
|
+
$variant === 'modal' ? '0 24px 80px rgba(14, 14, 17, 0.4)' : 'none'};
|
|
632
|
+
font-family: ${sansStack};
|
|
633
|
+
`
|
|
634
|
+
|
|
635
|
+
const Rail = styled.div`
|
|
636
|
+
display: flex;
|
|
637
|
+
flex-direction: column;
|
|
638
|
+
padding: 30px 22px;
|
|
639
|
+
background: ${surface};
|
|
640
|
+
border-right: 1px solid ${line};
|
|
641
|
+
`
|
|
642
|
+
|
|
643
|
+
const Wordmark = styled.div`
|
|
644
|
+
margin-bottom: 30px;
|
|
645
|
+
font-size: 19px;
|
|
646
|
+
font-weight: 700;
|
|
647
|
+
|
|
648
|
+
span {
|
|
649
|
+
color: ${faint};
|
|
650
|
+
}
|
|
651
|
+
strong {
|
|
652
|
+
color: var(--app-acc, #1f9d57);
|
|
653
|
+
font-weight: 700;
|
|
654
|
+
}
|
|
655
|
+
`
|
|
656
|
+
|
|
657
|
+
const RailKicker = styled.div`
|
|
658
|
+
margin-bottom: 12px;
|
|
659
|
+
font-family: ${monoStack};
|
|
660
|
+
font-size: 10px;
|
|
661
|
+
font-weight: 600;
|
|
662
|
+
letter-spacing: 0.14em;
|
|
663
|
+
text-transform: uppercase;
|
|
664
|
+
color: ${faint};
|
|
665
|
+
`
|
|
666
|
+
|
|
667
|
+
const RailItem = styled.button<{ $active?: boolean }>`
|
|
668
|
+
display: flex;
|
|
669
|
+
align-items: center;
|
|
670
|
+
gap: 11px;
|
|
671
|
+
width: 100%;
|
|
672
|
+
margin-bottom: 2px;
|
|
673
|
+
padding: 10px 12px;
|
|
674
|
+
border: 0;
|
|
675
|
+
border-left: 3px solid
|
|
676
|
+
${({ $active }) => ($active ? 'var(--app-acc, #1f9d57)' : 'transparent')};
|
|
677
|
+
border-radius: 7px;
|
|
678
|
+
background: ${({ $active }) =>
|
|
679
|
+
$active ? 'var(--app-bg, #eef7f1)' : 'transparent'};
|
|
680
|
+
color: ${({ $active }) => ($active ? 'var(--app-text, #15703f)' : '#4a4a50')};
|
|
681
|
+
font: inherit;
|
|
682
|
+
font-size: 14px;
|
|
683
|
+
font-weight: ${({ $active }) => ($active ? 600 : 400)};
|
|
684
|
+
text-align: left;
|
|
685
|
+
cursor: pointer;
|
|
686
|
+
|
|
687
|
+
&:hover {
|
|
688
|
+
background: ${({ $active }) =>
|
|
689
|
+
$active ? 'var(--app-bg, #eef7f1)' : 'rgba(20, 18, 22, 0.05)'};
|
|
690
|
+
}
|
|
691
|
+
`
|
|
692
|
+
|
|
693
|
+
const RailCount = styled.span`
|
|
694
|
+
margin-left: auto;
|
|
695
|
+
font-family: ${monoStack};
|
|
696
|
+
font-size: 11px;
|
|
697
|
+
color: ${faint};
|
|
698
|
+
`
|
|
699
|
+
|
|
700
|
+
const RailFooter = styled.div`
|
|
701
|
+
margin-top: auto;
|
|
702
|
+
padding-top: 22px;
|
|
703
|
+
border-top: 1px solid ${line};
|
|
704
|
+
`
|
|
705
|
+
|
|
706
|
+
const StorageText = styled.div`
|
|
707
|
+
font-size: 13px;
|
|
708
|
+
line-height: 1.5;
|
|
709
|
+
color: #6a6a70;
|
|
710
|
+
|
|
711
|
+
strong {
|
|
712
|
+
color: ${ink};
|
|
713
|
+
font-weight: 600;
|
|
714
|
+
}
|
|
715
|
+
`
|
|
716
|
+
|
|
717
|
+
const StorageMeta = styled.span`
|
|
718
|
+
display: block;
|
|
719
|
+
font-family: ${monoStack};
|
|
720
|
+
font-size: 11px;
|
|
721
|
+
color: ${faint};
|
|
722
|
+
`
|
|
723
|
+
|
|
724
|
+
const Main = styled.div`
|
|
725
|
+
display: flex;
|
|
726
|
+
flex-direction: column;
|
|
727
|
+
min-width: 0;
|
|
728
|
+
`
|
|
729
|
+
|
|
730
|
+
const MainHead = styled.div`
|
|
731
|
+
display: flex;
|
|
732
|
+
align-items: flex-start;
|
|
733
|
+
justify-content: space-between;
|
|
734
|
+
gap: 24px;
|
|
735
|
+
padding: 28px 40px 20px;
|
|
736
|
+
border-bottom: 1px solid ${line};
|
|
737
|
+
`
|
|
738
|
+
|
|
739
|
+
const Heading = styled.div`
|
|
740
|
+
font-family: ${serifStack};
|
|
741
|
+
font-size: 31px;
|
|
742
|
+
font-weight: 600;
|
|
743
|
+
letter-spacing: -0.01em;
|
|
744
|
+
line-height: 1.1;
|
|
745
|
+
`
|
|
746
|
+
|
|
747
|
+
const SubStats = styled.div`
|
|
748
|
+
margin-top: 6px;
|
|
749
|
+
font-family: ${monoStack};
|
|
750
|
+
font-size: 12px;
|
|
751
|
+
letter-spacing: 0.02em;
|
|
752
|
+
color: ${faint};
|
|
753
|
+
`
|
|
754
|
+
|
|
755
|
+
const HeadControls = styled.div`
|
|
756
|
+
display: flex;
|
|
757
|
+
align-items: center;
|
|
758
|
+
gap: 12px;
|
|
759
|
+
`
|
|
760
|
+
|
|
761
|
+
const FilterWrap = styled.div`
|
|
762
|
+
position: relative;
|
|
763
|
+
display: flex;
|
|
764
|
+
align-items: center;
|
|
765
|
+
|
|
766
|
+
svg {
|
|
767
|
+
position: absolute;
|
|
768
|
+
left: 13px;
|
|
769
|
+
color: ${faint};
|
|
770
|
+
pointer-events: none;
|
|
771
|
+
}
|
|
772
|
+
`
|
|
773
|
+
|
|
774
|
+
const FilterInput = styled.input`
|
|
775
|
+
width: 260px;
|
|
776
|
+
padding: 11px 14px 11px 38px;
|
|
777
|
+
border: 1px solid ${line};
|
|
778
|
+
border-radius: 8px;
|
|
779
|
+
background: ${surface};
|
|
780
|
+
color: ${ink};
|
|
781
|
+
font: inherit;
|
|
782
|
+
font-size: 14px;
|
|
783
|
+
outline: none;
|
|
784
|
+
|
|
785
|
+
&:focus {
|
|
786
|
+
border-color: var(--app-acc, #1f9d57);
|
|
787
|
+
}
|
|
788
|
+
`
|
|
789
|
+
|
|
790
|
+
const CloseButton = styled.button`
|
|
791
|
+
display: flex;
|
|
792
|
+
align-items: center;
|
|
793
|
+
justify-content: center;
|
|
794
|
+
width: 38px;
|
|
795
|
+
height: 38px;
|
|
796
|
+
border: 1px solid ${line};
|
|
797
|
+
border-radius: 8px;
|
|
798
|
+
background: ${elevated};
|
|
799
|
+
color: #6a6a70;
|
|
800
|
+
cursor: pointer;
|
|
801
|
+
|
|
802
|
+
&:hover {
|
|
803
|
+
background: ${surface};
|
|
804
|
+
}
|
|
805
|
+
`
|
|
806
|
+
|
|
807
|
+
const Body = styled.div`
|
|
808
|
+
flex: 1;
|
|
809
|
+
min-height: 0;
|
|
810
|
+
overflow-y: auto;
|
|
811
|
+
padding: 26px 40px 40px;
|
|
812
|
+
`
|
|
813
|
+
|
|
814
|
+
const CardRow = styled.div`
|
|
815
|
+
display: grid;
|
|
816
|
+
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
|
817
|
+
gap: 16px;
|
|
818
|
+
margin-bottom: 34px;
|
|
819
|
+
`
|
|
820
|
+
|
|
821
|
+
const cardBase = `
|
|
822
|
+
min-height: 190px;
|
|
823
|
+
border-radius: 10px;
|
|
824
|
+
cursor: pointer;
|
|
825
|
+
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
|
|
826
|
+
|
|
827
|
+
&:hover {
|
|
828
|
+
transform: translateY(-3px);
|
|
829
|
+
box-shadow: 0 12px 30px rgba(14, 14, 17, 0.16);
|
|
830
|
+
}
|
|
831
|
+
`
|
|
832
|
+
|
|
833
|
+
const NewCard = styled.button`
|
|
834
|
+
${cardBase}
|
|
835
|
+
display: flex;
|
|
836
|
+
flex-direction: column;
|
|
837
|
+
align-items: center;
|
|
838
|
+
justify-content: center;
|
|
839
|
+
gap: 12px;
|
|
840
|
+
border: 1.5px dashed var(--app-acc, #1f9d57);
|
|
841
|
+
background: var(--app-bg, #eef7f1);
|
|
842
|
+
font: inherit;
|
|
843
|
+
`
|
|
844
|
+
|
|
845
|
+
const NewIcon = styled.span`
|
|
846
|
+
display: flex;
|
|
847
|
+
align-items: center;
|
|
848
|
+
justify-content: center;
|
|
849
|
+
width: 46px;
|
|
850
|
+
height: 46px;
|
|
851
|
+
border-radius: 11px;
|
|
852
|
+
background: var(--app-block, #235240);
|
|
853
|
+
color: #fff;
|
|
854
|
+
`
|
|
855
|
+
|
|
856
|
+
const NewLabel = styled.span`
|
|
857
|
+
font-size: 15px;
|
|
858
|
+
font-weight: 600;
|
|
859
|
+
color: var(--app-text, #15703f);
|
|
860
|
+
`
|
|
861
|
+
|
|
862
|
+
const NewHint = styled.span`
|
|
863
|
+
font-family: ${monoStack};
|
|
864
|
+
font-size: 10.5px;
|
|
865
|
+
color: var(--app-text, #15703f);
|
|
866
|
+
opacity: 0.7;
|
|
867
|
+
`
|
|
868
|
+
|
|
869
|
+
const PreviewCard = styled.button`
|
|
870
|
+
${cardBase}
|
|
871
|
+
display: flex;
|
|
872
|
+
flex-direction: column;
|
|
873
|
+
overflow: hidden;
|
|
874
|
+
border: 1px solid ${line};
|
|
875
|
+
background: ${elevated};
|
|
876
|
+
font: inherit;
|
|
877
|
+
text-align: left;
|
|
878
|
+
`
|
|
879
|
+
|
|
880
|
+
const PreviewThumb = styled.div`
|
|
881
|
+
flex: 1;
|
|
882
|
+
min-height: 0;
|
|
883
|
+
padding: 14px;
|
|
884
|
+
background: ${surface};
|
|
885
|
+
border-bottom: 1px solid ${line};
|
|
886
|
+
overflow: hidden;
|
|
887
|
+
`
|
|
888
|
+
|
|
889
|
+
const PreviewMeta = styled.div`
|
|
890
|
+
display: flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
justify-content: space-between;
|
|
893
|
+
gap: 8px;
|
|
894
|
+
padding: 13px 15px;
|
|
895
|
+
`
|
|
896
|
+
|
|
897
|
+
const PreviewName = styled.div<{ $serif?: boolean }>`
|
|
898
|
+
overflow: hidden;
|
|
899
|
+
font-family: ${({ $serif }) => ($serif ? serifStack : sansStack)};
|
|
900
|
+
font-size: 14.5px;
|
|
901
|
+
font-weight: 600;
|
|
902
|
+
white-space: nowrap;
|
|
903
|
+
text-overflow: ellipsis;
|
|
904
|
+
`
|
|
905
|
+
|
|
906
|
+
const PreviewWhen = styled.div`
|
|
907
|
+
flex: none;
|
|
908
|
+
font-family: ${monoStack};
|
|
909
|
+
font-size: 10.5px;
|
|
910
|
+
color: ${faint};
|
|
911
|
+
`
|
|
912
|
+
|
|
913
|
+
const ListHead = styled.div`
|
|
914
|
+
display: flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
justify-content: space-between;
|
|
917
|
+
margin-bottom: 12px;
|
|
918
|
+
`
|
|
919
|
+
|
|
920
|
+
const ListKicker = styled.div`
|
|
921
|
+
font-family: ${monoStack};
|
|
922
|
+
font-size: 11px;
|
|
923
|
+
font-weight: 500;
|
|
924
|
+
letter-spacing: 0.12em;
|
|
925
|
+
text-transform: uppercase;
|
|
926
|
+
color: #6a6a70;
|
|
927
|
+
`
|
|
928
|
+
|
|
929
|
+
const SortControls = styled.div`
|
|
930
|
+
display: flex;
|
|
931
|
+
gap: 16px;
|
|
932
|
+
`
|
|
933
|
+
|
|
934
|
+
const SortButton = styled.button<{ $active?: boolean }>`
|
|
935
|
+
padding: 0 0 3px;
|
|
936
|
+
border: 0;
|
|
937
|
+
border-bottom: 2px solid
|
|
938
|
+
${({ $active }) => ($active ? 'var(--app-acc, #1f9d57)' : 'transparent')};
|
|
939
|
+
background: transparent;
|
|
940
|
+
color: ${({ $active }) => ($active ? 'var(--app-text, #15703f)' : faint)};
|
|
941
|
+
font-family: ${monoStack};
|
|
942
|
+
font-size: 11px;
|
|
943
|
+
cursor: pointer;
|
|
944
|
+
`
|
|
945
|
+
|
|
946
|
+
const ListBox = styled.div`
|
|
947
|
+
overflow: hidden;
|
|
948
|
+
border: 1px solid ${line};
|
|
949
|
+
border-radius: 10px;
|
|
950
|
+
`
|
|
951
|
+
|
|
952
|
+
const Row = styled.button<{ $selected?: boolean }>`
|
|
953
|
+
display: grid;
|
|
954
|
+
grid-template-columns: 34px 1fr 96px 92px;
|
|
955
|
+
align-items: center;
|
|
956
|
+
gap: 14px;
|
|
957
|
+
width: 100%;
|
|
958
|
+
padding: 13px 18px;
|
|
959
|
+
border: 0;
|
|
960
|
+
border-bottom: 1px solid ${line};
|
|
961
|
+
background: ${({ $selected }) =>
|
|
962
|
+
$selected ? 'var(--app-bg, #eef7f1)' : 'transparent'};
|
|
963
|
+
font: inherit;
|
|
964
|
+
text-align: left;
|
|
965
|
+
cursor: pointer;
|
|
966
|
+
|
|
967
|
+
&:last-child {
|
|
968
|
+
border-bottom: 0;
|
|
969
|
+
}
|
|
970
|
+
`
|
|
971
|
+
|
|
972
|
+
const RowIcon = styled.span`
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
justify-content: center;
|
|
976
|
+
width: 34px;
|
|
977
|
+
height: 34px;
|
|
978
|
+
border-radius: 8px;
|
|
979
|
+
background: var(--app-bg, #eef7f1);
|
|
980
|
+
color: var(--app-acc, #1f9d57);
|
|
981
|
+
`
|
|
982
|
+
|
|
983
|
+
const RowMain = styled.div`
|
|
984
|
+
min-width: 0;
|
|
985
|
+
`
|
|
986
|
+
|
|
987
|
+
const RowName = styled.div<{ $serif?: boolean }>`
|
|
988
|
+
overflow: hidden;
|
|
989
|
+
font-family: ${({ $serif }) => ($serif ? serifStack : sansStack)};
|
|
990
|
+
font-size: 14.5px;
|
|
991
|
+
font-weight: 600;
|
|
992
|
+
white-space: nowrap;
|
|
993
|
+
text-overflow: ellipsis;
|
|
994
|
+
`
|
|
995
|
+
|
|
996
|
+
const RowMeta = styled.div`
|
|
997
|
+
margin-top: 2px;
|
|
998
|
+
font-family: ${monoStack};
|
|
999
|
+
font-size: 10.5px;
|
|
1000
|
+
color: ${faint};
|
|
1001
|
+
`
|
|
1002
|
+
|
|
1003
|
+
const RowWhen = styled.div`
|
|
1004
|
+
font-family: ${monoStack};
|
|
1005
|
+
font-size: 11.5px;
|
|
1006
|
+
color: #6a6a70;
|
|
1007
|
+
`
|
|
1008
|
+
|
|
1009
|
+
const RowBadge = styled.span<{ $draft?: boolean }>`
|
|
1010
|
+
justify-self: end;
|
|
1011
|
+
padding: 3px 9px;
|
|
1012
|
+
border-radius: 999px;
|
|
1013
|
+
font-family: ${monoStack};
|
|
1014
|
+
font-size: 10px;
|
|
1015
|
+
letter-spacing: 0.06em;
|
|
1016
|
+
text-transform: uppercase;
|
|
1017
|
+
color: ${({ $draft }) => ($draft ? '#8a5a12' : '#6a6a70')};
|
|
1018
|
+
background: ${({ $draft }) => ($draft ? '#f6ecd6' : 'rgba(20,18,22,0.05)')};
|
|
1019
|
+
`
|
|
1020
|
+
|
|
1021
|
+
const EmptyNote = styled.p`
|
|
1022
|
+
margin: 0;
|
|
1023
|
+
padding: 34px 20px;
|
|
1024
|
+
text-align: center;
|
|
1025
|
+
color: ${faint};
|
|
1026
|
+
font-size: 13.5px;
|
|
1027
|
+
`
|
|
1028
|
+
|
|
1029
|
+
const FooterBar = styled.div`
|
|
1030
|
+
display: flex;
|
|
1031
|
+
align-items: center;
|
|
1032
|
+
justify-content: space-between;
|
|
1033
|
+
padding: 16px 40px;
|
|
1034
|
+
border-top: 1px solid ${line};
|
|
1035
|
+
background: ${surface};
|
|
1036
|
+
`
|
|
1037
|
+
|
|
1038
|
+
const FooterHints = styled.div`
|
|
1039
|
+
font-family: ${monoStack};
|
|
1040
|
+
font-size: 11px;
|
|
1041
|
+
color: ${faint};
|
|
1042
|
+
`
|
|
1043
|
+
|
|
1044
|
+
const FooterActions = styled.div`
|
|
1045
|
+
display: flex;
|
|
1046
|
+
gap: 10px;
|
|
1047
|
+
`
|
|
1048
|
+
|
|
1049
|
+
const SecondaryButton = styled.button`
|
|
1050
|
+
padding: 9px 16px;
|
|
1051
|
+
border: 1px solid ${line};
|
|
1052
|
+
border-radius: 8px;
|
|
1053
|
+
background: ${elevated};
|
|
1054
|
+
color: #4a4a50;
|
|
1055
|
+
font: inherit;
|
|
1056
|
+
font-size: 13px;
|
|
1057
|
+
font-weight: 600;
|
|
1058
|
+
cursor: pointer;
|
|
1059
|
+
|
|
1060
|
+
&:hover {
|
|
1061
|
+
background: ${surface};
|
|
1062
|
+
}
|
|
1063
|
+
`
|
|
1064
|
+
|
|
1065
|
+
const PrimaryButton = styled.button`
|
|
1066
|
+
padding: 9px 18px;
|
|
1067
|
+
border: 0;
|
|
1068
|
+
border-radius: 8px;
|
|
1069
|
+
background: var(--app-block, #235240);
|
|
1070
|
+
color: #fff;
|
|
1071
|
+
font: inherit;
|
|
1072
|
+
font-size: 13px;
|
|
1073
|
+
font-weight: 600;
|
|
1074
|
+
cursor: pointer;
|
|
1075
|
+
|
|
1076
|
+
&:disabled {
|
|
1077
|
+
opacity: 0.5;
|
|
1078
|
+
cursor: not-allowed;
|
|
1079
|
+
}
|
|
1080
|
+
`
|
|
1081
|
+
|
|
1082
|
+
// --- thumbnails ---
|
|
1083
|
+
|
|
1084
|
+
const ThumbFallback = styled.div`
|
|
1085
|
+
display: flex;
|
|
1086
|
+
align-items: center;
|
|
1087
|
+
justify-content: center;
|
|
1088
|
+
width: 100%;
|
|
1089
|
+
height: 100%;
|
|
1090
|
+
color: ${faint};
|
|
1091
|
+
`
|
|
1092
|
+
|
|
1093
|
+
const ThumbDoc = styled.div`
|
|
1094
|
+
display: flex;
|
|
1095
|
+
flex-direction: column;
|
|
1096
|
+
gap: 5px;
|
|
1097
|
+
`
|
|
1098
|
+
|
|
1099
|
+
const ThumbDocTitle = styled.div<{ $serif?: boolean }>`
|
|
1100
|
+
margin-bottom: 4px;
|
|
1101
|
+
font-family: ${({ $serif }) => ($serif ? serifStack : sansStack)};
|
|
1102
|
+
font-size: 13px;
|
|
1103
|
+
font-weight: 600;
|
|
1104
|
+
line-height: 1.25;
|
|
1105
|
+
color: ${ink};
|
|
1106
|
+
overflow: hidden;
|
|
1107
|
+
display: -webkit-box;
|
|
1108
|
+
-webkit-line-clamp: 2;
|
|
1109
|
+
-webkit-box-orient: vertical;
|
|
1110
|
+
`
|
|
1111
|
+
|
|
1112
|
+
const ThumbLine = styled.div`
|
|
1113
|
+
height: 5px;
|
|
1114
|
+
border-radius: 2px;
|
|
1115
|
+
background: rgba(20, 18, 22, 0.12);
|
|
1116
|
+
`
|
|
1117
|
+
|
|
1118
|
+
const ThumbPills = styled.div`
|
|
1119
|
+
display: flex;
|
|
1120
|
+
gap: 5px;
|
|
1121
|
+
margin-top: 6px;
|
|
1122
|
+
`
|
|
1123
|
+
|
|
1124
|
+
const ThumbPill = styled.span<{ $accent?: boolean }>`
|
|
1125
|
+
height: 5px;
|
|
1126
|
+
border-radius: 999px;
|
|
1127
|
+
background: ${({ $accent }) =>
|
|
1128
|
+
$accent ? 'var(--app-acc, #1f9d57)' : 'rgba(20,18,22,0.15)'};
|
|
1129
|
+
`
|
|
1130
|
+
|
|
1131
|
+
const ThumbGrid = styled.div`
|
|
1132
|
+
display: grid;
|
|
1133
|
+
grid-template-columns: 1fr 1fr;
|
|
1134
|
+
grid-template-rows: 1fr 1fr;
|
|
1135
|
+
gap: 6px;
|
|
1136
|
+
width: 100%;
|
|
1137
|
+
height: 100%;
|
|
1138
|
+
`
|
|
1139
|
+
|
|
1140
|
+
const ThumbCell = styled.div<{ $accent?: boolean }>`
|
|
1141
|
+
border-radius: 4px;
|
|
1142
|
+
background: ${elevated};
|
|
1143
|
+
border: 1px solid ${line};
|
|
1144
|
+
border-left: ${({ $accent }) =>
|
|
1145
|
+
$accent ? '2px solid var(--app-acc, #1f9d57)' : `1px solid ${line}`};
|
|
1146
|
+
`
|
|
1147
|
+
|
|
1148
|
+
const ThumbCols = styled.div`
|
|
1149
|
+
display: flex;
|
|
1150
|
+
gap: 7px;
|
|
1151
|
+
width: 100%;
|
|
1152
|
+
`
|
|
1153
|
+
|
|
1154
|
+
const ThumbCol = styled.div`
|
|
1155
|
+
display: flex;
|
|
1156
|
+
flex: 1;
|
|
1157
|
+
flex-direction: column;
|
|
1158
|
+
gap: 5px;
|
|
1159
|
+
`
|
|
1160
|
+
|
|
1161
|
+
const ThumbColHead = styled.div`
|
|
1162
|
+
height: 6px;
|
|
1163
|
+
border-radius: 2px;
|
|
1164
|
+
background: rgba(20, 18, 22, 0.14);
|
|
1165
|
+
`
|
|
1166
|
+
|
|
1167
|
+
const ThumbBlock = styled.div<{ $accent?: boolean }>`
|
|
1168
|
+
height: 22px;
|
|
1169
|
+
border-radius: 3px;
|
|
1170
|
+
background: ${elevated};
|
|
1171
|
+
border: 1px solid ${line};
|
|
1172
|
+
border-left: ${({ $accent }) =>
|
|
1173
|
+
$accent ? '2px solid var(--app-acc, #1f9d57)' : `1px solid ${line}`};
|
|
1174
|
+
`
|
|
1175
|
+
|
|
1176
|
+
//#endregion
|