@open-press/core 0.8.0 → 1.0.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.
Files changed (64) hide show
  1. package/README.md +6 -3
  2. package/engine/cli.mjs +8 -8
  3. package/engine/commands/_shared.mjs +37 -15
  4. package/engine/commands/image.mjs +29 -0
  5. package/engine/commands/skills-sync.mjs +71 -0
  6. package/engine/commands/typecheck.mjs +63 -1
  7. package/engine/commands/upgrade.mjs +3 -3
  8. package/engine/document-export.mjs +1 -1
  9. package/engine/output/chrome-pdf.mjs +92 -0
  10. package/engine/output/static-server.mjs +48 -9
  11. package/engine/react/comment-marker.mjs +13 -13
  12. package/engine/react/document-entry.mjs +35 -28
  13. package/engine/react/document-export.mjs +309 -170
  14. package/engine/react/mdx-compile.mjs +30 -0
  15. package/engine/react/measurement-css.mjs +21 -0
  16. package/engine/react/object-entities.mjs +85 -0
  17. package/engine/react/pagination/allocator.mjs +48 -3
  18. package/engine/react/pagination.mjs +1 -1
  19. package/engine/react/pipeline/allocate.mjs +31 -65
  20. package/engine/react/pipeline/frame-measurement.mjs +4 -0
  21. package/engine/react/press-tree-inspection.mjs +172 -0
  22. package/engine/react/sources/mdx-resolver.mjs +1 -1
  23. package/engine/react/style-discovery.mjs +22 -4
  24. package/engine/runtime/config.d.mts +8 -0
  25. package/engine/runtime/config.mjs +57 -60
  26. package/engine/runtime/file-utils.mjs +9 -1
  27. package/engine/runtime/page-geometry.mjs +131 -0
  28. package/engine/runtime/source-text-tools.mjs +1 -1
  29. package/engine/runtime/source-workspace.mjs +12 -3
  30. package/engine/runtime/validation.mjs +19 -10
  31. package/package.json +3 -5
  32. package/src/openpress/app/OpenPressApp.tsx +173 -17
  33. package/src/openpress/app/OpenPressRuntime.tsx +10 -2
  34. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  35. package/src/openpress/core/Frame.tsx +20 -7
  36. package/src/openpress/core/FrameContext.tsx +2 -0
  37. package/src/openpress/core/Press.tsx +25 -4
  38. package/src/openpress/core/Workspace.tsx +36 -0
  39. package/src/openpress/core/index.tsx +10 -3
  40. package/src/openpress/core/primitives.tsx +48 -1
  41. package/src/openpress/core/types.ts +86 -41
  42. package/src/openpress/core/useSource.ts +1 -1
  43. package/src/openpress/document-model/documentTypes.ts +9 -0
  44. package/src/openpress/document-model/index.ts +1 -0
  45. package/src/openpress/document-model/objectEntityModel.ts +4 -0
  46. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  47. package/src/openpress/mdx/index.ts +15 -7
  48. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  49. package/src/openpress/reader/index.ts +1 -0
  50. package/src/openpress/workbench/Workbench.tsx +120 -21
  51. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  52. package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
  53. package/src/openpress/workbench/actions/index.ts +1 -0
  54. package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
  55. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
  56. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
  57. package/src/openpress/workbench/workbenchFormatters.ts +2 -2
  58. package/src/styles/openpress/reader-runtime.css +9 -0
  59. package/src/styles/openpress/workbench-panels.css +113 -0
  60. package/src/styles/openpress/workspace-gallery.css +300 -0
  61. package/src/styles/openpress.css +1 -0
  62. package/tsconfig.json +1 -1
  63. package/engine/commands/init.mjs +0 -24
  64. package/engine/init.mjs +0 -90
@@ -4,7 +4,7 @@ import {
4
4
  useState,
5
5
  type CSSProperties,
6
6
  } from "react";
7
- import { ExternalLink, MousePointer2, Ruler } from "lucide-react";
7
+ import { ExternalLink, Home, MousePointer2, Ruler } from "lucide-react";
8
8
  import {
9
9
  getProjectIdentity,
10
10
  resolveAnchorPageIndex,
@@ -17,6 +17,7 @@ import { ProjectEntryPanel } from "./project";
17
17
  import {
18
18
  Bookmarks,
19
19
  CurrentPagePanel,
20
+ PageThumbnails,
20
21
  PUBLIC_DRAWER_BREAKPOINT,
21
22
  PublicPage,
22
23
  useReaderRuntime,
@@ -34,6 +35,7 @@ import {
34
35
  } from "./document";
35
36
  import {
36
37
  DeploymentControl,
38
+ ExportImageControl,
37
39
  PageZoomControl,
38
40
  SearchControl,
39
41
  useDeploymentWorkbench,
@@ -52,6 +54,7 @@ export function HtmlWorkbench({
52
54
  devMode,
53
55
  deploymentInfo,
54
56
  onDocumentRefresh,
57
+ onBackToWorkspace,
55
58
  extraControlPanels,
56
59
  }: {
57
60
  document: ReaderDocument;
@@ -60,6 +63,7 @@ export function HtmlWorkbench({
60
63
  devMode: boolean;
61
64
  deploymentInfo: DeploymentInfo;
62
65
  onDocumentRefresh?: () => void | Promise<void>;
66
+ onBackToWorkspace?: () => void;
63
67
  // Append extra panels into the right-side control panel. Built-in panels
64
68
  // (pending comments + project entry) render first; extra panels render
65
69
  // after them in the supplied order.
@@ -103,8 +107,15 @@ export function HtmlWorkbench({
103
107
  const inspectorToolbarExpanded = inspector.inspectorMode;
104
108
  const editStatusMessage = formatInlineEditStatus(inlineEditStatus);
105
109
 
110
+ // Inline source editing and inspector commenting are mutually exclusive
111
+ // interaction modes on the same blocks. While inspector mode is on, the
112
+ // user is selecting blocks to comment on — keeping contenteditable + the
113
+ // text cursor active would (a) show the I-beam instead of the inspector
114
+ // crosshair, (b) allow accidental text selection that paints the whole
115
+ // page (notably covers) with the browser ::selection color.
116
+ const inlineEditEnabled = devMode && !inspector.inspectorMode;
106
117
  useInlineDocumentEditor({
107
- enabled: devMode,
118
+ enabled: inlineEditEnabled,
108
119
  sourceContainerRef,
109
120
  sourceBlockMap,
110
121
  onStatusChange: setInlineEditStatus,
@@ -173,7 +184,12 @@ export function HtmlWorkbench({
173
184
  ]);
174
185
 
175
186
  const currentSourcePath = displayPages[reader.currentPageIndex]?.source;
176
- const builtInControlPanels: WorkbenchPanel[] = [
187
+ // Stabilize the panel registry across keystrokes in the inspector
188
+ // composer. Without `useMemo` the registry array (and the JSX closures
189
+ // inside) would be recreated on every Workbench render, so typing a
190
+ // single character would force WorkbenchControlPanel + every panel to
191
+ // diff fresh React elements.
192
+ const builtInControlPanels = useMemo<WorkbenchPanel[]>(() => [
177
193
  {
178
194
  id: "pending-comments",
179
195
  render: () => (
@@ -198,13 +214,43 @@ export function HtmlWorkbench({
198
214
  />
199
215
  ),
200
216
  },
201
- ];
202
- const controlPanels = extraControlPanels
203
- ? [...builtInControlPanels, ...extraControlPanels]
204
- : builtInControlPanels;
217
+ ], [
218
+ comments.clearPendingComment,
219
+ comments.commentsError,
220
+ comments.commentsStatus,
221
+ comments.handleSelectPendingComment,
222
+ comments.pendingComments,
223
+ comments.refreshPendingComments,
224
+ currentSourcePath,
225
+ mediaAssets,
226
+ projectComponentUsages,
227
+ projectMentionItems,
228
+ ]);
229
+ const controlPanels = useMemo(
230
+ () => (extraControlPanels ? [...builtInControlPanels, ...extraControlPanels] : builtInControlPanels),
231
+ [builtInControlPanels, extraControlPanels],
232
+ );
205
233
 
206
- const toolbarActions = (
234
+ // Memoize so composer keystrokes (which only flip `comments.inspectorCommentText`)
235
+ // don't rebuild the toolbar JSX. The toolbar depends on deploy/page/zoom
236
+ // state and inspector mode, but never on the composer draft text.
237
+ const toolbarActions = useMemo(() => (
207
238
  <>
239
+ {onBackToWorkspace ? (
240
+ <div className="openpress-workbench-toolbar__group" aria-label="工作台導覽">
241
+ <button
242
+ type="button"
243
+ className="openpress-workbench-toolbar-action openpress-workbench-toolbar-action--back"
244
+ data-openpress-back-to-workspace
245
+ onClick={onBackToWorkspace}
246
+ title="回到工作台"
247
+ aria-label="回到工作台"
248
+ >
249
+ <Home aria-hidden="true" />
250
+ <span className="openpress-workbench-toolbar-action__label">工作台</span>
251
+ </button>
252
+ </div>
253
+ ) : null}
208
254
  <div className="openpress-workbench-toolbar__group" aria-label="輸出">
209
255
  <button
210
256
  type="button"
@@ -231,6 +277,11 @@ export function HtmlWorkbench({
231
277
  </span>
232
278
  ) : null}
233
279
  </button>
280
+ <ExportImageControl
281
+ currentPageIndex={reader.currentPageIndex}
282
+ currentPageLabel={reader.currentPageLabel}
283
+ pressTitle={projectIdentity.name}
284
+ />
234
285
  </div>
235
286
  <div className="openpress-workbench-toolbar__group openpress-workbench-toolbar__group--page" aria-label="頁面規格">
236
287
  <button
@@ -307,7 +358,40 @@ export function HtmlWorkbench({
307
358
  ) : null}
308
359
  </div>
309
360
  </>
310
- );
361
+ ), [
362
+ comments.inspectorCommentStatus,
363
+ comments.inspectorCommentStatusMessage,
364
+ deployment.currentDeploymentInfo,
365
+ deployment.handleDeploy,
366
+ deployment.handleOpenWorkbenchPdf,
367
+ deployment.localDeployEnabled,
368
+ deployment.pdfActionStatus,
369
+ deployment.pdfButtonDisabled,
370
+ deployment.pdfButtonText,
371
+ deployment.pdfStatusMessage,
372
+ deployment.pdfToolbarExpanded,
373
+ deployment.status,
374
+ devMode,
375
+ editStatusMessage,
376
+ inlineEditStatus.state,
377
+ inspector.inspectorMode,
378
+ inspector.setInspectorMode,
379
+ inspectorSelectionLabel,
380
+ inspectorToolbarExpanded,
381
+ pageGeometry.dimensions,
382
+ pageGeometry.label,
383
+ pageGeometry.title,
384
+ pageLayoutMode,
385
+ pageViewport.scaleLabel,
386
+ pageViewport.scaleMode,
387
+ pageViewport.setScaleMode,
388
+ selectWorkspacePage,
389
+ sourceBlocksByPath,
390
+ onBackToWorkspace,
391
+ reader.currentPageIndex,
392
+ reader.currentPageLabel,
393
+ projectIdentity.name,
394
+ ]);
311
395
 
312
396
  return (
313
397
  <WorkbenchShell
@@ -315,7 +399,7 @@ export function HtmlWorkbench({
315
399
  devMode={devMode}
316
400
  viewMode={viewMode}
317
401
  inspectorMode={inspector.inspectorMode}
318
- editMode={devMode}
402
+ editMode={inlineEditEnabled}
319
403
  leftPanelOpen={reader.leftPanelOpen}
320
404
  rightPanelOpen={reader.rightPanelOpen}
321
405
  onToggleLeftPanel={reader.toggleLeftPanel}
@@ -334,20 +418,35 @@ export function HtmlWorkbench({
334
418
  {projectIdentity.label ? <span>{projectIdentity.label}</span> : null}
335
419
  </section>
336
420
 
337
- <section
338
- id="openpress-bookmarks"
339
- className="openpress-panel-section openpress-panel-section--bookmarks"
340
- aria-label="章節書籤"
341
- >
342
- <nav className="reader-bookmarks" aria-label="章節導覽" data-openpress-react-bookmarks="true">
343
- <div className="reader-bookmarks-rail" aria-hidden="true" />
344
- <Bookmarks
345
- items={bookmarks}
421
+ {bookmarks.length > 0 ? (
422
+ <section
423
+ id="openpress-bookmarks"
424
+ className="openpress-panel-section openpress-panel-section--bookmarks"
425
+ aria-label="章節書籤"
426
+ >
427
+ <nav className="reader-bookmarks" aria-label="章節導覽" data-openpress-react-bookmarks="true">
428
+ <div className="reader-bookmarks-rail" aria-hidden="true" />
429
+ <Bookmarks
430
+ items={bookmarks}
431
+ currentPageIndex={reader.currentPageIndex}
432
+ onSelectPage={selectWorkspacePage}
433
+ />
434
+ </nav>
435
+ </section>
436
+ ) : (
437
+ <section
438
+ id="openpress-thumbnails"
439
+ className="openpress-panel-section openpress-panel-section--thumbnails"
440
+ aria-label="頁面縮圖"
441
+ >
442
+ <PageThumbnails
443
+ pages={displayPages}
346
444
  currentPageIndex={reader.currentPageIndex}
347
445
  onSelectPage={selectWorkspacePage}
446
+ theme={document.theme}
348
447
  />
349
- </nav>
350
- </section>
448
+ </section>
449
+ )}
351
450
  <CurrentPagePanel
352
451
  currentPageLabel={reader.currentPageLabel}
353
452
  totalPageLabel={reader.totalPageLabel}
@@ -0,0 +1,96 @@
1
+ import { useCallback, useState } from "react";
2
+ import { Camera } from "lucide-react";
3
+ import { toPng } from "html-to-image";
4
+
5
+ type ExportStatus = "idle" | "exporting" | "done" | "error";
6
+
7
+ // Exports the currently visible page as a PNG. Locates the page DOM via
8
+ // the data-openpress-page-index attribute (set in PublicReaderPage) and
9
+ // hands it to html-to-image, then triggers a browser download.
10
+ //
11
+ // Lives in the workbench toolbar so it's reachable for any Press shape
12
+ // (manuscript / canvas / slide); for multi-page Press the user navigates
13
+ // to the page first, then exports.
14
+ export function ExportImageControl({
15
+ currentPageIndex,
16
+ currentPageLabel,
17
+ pressTitle,
18
+ }: {
19
+ currentPageIndex: number;
20
+ currentPageLabel: string;
21
+ pressTitle: string;
22
+ }) {
23
+ const [status, setStatus] = useState<ExportStatus>("idle");
24
+
25
+ const handleExport = useCallback(async () => {
26
+ if (status === "exporting") return;
27
+ setStatus("exporting");
28
+
29
+ try {
30
+ const pageEl = typeof window === "undefined"
31
+ ? null
32
+ : window.document.querySelector<HTMLElement>(
33
+ `[data-openpress-page-index="${currentPageIndex}"]`,
34
+ );
35
+ if (!pageEl) throw new Error("找不到目前頁面");
36
+
37
+ // pixelRatio: 2 — retina-ish; keeps text crisp without blowing the file size.
38
+ // cacheBust: true — force re-fetch of images so stale CORS doesn't taint the canvas.
39
+ const dataUrl = await toPng(pageEl, {
40
+ pixelRatio: 2,
41
+ cacheBust: true,
42
+ backgroundColor: "#ffffff",
43
+ });
44
+
45
+ const safeTitle = sanitizeFilename(pressTitle) || "openpress";
46
+ const safePage = sanitizeFilename(currentPageLabel) || String(currentPageIndex + 1);
47
+ const link = window.document.createElement("a");
48
+ link.href = dataUrl;
49
+ link.download = `${safeTitle}-${safePage}.png`;
50
+ window.document.body.appendChild(link);
51
+ link.click();
52
+ link.remove();
53
+
54
+ setStatus("done");
55
+ window.setTimeout(() => setStatus("idle"), 1600);
56
+ } catch (error) {
57
+ console.error("[openpress] page PNG export failed", error);
58
+ setStatus("error");
59
+ window.setTimeout(() => setStatus("idle"), 2400);
60
+ }
61
+ }, [currentPageIndex, currentPageLabel, pressTitle, status]);
62
+
63
+ const label = status === "exporting"
64
+ ? "匯出中…"
65
+ : status === "done"
66
+ ? "已下載"
67
+ : status === "error"
68
+ ? "匯出失敗"
69
+ : "PNG";
70
+ const title = "將目前頁面匯出為 PNG";
71
+
72
+ return (
73
+ <button
74
+ type="button"
75
+ className="openpress-workbench-toolbar-action"
76
+ data-openpress-page-png-export
77
+ data-openpress-export-status={status}
78
+ disabled={status === "exporting"}
79
+ onClick={handleExport}
80
+ title={title}
81
+ aria-label={title}
82
+ >
83
+ <Camera aria-hidden="true" />
84
+ <span className="openpress-workbench-toolbar-action__label">{label}</span>
85
+ </button>
86
+ );
87
+ }
88
+
89
+ function sanitizeFilename(value: string): string {
90
+ return value
91
+ .replace(/[\\/:*?"<>|]+/g, "-")
92
+ .replace(/\s+/g, "-")
93
+ .replace(/-+/g, "-")
94
+ .replace(/^-+|-+$/g, "")
95
+ .slice(0, 80);
96
+ }
@@ -336,10 +336,10 @@ function resolveSearchJumpTarget(
336
336
  function sourcePathKeys(value: string) {
337
337
  const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\//, "");
338
338
  const keys = [normalized];
339
- if (normalized.startsWith("document/")) {
340
- keys.push(normalized.replace(/^document\//, ""));
339
+ if (normalized.startsWith("press/")) {
340
+ keys.push(normalized.replace(/^press\//, ""));
341
341
  } else {
342
- keys.push(`document/${normalized}`);
342
+ keys.push(`press/${normalized}`);
343
343
  }
344
344
  return Array.from(new Set(keys));
345
345
  }
@@ -1,5 +1,6 @@
1
1
  export * from "./deploymentStatusModel";
2
2
  export * from "./DeploymentControl";
3
+ export * from "./ExportImageControl";
3
4
  export * from "./PageZoomControl";
4
5
  export * from "./SearchControl";
5
6
  export * from "./useDeploymentWorkbench";
@@ -67,7 +67,13 @@ export function useInspectorComments({
67
67
 
68
68
  const inspectorCommentDisabled =
69
69
  !inspector.selectedBlock || !inspectorCommentText.trim() || inspectorCommentStatus === "submitting";
70
- const inspectorCommentStatusMessage = formatInspectorCommentStatus(inspectorCommentStatus, inspectorCommentError);
70
+ // Memoize the status message so its identity is stable while only
71
+ // composer text changes — the toolbar and other consumers that depend
72
+ // on it can then memoize without keystrokes invalidating their cache.
73
+ const inspectorCommentStatusMessage = useMemo(
74
+ () => formatInspectorCommentStatus(inspectorCommentStatus, inspectorCommentError),
75
+ [inspectorCommentStatus, inspectorCommentError],
76
+ );
71
77
 
72
78
  const refreshPendingComments = useCallback(async () => {
73
79
  if (!devMode) return;
@@ -1,3 +1,4 @@
1
+ import { memo } from "react";
1
2
  import { Trash2 } from "lucide-react";
2
3
  import type { PendingComment } from "../inspector";
3
4
  import {
@@ -7,7 +8,7 @@ import {
7
8
  import type { PendingCommentsStatus } from "../workbenchTypes";
8
9
  import { Panel } from "./Panel";
9
10
 
10
- export function PendingCommentsPanel({
11
+ function PendingCommentsPanelImpl({
11
12
  comments,
12
13
  status,
13
14
  error,
@@ -74,3 +75,6 @@ export function PendingCommentsPanel({
74
75
  </Panel>
75
76
  );
76
77
  }
78
+
79
+ export const PendingCommentsPanel = memo(PendingCommentsPanelImpl);
80
+ PendingCommentsPanel.displayName = "PendingCommentsPanel";
@@ -1,4 +1,4 @@
1
- import { useState, type CSSProperties } from "react";
1
+ import { memo, useState, type CSSProperties } from "react";
2
2
  import { Component as ComponentIcon, Images, Palette, type LucideIcon } from "lucide-react";
3
3
  import type { BookmarkItem, BookmarkSubItem, MediaAssetItem } from "../../document-model";
4
4
  import { projectSourceDirectoryPath, PROJECT_SOURCES } from "./projectSourceModel";
@@ -77,7 +77,7 @@ export function createProjectMentionItems(
77
77
  return [...PROJECT_SKILL_MENTIONS, ...referenceItems, ...mediaItems, ...componentItems];
78
78
  }
79
79
 
80
- export function ProjectEntryPanel({
80
+ function ProjectEntryPanelImpl({
81
81
  mediaAssets,
82
82
  componentUsages,
83
83
  mentionItems,
@@ -164,6 +164,8 @@ export function ProjectEntryPanel({
164
164
  );
165
165
  }
166
166
 
167
+ export const ProjectEntryPanel = memo(ProjectEntryPanelImpl);
168
+ ProjectEntryPanel.displayName = "ProjectEntryPanel";
167
169
 
168
170
  export function ProjectPreviewPanel({
169
171
  mediaAssets,
@@ -33,11 +33,11 @@ export function formatCommentsCount(count: number, status: PendingCommentsStatus
33
33
  return `${count} 則待處理`;
34
34
  }
35
35
 
36
- export function formatPageGeometrySpec(theme?: Pick<Theme, "pageWidth" | "pageHeight">): PageGeometrySpec {
36
+ export function formatPageGeometrySpec(theme?: Pick<Theme, "pageLabel" | "pageWidth" | "pageHeight">): PageGeometrySpec {
37
37
  const width = parseCssLength(theme?.pageWidth ?? DEFAULT_PAGE_GEOMETRY.pageWidth);
38
38
  const height = parseCssLength(theme?.pageHeight ?? DEFAULT_PAGE_GEOMETRY.pageHeight);
39
39
  const dimensions = formatLengthPair(width, height);
40
- const label = pageGeometryLabel(width, height);
40
+ const label = theme?.pageLabel?.trim() || pageGeometryLabel(width, height);
41
41
 
42
42
  return {
43
43
  label,
@@ -61,6 +61,15 @@
61
61
  white-space: nowrap;
62
62
  }
63
63
 
64
+ .openpress-reader-app[data-openpress-inspector-mode="on"] .openpress-html-page__html {
65
+ /* Inspector mode is a click-to-select-block flow. Disable text selection
66
+ across the rendered page so drag-selects / Cmd+A don't paint the whole
67
+ page with the browser ::selection color — most visible on covers and
68
+ back-covers where there's little text and lots of background. */
69
+ user-select: none;
70
+ -webkit-user-select: none;
71
+ }
72
+
64
73
  .openpress-reader-app[data-openpress-inspector-mode="on"] .openpress-html-page__html [data-openpress-block-id] {
65
74
  cursor: crosshair;
66
75
  }
@@ -592,3 +592,116 @@
592
592
  clip: rect(0 0 0 0);
593
593
  white-space: nowrap;
594
594
  }
595
+
596
+ /* Page thumbnails — left panel fallback when no MDX bookmarks exist
597
+ (canvas-style Press: social posts, slides). Renders the actual page
598
+ HTML scaled down so the user can navigate by visual recognition. */
599
+ .openpress-reader-app .openpress-panel-section--thumbnails {
600
+ display: flex;
601
+ flex-direction: column;
602
+ gap: 10px;
603
+ padding: 8px 14px 16px;
604
+ min-height: 0;
605
+ }
606
+
607
+ .openpress-reader-app .openpress-thumb-list {
608
+ display: flex;
609
+ flex-direction: column;
610
+ gap: 10px;
611
+ margin: 0;
612
+ padding: 0;
613
+ list-style: none;
614
+ }
615
+
616
+ .openpress-reader-app .openpress-thumb-card {
617
+ display: grid;
618
+ grid-template-columns: 20px minmax(0, 1fr);
619
+ align-items: stretch;
620
+ gap: 6px;
621
+ padding: 6px 8px 6px 0;
622
+ border: 1px solid rgb(242 242 240 / 12%);
623
+ border-radius: 7px;
624
+ background: rgb(20 20 20 / 50%);
625
+ color: inherit;
626
+ cursor: pointer;
627
+ text-align: left;
628
+ font: inherit;
629
+ overflow: hidden;
630
+ transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
631
+ }
632
+
633
+ .openpress-reader-app .openpress-thumb-card:hover {
634
+ border-color: rgb(242 242 240 / 26%);
635
+ transform: translateY(-1px);
636
+ }
637
+
638
+ .openpress-reader-app .openpress-thumb-card.is-active {
639
+ border-color: var(--openpress-accent, #df4b21);
640
+ box-shadow: 0 0 0 1px var(--openpress-accent, #df4b21) inset;
641
+ }
642
+
643
+ .openpress-reader-app .openpress-thumb-card__surface {
644
+ position: relative;
645
+ grid-column: 2;
646
+ grid-row: 1;
647
+ display: grid;
648
+ place-items: center;
649
+ width: 100%;
650
+ overflow: hidden;
651
+ background: #fff;
652
+ border: 1px solid rgb(242 242 240 / 12%);
653
+ border-radius: 4px;
654
+ }
655
+
656
+ .openpress-reader-app .openpress-thumb-card__page-host {
657
+ position: absolute;
658
+ inset: 0;
659
+ }
660
+
661
+ .openpress-reader-app .openpress-thumb-card__page-host .openpress-public-page {
662
+ display: block;
663
+ pointer-events: none;
664
+ user-select: none;
665
+ }
666
+
667
+ .openpress-reader-app .openpress-thumb-card__frame {
668
+ position: relative;
669
+ }
670
+
671
+ .openpress-reader-app .openpress-thumb-card__surface .openpress-html-page {
672
+ pointer-events: none;
673
+ user-select: none;
674
+ }
675
+
676
+ .openpress-reader-app .openpress-thumb-card__meta {
677
+ display: grid;
678
+ grid-column: 1;
679
+ grid-row: 1;
680
+ grid-template-rows: auto;
681
+ justify-items: center;
682
+ align-items: center;
683
+ padding: 0 0 1px;
684
+ font-size: 11px;
685
+ color: rgb(242 242 240 / 58%);
686
+ min-width: 0;
687
+ }
688
+
689
+ .openpress-reader-app .openpress-thumb-card__index {
690
+ font-family: var(--openpress-mono, ui-monospace, monospace);
691
+ font-size: 11px;
692
+ letter-spacing: 0;
693
+ color: rgb(242 242 240 / 68%);
694
+ }
695
+
696
+ .openpress-reader-app .openpress-thumb-card.is-active .openpress-thumb-card__index {
697
+ color: var(--openpress-accent, #df4b21);
698
+ }
699
+
700
+ .openpress-reader-app .openpress-thumb-card__title {
701
+ position: absolute;
702
+ width: 1px;
703
+ height: 1px;
704
+ overflow: hidden;
705
+ clip: rect(0 0 0 0);
706
+ white-space: nowrap;
707
+ }