@beyondwork/docx-react-component 1.0.106 → 1.0.109
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 +19 -5
- package/src/api/geometry-overlay-rects.ts +5 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/page-anchor-id.ts +5 -0
- package/src/api/public-types.ts +16 -9
- package/src/api/table-node-specs.ts +6 -0
- package/src/api/v3/_create.ts +2 -1
- package/src/api/v3/_page-anchor-id.ts +52 -0
- package/src/api/v3/_runtime-handle.ts +92 -1
- package/src/api/v3/ai/_audit-time.ts +5 -0
- package/src/api/v3/ai/_pe2-evidence.ts +38 -0
- package/src/api/v3/ai/attach.ts +7 -2
- package/src/api/v3/ai/replacement.ts +101 -18
- package/src/api/v3/ai/resolve.ts +2 -2
- package/src/api/v3/ai/review.ts +177 -3
- package/src/api/v3/index.ts +1 -0
- package/src/api/v3/runtime/collab.ts +462 -0
- package/src/api/v3/runtime/document.ts +503 -20
- package/src/api/v3/runtime/geometry.ts +97 -0
- package/src/api/v3/runtime/layout.ts +744 -0
- package/src/api/v3/runtime/perf-probe.ts +14 -0
- package/src/api/v3/runtime/viewport.ts +9 -8
- package/src/api/v3/ui/_types.ts +149 -55
- package/src/api/v3/ui/chrome-preset-model.ts +5 -5
- package/src/api/v3/ui/debug.ts +115 -2
- package/src/api/v3/ui/index.ts +13 -0
- package/src/api/v3/ui/overlays.ts +0 -8
- package/src/api/v3/ui/surface.ts +56 -0
- package/src/api/v3/ui/viewport.ts +22 -9
- package/src/core/commands/image-commands.ts +1 -0
- package/src/core/commands/index.ts +6 -0
- package/src/core/schema/text-schema.ts +43 -5
- package/src/core/selection/mapping.ts +8 -1
- package/src/core/selection/review-anchors.ts +5 -1
- package/src/core/state/text-transaction.ts +8 -2
- package/src/io/export/serialize-revisions.ts +149 -1
- package/src/io/normalize/normalize-text.ts +6 -0
- package/src/io/ooxml/parse-bookmark-references.ts +55 -0
- package/src/io/ooxml/parse-fields.ts +24 -2
- package/src/io/ooxml/parse-headers-footers.ts +38 -5
- package/src/io/ooxml/parse-main-document.ts +153 -9
- package/src/io/ooxml/parse-numbering.ts +20 -0
- package/src/io/ooxml/parse-revisions.ts +19 -8
- package/src/io/opc/package-reader.ts +98 -8
- package/src/model/anchor.ts +4 -3
- package/src/model/canonical-document.ts +220 -2
- package/src/model/canonical-hash.ts +221 -0
- package/src/model/canonical-layout-inputs.ts +245 -6
- package/src/model/layout/index.ts +1 -0
- package/src/model/layout/page-graph-types.ts +118 -1
- package/src/model/review/revision-types.ts +14 -3
- package/src/preservation/store.ts +20 -4
- package/src/review/README.md +1 -1
- package/src/review/store/revision-actions.ts +14 -2
- package/src/runtime/collab/event-types.ts +67 -1
- package/src/runtime/collab/runtime-collab-sync.ts +177 -5
- package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
- package/src/runtime/document-heading-outline.ts +147 -0
- package/src/runtime/document-navigation.ts +8 -243
- package/src/runtime/document-runtime.ts +240 -97
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
- package/src/runtime/formatting/layout-inputs.ts +38 -5
- package/src/runtime/formatting/numbering/geometry.ts +28 -2
- package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
- package/src/runtime/geometry/caret-geometry.ts +5 -6
- package/src/runtime/geometry/geometry-facet.ts +60 -10
- package/src/runtime/geometry/geometry-index.ts +591 -20
- package/src/runtime/geometry/geometry-types.ts +59 -0
- package/src/runtime/geometry/hit-test.ts +11 -1
- package/src/runtime/geometry/overlay-rects.ts +5 -3
- package/src/runtime/geometry/project-anchors.ts +1 -1
- package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
- package/src/runtime/layout/index.ts +6 -0
- package/src/runtime/layout/layout-engine-instance.ts +6 -1
- package/src/runtime/layout/layout-engine-version.ts +181 -16
- package/src/runtime/layout/layout-facet-types.ts +6 -0
- package/src/runtime/layout/page-graph.ts +21 -4
- package/src/runtime/layout/paginated-layout-engine.ts +139 -15
- package/src/runtime/layout/project-block-fragments.ts +265 -7
- package/src/runtime/layout/public-facet.ts +78 -24
- package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
- package/src/runtime/layout/table-row-split.ts +92 -35
- package/src/runtime/prerender/cache-envelope.ts +2 -2
- package/src/runtime/prerender/cache-key.ts +5 -4
- package/src/runtime/prerender/customxml-cache.ts +0 -1
- package/src/runtime/render/render-kernel.ts +1 -1
- package/src/runtime/revision-runtime.ts +112 -10
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +22 -2
- package/src/runtime/scopes/capabilities.ts +316 -0
- package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
- package/src/runtime/scopes/compiler-service.ts +108 -4
- package/src/runtime/scopes/content-control-evidence.ts +79 -0
- package/src/runtime/scopes/create-issue.ts +5 -5
- package/src/runtime/scopes/evidence.ts +91 -0
- package/src/runtime/scopes/formatting/apply.ts +2 -0
- package/src/runtime/scopes/geometry-evidence.ts +130 -0
- package/src/runtime/scopes/index.ts +54 -0
- package/src/runtime/scopes/issue-lifecycle.ts +224 -0
- package/src/runtime/scopes/layout-evidence.ts +374 -0
- package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
- package/src/runtime/scopes/preservation-boundary.ts +7 -1
- package/src/runtime/scopes/replacement/apply.ts +97 -34
- package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
- package/src/runtime/scopes/semantic-scope-types.ts +242 -3
- package/src/runtime/scopes/visualization.ts +28 -0
- package/src/runtime/surface-projection.ts +44 -5
- package/src/runtime/telemetry/perf-probe.ts +216 -0
- package/src/runtime/virtualized-rendering.ts +36 -1
- package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
- package/src/runtime/workflow/coordinator.ts +39 -11
- package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
- package/src/runtime/workflow/index.ts +3 -0
- package/src/runtime/workflow/overlay-lane-types.ts +58 -0
- package/src/runtime/workflow/overlay-lanes.ts +168 -10
- package/src/runtime/workflow/overlay-store.ts +2 -2
- package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
- package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
- package/src/session/_sync-legacy.ts +17 -27
- package/src/session/import/loader.ts +6 -4
- package/src/session/import/source-package-evidence.ts +186 -2
- package/src/session/index.ts +5 -6
- package/src/session/session.ts +30 -56
- package/src/session/types.ts +8 -13
- package/src/shell/session-bootstrap.ts +155 -81
- package/src/ui/WordReviewEditor.tsx +520 -12
- package/src/ui/editor-shell-view.tsx +14 -4
- package/src/ui/editor-surface-controller.tsx +5 -3
- package/src/ui/headless/selection-tool-resolver.ts +1 -2
- package/src/ui/presence-overlay-lane.ts +0 -1
- package/src/ui/ui-controller-factory.ts +7 -0
- package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
- package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
- package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
- package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
- package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
- package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
- package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
- package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
- package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
- package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
- package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
- package/src/ui-tailwind/debug/README.md +4 -1
- package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
- package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
- package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
- package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
- package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
- package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
- package/src/ui-tailwind/review-workspace/types.ts +26 -12
- package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
- package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
- package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
- package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
- package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
- package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
* - hyperlink → `<a>` tag.
|
|
34
34
|
* - opaque-block → `data-block-kind="opaque"`
|
|
35
35
|
* added to `TwOpaqueBlock` in Phase D.5 as the explicit marker.
|
|
36
|
+
* - template-slot → `data-node-type="sdt_block"` OR
|
|
37
|
+
* `data-block-kind="sdt"` / `data-sdt-type`
|
|
38
|
+
* emitted by the PM/page-stack SDT renderers.
|
|
36
39
|
*
|
|
37
40
|
* Multiple kinds can apply — a table cell that contains a hyperlink
|
|
38
41
|
* returns both `table-cell` and `hyperlink`. Caller merges the
|
|
@@ -215,6 +218,16 @@ export function resolveTargetKind(
|
|
|
215
218
|
if (hasAncestorAttributeValue(el, "data-block-kind", "opaque", root)) {
|
|
216
219
|
kinds.push("opaque-block");
|
|
217
220
|
}
|
|
221
|
+
|
|
222
|
+
// SDT / content-control template slots. These are user-facing chrome
|
|
223
|
+
// affordances only; import/export/model truth stays with the lower layers.
|
|
224
|
+
if (
|
|
225
|
+
hasAncestorAttributeValue(el, "data-node-type", "sdt_block", root) ||
|
|
226
|
+
hasAncestorAttributeValue(el, "data-block-kind", "sdt", root) ||
|
|
227
|
+
hasAncestorAttribute(el, ["data-sdt-type"], root)
|
|
228
|
+
) {
|
|
229
|
+
if (!kinds.includes("template-slot")) kinds.push("template-slot");
|
|
230
|
+
}
|
|
218
231
|
}
|
|
219
232
|
|
|
220
233
|
// Plain-text baseline so clipboard remains available regardless of
|
|
@@ -18,6 +18,12 @@ export interface TwAlertBannerProps {
|
|
|
18
18
|
* the prop preserves the signal-only banner.
|
|
19
19
|
*/
|
|
20
20
|
onShowDetail?: () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Opaque/preserve-only diagnostics are debug/operator UX. Default
|
|
23
|
+
* product chrome keeps them silent because the document content is
|
|
24
|
+
* preserved for export, not actionable for the reader.
|
|
25
|
+
*/
|
|
26
|
+
showPreservationDiagnostics?: boolean;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -25,7 +31,8 @@ export interface TwAlertBannerProps {
|
|
|
25
31
|
*
|
|
26
32
|
* Lane 6b §6b.S5 contract:
|
|
27
33
|
* (1) ONE banner at a time, highest severity wins.
|
|
28
|
-
* (2) Precedence: fatalError > blockExport > workflowBlocked
|
|
34
|
+
* (2) Precedence: fatalError > blockExport > workflowBlocked; preserveOnly
|
|
35
|
+
* is debug/operator-only via `showPreservationDiagnostics`.
|
|
29
36
|
* (3) Severity tones bind Lane 6a `--color-semantic-*` tokens — no legacy
|
|
30
37
|
* `bg-danger-soft` / `bg-warning-soft` / `bg-amber-50` class names.
|
|
31
38
|
* (4) Error (fatal / blockExport) → `--color-semantic-error(-soft)`
|
|
@@ -84,6 +91,7 @@ export function TwAlertBanner(
|
|
|
84
91
|
preserveOnlyCount,
|
|
85
92
|
workflowBlockedReasons = [],
|
|
86
93
|
onShowDetail,
|
|
94
|
+
showPreservationDiagnostics = false,
|
|
87
95
|
} = props;
|
|
88
96
|
|
|
89
97
|
const showDetailProp: Pick<BannerRender, "onShowDetail"> = onShowDetail
|
|
@@ -143,8 +151,8 @@ export function TwAlertBanner(
|
|
|
143
151
|
});
|
|
144
152
|
}
|
|
145
153
|
|
|
146
|
-
// 4. Preserve-only features —
|
|
147
|
-
if (preserveOnlyCount > 0) {
|
|
154
|
+
// 4. Preserve-only features — debug/operator-only warning.
|
|
155
|
+
if (showPreservationDiagnostics && preserveOnlyCount > 0) {
|
|
148
156
|
return renderBanner({
|
|
149
157
|
severity: "warning",
|
|
150
158
|
icon: (
|
|
@@ -72,6 +72,30 @@ interface FlatItem extends CommandPaletteItem {
|
|
|
72
72
|
groupLabel: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function firstEnabledIndex(items: readonly FlatItem[]): number {
|
|
76
|
+
return items.findIndex((item) => !item.disabled);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nextEnabledIndex(
|
|
80
|
+
items: readonly FlatItem[],
|
|
81
|
+
currentIndex: number,
|
|
82
|
+
direction: 1 | -1,
|
|
83
|
+
): number {
|
|
84
|
+
if (items.length === 0) return 0;
|
|
85
|
+
const start =
|
|
86
|
+
currentIndex >= 0 && currentIndex < items.length
|
|
87
|
+
? currentIndex
|
|
88
|
+
: direction > 0
|
|
89
|
+
? -1
|
|
90
|
+
: 0;
|
|
91
|
+
for (let offset = 1; offset <= items.length; offset += 1) {
|
|
92
|
+
const index = (start + direction * offset + items.length) % items.length;
|
|
93
|
+
if (!items[index]?.disabled) return index;
|
|
94
|
+
}
|
|
95
|
+
const first = firstEnabledIndex(items);
|
|
96
|
+
return first >= 0 ? first : 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
75
99
|
/**
|
|
76
100
|
* Fuzzy matcher — returns `true` if the needle matches the haystack as
|
|
77
101
|
* substring OR as a character subsequence. Cheap, deterministic, and
|
|
@@ -145,8 +169,16 @@ export function TwCommandPalette(
|
|
|
145
169
|
|
|
146
170
|
// Keep activeIndex inside the filtered result range.
|
|
147
171
|
useEffect(() => {
|
|
148
|
-
if (
|
|
149
|
-
|
|
172
|
+
if (flat.length === 0) {
|
|
173
|
+
if (activeIndex !== 0) setActiveIndex(0);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const active = flat[activeIndex];
|
|
177
|
+
if (!active || active.disabled) {
|
|
178
|
+
const firstEnabled = firstEnabledIndex(flat);
|
|
179
|
+
setActiveIndex(firstEnabled >= 0 ? firstEnabled : 0);
|
|
180
|
+
}
|
|
181
|
+
}, [activeIndex, flat]);
|
|
150
182
|
|
|
151
183
|
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
152
184
|
|
|
@@ -169,14 +201,12 @@ export function TwCommandPalette(
|
|
|
169
201
|
}
|
|
170
202
|
if (event.key === "ArrowDown") {
|
|
171
203
|
event.preventDefault();
|
|
172
|
-
setActiveIndex((i) => (flat
|
|
204
|
+
setActiveIndex((i) => nextEnabledIndex(flat, i, 1));
|
|
173
205
|
return;
|
|
174
206
|
}
|
|
175
207
|
if (event.key === "ArrowUp") {
|
|
176
208
|
event.preventDefault();
|
|
177
|
-
setActiveIndex((i) =>
|
|
178
|
-
flat.length === 0 ? 0 : (i - 1 + flat.length) % flat.length,
|
|
179
|
-
);
|
|
209
|
+
setActiveIndex((i) => nextEnabledIndex(flat, i, -1));
|
|
180
210
|
return;
|
|
181
211
|
}
|
|
182
212
|
if (event.key === "Enter") {
|
|
@@ -46,6 +46,8 @@ export interface ContextMenuItem {
|
|
|
46
46
|
id: string;
|
|
47
47
|
/** Display label. */
|
|
48
48
|
label: string;
|
|
49
|
+
/** Optional explanation, used for disabled product-signpost rows. */
|
|
50
|
+
description?: string;
|
|
49
51
|
/** Optional icon element rendered left of the label. */
|
|
50
52
|
icon?: React.ReactNode;
|
|
51
53
|
/** Shortcut key sequence shown right-aligned via TwShortcutHint. */
|
|
@@ -210,13 +212,16 @@ interface ContextMenuRowProps {
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
function ContextMenuRow({ item, platform }: ContextMenuRowProps): React.JSX.Element {
|
|
213
|
-
const { label, icon, shortcut, disabled, onSelect } = item;
|
|
215
|
+
const { label, description, icon, shortcut, disabled, onSelect } = item;
|
|
216
|
+
const disabledExplanation = disabled && description ? description : undefined;
|
|
214
217
|
|
|
215
218
|
return (
|
|
216
219
|
<button
|
|
217
220
|
role="menuitem"
|
|
218
221
|
data-testid={`context-menu-item-${item.id}`}
|
|
219
222
|
disabled={disabled}
|
|
223
|
+
title={disabledExplanation}
|
|
224
|
+
data-disabled-reason={disabledExplanation}
|
|
220
225
|
onClick={disabled ? undefined : onSelect}
|
|
221
226
|
className={[
|
|
222
227
|
"flex items-center justify-between h-[30px] w-full px-3",
|
|
@@ -13,15 +13,14 @@
|
|
|
13
13
|
* names sees the right option highlighted without code changes.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import React, {
|
|
17
|
-
import
|
|
16
|
+
import React, { useState } from "react";
|
|
17
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
18
18
|
import { ChevronDown, Eye, EyeOff, Highlighter, Scroll } from "lucide-react";
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
normalizeMarkupDisplay,
|
|
22
22
|
type MarkupDisplay,
|
|
23
23
|
} from "../../ui/headless/comment-decoration-model";
|
|
24
|
-
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
25
24
|
|
|
26
25
|
export type DisplayMode = "all-markup" | "simple-markup" | "no-markup" | "original";
|
|
27
26
|
|
|
@@ -68,136 +67,70 @@ const MODES: readonly ModeEntry[] = [
|
|
|
68
67
|
|
|
69
68
|
export function TwDisplayModeSelector(props: TwDisplayModeSelectorProps): React.ReactElement {
|
|
70
69
|
const [open, setOpen] = useState(false);
|
|
71
|
-
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
72
70
|
const canonical = normalizeMarkupDisplay(props.value);
|
|
73
71
|
const activeEntry = MODES.find((m) => m.mode === canonical) ?? MODES[0]!;
|
|
74
72
|
|
|
75
73
|
return (
|
|
76
|
-
|
|
74
|
+
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
75
|
+
<Popover.Trigger asChild>
|
|
77
76
|
<button
|
|
78
|
-
ref={triggerRef}
|
|
79
77
|
type="button"
|
|
80
78
|
disabled={props.disabled}
|
|
81
79
|
data-testid={props["data-testid"] ?? "display-mode-selector-trigger"}
|
|
82
80
|
aria-label={`Display mode: ${activeEntry.label}`}
|
|
83
|
-
|
|
84
|
-
aria-haspopup="menu"
|
|
85
|
-
onMouseDown={preserveEditorSelectionMouseDown}
|
|
86
|
-
onClick={(event) => {
|
|
87
|
-
event.preventDefault();
|
|
88
|
-
setOpen((value) => !value);
|
|
89
|
-
}}
|
|
90
|
-
className={[
|
|
91
|
-
"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium text-primary",
|
|
92
|
-
"hover:bg-surface focus-visible:outline-none focus-visible:bg-surface disabled:opacity-50",
|
|
93
|
-
open ? "bg-canvas text-accent ring-1 ring-accent/30 shadow-sm" : "",
|
|
94
|
-
].join(" ")}
|
|
81
|
+
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium text-primary hover:bg-surface focus-visible:outline-none focus-visible:bg-surface disabled:opacity-50"
|
|
95
82
|
>
|
|
96
83
|
<activeEntry.icon className="h-3.5 w-3.5 text-tertiary" />
|
|
97
84
|
<span>{activeEntry.label}</span>
|
|
98
85
|
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
99
86
|
</button>
|
|
100
|
-
|
|
87
|
+
</Popover.Trigger>
|
|
88
|
+
<Popover.Portal>
|
|
89
|
+
<Popover.Content
|
|
90
|
+
className="z-50 w-[260px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
|
|
91
|
+
sideOffset={8}
|
|
92
|
+
align="end"
|
|
93
|
+
data-testid="display-mode-selector-content"
|
|
94
|
+
>
|
|
101
95
|
<div className="mb-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
102
96
|
Display mode
|
|
103
97
|
</div>
|
|
104
98
|
{MODES.map((entry) => {
|
|
105
99
|
const isActive = entry.mode === canonical;
|
|
106
100
|
return (
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
101
|
+
<Popover.Close key={entry.mode} asChild>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
role="menuitemradio"
|
|
105
|
+
aria-checked={isActive}
|
|
106
|
+
onClick={() => {
|
|
107
|
+
props.onChange(entry.mode);
|
|
108
|
+
}}
|
|
109
|
+
className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left text-[11px] transition-colors hover:bg-surface focus-visible:outline-none focus-visible:bg-surface"
|
|
110
|
+
data-testid={`display-mode-option-${entry.mode}`}
|
|
111
|
+
data-mode={entry.mode}
|
|
112
|
+
data-active={isActive ? "true" : undefined}
|
|
113
|
+
>
|
|
114
|
+
<entry.icon
|
|
115
|
+
className={[
|
|
116
|
+
"mt-0.5 h-3.5 w-3.5 shrink-0",
|
|
117
|
+
isActive ? "text-accent" : "text-tertiary",
|
|
118
|
+
].join(" ")}
|
|
119
|
+
/>
|
|
120
|
+
<span className="flex flex-col">
|
|
121
|
+
<span className={`font-medium ${isActive ? "text-accent" : "text-primary"}`}>
|
|
122
|
+
{entry.label}
|
|
123
|
+
</span>
|
|
124
|
+
<span className="text-[10px] text-secondary">{entry.hint}</span>
|
|
130
125
|
</span>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</button>
|
|
126
|
+
</button>
|
|
127
|
+
</Popover.Close>
|
|
134
128
|
);
|
|
135
129
|
})}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function DisplayModePortalMenu(props: {
|
|
142
|
-
anchorRef: React.RefObject<HTMLButtonElement | null>;
|
|
143
|
-
children: React.ReactNode;
|
|
144
|
-
open: boolean;
|
|
145
|
-
}): React.ReactPortal | null {
|
|
146
|
-
const style = useDisplayModePortalPosition(props.anchorRef, props.open);
|
|
147
|
-
const body = props.anchorRef.current?.ownerDocument?.body;
|
|
148
|
-
if (!props.open || !body) return null;
|
|
149
|
-
return createPortal(
|
|
150
|
-
<div
|
|
151
|
-
className="z-50 w-[260px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
|
|
152
|
-
data-testid="display-mode-selector-content"
|
|
153
|
-
style={style}
|
|
154
|
-
>
|
|
155
|
-
{props.children}
|
|
156
|
-
</div>,
|
|
157
|
-
body,
|
|
130
|
+
</Popover.Content>
|
|
131
|
+
</Popover.Portal>
|
|
132
|
+
</Popover.Root>
|
|
158
133
|
);
|
|
159
134
|
}
|
|
160
135
|
|
|
161
|
-
function useDisplayModePortalPosition(
|
|
162
|
-
anchorRef: React.RefObject<HTMLButtonElement | null>,
|
|
163
|
-
open: boolean,
|
|
164
|
-
): CSSProperties {
|
|
165
|
-
const [style, setStyle] = useState<CSSProperties>({
|
|
166
|
-
left: 8,
|
|
167
|
-
position: "fixed",
|
|
168
|
-
top: 8,
|
|
169
|
-
zIndex: 50,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
useLayoutEffect(() => {
|
|
173
|
-
if (!open) return;
|
|
174
|
-
const anchor = anchorRef.current;
|
|
175
|
-
const ownerWindow = anchor?.ownerDocument?.defaultView;
|
|
176
|
-
if (!anchor || !ownerWindow) return;
|
|
177
|
-
const update = () => {
|
|
178
|
-
const rect = anchor.getBoundingClientRect();
|
|
179
|
-
const width = 260;
|
|
180
|
-
const left = Math.min(
|
|
181
|
-
Math.max(8, rect.right - width),
|
|
182
|
-
Math.max(8, (ownerWindow.innerWidth || width + 16) - width - 8),
|
|
183
|
-
);
|
|
184
|
-
setStyle({
|
|
185
|
-
left,
|
|
186
|
-
position: "fixed",
|
|
187
|
-
top: Math.max(8, rect.bottom + 8),
|
|
188
|
-
zIndex: 50,
|
|
189
|
-
});
|
|
190
|
-
};
|
|
191
|
-
update();
|
|
192
|
-
ownerWindow.addEventListener("resize", update);
|
|
193
|
-
ownerWindow.addEventListener("scroll", update, true);
|
|
194
|
-
return () => {
|
|
195
|
-
ownerWindow.removeEventListener("resize", update);
|
|
196
|
-
ownerWindow.removeEventListener("scroll", update, true);
|
|
197
|
-
};
|
|
198
|
-
}, [anchorRef, open]);
|
|
199
|
-
|
|
200
|
-
return style;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
136
|
export default TwDisplayModeSelector;
|
|
@@ -16,6 +16,16 @@ export interface TwInlineFindBarProps {
|
|
|
16
16
|
export function TwInlineFindBar(props: TwInlineFindBarProps): React.JSX.Element {
|
|
17
17
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
18
18
|
const hasResults = props.resultCount > 0;
|
|
19
|
+
const hasQuery = props.query.trim().length > 0;
|
|
20
|
+
const resultState = hasQuery ? (hasResults ? "matches" : "empty") : "idle";
|
|
21
|
+
const statusLabel = hasQuery
|
|
22
|
+
? hasResults
|
|
23
|
+
? `${props.activeIndex + 1}/${props.resultCount}`
|
|
24
|
+
: "No results"
|
|
25
|
+
: "Find";
|
|
26
|
+
const navDisabledReason = hasQuery
|
|
27
|
+
? "No matches to navigate."
|
|
28
|
+
: "Enter a query to navigate matches.";
|
|
19
29
|
|
|
20
30
|
React.useEffect(() => {
|
|
21
31
|
inputRef.current?.focus();
|
|
@@ -26,6 +36,7 @@ export function TwInlineFindBar(props: TwInlineFindBarProps): React.JSX.Element
|
|
|
26
36
|
<div
|
|
27
37
|
className="pointer-events-auto flex w-[min(420px,calc(100vw-2rem))] items-center gap-2 rounded-2xl border border-[color:color-mix(in_srgb,var(--color-accent-primary)_28%,var(--color-border-subtle))] bg-[color:color-mix(in_srgb,var(--color-bg-canvas)_94%,white)] px-2.5 py-2 shadow-[0_18px_50px_rgba(20,31,29,0.18)]"
|
|
28
38
|
data-testid="inline-find-bar"
|
|
39
|
+
data-result-state={resultState}
|
|
29
40
|
role="search"
|
|
30
41
|
aria-label="Find in document"
|
|
31
42
|
>
|
|
@@ -33,6 +44,7 @@ export function TwInlineFindBar(props: TwInlineFindBarProps): React.JSX.Element
|
|
|
33
44
|
<input
|
|
34
45
|
ref={inputRef}
|
|
35
46
|
aria-label="Find text"
|
|
47
|
+
aria-describedby="inline-find-bar-status"
|
|
36
48
|
className="min-w-0 flex-1 bg-transparent px-1 text-[13px] font-medium text-primary outline-none placeholder:text-tertiary"
|
|
37
49
|
placeholder="Find in document"
|
|
38
50
|
type="search"
|
|
@@ -55,19 +67,24 @@ export function TwInlineFindBar(props: TwInlineFindBarProps): React.JSX.Element
|
|
|
55
67
|
}}
|
|
56
68
|
/>
|
|
57
69
|
<span
|
|
58
|
-
|
|
70
|
+
id="inline-find-bar-status"
|
|
71
|
+
data-testid="inline-find-bar-status"
|
|
72
|
+
className={`min-w-[72px] rounded-full px-2 py-1 text-center text-[11px] font-semibold tabular-nums ${
|
|
73
|
+
resultState === "empty"
|
|
74
|
+
? "bg-[var(--color-semantic-warning-soft)] text-[var(--color-semantic-warning)]"
|
|
75
|
+
: "bg-[color:color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)] text-accent"
|
|
76
|
+
}`}
|
|
59
77
|
aria-live="polite"
|
|
60
78
|
>
|
|
61
|
-
{
|
|
62
|
-
? hasResults
|
|
63
|
-
? `${props.activeIndex + 1}/${props.resultCount}`
|
|
64
|
-
: "0/0"
|
|
65
|
-
: "Find"}
|
|
79
|
+
{statusLabel}
|
|
66
80
|
</span>
|
|
67
81
|
<button
|
|
68
82
|
type="button"
|
|
69
83
|
aria-label="Previous match"
|
|
84
|
+
aria-keyshortcuts="Shift+Enter"
|
|
70
85
|
disabled={!hasResults}
|
|
86
|
+
title={hasResults ? "Previous match (Shift+Enter)" : navDisabledReason}
|
|
87
|
+
data-testid="inline-find-bar-previous"
|
|
71
88
|
className="inline-flex h-7 w-7 items-center justify-center rounded-full text-secondary transition-colors hover:bg-hover disabled:cursor-not-allowed disabled:opacity-35"
|
|
72
89
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
73
90
|
onClick={props.onPrevious}
|
|
@@ -77,7 +94,10 @@ export function TwInlineFindBar(props: TwInlineFindBarProps): React.JSX.Element
|
|
|
77
94
|
<button
|
|
78
95
|
type="button"
|
|
79
96
|
aria-label="Next match"
|
|
97
|
+
aria-keyshortcuts="Enter"
|
|
80
98
|
disabled={!hasResults}
|
|
99
|
+
title={hasResults ? "Next match (Enter)" : navDisabledReason}
|
|
100
|
+
data-testid="inline-find-bar-next"
|
|
81
101
|
className="inline-flex h-7 w-7 items-center justify-center rounded-full text-secondary transition-colors hover:bg-hover disabled:cursor-not-allowed disabled:opacity-35"
|
|
82
102
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
83
103
|
onClick={props.onNext}
|