@zephytiju/prism-main-editor 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.
Files changed (69) hide show
  1. package/README.md +252 -0
  2. package/dist/CommentsPane.d.ts +32 -0
  3. package/dist/CommentsPane.d.ts.map +1 -0
  4. package/dist/CommentsPane.js +85 -0
  5. package/dist/MainEditor.d.ts +59 -0
  6. package/dist/MainEditor.d.ts.map +1 -0
  7. package/dist/MainEditor.js +102 -0
  8. package/dist/TitleBar.d.ts +28 -0
  9. package/dist/TitleBar.d.ts.map +1 -0
  10. package/dist/TitleBar.js +55 -0
  11. package/dist/blocks/AssessmentBlockView.d.ts +21 -0
  12. package/dist/blocks/AssessmentBlockView.d.ts.map +1 -0
  13. package/dist/blocks/AssessmentBlockView.js +34 -0
  14. package/dist/blocks/BlockRow.d.ts +33 -0
  15. package/dist/blocks/BlockRow.d.ts.map +1 -0
  16. package/dist/blocks/BlockRow.js +56 -0
  17. package/dist/blocks/BlockStack.d.ts +27 -0
  18. package/dist/blocks/BlockStack.d.ts.map +1 -0
  19. package/dist/blocks/BlockStack.js +75 -0
  20. package/dist/blocks/DragMenu.d.ts +22 -0
  21. package/dist/blocks/DragMenu.d.ts.map +1 -0
  22. package/dist/blocks/DragMenu.js +219 -0
  23. package/dist/blocks/EmbeddedBlockChrome.d.ts +34 -0
  24. package/dist/blocks/EmbeddedBlockChrome.d.ts.map +1 -0
  25. package/dist/blocks/EmbeddedBlockChrome.js +81 -0
  26. package/dist/blocks/EvidenceTableView.d.ts +25 -0
  27. package/dist/blocks/EvidenceTableView.d.ts.map +1 -0
  28. package/dist/blocks/EvidenceTableView.js +30 -0
  29. package/dist/blocks/GeovisionEmbedView.d.ts +28 -0
  30. package/dist/blocks/GeovisionEmbedView.d.ts.map +1 -0
  31. package/dist/blocks/GeovisionEmbedView.js +33 -0
  32. package/dist/blocks/InsertMenu.d.ts +22 -0
  33. package/dist/blocks/InsertMenu.d.ts.map +1 -0
  34. package/dist/blocks/InsertMenu.js +59 -0
  35. package/dist/blocks/InsertRail.d.ts +28 -0
  36. package/dist/blocks/InsertRail.d.ts.map +1 -0
  37. package/dist/blocks/InsertRail.js +29 -0
  38. package/dist/blocks/NarrativeBlockView.d.ts +22 -0
  39. package/dist/blocks/NarrativeBlockView.d.ts.map +1 -0
  40. package/dist/blocks/NarrativeBlockView.js +44 -0
  41. package/dist/blocks/NativeTableView.d.ts +12 -0
  42. package/dist/blocks/NativeTableView.d.ts.map +1 -0
  43. package/dist/blocks/NativeTableView.js +36 -0
  44. package/dist/blocks/SavedViewDropdown.d.ts +17 -0
  45. package/dist/blocks/SavedViewDropdown.d.ts.map +1 -0
  46. package/dist/blocks/SavedViewDropdown.js +85 -0
  47. package/dist/blocks/SelectionToolbar.d.ts +25 -0
  48. package/dist/blocks/SelectionToolbar.d.ts.map +1 -0
  49. package/dist/blocks/SelectionToolbar.js +175 -0
  50. package/dist/blocks/menuStyles.d.ts +32 -0
  51. package/dist/blocks/menuStyles.d.ts.map +1 -0
  52. package/dist/blocks/menuStyles.js +32 -0
  53. package/dist/index.d.ts +26 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +11 -0
  56. package/dist/locales/en.json +37 -0
  57. package/dist/locales/index.d.ts +105 -0
  58. package/dist/locales/index.d.ts.map +1 -0
  59. package/dist/locales/index.js +27 -0
  60. package/dist/locales/zh-CN.json +37 -0
  61. package/dist/tokens.d.ts +42 -0
  62. package/dist/tokens.d.ts.map +1 -0
  63. package/dist/tokens.js +43 -0
  64. package/dist/types.d.ts +207 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +1 -0
  67. package/locales/en.json +37 -0
  68. package/locales/zh-CN.json +37 -0
  69. package/package.json +79 -0
@@ -0,0 +1,21 @@
1
+ import type { AssessmentBlock as AssessmentBlockModel } from "../types.js";
2
+ import type { MainEditorLocale } from "../locales/index.js";
3
+ export interface AssessmentBlockViewProps {
4
+ readonly block: AssessmentBlockModel;
5
+ readonly locale: MainEditorLocale;
6
+ /**
7
+ * D11: in the editor variant the judgment body is an editable text surface —
8
+ * selecting inside it opens the floating selection toolbar.
9
+ */
10
+ readonly editable?: boolean;
11
+ }
12
+ /**
13
+ * Normal document block — analyst-judgment highlight (D10): an accent bar +
14
+ * tint ONLY — no outlining box, no card chrome, no kind pill outside the
15
+ * label. The tint and the bar both derive from the warn token; the label
16
+ * defaults to the locale's "ANALYST JUDGMENT" string. When editable (D11) the
17
+ * body carries the data-main-editor-editable attribute the selection toolbar
18
+ * anchors to.
19
+ */
20
+ export declare function AssessmentBlockView({ block, locale, editable }: AssessmentBlockViewProps): import("react").JSX.Element;
21
+ //# sourceMappingURL=AssessmentBlockView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssessmentBlockView.d.ts","sourceRoot":"","sources":["../../src/blocks/AssessmentBlockView.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,IAAI,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAgB,EAAE,EAAE,wBAAwB,+BAwDhG"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Group, Stack, Text } from "@mantine/core";
3
+ import { stringsForLocale } from "../locales/index.js";
4
+ import { HIGHLIGHT_TINT, MONO, WARN } from "../tokens.js";
5
+ /**
6
+ * Normal document block — analyst-judgment highlight (D10): an accent bar +
7
+ * tint ONLY — no outlining box, no card chrome, no kind pill outside the
8
+ * label. The tint and the bar both derive from the warn token; the label
9
+ * defaults to the locale's "ANALYST JUDGMENT" string. When editable (D11) the
10
+ * body carries the data-main-editor-editable attribute the selection toolbar
11
+ * anchors to.
12
+ */
13
+ export function AssessmentBlockView({ block, locale, editable = false }) {
14
+ const strings = stringsForLocale(locale);
15
+ return (_jsx(Box, { px: 16, py: 14, style: { background: HIGHLIGHT_TINT, borderRadius: 4 }, "data-testid": `main-editor-block-${block.id}`, children: _jsxs(Group, { align: "stretch", gap: 16, wrap: "nowrap", children: [_jsx("span", { style: {
16
+ display: "inline-block",
17
+ width: 5,
18
+ borderRadius: 2,
19
+ background: WARN,
20
+ alignSelf: "stretch",
21
+ flexShrink: 0,
22
+ }, "data-testid": `main-editor-assessment-bar-${block.id}` }), _jsxs(Stack, { gap: 8, miw: 0, style: { flex: 1 }, children: [_jsx(Text, { ff: MONO, fz: 9, fw: 500, style: { color: WARN, letterSpacing: "0.08em" }, "data-testid": `main-editor-assessment-label-${block.id}`, children: block.label ?? strings.kindAssessment }), _jsx(Text, { fz: 14, fw: 500, c: "text", style: {
23
+ lineHeight: 1.45,
24
+ letterSpacing: "0.01em",
25
+ ...(editable ? { cursor: "text", outline: "none" } : {}),
26
+ }, "data-testid": `main-editor-assessment-text-${block.id}`, ...(editable
27
+ ? {
28
+ contentEditable: true,
29
+ suppressContentEditableWarning: true,
30
+ spellCheck: false,
31
+ "data-main-editor-editable": block.id,
32
+ }
33
+ : {}), children: block.text })] })] }) }));
34
+ }
@@ -0,0 +1,33 @@
1
+ import type { MainEditorLocale } from "../locales/index.js";
2
+ import type { MainEditorBlock, MainEditorBlockKind } from "../types.js";
3
+ import type { KindLabelFn } from "./InsertMenu.js";
4
+ export interface BlockRowProps {
5
+ readonly block: MainEditorBlock;
6
+ readonly locale: MainEditorLocale;
7
+ /** editor — insert + drag controls render; readonly — published view. */
8
+ readonly editor: boolean;
9
+ readonly showInsert: boolean;
10
+ readonly kinds: readonly MainEditorBlockKind[];
11
+ readonly kindLabel: KindLabelFn;
12
+ readonly canMoveUp: boolean;
13
+ readonly canMoveDown: boolean;
14
+ readonly focused: boolean;
15
+ /**
16
+ * D11: narrative + assessment text surfaces are editable (selection toolbar
17
+ * targets them) in the editor variant only.
18
+ */
19
+ readonly editable: boolean;
20
+ /** Publishes "main-editor.focused-block" for the clicked block id. */
21
+ readonly onFocusBlock: (blockId: string) => void;
22
+ }
23
+ /**
24
+ * One row of the typed block stack: the editor-variant insert rail (gutter)
25
+ * beside the block surface. The gutter renders ONLY on row hover or keyboard
26
+ * focus within the row (D11 — a 120ms opacity fade, kept reachable by TAB
27
+ * because the hidden controls stay focusable and reveal on focus). Clicking
28
+ * the surface publishes "main-editor.focused-block"; the focus wash is an
29
+ * interaction state, not an outlining box — normal blocks stay plain
30
+ * document elements.
31
+ */
32
+ export declare function BlockRow({ block, locale, editor, showInsert, kinds, kindLabel, canMoveUp, canMoveDown, focused, editable, onFocusBlock, }: BlockRowProps): import("react").JSX.Element;
33
+ //# sourceMappingURL=BlockRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockRow.d.ts","sourceRoot":"","sources":["../../src/blocks/BlockRow.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAMxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,MAAM,EACN,MAAM,EACN,UAAU,EACV,KAAK,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,OAAO,EACP,QAAQ,EACR,YAAY,GACb,EAAE,aAAa,+BAqDf"}
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box } from "@mantine/core";
4
+ import { FOCUS_WASH } from "../tokens.js";
5
+ import { AssessmentBlockView } from "./AssessmentBlockView.js";
6
+ import { EvidenceTableView } from "./EvidenceTableView.js";
7
+ import { GeovisionEmbedView } from "./GeovisionEmbedView.js";
8
+ import { InsertRail } from "./InsertRail.js";
9
+ import { NarrativeBlockView } from "./NarrativeBlockView.js";
10
+ import { NativeTableView } from "./NativeTableView.js";
11
+ /**
12
+ * One row of the typed block stack: the editor-variant insert rail (gutter)
13
+ * beside the block surface. The gutter renders ONLY on row hover or keyboard
14
+ * focus within the row (D11 — a 120ms opacity fade, kept reachable by TAB
15
+ * because the hidden controls stay focusable and reveal on focus). Clicking
16
+ * the surface publishes "main-editor.focused-block"; the focus wash is an
17
+ * interaction state, not an outlining box — normal blocks stay plain
18
+ * document elements.
19
+ */
20
+ export function BlockRow({ block, locale, editor, showInsert, kinds, kindLabel, canMoveUp, canMoveDown, focused, editable, onFocusBlock, }) {
21
+ const [hovered, setHovered] = useState(false);
22
+ const [focusWithin, setFocusWithin] = useState(false);
23
+ return (_jsxs(Box, { style: { display: "flex", alignItems: "flex-start", width: "100%" }, "data-testid": `main-editor-block-row-${block.id}`, "data-block-row-id": block.id, onMouseEnter: () => {
24
+ setHovered(true);
25
+ }, onMouseLeave: () => {
26
+ setHovered(false);
27
+ }, onFocus: () => {
28
+ setFocusWithin(true);
29
+ }, onBlur: (event) => {
30
+ if (!event.currentTarget.contains(event.relatedTarget)) {
31
+ setFocusWithin(false);
32
+ }
33
+ }, children: [editor ? (_jsx(InsertRail, { blockId: block.id, kinds: kinds, kindLabel: kindLabel, canMoveUp: canMoveUp, canMoveDown: canMoveDown, locale: locale, showInsert: showInsert, revealed: hovered || focusWithin })) : null, _jsx(Box, { w: editor ? 860 : "100%", onClick: () => {
34
+ onFocusBlock(block.id);
35
+ }, style: {
36
+ background: focused ? FOCUS_WASH : "transparent",
37
+ borderRadius: 6,
38
+ cursor: "pointer",
39
+ transition: "background .15s ease",
40
+ }, "data-testid": `main-editor-block-surface-${block.id}`, children: _jsx(BlockView, { block: block, locale: locale, editable: editable }) })] }));
41
+ }
42
+ /** Dispatches one dossier block to its D10 block-surface view. */
43
+ function BlockView({ block, locale, editable, }) {
44
+ switch (block.kind) {
45
+ case "narrative":
46
+ return _jsx(NarrativeBlockView, { block: block, editable: editable });
47
+ case "assessment":
48
+ return _jsx(AssessmentBlockView, { block: block, locale: locale, editable: editable });
49
+ case "geovision-embed":
50
+ return _jsx(GeovisionEmbedView, { block: block, locale: locale });
51
+ case "evidence-table":
52
+ return _jsx(EvidenceTableView, { block: block, locale: locale });
53
+ case "native-table":
54
+ return _jsx(NativeTableView, { block: block });
55
+ }
56
+ }
@@ -0,0 +1,27 @@
1
+ import type { MainEditorLocale } from "../locales/index.js";
2
+ import type { MainEditorBlockKind, MainEditorDossier } from "../types.js";
3
+ export interface BlockStackProps {
4
+ readonly dossier: MainEditorDossier;
5
+ readonly locale: MainEditorLocale;
6
+ /** editor — insert + drag controls (default); readonly — published view. */
7
+ readonly variant: "editor" | "readonly";
8
+ /** Block kinds the insert menu offers (configuration key enabledBlockKinds). */
9
+ readonly enabledBlockKinds: readonly MainEditorBlockKind[];
10
+ /** Configuration key showInsertControls; drag handles follow the editor variant. */
11
+ readonly showInsertControls: boolean;
12
+ readonly focusedBlockId: string | null;
13
+ }
14
+ /**
15
+ * The typed block stack (D3/D10/D11): same-kind blocks added at app runtime
16
+ * through dossier actions, each preceded by its hover-revealed circular
17
+ * insert control (the + opens the block-kind menu and emits
18
+ * "main-editor.insert-block-requested") and ⠿ drag handle (single click opens
19
+ * the Feishu-style block popover; drag reorders — both emit
20
+ * "main-editor.reorder-block-requested" / "main-editor.remove-block-requested"
21
+ * / "main-editor.duplicate-block-requested" dossier actions the host applies,
22
+ * never composition mutations). Clicking a block publishes
23
+ * "main-editor.focused-block"; the focus wash is an interaction state, not an
24
+ * outlining box — normal blocks stay plain document elements.
25
+ */
26
+ export declare function BlockStack({ dossier, locale, variant, enabledBlockKinds, showInsertControls, focusedBlockId, }: BlockStackProps): import("react").JSX.Element;
27
+ //# sourceMappingURL=BlockStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockStack.d.ts","sourceRoot":"","sources":["../../src/blocks/BlockStack.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EACV,mBAAmB,EACnB,iBAAiB,EAElB,MAAM,aAAa,CAAC;AAMrB,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,UAAU,CAAC;IACxC,gFAAgF;IAChF,QAAQ,CAAC,iBAAiB,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC3D,oFAAoF;IACpF,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,MAAM,EACN,OAAO,EACP,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,GACf,EAAE,eAAe,+BA+EjB"}
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Stack, Text } from "@mantine/core";
4
+ import { usePrismStateSetter } from "@zephytiju/prism-react";
5
+ import { formatMessage, stringsForLocale } from "../locales/index.js";
6
+ import { BlockRow } from "./BlockRow.js";
7
+ import { InsertMenu } from "./InsertMenu.js";
8
+ import { BORDER, MONO, MUTED } from "../tokens.js";
9
+ const ALL_KINDS = [
10
+ "narrative",
11
+ "assessment",
12
+ "geovision-embed",
13
+ "evidence-table",
14
+ "native-table",
15
+ ];
16
+ /**
17
+ * The typed block stack (D3/D10/D11): same-kind blocks added at app runtime
18
+ * through dossier actions, each preceded by its hover-revealed circular
19
+ * insert control (the + opens the block-kind menu and emits
20
+ * "main-editor.insert-block-requested") and ⠿ drag handle (single click opens
21
+ * the Feishu-style block popover; drag reorders — both emit
22
+ * "main-editor.reorder-block-requested" / "main-editor.remove-block-requested"
23
+ * / "main-editor.duplicate-block-requested" dossier actions the host applies,
24
+ * never composition mutations). Clicking a block publishes
25
+ * "main-editor.focused-block"; the focus wash is an interaction state, not an
26
+ * outlining box — normal blocks stay plain document elements.
27
+ */
28
+ export function BlockStack({ dossier, locale, variant, enabledBlockKinds, showInsertControls, focusedBlockId, }) {
29
+ const strings = stringsForLocale(locale);
30
+ const kinds = enabledBlockKinds.length > 0 ? enabledBlockKinds : ALL_KINDS;
31
+ const publishFocusedBlock = usePrismStateSetter("main-editor.focused-block");
32
+ const editor = variant === "editor";
33
+ const showInsert = editor && showInsertControls;
34
+ const kindLabel = (kind) => kind === "narrative"
35
+ ? strings.kindNarrative
36
+ : kind === "assessment"
37
+ ? strings.kindAssessment
38
+ : kind === "geovision-embed"
39
+ ? strings.kindGeovision
40
+ : kind === "evidence-table"
41
+ ? strings.kindEvidenceTable
42
+ : strings.kindNativeTable;
43
+ return (_jsxs(Stack, { gap: 16, miw: 0, "data-testid": "main-editor-block-stack", children: [_jsxs(Stack, { gap: 4, miw: 0, children: [_jsx(Text, { ff: MONO, fz: 9, fw: 500, c: "accent", style: { letterSpacing: "0.05em" }, "data-testid": "main-editor-workspace-code", children: `${strings.docPrefix} / ${dossier.docCode}` }), _jsx(Text, { fz: 26, fw: 700, c: "text", style: { lineHeight: 1.25, letterSpacing: "0.01em" }, "data-testid": "main-editor-workspace-title", children: dossier.title }), _jsx(Text, { ff: MONO, fz: 8, style: { color: MUTED, letterSpacing: "0.03em" }, "data-testid": "main-editor-workspace-meta", children: `${dossier.docMeta} • ${formatMessage(strings.blocksCount, { count: dossier.blocks.length })}` })] }), _jsxs(Stack, { gap: 20, miw: 0, align: "flex-start", children: [dossier.blocks.map((block, index) => (_jsx(BlockRow, { block: block, locale: locale, editor: editor, showInsert: showInsert, editable: editor, kinds: kinds, kindLabel: kindLabel, canMoveUp: index > 0, canMoveDown: index < dossier.blocks.length - 1, focused: focusedBlockId === block.id, onFocusBlock: (blockId) => {
44
+ publishFocusedBlock({ blockId });
45
+ } }, block.id))), showInsert ? (_jsx(EndInsertRail, { kinds: kinds, kindLabel: kindLabel, locale: locale, afterBlockId: dossier.blocks.length > 0 ? dossier.blocks[dossier.blocks.length - 1].id : null })) : null] })] }));
46
+ }
47
+ /**
48
+ * The end-of-stack insert affordance (D11): like every block-row gutter it
49
+ * renders ONLY on hover (or focus within), fading in over ~120ms.
50
+ */
51
+ function EndInsertRail({ kinds, kindLabel, locale, afterBlockId, }) {
52
+ const [revealed, setRevealed] = useState(false);
53
+ return (_jsx(Box, { w: 72, pl: 0, style: { display: "flex", alignItems: "center", flexShrink: 0 }, "data-testid": "main-editor-end-insert", onMouseEnter: () => {
54
+ setRevealed(true);
55
+ }, onMouseLeave: () => {
56
+ setRevealed(false);
57
+ }, onFocus: () => {
58
+ setRevealed(true);
59
+ }, onBlur: (event) => {
60
+ if (!event.currentTarget.contains(event.relatedTarget)) {
61
+ setRevealed(false);
62
+ }
63
+ }, children: _jsxs(Box, { style: {
64
+ display: "flex",
65
+ alignItems: "center",
66
+ gap: 6,
67
+ flex: 1,
68
+ opacity: revealed ? 1 : 0,
69
+ transition: "opacity .12s ease",
70
+ }, "data-testid": "main-editor-end-gutter", children: [_jsx(InsertMenu, { buttonTestId: "main-editor-insert-end", kinds: kinds, kindLabel: kindLabel, afterBlockId: afterBlockId, locale: locale, compact: true }), _jsx("span", { style: {
71
+ flex: 1,
72
+ borderTop: `1px dashed ${BORDER}`,
73
+ marginTop: 2,
74
+ }, "aria-hidden": "true" })] }) }));
75
+ }
@@ -0,0 +1,22 @@
1
+ import type { MainEditorLocale } from "../locales/index.js";
2
+ export interface DragMenuProps {
3
+ readonly blockId: string;
4
+ readonly canMoveUp: boolean;
5
+ readonly canMoveDown: boolean;
6
+ readonly locale: MainEditorLocale;
7
+ }
8
+ /**
9
+ * The block gutter's ⠿ handle (D3/D11): the drag-to-reorder affordance AND a
10
+ * single-click popover trigger. Click vs drag is distinguished by pointer
11
+ * travel — moving less than DRAG_THRESHOLD_PX before release is a click and
12
+ * opens the Feishu-style block popover; crossing the threshold starts a live
13
+ * drag reorder that emits "main-editor.reorder-block-requested" stepwise each
14
+ * time the pointer crosses onto another block row (the host applies each
15
+ * action and republishes, so the dragged block follows the pointer). The
16
+ * popover mutates the block (move up / move down / duplicate / delete — all
17
+ * IDossierDoc ACTIONS the host applies, never composition mutations) or
18
+ * anchors a new comment thread to the block via
19
+ * "main-editor.comment-requested" (D11). It closes on outside click and ESC.
20
+ */
21
+ export declare function DragMenu({ blockId, canMoveUp, canMoveDown, locale }: DragMenuProps): import("react").JSX.Element;
22
+ //# sourceMappingURL=DragMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DragMenu.d.ts","sourceRoot":"","sources":["../../src/blocks/DragMenu.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAO5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC;AAKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,aAAa,+BAoSlF"}
@@ -0,0 +1,219 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box } from "@mantine/core";
4
+ import { emitPrismEvent } from "@zephytiju/prism-react";
5
+ import { stringsForLocale } from "../locales/index.js";
6
+ import { ACCENT, BORDER, CARD, LINE, MUTED, POPOVER_SHADOW } from "../tokens.js";
7
+ /** Pointer-travel beyond this many px counts as a drag, not a click (D11). */
8
+ const DRAG_THRESHOLD_PX = 4;
9
+ /**
10
+ * The block gutter's ⠿ handle (D3/D11): the drag-to-reorder affordance AND a
11
+ * single-click popover trigger. Click vs drag is distinguished by pointer
12
+ * travel — moving less than DRAG_THRESHOLD_PX before release is a click and
13
+ * opens the Feishu-style block popover; crossing the threshold starts a live
14
+ * drag reorder that emits "main-editor.reorder-block-requested" stepwise each
15
+ * time the pointer crosses onto another block row (the host applies each
16
+ * action and republishes, so the dragged block follows the pointer). The
17
+ * popover mutates the block (move up / move down / duplicate / delete — all
18
+ * IDossierDoc ACTIONS the host applies, never composition mutations) or
19
+ * anchors a new comment thread to the block via
20
+ * "main-editor.comment-requested" (D11). It closes on outside click and ESC.
21
+ */
22
+ export function DragMenu({ blockId, canMoveUp, canMoveDown, locale }) {
23
+ const [open, setOpen] = useState(false);
24
+ const [dragging, setDragging] = useState(false);
25
+ const strings = stringsForLocale(locale);
26
+ const pointerStart = useRef(null);
27
+ const didDrag = useRef(false);
28
+ const draggedRow = useRef(null);
29
+ const lastRowUnder = useRef(null);
30
+ const buttonRef = useRef(null);
31
+ useEffect(() => {
32
+ if (!dragging) {
33
+ return;
34
+ }
35
+ // Global cursor + selection lock while the block is being dragged.
36
+ const previousCursor = document.body.style.cursor;
37
+ const previousSelect = document.body.style.userSelect;
38
+ document.body.style.cursor = "grabbing";
39
+ document.body.style.userSelect = "none";
40
+ return () => {
41
+ document.body.style.cursor = previousCursor;
42
+ document.body.style.userSelect = previousSelect;
43
+ };
44
+ }, [dragging]);
45
+ const act = (direction) => {
46
+ emitPrismEvent("main-editor.reorder-block-requested", { blockId, direction });
47
+ setOpen(false);
48
+ };
49
+ /** Rows of the block stack in DOM order — DOM order is the block order. */
50
+ const stackRows = () => {
51
+ const row = draggedRow.current;
52
+ if (row === null || row.parentElement === null) {
53
+ return [];
54
+ }
55
+ return Array.from(row.parentElement.children).filter((child) => child instanceof HTMLElement && child.hasAttribute("data-block-row-id"));
56
+ };
57
+ /** Live drag reorder: emit one single-step move per row boundary crossed. */
58
+ const dragTo = (clientX, clientY) => {
59
+ const row = draggedRow.current;
60
+ if (row === null) {
61
+ return;
62
+ }
63
+ const under = document
64
+ .elementsFromPoint(clientX, clientY)
65
+ .map((element) => element instanceof HTMLElement ? element.closest("[data-block-row-id]") : null)
66
+ .find((candidate) => candidate !== null);
67
+ if (under === undefined || under === null) {
68
+ return;
69
+ }
70
+ if (under === row) {
71
+ lastRowUnder.current = null;
72
+ return;
73
+ }
74
+ if (under === lastRowUnder.current) {
75
+ return;
76
+ }
77
+ lastRowUnder.current = under;
78
+ const rows = stackRows();
79
+ const from = rows.indexOf(row);
80
+ const to = rows.indexOf(under);
81
+ if (from < 0 || to < 0) {
82
+ return;
83
+ }
84
+ emitPrismEvent("main-editor.reorder-block-requested", {
85
+ blockId,
86
+ direction: to < from ? "up" : "down",
87
+ });
88
+ };
89
+ return (_jsxs(Box, { style: { position: "relative", flexShrink: 0 }, onKeyDown: (event) => {
90
+ // ESC closes the popover from the handle OR any item (D11).
91
+ if (open && event.key === "Escape") {
92
+ setOpen(false);
93
+ buttonRef.current?.focus();
94
+ }
95
+ }, children: [_jsx("button", { ref: buttonRef, type: "button", style: {
96
+ width: 24,
97
+ height: 30,
98
+ fontSize: 16,
99
+ color: dragging ? ACCENT : MUTED,
100
+ background: "transparent",
101
+ border: "none",
102
+ cursor: dragging ? "grabbing" : "grab",
103
+ display: "inline-flex",
104
+ alignItems: "center",
105
+ justifyContent: "center",
106
+ lineHeight: 1,
107
+ touchAction: "none",
108
+ }, title: strings.dragMenuTitle, "aria-haspopup": "menu", "aria-expanded": open, "data-testid": `main-editor-drag-${blockId}`, onPointerDown: (event) => {
109
+ // Browsers always report the primary button as 0; synthetic events
110
+ // without button data (undefined) are treated as primary presses.
111
+ if (event.button !== undefined && event.button !== 0) {
112
+ return;
113
+ }
114
+ pointerStart.current = { x: event.clientX, y: event.clientY };
115
+ didDrag.current = false;
116
+ draggedRow.current = event.currentTarget.closest("[data-block-row-id]");
117
+ // Pointer capture routes the whole gesture to the handle; guarded
118
+ // because non-browser surfaces (jsdom) lack the API.
119
+ if (typeof event.currentTarget.setPointerCapture === "function") {
120
+ event.currentTarget.setPointerCapture(event.pointerId);
121
+ }
122
+ }, onPointerMove: (event) => {
123
+ const start = pointerStart.current;
124
+ const primaryHeld = event.buttons === undefined || (event.buttons & 1) !== 0;
125
+ if (start === null || !primaryHeld) {
126
+ return;
127
+ }
128
+ const dx = event.clientX - start.x;
129
+ const dy = event.clientY - start.y;
130
+ if (!didDrag.current && dx * dx + dy * dy < DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) {
131
+ return;
132
+ }
133
+ didDrag.current = true;
134
+ setDragging(true);
135
+ dragTo(event.clientX, event.clientY);
136
+ }, onPointerUp: (event) => {
137
+ if (pointerStart.current !== null &&
138
+ typeof event.currentTarget.hasPointerCapture === "function" &&
139
+ event.currentTarget.hasPointerCapture(event.pointerId)) {
140
+ event.currentTarget.releasePointerCapture(event.pointerId);
141
+ }
142
+ pointerStart.current = null;
143
+ lastRowUnder.current = null;
144
+ draggedRow.current = null;
145
+ if (didDrag.current) {
146
+ setDragging(false);
147
+ }
148
+ }, onPointerCancel: () => {
149
+ pointerStart.current = null;
150
+ lastRowUnder.current = null;
151
+ draggedRow.current = null;
152
+ setDragging(false);
153
+ }, onClick: () => {
154
+ // A completed drag suppresses the trailing click; only a true click
155
+ // (pointer travel under the threshold) toggles the popover.
156
+ if (didDrag.current) {
157
+ didDrag.current = false;
158
+ return;
159
+ }
160
+ setOpen((value) => !value);
161
+ }, children: "\u283F" }), open ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { position: "fixed", inset: 0, zIndex: 8 }, "data-testid": `main-editor-drag-${blockId}-overlay`, onClick: () => {
162
+ setOpen(false);
163
+ } }), _jsxs(Box, { component: "ul", w: 240, py: 4, style: {
164
+ position: "absolute",
165
+ top: 32,
166
+ left: 0,
167
+ background: CARD,
168
+ border: `1px solid ${LINE}`,
169
+ borderRadius: 8,
170
+ boxShadow: POPOVER_SHADOW,
171
+ zIndex: 9,
172
+ listStyle: "none",
173
+ margin: 0,
174
+ }, "data-testid": `main-editor-drag-${blockId}-menu`, role: "menu", "aria-label": strings.dragMenuTitle, children: [_jsx("li", { children: _jsxs("button", { type: "button", role: "menuitem", style: { ...popoverItemStyle, color: ACCENT }, "data-testid": `main-editor-drag-${blockId}-comment`, onClick: () => {
175
+ const request = { blockId };
176
+ emitPrismEvent("main-editor.comment-requested", request);
177
+ setOpen(false);
178
+ }, children: [_jsx("span", { "aria-hidden": "true", style: { fontSize: 12 }, children: "\uD83D\uDCAC" }), strings.commentOnBlock] }) }), _jsx("li", { style: popoverDividerStyle, "aria-hidden": "true" }), _jsx("li", { children: _jsxs("button", { type: "button", role: "menuitem", disabled: !canMoveUp, style: {
179
+ ...popoverItemStyle,
180
+ opacity: canMoveUp ? 1 : 0.4,
181
+ cursor: canMoveUp ? "pointer" : "default",
182
+ }, "data-testid": `main-editor-drag-${blockId}-up`, onClick: () => {
183
+ act("up");
184
+ }, children: [_jsx("span", { "aria-hidden": "true", children: "\u2912" }), strings.moveUp] }) }), _jsx("li", { children: _jsxs("button", { type: "button", role: "menuitem", disabled: !canMoveDown, style: {
185
+ ...popoverItemStyle,
186
+ opacity: canMoveDown ? 1 : 0.4,
187
+ cursor: canMoveDown ? "pointer" : "default",
188
+ }, "data-testid": `main-editor-drag-${blockId}-down`, onClick: () => {
189
+ act("down");
190
+ }, children: [_jsx("span", { "aria-hidden": "true", children: "\u2913" }), strings.moveDown] }) }), _jsx("li", { children: _jsxs("button", { type: "button", role: "menuitem", style: popoverItemStyle, "data-testid": `main-editor-drag-${blockId}-duplicate`, onClick: () => {
191
+ const request = { blockId };
192
+ emitPrismEvent("main-editor.duplicate-block-requested", request);
193
+ setOpen(false);
194
+ }, children: [_jsx("span", { "aria-hidden": "true", children: "\u29C9" }), strings.duplicateBlock] }) }), _jsx("li", { children: _jsxs("button", { type: "button", role: "menuitem", style: { ...popoverItemStyle, color: "var(--mantine-color-threat-filled)" }, "data-testid": `main-editor-drag-${blockId}-remove`, onClick: () => {
195
+ emitPrismEvent("main-editor.remove-block-requested", { blockId });
196
+ setOpen(false);
197
+ }, children: [_jsx("span", { "aria-hidden": "true", children: "\u2715" }), strings.removeBlock] }) })] })] })) : null] }));
198
+ }
199
+ /** One row of the ⠿ block popover (the v10 prototype's .bpitem). */
200
+ const popoverItemStyle = {
201
+ width: "100%",
202
+ height: 28,
203
+ display: "flex",
204
+ alignItems: "center",
205
+ gap: 10,
206
+ padding: "0 12px",
207
+ fontSize: 11,
208
+ color: "var(--mantine-color-text-filled)",
209
+ background: "transparent",
210
+ border: "none",
211
+ cursor: "pointer",
212
+ textAlign: "left",
213
+ };
214
+ /** The popover's divider rule (the v10 prototype's .bpdiv). */
215
+ const popoverDividerStyle = {
216
+ height: 1,
217
+ background: BORDER,
218
+ margin: "4px 12px",
219
+ };
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from "react";
2
+ import type { MainEditorLocale } from "../locales/index.js";
3
+ import type { EvidenceTableBlock, GeovisionEmbedBlock } from "../types.js";
4
+ /** Any embedded (file-preview) block the chrome wraps. */
5
+ export type EmbeddedBlockModel = GeovisionEmbedBlock | EvidenceTableBlock;
6
+ export interface EmbeddedBlockChromeProps {
7
+ readonly block: EmbeddedBlockModel;
8
+ /** Locale label for the kind tag (e.g. "GEOVISION 3D"). */
9
+ readonly kindLabel: string;
10
+ readonly locale: MainEditorLocale;
11
+ /** The live preview of the selected saved view (the render slot body). */
12
+ readonly children: ReactNode;
13
+ }
14
+ /**
15
+ * Card chrome shared by the embedded file-preview blocks (D10/D11): an
16
+ * outlined card with a kind tag, the SRC FILE chip naming the previewed
17
+ * source file, the source-file variant dropdown (SavedViewDropdown), and —
18
+ * per the D10 addendum — the ↗ open arrow at the block's top-right BESIDE the
19
+ * dropdown. The embedded body keeps its simple interactive features inline
20
+ * and is NOT click-to-open: the only navigation out of the block is the
21
+ * arrow, which emits "main-editor.open-view-requested" {sourceFileRef,
22
+ * viewId} for the host to open the source view.
23
+ *
24
+ * Render handoff (coordination-channel contract, FINALIZED 2026-09-22): the
25
+ * card body is a render slot stamped with the seven-field handoff
26
+ * `{sourceFileRef, viewId, mode, interactionMode, locale, width, height}`
27
+ * (mode "static" in v1, interactionMode "rich"); the slot enforces the
28
+ * contracted 240px min-height and may be pointer-captured by the hosted
29
+ * component. The Main Editor keeps its internal item-template renders — real
30
+ * hosting of the published GeoVision render package happens in the separate
31
+ * embed-block package, which receives exactly this shape.
32
+ */
33
+ export declare function EmbeddedBlockChrome({ block, kindLabel, locale, children }: EmbeddedBlockChromeProps): import("react").JSX.Element;
34
+ //# sourceMappingURL=EmbeddedBlockChrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbeddedBlockChrome.d.ts","sourceRoot":"","sources":["../../src/blocks/EmbeddedBlockChrome.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAEV,kBAAkB,EAClB,mBAAmB,EAGpB,MAAM,aAAa,CAAC;AAarB,0DAA0D;AAC1D,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAE1E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,wBAAwB,+BA4GnG"}
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Group } from "@mantine/core";
3
+ import { emitPrismEvent, usePrismStateValue } from "@zephytiju/prism-react";
4
+ import { formatMessage, stringsForLocale } from "../locales/index.js";
5
+ import { ACCENT, BORDER, CARD, CARD_DARK, EMBED_SLOT_MIN_HEIGHT, EMBED_SLOT_WIDTH, MONO, MUTED, } from "../tokens.js";
6
+ import { SavedViewDropdown } from "./SavedViewDropdown.js";
7
+ /**
8
+ * Card chrome shared by the embedded file-preview blocks (D10/D11): an
9
+ * outlined card with a kind tag, the SRC FILE chip naming the previewed
10
+ * source file, the source-file variant dropdown (SavedViewDropdown), and —
11
+ * per the D10 addendum — the ↗ open arrow at the block's top-right BESIDE the
12
+ * dropdown. The embedded body keeps its simple interactive features inline
13
+ * and is NOT click-to-open: the only navigation out of the block is the
14
+ * arrow, which emits "main-editor.open-view-requested" {sourceFileRef,
15
+ * viewId} for the host to open the source view.
16
+ *
17
+ * Render handoff (coordination-channel contract, FINALIZED 2026-09-22): the
18
+ * card body is a render slot stamped with the seven-field handoff
19
+ * `{sourceFileRef, viewId, mode, interactionMode, locale, width, height}`
20
+ * (mode "static" in v1, interactionMode "rich"); the slot enforces the
21
+ * contracted 240px min-height and may be pointer-captured by the hosted
22
+ * component. The Main Editor keeps its internal item-template renders — real
23
+ * hosting of the published GeoVision render package happens in the separate
24
+ * embed-block package, which receives exactly this shape.
25
+ */
26
+ export function EmbeddedBlockChrome({ block, kindLabel, locale, children }) {
27
+ const strings = stringsForLocale(locale);
28
+ const selectedView = usePrismStateValue("main-editor.selected-view");
29
+ const viewId = selectedView?.blockId === block.id ? selectedView.viewId : block.sourceFile.defaultViewId;
30
+ const handoff = {
31
+ sourceFileRef: block.sourceFile.id,
32
+ viewId,
33
+ mode: "static",
34
+ interactionMode: "rich",
35
+ locale,
36
+ width: EMBED_SLOT_WIDTH,
37
+ height: EMBED_SLOT_MIN_HEIGHT,
38
+ };
39
+ return (_jsxs(Box, { p: 12, style: { background: CARD_DARK, border: `1px solid ${BORDER}`, borderRadius: 6 }, "data-testid": `main-editor-block-${block.id}`, children: [_jsxs(Group, { justify: "space-between", align: "flex-start", gap: 10, wrap: "nowrap", children: [_jsxs(Group, { gap: 10, align: "center", wrap: "nowrap", miw: 0, children: [_jsx(Box, { h: 22, px: 10, style: {
40
+ borderRadius: 11,
41
+ background: CARD,
42
+ border: `1px solid ${ACCENT}`,
43
+ fontFamily: MONO,
44
+ fontSize: 7,
45
+ fontWeight: 500,
46
+ color: ACCENT,
47
+ display: "inline-flex",
48
+ alignItems: "center",
49
+ letterSpacing: "0.03em",
50
+ flexShrink: 0,
51
+ }, "data-testid": `main-editor-kind-tag-${block.id}`, children: kindLabel }), _jsx("span", { style: {
52
+ fontFamily: MONO,
53
+ fontSize: 7,
54
+ color: MUTED,
55
+ letterSpacing: "0.05em",
56
+ whiteSpace: "nowrap",
57
+ }, "data-testid": `main-editor-src-file-${block.id}`, children: formatMessage(strings.srcFile, { file: block.sourceFile.name }) })] }), _jsxs(Group, { gap: 6, align: "center", wrap: "nowrap", children: [_jsx(SavedViewDropdown, { blockId: block.id, sourceFile: block.sourceFile, locale: locale }), _jsx("button", { type: "button", style: {
58
+ width: 24,
59
+ height: 22,
60
+ background: CARD,
61
+ border: `1px solid ${BORDER}`,
62
+ borderRadius: 5,
63
+ fontFamily: MONO,
64
+ fontSize: 11,
65
+ fontWeight: 500,
66
+ color: ACCENT,
67
+ display: "inline-flex",
68
+ alignItems: "center",
69
+ justifyContent: "center",
70
+ cursor: "pointer",
71
+ flexShrink: 0,
72
+ lineHeight: 1,
73
+ }, title: strings.openSourceView, "aria-label": strings.openSourceView, "data-testid": `main-editor-open-${block.id}`, onClick: (event) => {
74
+ event.stopPropagation();
75
+ const request = {
76
+ sourceFileRef: block.sourceFile.id,
77
+ viewId,
78
+ };
79
+ emitPrismEvent("main-editor.open-view-requested", request);
80
+ }, children: "\u2197" })] })] }), _jsx(Box, { mt: 8, style: { minHeight: EMBED_SLOT_MIN_HEIGHT }, "data-embed-handoff": JSON.stringify(handoff), "data-testid": `main-editor-embed-slot-${block.id}`, children: children })] }));
81
+ }