@open-press/core 1.1.1 → 1.1.3

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.
@@ -32,6 +32,8 @@ type DocumentWithCaretFromPoint = Document & {
32
32
 
33
33
  const EDITABLE_SELECTOR = "[data-openpress-editable-block='true']";
34
34
  const SOURCE_SELECTOR = "[data-openpress-source-editable-block='true']";
35
+ const EDITABLE_OBJECT_TEXT_SELECTOR = "[data-openpress-object-kind='text'][data-openpress-object-source]";
36
+ const EDITABLE_SOURCE_TARGET_SELECTOR = `[data-openpress-block-id], ${EDITABLE_OBJECT_TEXT_SELECTOR}`;
35
37
  const SAVED_EDIT_STATE_RESET_DELAY_MS = 900;
36
38
  const UNSAFE_EDITABLE_CHILDREN = [
37
39
  "a",
@@ -50,7 +52,6 @@ const UNSAFE_EDITABLE_CHILDREN = [
50
52
  "ul",
51
53
  "video",
52
54
  ].join(",");
53
- const EDITABLE_COMPONENT_CAPTION_NAMES = new Set(["MediaFigure", "ImageFigure"]);
54
55
 
55
56
  export function useInlineDocumentEditor({
56
57
  enabled,
@@ -233,7 +234,7 @@ function markEditableElements(
233
234
  sourceBlockMap: Record<string, SourceBlock>,
234
235
  markedElements: Set<HTMLElement>,
235
236
  ) {
236
- root.querySelectorAll<HTMLElement>("[data-openpress-block-id]").forEach((element) => {
237
+ root.querySelectorAll<HTMLElement>(EDITABLE_SOURCE_TARGET_SELECTOR).forEach((element) => {
237
238
  const sourceBlock = blockFromElement(element, sourceBlockMap);
238
239
  if (sourceBlock?.kind === "table-row") {
239
240
  markEditableTableCells(element, sourceBlock, markedElements);
@@ -244,6 +245,15 @@ function markEditableElements(
244
245
  return;
245
246
  }
246
247
 
248
+ if (sourceBlock?.kind === "object-text") {
249
+ element.dataset.openpressBlockId = sourceBlock.id;
250
+ element.dataset.openpressInheritedBlockId = "true";
251
+ element.dataset.openpressEditKind = "object-text";
252
+ element.dataset.openpressEditName = "text";
253
+ markEditableTextElement(element, markedElements, { label: "編輯文字" });
254
+ return;
255
+ }
256
+
247
257
  if (isEditableTextBlockElement(element, sourceBlockMap)) {
248
258
  markEditableTextElement(element, markedElements, {
249
259
  label: sourceBlock?.name === "pre" ? "編輯程式碼文字" : "編輯文字",
@@ -267,7 +277,6 @@ function markEditableComponentCaption(
267
277
  markedElements: Set<HTMLElement>,
268
278
  ) {
269
279
  if (sourceBlock.kind !== "component") return false;
270
- if (!EDITABLE_COMPONENT_CAPTION_NAMES.has(String(sourceBlock.name))) return false;
271
280
  if (!sourceBlock.path || !sourceBlock.source?.line) return false;
272
281
 
273
282
  const caption = componentElement.querySelector<HTMLElement>("figcaption");
@@ -407,7 +416,76 @@ function eventTargetElement(event: Event) {
407
416
 
408
417
  function blockFromElement(element: HTMLElement, sourceBlockMap: Record<string, SourceBlock>) {
409
418
  const blockId = element.dataset.openpressBlockId;
410
- return blockId ? sourceBlockMap[blockId] : undefined;
419
+ if (blockId && sourceBlockMap[blockId]) return sourceBlockMap[blockId];
420
+ return sourceBlockFromObjectElement(element);
421
+ }
422
+
423
+ function sourceBlockFromObjectElement(element: HTMLElement): SourceBlock | undefined {
424
+ if (element.dataset.openpressObjectKind !== "text") return undefined;
425
+ const sourceRef = parseObjectSourceRef(element.dataset.openpressObjectSource);
426
+ if (typeof sourceRef?.path !== "string" || !sourceRef.path) return undefined;
427
+ const source = sourceLocationFromSourceRef(sourceRef);
428
+ if (!source?.line) return undefined;
429
+ const objectId = element.dataset.openpressObjectId || (typeof sourceRef.objectId === "string" ? sourceRef.objectId : undefined);
430
+ if (!objectId) return undefined;
431
+ return {
432
+ id: `object-text:${objectId}`,
433
+ kind: "object-text",
434
+ name: "text",
435
+ path: sourceRef.path,
436
+ source,
437
+ frameKey: element.dataset.openpressObjectFrameKey,
438
+ chainId: element.dataset.openpressObjectChainId,
439
+ };
440
+ }
441
+
442
+ type ObjectSourceRefCandidate = {
443
+ path?: unknown;
444
+ objectId?: unknown;
445
+ source?: unknown;
446
+ line?: unknown;
447
+ column?: unknown;
448
+ endLine?: unknown;
449
+ endColumn?: unknown;
450
+ };
451
+
452
+ type SourceLocationCandidate = {
453
+ line?: unknown;
454
+ column?: unknown;
455
+ endLine?: unknown;
456
+ endColumn?: unknown;
457
+ };
458
+
459
+ function parseObjectSourceRef(value: string | undefined): ObjectSourceRefCandidate | undefined {
460
+ if (!value) return undefined;
461
+ try {
462
+ const parsed = JSON.parse(value) as unknown;
463
+ return parsed && typeof parsed === "object" ? parsed : undefined;
464
+ } catch {
465
+ return undefined;
466
+ }
467
+ }
468
+
469
+ function sourceLocationFromSourceRef(sourceRef: ReturnType<typeof parseObjectSourceRef>): SourceBlock["source"] | undefined {
470
+ if (!sourceRef) return undefined;
471
+ const nestedSource = sourceLocationCandidate(sourceRef.source);
472
+ const line = numberValue(sourceRef.line) ?? numberValue(nestedSource?.line);
473
+ if (line === undefined) return undefined;
474
+ return {
475
+ line,
476
+ column: numberValue(sourceRef.column) ?? numberValue(nestedSource?.column) ?? 1,
477
+ endLine: numberValue(sourceRef.endLine) ?? numberValue(nestedSource?.endLine),
478
+ endColumn: numberValue(sourceRef.endColumn) ?? numberValue(nestedSource?.endColumn),
479
+ };
480
+ }
481
+
482
+ function sourceLocationCandidate(value: unknown): SourceLocationCandidate | undefined {
483
+ if (!value || typeof value !== "object") return undefined;
484
+ return value;
485
+ }
486
+
487
+ function numberValue(value: unknown): number | undefined {
488
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
411
489
  }
412
490
 
413
491
  async function persistElementEdit(
@@ -417,8 +495,8 @@ async function persistElementEdit(
417
495
  onStatusChange: InlineDocumentEditorOptions["onStatusChange"],
418
496
  onDocumentEdited: InlineDocumentEditorOptions["onDocumentEdited"],
419
497
  ) {
420
- const blockId = element.dataset.openpressBlockId;
421
- const sourceBlock = blockId ? sourceBlockMap[blockId] : undefined;
498
+ const sourceBlock = blockFromElement(element, sourceBlockMap);
499
+ const blockId = sourceBlock?.id ?? element.dataset.openpressBlockId;
422
500
  const preserveLineBreaks = element.dataset.openpressPreserveLineBreaks === "true";
423
501
  const originalText = normalizeEditableText(element.dataset.openpressOriginalText ?? "", { preserveLineBreaks });
424
502
  const nextText = normalizeEditableText(readableElementText(element), { preserveLineBreaks });
@@ -20,6 +20,8 @@ function WorkbenchShellRoot({
20
20
  style,
21
21
  devMode,
22
22
  viewMode,
23
+ pressType = "pages",
24
+ presentationMode = false,
23
25
  inspectorMode,
24
26
  editMode = false,
25
27
  leftPanelOpen,
@@ -31,6 +33,8 @@ function WorkbenchShellRoot({
31
33
  style: CSSProperties;
32
34
  devMode: boolean;
33
35
  viewMode: string;
36
+ pressType?: string;
37
+ presentationMode?: boolean;
34
38
  inspectorMode: boolean;
35
39
  editMode?: boolean;
36
40
  leftPanelOpen: boolean;
@@ -45,6 +49,7 @@ function WorkbenchShellRoot({
45
49
  "reader-app openpress-reader-app openpress-public-viewer openpress-dev-public-viewer openpress-workbench-shell is-ready",
46
50
  leftPanelOpen ? "" : "is-closed-left",
47
51
  rightPanelOpen ? "" : "is-closed-right",
52
+ presentationMode ? "is-presentation-mode" : "",
48
53
  ].filter(Boolean).join(" ");
49
54
 
50
55
  return (
@@ -54,6 +59,8 @@ function WorkbenchShellRoot({
54
59
  className={shellClassName}
55
60
  data-openpress-react-runtime="true"
56
61
  data-openpress-view-mode={viewMode}
62
+ data-openpress-press-type={pressType}
63
+ data-openpress-presentation-mode={presentationMode ? "on" : "off"}
57
64
  data-openpress-inspector-mode={inspectorMode ? "on" : "off"}
58
65
  data-openpress-edit-mode={editMode ? "on" : "off"}
59
66
  data-openpress-workbench-shell
@@ -75,93 +75,51 @@
75
75
  }
76
76
 
77
77
  .openpress-reader-app[data-openpress-inspector-mode="on"] .openpress-html-page__html [data-openpress-block-id]:hover {
78
- outline: 2px solid color-mix(in srgb, var(--openpress-accent, #df4b21) 70%, transparent);
79
- outline-offset: 4px;
78
+ cursor: crosshair;
80
79
  }
81
80
 
82
81
  .openpress-reader-app[data-openpress-inspector-mode="on"] .openpress-html-page__html [data-openpress-inspector-selected="true"] {
83
- outline: 2px solid var(--openpress-accent, #df4b21);
84
- outline-offset: 5px;
85
- background: color-mix(in srgb, var(--openpress-accent, #df4b21) 16%, transparent);
82
+ cursor: crosshair;
86
83
  }
87
84
 
88
85
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"] {
89
- border-radius: 3px;
90
- background: transparent;
91
86
  cursor: text;
92
87
  caret-color: var(--openpress-accent, #df4b21);
93
- outline: none;
94
- transition:
95
- background 140ms ease,
96
- box-shadow 140ms ease,
97
- color 140ms ease;
88
+ /* Chrome applies break-word to contenteditable editing hosts. That changes
89
+ fixed-layout canvas line breaks, so OpenPress owns the edit-mode wrapping. */
90
+ overflow-wrap: normal;
91
+ word-break: normal;
98
92
  }
99
93
 
100
94
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"]:hover {
101
- background:
102
- linear-gradient(90deg, color-mix(in srgb, var(--openpress-accent, #df4b21) 9%, transparent), transparent 88%),
103
- color-mix(in srgb, var(--openpress-accent, #df4b21) 5%, transparent);
104
- box-shadow: inset 0 -0.18em 0 color-mix(in srgb, var(--openpress-accent, #df4b21) 16%, transparent);
95
+ cursor: text;
105
96
  }
106
97
 
107
98
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"][data-openpress-editing="true"],
108
99
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"]:focus {
109
- background:
110
- linear-gradient(90deg, color-mix(in srgb, var(--openpress-accent, #df4b21) 13%, transparent), transparent 90%),
111
- color-mix(in srgb, var(--openpress-accent, #df4b21) 7%, transparent);
112
- box-shadow: inset 0 -0.2em 0 color-mix(in srgb, var(--openpress-accent, #df4b21) 20%, transparent);
100
+ cursor: text;
113
101
  }
114
102
 
115
103
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"][data-openpress-edit-state="saving"] {
116
- background:
117
- linear-gradient(
118
- 90deg,
119
- color-mix(in srgb, var(--openpress-accent, #df4b21) 7%, transparent) 0%,
120
- color-mix(in srgb, var(--openpress-accent, #df4b21) 18%, transparent) 45%,
121
- color-mix(in srgb, var(--openpress-accent, #df4b21) 7%, transparent) 90%
122
- );
123
- background-size: 220% 100%;
124
- box-shadow: none;
125
- color: transparent;
126
- text-shadow: none;
127
- animation: openpress-inline-edit-skeleton 920ms linear infinite;
128
104
  pointer-events: none;
129
105
  user-select: none;
130
106
  }
131
107
 
132
108
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"][data-openpress-edit-state="saved"] {
133
- background: color-mix(in srgb, #22c55e 14%, transparent);
134
- box-shadow: inset 0 -0.18em 0 color-mix(in srgb, #22c55e 24%, transparent);
109
+ cursor: text;
135
110
  }
136
111
 
137
112
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-editable-block="true"][data-openpress-edit-state="failed"] {
138
- background: color-mix(in srgb, #ef4444 12%, transparent);
139
- box-shadow: inset 0 -0.18em 0 color-mix(in srgb, #ef4444 24%, transparent);
113
+ cursor: text;
140
114
  }
141
115
 
142
116
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-source-editable-block="true"] {
143
117
  cursor: pointer;
144
- outline: 1px dashed transparent;
145
- outline-offset: 5px;
146
- transition:
147
- background 140ms ease,
148
- outline-color 140ms ease;
149
118
  }
150
119
 
151
120
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-source-editable-block="true"]:hover,
152
121
  .openpress-reader-app[data-openpress-edit-mode="on"] .openpress-html-page__html [data-openpress-source-editable-block="true"]:focus {
153
- outline-color: color-mix(in srgb, var(--openpress-accent, #df4b21) 42%, transparent);
154
- background: color-mix(in srgb, var(--openpress-accent, #df4b21) 7%, transparent);
155
- }
156
-
157
- @keyframes openpress-inline-edit-skeleton {
158
- from {
159
- background-position: 120% 0;
160
- }
161
-
162
- to {
163
- background-position: -120% 0;
164
- }
122
+ cursor: pointer;
165
123
  }
166
124
 
167
125
  .openpress-inline-inspector-layer {
@@ -50,6 +50,7 @@
50
50
 
51
51
  .openpress-control-panel {
52
52
  display: grid;
53
+ grid-auto-rows: max-content;
53
54
  align-content: start;
54
55
  min-height: 0;
55
56
  gap: 22px;
@@ -597,24 +598,36 @@
597
598
  (canvas-style Press: social posts, slides). Renders the actual page
598
599
  HTML scaled down so the user can navigate by visual recognition. */
599
600
  .openpress-reader-app .openpress-panel-section--thumbnails {
600
- display: flex;
601
- flex-direction: column;
602
- gap: 10px;
603
- padding: 8px 14px 16px;
601
+ display: grid;
602
+ grid-template-rows: minmax(0, 1fr);
604
603
  min-height: 0;
604
+ overflow: hidden;
605
+ padding: 8px 14px 12px;
605
606
  }
606
607
 
607
608
  .openpress-reader-app .openpress-thumb-list {
608
609
  display: flex;
610
+ min-height: 0;
609
611
  flex-direction: column;
610
612
  gap: 10px;
611
613
  margin: 0;
612
- padding: 0;
614
+ overflow: auto;
615
+ padding: 0 0 10px;
613
616
  list-style: none;
617
+ overscroll-behavior: contain;
618
+ scrollbar-width: none;
619
+ }
620
+
621
+ .openpress-reader-app .openpress-thumb-list::-webkit-scrollbar {
622
+ width: 0;
623
+ height: 0;
624
+ display: none;
614
625
  }
615
626
 
616
627
  .openpress-reader-app .openpress-thumb-card {
617
628
  display: grid;
629
+ width: 100%;
630
+ min-width: 0;
618
631
  grid-template-columns: 20px minmax(0, 1fr);
619
632
  align-items: stretch;
620
633
  gap: 6px;
@@ -31,6 +31,141 @@
31
31
  display: none;
32
32
  }
33
33
 
34
+ .openpress-slide-presenter {
35
+ position: fixed;
36
+ inset: 0;
37
+ display: grid;
38
+ width: 100%;
39
+ height: 100dvh;
40
+ min-height: 100dvh;
41
+ overflow: hidden;
42
+ background:
43
+ radial-gradient(circle at 50% 0, rgb(255 255 255 / 8%), transparent 42%),
44
+ #070707;
45
+ color: rgb(245 245 242 / 92%);
46
+ overscroll-behavior: none;
47
+ }
48
+
49
+ .openpress-slide-presenter__stage {
50
+ position: relative;
51
+ min-width: 0;
52
+ min-height: 0;
53
+ overflow: hidden;
54
+ cursor: pointer;
55
+ }
56
+
57
+ .openpress-reader-app.openpress-slide-presenter .reader-stage {
58
+ width: 100%;
59
+ height: 100%;
60
+ min-height: 0;
61
+ overflow: hidden;
62
+ background: transparent;
63
+ overscroll-behavior: contain;
64
+ scroll-snap-type: none;
65
+ scrollbar-width: none;
66
+ }
67
+
68
+ .openpress-reader-app.openpress-slide-presenter .reader-stage::-webkit-scrollbar {
69
+ width: 0;
70
+ height: 0;
71
+ display: none;
72
+ }
73
+
74
+ .openpress-reader-app.openpress-slide-presenter .reader-pages {
75
+ --openpress-page-gap: 0;
76
+ width: 100%;
77
+ height: 100%;
78
+ min-height: 100%;
79
+ align-content: center;
80
+ padding: 0;
81
+ align-items: center;
82
+ justify-items: center;
83
+ }
84
+
85
+ .openpress-reader-app.openpress-slide-presenter .openpress-html-page {
86
+ min-height: 0;
87
+ scroll-margin-top: 0;
88
+ scroll-snap-align: center;
89
+ }
90
+
91
+ .openpress-reader-app.openpress-slide-presenter .openpress-html-page__html {
92
+ box-shadow:
93
+ 0 28px 80px rgb(0 0 0 / 34%),
94
+ 0 0 0 1px rgb(255 255 255 / 10%);
95
+ }
96
+
97
+ .openpress-slide-presenter__hud {
98
+ position: fixed;
99
+ right: 18px;
100
+ bottom: 18px;
101
+ z-index: 40;
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ border: 1px solid rgb(255 255 255 / 10%);
106
+ border-radius: 999px;
107
+ padding: 6px;
108
+ background: rgb(18 18 18 / 74%);
109
+ box-shadow: 0 18px 44px rgb(0 0 0 / 28%);
110
+ backdrop-filter: blur(18px);
111
+ opacity: 1;
112
+ transform: translateY(0);
113
+ transition:
114
+ opacity 160ms ease,
115
+ transform 160ms ease;
116
+ }
117
+
118
+ .openpress-slide-presenter[data-openpress-present-ui="immersive"] {
119
+ cursor: none;
120
+ }
121
+
122
+ .openpress-slide-presenter[data-openpress-present-ui="immersive"] .openpress-slide-presenter__stage {
123
+ cursor: none;
124
+ }
125
+
126
+ .openpress-slide-presenter[data-openpress-present-ui="immersive"] .openpress-slide-presenter__hud {
127
+ opacity: 0;
128
+ pointer-events: none;
129
+ transform: translateY(8px);
130
+ }
131
+
132
+ .openpress-slide-presenter__progress {
133
+ min-width: 64px;
134
+ padding: 0 10px;
135
+ color: rgb(245 245 242 / 72%);
136
+ font-family: var(--openpress-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
137
+ font-size: 12px;
138
+ font-weight: 600;
139
+ letter-spacing: 0.08em;
140
+ line-height: 30px;
141
+ text-align: center;
142
+ }
143
+
144
+ .openpress-slide-presenter__button {
145
+ display: inline-flex;
146
+ width: 30px;
147
+ height: 30px;
148
+ align-items: center;
149
+ justify-content: center;
150
+ border: 0;
151
+ border-radius: 999px;
152
+ padding: 0;
153
+ background: transparent;
154
+ color: rgb(245 245 242 / 68%);
155
+ cursor: pointer;
156
+ }
157
+
158
+ .openpress-slide-presenter__button:hover,
159
+ .openpress-slide-presenter__button:focus-visible {
160
+ background: rgb(255 255 255 / 10%);
161
+ color: rgb(245 245 242 / 96%);
162
+ }
163
+
164
+ .openpress-slide-presenter__button svg {
165
+ width: 15px;
166
+ height: 15px;
167
+ }
168
+
34
169
  .openpress-public-viewer.openpress-workbench-shell.openpress-reader-app,
35
170
  .openpress-public-viewer.openpress-workbench-shell.openpress-reader-app.is-closed-left,
36
171
  .openpress-public-viewer.openpress-workbench-shell.openpress-reader-app.is-closed-right,
@@ -58,6 +193,10 @@
58
193
  grid-template-columns: 0 minmax(0, 1fr) 0;
59
194
  }
60
195
 
196
+ .openpress-public-viewer.openpress-workbench-shell.openpress-reader-app.is-presentation-mode {
197
+ grid-template-columns: 0 minmax(0, 1fr) 0;
198
+ }
199
+
61
200
  .openpress-workbench-toolbar {
62
201
  z-index: 30;
63
202
  display: flex;
@@ -1228,6 +1367,16 @@
1228
1367
  opacity: 0;
1229
1368
  }
1230
1369
 
1370
+ .openpress-reader-app.is-presentation-mode .openpress-workbench-left-panel,
1371
+ .openpress-reader-app.is-presentation-mode .openpress-workbench-right-panel {
1372
+ pointer-events: none;
1373
+ }
1374
+
1375
+ .openpress-reader-app.is-presentation-mode .openpress-workbench-left-panel > *,
1376
+ .openpress-reader-app.is-presentation-mode .openpress-workbench-right-panel > * {
1377
+ opacity: 0;
1378
+ }
1379
+
1231
1380
  .openpress-reader-app .reader-stage {
1232
1381
  position: relative;
1233
1382
  width: 100%;