@beyondwork/docx-react-component 1.0.53 → 1.0.55
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 +1 -1
- package/src/api/public-types.ts +125 -7
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +27 -3
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/parse-field-switches.ts +134 -0
- package/src/io/ooxml/parse-fields.ts +28 -2
- package/src/model/canonical-document.ts +13 -2
- package/src/runtime/chart/chart-model-store.ts +88 -0
- package/src/runtime/chart/chart-snapshot.ts +239 -0
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +1 -2
- package/src/runtime/document-runtime.ts +115 -13
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +58 -1
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/page-number-format.ts +207 -0
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/surface-projection.ts +32 -3
- package/src/ui/WordReviewEditor.tsx +57 -3
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +90 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +4 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +14 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/editor-theme.css +249 -22
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -43,6 +43,13 @@ export interface ResolveSelectionToolPlacementInput {
|
|
|
43
43
|
edgePaddingPx?: number;
|
|
44
44
|
/** Gap between anchor and toolbar in px. Defaults to 12. */
|
|
45
45
|
gapPx?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Width of the review rail in px. When set, the available viewport
|
|
48
|
+
* width is reduced by this amount on the right side so the toolbar
|
|
49
|
+
* never overlaps the rail. A 1.5 rem (24px) margin is applied beyond
|
|
50
|
+
* the rail edge. Defaults to 0 (no rail).
|
|
51
|
+
*/
|
|
52
|
+
railWidthPx?: number;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
const DEFAULT_EDGE_PADDING = 16;
|
|
@@ -79,6 +86,11 @@ export function resolveSelectionToolPlacement(
|
|
|
79
86
|
),
|
|
80
87
|
);
|
|
81
88
|
|
|
89
|
+
// Rail-aware right boundary: viewport width minus rail width minus 1.5rem (24px) margin.
|
|
90
|
+
const RAIL_MARGIN = 24; // 1.5rem
|
|
91
|
+
const railWidthPx = input.railWidthPx ?? 0;
|
|
92
|
+
const effectiveRightEdge = container.widthPx - railWidthPx - (railWidthPx > 0 ? RAIL_MARGIN : 0);
|
|
93
|
+
|
|
82
94
|
const anchorLeft = anchor.leftPx;
|
|
83
95
|
const anchorRight = anchor.leftPx + anchor.widthPx;
|
|
84
96
|
const anchorTop = anchor.topPx;
|
|
@@ -86,14 +98,14 @@ export function resolveSelectionToolPlacement(
|
|
|
86
98
|
const centerX = anchorLeft + anchor.widthPx / 2;
|
|
87
99
|
const centerY = anchorTop + anchor.heightPx / 2;
|
|
88
100
|
|
|
89
|
-
const rightClearance =
|
|
101
|
+
const rightClearance = effectiveRightEdge - anchorRight - gapPx - edgePadding;
|
|
90
102
|
const leftClearance = anchorLeft - gapPx - edgePadding;
|
|
91
103
|
|
|
92
104
|
const clampedCenterX = Math.max(
|
|
93
105
|
edgePadding,
|
|
94
106
|
Math.min(
|
|
95
107
|
centerX,
|
|
96
|
-
Math.max(edgePadding,
|
|
108
|
+
Math.max(edgePadding, effectiveRightEdge - edgePadding),
|
|
97
109
|
),
|
|
98
110
|
);
|
|
99
111
|
const clampedCenterY = Math.max(
|
|
@@ -106,7 +118,7 @@ export function resolveSelectionToolPlacement(
|
|
|
106
118
|
),
|
|
107
119
|
),
|
|
108
120
|
);
|
|
109
|
-
const maxWidthPx = Math.max(220,
|
|
121
|
+
const maxWidthPx = Math.max(220, effectiveRightEdge - edgePadding * 2);
|
|
110
122
|
|
|
111
123
|
if (rightClearance >= toolbarWidth) {
|
|
112
124
|
return {
|
|
@@ -17,6 +17,7 @@ export interface TwSelectionToolbarProps {
|
|
|
17
17
|
onSetTextColor?: (color: string) => void;
|
|
18
18
|
onSetHighlightColor?: (color: string | null) => void;
|
|
19
19
|
onAddComment?: () => void;
|
|
20
|
+
density?: "micro" | "full";
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const focusRingClass =
|
|
@@ -33,6 +34,7 @@ const DEFAULT_HIGHLIGHT_COLOR = "#ffff00";
|
|
|
33
34
|
|
|
34
35
|
export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarProps>(function TwSelectionToolbar(props, ref) {
|
|
35
36
|
const { model } = props;
|
|
37
|
+
const density = props.density ?? "full";
|
|
36
38
|
const addCommentDisabled = !model.canAddComment;
|
|
37
39
|
const formattingDisabled = !model.canToggleFormatting;
|
|
38
40
|
const contextLabel = summarizeSelectionContext(model);
|
|
@@ -44,7 +46,13 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
44
46
|
<div
|
|
45
47
|
ref={ref}
|
|
46
48
|
data-testid="selection-toolbar"
|
|
47
|
-
className="inline-flex max-w-[min(22rem,calc(100vw-1.5rem))] items-center gap-1 rounded-lg border border-border/80 bg-canvas px-1 py-1 shadow-
|
|
49
|
+
className="inline-flex max-w-[min(22rem,calc(100vw-1.5rem))] items-center gap-1 rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)]/80 bg-[var(--color-bg-canvas)] px-1.5 py-1 shadow-[var(--shadow-float)] ring-1 ring-[var(--color-border-subtle)]/70"
|
|
50
|
+
style={{
|
|
51
|
+
paddingLeft: "calc(6px * var(--space-density-multiplier))",
|
|
52
|
+
paddingRight: "calc(6px * var(--space-density-multiplier))",
|
|
53
|
+
paddingTop: "calc(4px * var(--space-density-multiplier))",
|
|
54
|
+
paddingBottom: "calc(4px * var(--space-density-multiplier))",
|
|
55
|
+
}}
|
|
48
56
|
role="toolbar"
|
|
49
57
|
aria-label="Selection actions"
|
|
50
58
|
onFocusCapture={props.onFocusCapture}
|
|
@@ -71,24 +79,28 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
71
79
|
disabled={formattingDisabled}
|
|
72
80
|
onClick={props.onToggleUnderline}
|
|
73
81
|
/>
|
|
74
|
-
<ToolbarActionButton
|
|
75
|
-
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
76
|
-
label={`Apply ${model.textColorDefault ?? DEFAULT_TEXT_COLOR}`}
|
|
77
|
-
pressed={false}
|
|
78
|
-
disabled={formattingDisabled}
|
|
79
|
-
onClick={() => props.onSetTextColor?.(model.textColorDefault ?? DEFAULT_TEXT_COLOR)}
|
|
80
|
-
/>
|
|
81
|
-
<ToolbarActionButton
|
|
82
|
-
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
83
|
-
label={`Apply ${model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR} highlight`}
|
|
84
|
-
pressed={false}
|
|
85
|
-
disabled={formattingDisabled}
|
|
86
|
-
onClick={() =>
|
|
87
|
-
props.onSetHighlightColor?.(model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR)
|
|
88
|
-
}
|
|
89
|
-
/>
|
|
90
82
|
|
|
91
|
-
|
|
83
|
+
{density === "full" && (
|
|
84
|
+
<>
|
|
85
|
+
<ToolbarActionButton
|
|
86
|
+
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
87
|
+
label={`Apply ${model.textColorDefault ?? DEFAULT_TEXT_COLOR}`}
|
|
88
|
+
pressed={false}
|
|
89
|
+
disabled={formattingDisabled}
|
|
90
|
+
onClick={() => props.onSetTextColor?.(model.textColorDefault ?? DEFAULT_TEXT_COLOR)}
|
|
91
|
+
/>
|
|
92
|
+
<ToolbarActionButton
|
|
93
|
+
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
94
|
+
label={`Apply ${model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR} highlight`}
|
|
95
|
+
pressed={false}
|
|
96
|
+
disabled={formattingDisabled}
|
|
97
|
+
onClick={() =>
|
|
98
|
+
props.onSetHighlightColor?.(model.highlightColorDefault ?? DEFAULT_HIGHLIGHT_COLOR)
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
<div className="mx-0.5 h-4 w-px bg-[var(--color-border-subtle)]" />
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
92
104
|
|
|
93
105
|
<Tooltip.Root>
|
|
94
106
|
<Tooltip.Trigger asChild>
|
|
@@ -115,7 +127,7 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
115
127
|
|
|
116
128
|
{model.previewText ? (
|
|
117
129
|
<>
|
|
118
|
-
<div className="mx-0.5 h-4 w-px bg-border" />
|
|
130
|
+
<div className="mx-0.5 h-4 w-px bg-[var(--color-border-subtle)]" />
|
|
119
131
|
<span className="max-w-[7rem] truncate text-[10px] text-secondary">
|
|
120
132
|
{model.previewText}
|
|
121
133
|
</span>
|
|
@@ -124,7 +136,7 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
124
136
|
|
|
125
137
|
{contextLabel ? (
|
|
126
138
|
<>
|
|
127
|
-
{!model.previewText ? <div className="mx-0.5 h-4 w-px bg-border" /> : null}
|
|
139
|
+
{!model.previewText ? <div className="mx-0.5 h-4 w-px bg-[var(--color-border-subtle)]" /> : null}
|
|
128
140
|
<span
|
|
129
141
|
className={`min-w-0 max-w-[9rem] truncate rounded-full px-1.5 py-0.5 text-[9px] font-medium tracking-[0.08em] ${
|
|
130
142
|
model.badges.some((badge) => badge.tone === "accent")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type ShortcutKey =
|
|
4
|
+
| "Cmd" | "Ctrl" | "Shift" | "Alt" | "Enter" | "Esc" | "Tab" | "Backspace"
|
|
5
|
+
| string; // fall-through for single letters/digits
|
|
6
|
+
|
|
7
|
+
export interface TwShortcutHintProps {
|
|
8
|
+
keys: ShortcutKey[];
|
|
9
|
+
platform?: "mac" | "win";
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MAC_SYMBOL: Record<string, string> = {
|
|
14
|
+
Cmd: "⌘",
|
|
15
|
+
Shift: "⇧",
|
|
16
|
+
Alt: "⌥",
|
|
17
|
+
Ctrl: "⌃",
|
|
18
|
+
Enter: "↵",
|
|
19
|
+
Esc: "Esc",
|
|
20
|
+
Tab: "⇥",
|
|
21
|
+
Backspace: "⌫",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function detectPlatform(): "mac" | "win" {
|
|
25
|
+
if (typeof navigator === "undefined") return "win";
|
|
26
|
+
return /Mac|iPhone|iPad/.test(navigator.platform) ? "mac" : "win";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function TwShortcutHint(props: TwShortcutHintProps): React.JSX.Element {
|
|
30
|
+
const { keys, className } = props;
|
|
31
|
+
const platform = props.platform ?? detectPlatform();
|
|
32
|
+
|
|
33
|
+
const mapped = keys.map((k) =>
|
|
34
|
+
platform === "mac"
|
|
35
|
+
? (MAC_SYMBOL[k] ?? k)
|
|
36
|
+
: (k === "Cmd" ? "Ctrl" : k)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const containerClass = [
|
|
40
|
+
"inline-flex items-center gap-0.5",
|
|
41
|
+
"text-[9px] uppercase tracking-[0.06em]",
|
|
42
|
+
"text-[var(--color-text-tertiary)] opacity-75",
|
|
43
|
+
className,
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ");
|
|
47
|
+
|
|
48
|
+
const chipClass = [
|
|
49
|
+
"rounded-[var(--radius-sm)]",
|
|
50
|
+
"border border-[var(--color-border-default)]",
|
|
51
|
+
"bg-[var(--color-bg-muted)]",
|
|
52
|
+
"px-1 py-px leading-none font-medium",
|
|
53
|
+
].join(" ");
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<span className={containerClass} data-testid="tw-shortcut-hint">
|
|
57
|
+
{mapped.map((label, i) => (
|
|
58
|
+
<kbd
|
|
59
|
+
key={i}
|
|
60
|
+
className={chipClass}
|
|
61
|
+
data-testid="tw-shortcut-hint__chip"
|
|
62
|
+
>
|
|
63
|
+
{label}
|
|
64
|
+
</kbd>
|
|
65
|
+
))}
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -29,18 +29,18 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
|
29
29
|
return (
|
|
30
30
|
<div
|
|
31
31
|
data-testid="suggestion-card"
|
|
32
|
-
className="inline-flex max-w-[min(24rem,calc(100vw-1.5rem))] flex-col gap-1.5 rounded-xl border border-border/80 bg-canvas px-
|
|
32
|
+
className="inline-flex max-w-[min(24rem,calc(100vw-1.5rem))] flex-col gap-1.5 rounded-[var(--radius-xl)] border border-[var(--color-border-subtle)]/80 bg-[var(--color-bg-canvas)] px-[10px] py-[10px] shadow-[var(--shadow-float)] ring-1 ring-[var(--color-border-subtle)]/75"
|
|
33
33
|
onFocusCapture={props.onFocusCapture}
|
|
34
34
|
onBlurCapture={props.onBlurCapture}
|
|
35
|
-
role="
|
|
36
|
-
aria-label="Suggestion
|
|
35
|
+
role="region"
|
|
36
|
+
aria-label="Suggestion"
|
|
37
37
|
>
|
|
38
38
|
<div className="flex items-start justify-between gap-2">
|
|
39
39
|
<div className="min-w-0">
|
|
40
|
-
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-warning">
|
|
40
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--color-semantic-warning)]">
|
|
41
41
|
{props.model.kindLabel}
|
|
42
42
|
</div>
|
|
43
|
-
<div className="mt-0.5 max-w-[14rem] truncate text-[13px] text-primary">
|
|
43
|
+
<div className="mt-0.5 max-w-[14rem] truncate text-[13px] text-[var(--color-text-primary)]">
|
|
44
44
|
{props.model.previewText}
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
@@ -81,7 +81,7 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
|
81
81
|
disabled={commentDisabled}
|
|
82
82
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
83
83
|
onClick={props.onAddComment}
|
|
84
|
-
className={`inline-flex h-7 items-center gap-1 rounded-md border border-border px-2 text-[11px] font-medium text-secondary transition-colors hover:bg-
|
|
84
|
+
className={`inline-flex h-7 items-center gap-1 rounded-md border border-[var(--color-border-default)] px-2 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-hover)] disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
85
85
|
>
|
|
86
86
|
<MessageSquare className="h-3 w-3" />
|
|
87
87
|
Comment
|
|
@@ -118,10 +118,10 @@ function SuggestionActionButton(props: {
|
|
|
118
118
|
onClick?: () => void;
|
|
119
119
|
}) {
|
|
120
120
|
const toneClass = props.tone === "accept"
|
|
121
|
-
? "border-
|
|
121
|
+
? "border border-transparent bg-[var(--color-accent-primary)] text-[var(--color-text-on-accent)] hover:bg-[var(--color-accent-primary-hover)]"
|
|
122
122
|
: props.tone === "reject"
|
|
123
|
-
? "border-
|
|
124
|
-
: "border-border text-secondary hover:bg-
|
|
123
|
+
? "border border-[var(--color-semantic-error)]/35 bg-[var(--color-bg-canvas)] text-[var(--color-semantic-error)] hover:bg-[var(--color-semantic-error-soft)]"
|
|
124
|
+
: "border border-[var(--color-border-default)] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]";
|
|
125
125
|
|
|
126
126
|
return (
|
|
127
127
|
<button
|
|
@@ -130,7 +130,7 @@ function SuggestionActionButton(props: {
|
|
|
130
130
|
disabled={props.disabled}
|
|
131
131
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
132
132
|
onClick={props.onClick}
|
|
133
|
-
className={`inline-flex h-7 items-center gap-1 rounded-md
|
|
133
|
+
className={`inline-flex h-7 items-center gap-1 rounded-md px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${toneClass} ${focusRingClass}`}
|
|
134
134
|
>
|
|
135
135
|
{props.icon}
|
|
136
136
|
{props.label.replace(" suggestion", "").replace(" on suggestion", "")}
|
|
@@ -108,9 +108,9 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
108
108
|
<div
|
|
109
109
|
data-testid="table-context-toolbar"
|
|
110
110
|
data-tier={tier}
|
|
111
|
-
className={`flex ${widthCap} flex-wrap items-start gap-
|
|
111
|
+
className={`flex ${widthCap} flex-wrap items-start gap-[4px] rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)] bg-[var(--color-bg-canvas)] px-2.5 py-1.5 shadow-[var(--shadow-float)]`}
|
|
112
112
|
>
|
|
113
|
-
<span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
113
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]">
|
|
114
114
|
{tierLabel(tier)}
|
|
115
115
|
</span>
|
|
116
116
|
{tableSizeLabel ? <ToolbarBadge>{tableSizeLabel}</ToolbarBadge> : null}
|
|
@@ -120,6 +120,7 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
120
120
|
) : null}
|
|
121
121
|
|
|
122
122
|
{/* T5 whole-table: table alignment */}
|
|
123
|
+
{tier === "whole-table" ? <GroupDivider /> : null}
|
|
123
124
|
{tier === "whole-table" ? (
|
|
124
125
|
<ToolbarSection label="Align">
|
|
125
126
|
{(["left", "center", "right"] as const).map((align) => (
|
|
@@ -166,6 +167,9 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
166
167
|
</ToolbarSection>
|
|
167
168
|
) : null}
|
|
168
169
|
|
|
170
|
+
{/* separator before row/column structure group */}
|
|
171
|
+
<GroupDivider />
|
|
172
|
+
|
|
169
173
|
{/* T2 / T4a row-selected: row ops */}
|
|
170
174
|
{(tier === "caret-in-cell" || tier === "row-selected") ? (
|
|
171
175
|
<ToolbarSection label="Rows">
|
|
@@ -261,6 +265,11 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
261
265
|
</ToolbarSection>
|
|
262
266
|
) : null}
|
|
263
267
|
|
|
268
|
+
{/* separator before merge/split group (multi-cell, row, column) */}
|
|
269
|
+
{tier === "multi-cell" || tier === "row-selected" || tier === "column-selected" ? (
|
|
270
|
+
<GroupDivider />
|
|
271
|
+
) : null}
|
|
272
|
+
|
|
264
273
|
{/* T3 multi-cell: merge/split */}
|
|
265
274
|
{tier === "multi-cell" ||
|
|
266
275
|
tier === "row-selected" ||
|
|
@@ -333,6 +342,9 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
333
342
|
</ToolbarSection>
|
|
334
343
|
) : null}
|
|
335
344
|
|
|
345
|
+
{/* separator before delete table (whole-table only) */}
|
|
346
|
+
{tier === "whole-table" ? <GroupDivider /> : null}
|
|
347
|
+
|
|
336
348
|
{/* T5 only: delete table (danger) */}
|
|
337
349
|
{tier === "whole-table" ? (
|
|
338
350
|
<ToolbarSection label="Table">
|
|
@@ -399,6 +411,15 @@ function tierWidthCap(tier: TableTier): string {
|
|
|
399
411
|
}
|
|
400
412
|
}
|
|
401
413
|
|
|
414
|
+
function GroupDivider() {
|
|
415
|
+
return (
|
|
416
|
+
<div
|
|
417
|
+
className="mx-0.5 h-5 w-px bg-[var(--color-border-subtle)]"
|
|
418
|
+
aria-hidden="true"
|
|
419
|
+
/>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
402
423
|
function ToolbarBadge(props: {
|
|
403
424
|
children: React.ReactNode;
|
|
404
425
|
tone?: "neutral" | "accent";
|
|
@@ -422,8 +443,8 @@ function ToolbarSection(props: {
|
|
|
422
443
|
children: React.ReactNode;
|
|
423
444
|
}) {
|
|
424
445
|
return (
|
|
425
|
-
<div className="flex flex-wrap items-center gap-1 rounded-md bg-
|
|
426
|
-
<span className="text-[9px] font-semibold uppercase tracking-[0.08em] text-tertiary">
|
|
446
|
+
<div className="flex flex-wrap items-center gap-1 rounded-md bg-[var(--color-bg-muted)]/60 px-1.5 py-1 ring-1 ring-[var(--color-border-subtle)]/35">
|
|
447
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]">
|
|
427
448
|
{props.label}
|
|
428
449
|
</span>
|
|
429
450
|
<div className="flex flex-wrap items-center gap-1">{props.children}</div>
|
|
@@ -455,7 +476,7 @@ function ToolbarButton(props: {
|
|
|
455
476
|
props.active
|
|
456
477
|
? "bg-accent/15 text-accent"
|
|
457
478
|
: props.danger
|
|
458
|
-
? "text-
|
|
479
|
+
? "text-[var(--color-semantic-error)] hover:bg-[var(--color-semantic-error-soft)]"
|
|
459
480
|
: "text-primary hover:bg-surface"
|
|
460
481
|
}`}
|
|
461
482
|
>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* grip so PM still receives the click and places the caret.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import React, { useCallback, useEffect, useRef } from "react";
|
|
24
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
25
25
|
|
|
26
26
|
import type {
|
|
27
27
|
TableStructureContextSnapshot,
|
|
@@ -32,7 +32,8 @@ import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector
|
|
|
32
32
|
import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
|
|
33
33
|
import { forwardNonDragClick } from "./forward-non-drag-click";
|
|
34
34
|
|
|
35
|
-
const GRIP_PX =
|
|
35
|
+
const GRIP_PX = 2;
|
|
36
|
+
const GRIP_HIT_PX = 8; // total hit area (2 px visual + 3 px pad each side)
|
|
36
37
|
const DRAG_THRESHOLD_PX = 3;
|
|
37
38
|
const MIN_COLUMN_TWIPS = 720;
|
|
38
39
|
const MIN_ROW_TWIPS = 120;
|
|
@@ -143,6 +144,7 @@ function ColResizeGrip({
|
|
|
143
144
|
dragStarted: boolean;
|
|
144
145
|
gripEl: HTMLElement;
|
|
145
146
|
} | null>(null);
|
|
147
|
+
const [isActive, setIsActive] = useState(false);
|
|
146
148
|
|
|
147
149
|
const handleMouseDown = useCallback(
|
|
148
150
|
(e: React.MouseEvent<HTMLElement>) => {
|
|
@@ -164,14 +166,17 @@ function ColResizeGrip({
|
|
|
164
166
|
if (!drag.dragStarted) {
|
|
165
167
|
if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD_PX) return;
|
|
166
168
|
drag.dragStarted = true;
|
|
169
|
+
setIsActive(true);
|
|
167
170
|
}
|
|
168
171
|
e.preventDefault();
|
|
169
172
|
};
|
|
170
173
|
const handleUp = (e: MouseEvent) => {
|
|
171
174
|
const drag = dragRef.current;
|
|
172
175
|
if (!drag) return;
|
|
176
|
+
const wasDragging = drag.dragStarted;
|
|
173
177
|
dragRef.current = null;
|
|
174
|
-
|
|
178
|
+
setIsActive(false);
|
|
179
|
+
if (!wasDragging) {
|
|
175
180
|
forwardNonDragClick(drag.gripEl, e);
|
|
176
181
|
return;
|
|
177
182
|
}
|
|
@@ -196,18 +201,17 @@ function ColResizeGrip({
|
|
|
196
201
|
aria-orientation="vertical"
|
|
197
202
|
aria-label={`Resize column ${colIndex + 1}`}
|
|
198
203
|
data-testid={`col-resize-grip-${colIndex}`}
|
|
204
|
+
data-active={isActive ? "true" : "false"}
|
|
199
205
|
className={[
|
|
200
|
-
"pointer-events-auto absolute",
|
|
201
|
-
disabled
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
"bg-accent",
|
|
206
|
-
].join(" ")}
|
|
206
|
+
"wre-table-grip-col pointer-events-auto absolute",
|
|
207
|
+
disabled ? "opacity-0 cursor-default" : "",
|
|
208
|
+
]
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.join(" ")}
|
|
207
211
|
style={{
|
|
208
|
-
left: `calc(${pos.left} - ${
|
|
212
|
+
left: `calc(${pos.left} - ${GRIP_HIT_PX / 2}px)`,
|
|
209
213
|
top: pos.top,
|
|
210
|
-
width: `${
|
|
214
|
+
width: `${GRIP_HIT_PX}px`,
|
|
211
215
|
height: pos.height,
|
|
212
216
|
}}
|
|
213
217
|
onMouseDown={handleMouseDown}
|
|
@@ -243,6 +247,7 @@ function RowResizeGrip({
|
|
|
243
247
|
dragStarted: boolean;
|
|
244
248
|
gripEl: HTMLElement;
|
|
245
249
|
} | null>(null);
|
|
250
|
+
const [isActive, setIsActive] = useState(false);
|
|
246
251
|
|
|
247
252
|
const handleMouseDown = useCallback(
|
|
248
253
|
(e: React.MouseEvent<HTMLElement>) => {
|
|
@@ -263,14 +268,17 @@ function RowResizeGrip({
|
|
|
263
268
|
if (!drag.dragStarted) {
|
|
264
269
|
if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
|
|
265
270
|
drag.dragStarted = true;
|
|
271
|
+
setIsActive(true);
|
|
266
272
|
}
|
|
267
273
|
e.preventDefault();
|
|
268
274
|
};
|
|
269
275
|
const handleUp = (e: MouseEvent) => {
|
|
270
276
|
const drag = dragRef.current;
|
|
271
277
|
if (!drag) return;
|
|
278
|
+
const wasDragging = drag.dragStarted;
|
|
272
279
|
dragRef.current = null;
|
|
273
|
-
|
|
280
|
+
setIsActive(false);
|
|
281
|
+
if (!wasDragging) {
|
|
274
282
|
forwardNonDragClick(drag.gripEl, e);
|
|
275
283
|
return;
|
|
276
284
|
}
|
|
@@ -296,19 +304,18 @@ function RowResizeGrip({
|
|
|
296
304
|
aria-orientation="horizontal"
|
|
297
305
|
aria-label={`Resize row ${rowIndex + 1}`}
|
|
298
306
|
data-testid={`row-resize-grip-${rowIndex}`}
|
|
307
|
+
data-active={isActive ? "true" : "false"}
|
|
299
308
|
className={[
|
|
300
|
-
"pointer-events-auto absolute",
|
|
301
|
-
disabled
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"
|
|
305
|
-
"bg-accent",
|
|
306
|
-
].join(" ")}
|
|
309
|
+
"wre-table-grip-row pointer-events-auto absolute",
|
|
310
|
+
disabled ? "opacity-0 cursor-default" : "",
|
|
311
|
+
]
|
|
312
|
+
.filter(Boolean)
|
|
313
|
+
.join(" ")}
|
|
307
314
|
style={{
|
|
308
315
|
left: pos.left,
|
|
309
|
-
top: `calc(${pos.top} - ${
|
|
316
|
+
top: `calc(${pos.top} - ${GRIP_HIT_PX / 2}px)`,
|
|
310
317
|
width: pos.width,
|
|
311
|
-
height: `${
|
|
318
|
+
height: `${GRIP_HIT_PX}px`,
|
|
312
319
|
}}
|
|
313
320
|
onMouseDown={handleMouseDown}
|
|
314
321
|
/>
|
|
@@ -8,28 +8,76 @@ export interface TwUnsavedModalProps {
|
|
|
8
8
|
onCancel: () => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* TwUnsavedModal — blocking confirmation modal for close-with-unsaved-changes
|
|
13
|
+
* (designsystem §6.18). Lane 6b §6b.S7 rebinds every surface, shadow,
|
|
14
|
+
* radius, and colour to the Lane 6a token vocabulary:
|
|
15
|
+
*
|
|
16
|
+
* backdrop → bg-[var(--color-bg-overlay)] + backdrop-blur
|
|
17
|
+
* card → bg-[var(--color-bg-elevated)]
|
|
18
|
+
* shadow-[var(--shadow-float)]
|
|
19
|
+
* ring-1 ring-[var(--color-border-subtle)]
|
|
20
|
+
* rounded-[var(--radius-sm)]
|
|
21
|
+
* icon → bg-[var(--color-semantic-warning-soft)] / text-semantic-warning
|
|
22
|
+
* cancel → hover:bg-[var(--color-bg-hover)]
|
|
23
|
+
* discard → bg-[var(--color-semantic-error)] + hover shade
|
|
24
|
+
*
|
|
25
|
+
* Every transition binds `duration-[var(--motion-fast)]` so the reduced-
|
|
26
|
+
* motion media override in tokens.css zeroes out appropriately.
|
|
27
|
+
*/
|
|
11
28
|
export function TwUnsavedModal(props: TwUnsavedModalProps) {
|
|
12
29
|
if (!props.open) return null;
|
|
13
30
|
|
|
14
31
|
return (
|
|
15
|
-
<div
|
|
32
|
+
<div
|
|
33
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
34
|
+
role="dialog"
|
|
35
|
+
aria-modal="true"
|
|
36
|
+
aria-labelledby="tw-unsaved-modal__title"
|
|
37
|
+
data-testid="tw-unsaved-modal"
|
|
38
|
+
>
|
|
16
39
|
{/* Backdrop */}
|
|
17
40
|
<div
|
|
18
|
-
className=
|
|
41
|
+
className={[
|
|
42
|
+
"absolute inset-0 bg-[var(--color-bg-overlay)] backdrop-blur-sm",
|
|
43
|
+
"transition-opacity duration-[var(--motion-fast)]",
|
|
44
|
+
].join(" ")}
|
|
19
45
|
onClick={props.onCancel}
|
|
46
|
+
data-testid="tw-unsaved-modal__backdrop"
|
|
20
47
|
/>
|
|
21
48
|
|
|
22
|
-
{/*
|
|
23
|
-
<div
|
|
49
|
+
{/* Card */}
|
|
50
|
+
<div
|
|
51
|
+
className={[
|
|
52
|
+
"relative mx-4 w-full max-w-md p-6",
|
|
53
|
+
"rounded-[var(--radius-sm)]",
|
|
54
|
+
"bg-[var(--color-bg-elevated)]",
|
|
55
|
+
"shadow-[var(--shadow-float)]",
|
|
56
|
+
"ring-1 ring-[var(--color-border-subtle)]",
|
|
57
|
+
].join(" ")}
|
|
58
|
+
data-testid="tw-unsaved-modal__card"
|
|
59
|
+
>
|
|
24
60
|
<div className="flex items-start gap-3">
|
|
25
|
-
<div
|
|
26
|
-
|
|
61
|
+
<div
|
|
62
|
+
className={[
|
|
63
|
+
"flex h-9 w-9 shrink-0 items-center justify-center",
|
|
64
|
+
"rounded-[var(--radius-pill)]",
|
|
65
|
+
"bg-[var(--color-semantic-warning-soft)]",
|
|
66
|
+
].join(" ")}
|
|
67
|
+
>
|
|
68
|
+
<AlertTriangle
|
|
69
|
+
className="h-5 w-5 text-[var(--color-semantic-warning)]"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
/>
|
|
27
72
|
</div>
|
|
28
73
|
<div className="flex-1">
|
|
29
|
-
<h3
|
|
74
|
+
<h3
|
|
75
|
+
id="tw-unsaved-modal__title"
|
|
76
|
+
className="text-base font-semibold text-[var(--color-text-primary)]"
|
|
77
|
+
>
|
|
30
78
|
Unsaved changes
|
|
31
79
|
</h3>
|
|
32
|
-
<p className="mt-1.5 text-sm text-secondary
|
|
80
|
+
<p className="mt-1.5 text-sm leading-relaxed text-[var(--color-text-secondary)]">
|
|
33
81
|
{props.message ??
|
|
34
82
|
"You have unsaved changes that will be lost. Your work is being autosaved, but the latest edits may not be saved yet."}
|
|
35
83
|
</p>
|
|
@@ -40,14 +88,28 @@ export function TwUnsavedModal(props: TwUnsavedModalProps) {
|
|
|
40
88
|
<button
|
|
41
89
|
type="button"
|
|
42
90
|
onClick={props.onCancel}
|
|
43
|
-
className=
|
|
91
|
+
className={[
|
|
92
|
+
"rounded-[var(--radius-sm)] px-4 py-2 text-sm font-medium",
|
|
93
|
+
"text-[var(--color-text-secondary)]",
|
|
94
|
+
"hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-primary)]",
|
|
95
|
+
"transition-colors duration-[var(--motion-fast)]",
|
|
96
|
+
"focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
|
|
97
|
+
].join(" ")}
|
|
98
|
+
data-testid="tw-unsaved-modal__cancel"
|
|
44
99
|
>
|
|
45
100
|
Keep editing
|
|
46
101
|
</button>
|
|
47
102
|
<button
|
|
48
103
|
type="button"
|
|
49
104
|
onClick={props.onDiscard}
|
|
50
|
-
className=
|
|
105
|
+
className={[
|
|
106
|
+
"rounded-[var(--radius-sm)] px-4 py-2 text-sm font-medium",
|
|
107
|
+
"bg-[var(--color-semantic-error)] text-[var(--color-text-on-accent)]",
|
|
108
|
+
"hover:bg-[var(--color-semantic-error)]/90",
|
|
109
|
+
"transition-colors duration-[var(--motion-fast)]",
|
|
110
|
+
"focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
|
|
111
|
+
].join(" ")}
|
|
112
|
+
data-testid="tw-unsaved-modal__discard"
|
|
51
113
|
>
|
|
52
114
|
Discard changes
|
|
53
115
|
</button>
|