@plannotator/ui 0.52.1 → 0.53.1
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/HANDOFF.md +284 -29
- package/README.md +45 -5
- package/components/AnnotationPanel.tsx +195 -118
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +54 -40
- package/components/BlockRenderer.tsx +20 -14
- package/components/CodeFilePopout.tsx +132 -69
- package/components/CommentPopover.tsx +8 -349
- package/components/ComposerTokenTextarea.tsx +379 -0
- package/components/ExportModal.tsx +6 -1
- package/components/HostTitle.tsx +108 -0
- package/components/HtmlSurfaceControls.tsx +2 -1
- package/components/ImageLightbox.tsx +288 -17
- package/components/ImagePeek.tsx +255 -0
- package/components/ImageResolverContext.tsx +31 -0
- package/components/InlineMarkdown.tsx +123 -15
- package/components/LocalLinkContext.tsx +49 -0
- package/components/MentionPicker.tsx +51 -0
- package/components/MenuVersionSection.tsx +51 -25
- package/components/OpenInAppButton.tsx +18 -49
- package/components/PlanHeaderMenu.tsx +2 -8
- package/components/ProviderIcons.tsx +10 -4
- package/components/RenderedMarkdown.tsx +40 -4
- package/components/Settings.tsx +33 -5
- package/components/SkillReferenceMenu.tsx +67 -18
- package/components/Tooltip.tsx +51 -4
- package/components/Viewer.tsx +220 -49
- package/components/ai/AIProviderBar.tsx +11 -0
- package/components/ai/AskSessionSetupHint.tsx +12 -0
- package/components/ai/DocumentAIChatPanel.tsx +17 -5
- package/components/ai/SessionAskNotice.tsx +4 -3
- package/components/blocks/BlockquoteBlock.tsx +159 -0
- package/components/blocks/HtmlBlock.tsx +121 -9
- package/components/blocks/QuestionBlock.tsx +183 -96
- package/components/blocks/QuestionContext.tsx +3 -0
- package/components/diagram/DiagramViewer.tsx +41 -4
- package/components/diagram/index.ts +1 -1
- package/components/html-viewer/HtmlViewer.tsx +95 -3
- package/components/html-viewer/bridge-script.asset.js +413 -7
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +454 -7
- package/components/html-viewer/useHtmlAnnotation.ts +51 -3
- package/components/icons/AgentIcons.tsx +9 -7
- package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
- package/components/plan-diff/PlanDiffViewer.tsx +6 -1
- package/configure.ts +27 -1
- package/hooks/useAgentJobs.ts +8 -3
- package/hooks/useAnnotationHighlighter.ts +128 -23
- package/hooks/useExternalAnnotations.ts +6 -0
- package/hooks/useMentionAutocomplete.ts +1 -1
- package/hooks/useOpenInApps.ts +88 -0
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useUpdateCheck.ts +38 -98
- package/hooks/useUpdateCheckSetting.ts +43 -0
- package/hooks/useUpdateToast.ts +101 -0
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +2 -2
- package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
- package/shortcuts/index.ts +1 -0
- package/shortcuts/runtime.ts +10 -3
- package/styles.css +1 -1
- package/types.ts +48 -0
- package/utils/agentClosed.ts +26 -0
- package/utils/agentToolSetting.ts +16 -9
- package/utils/aiProvider.ts +7 -0
- package/utils/askSessionSetupHint.ts +27 -0
- package/utils/blockTargeting.ts +14 -3
- package/utils/mentions.ts +34 -5
- package/utils/parser.ts +65 -51
- package/utils/pasteInsert.ts +234 -0
- package/utils/planDiffEngine.ts +272 -2
- package/utils/skillCatalog.ts +15 -2
- package/utils/skillReferences.ts +50 -13
- package/utils/storage.ts +5 -0
- package/utils/textCut.ts +41 -0
- package/utils/updateNotice.ts +120 -0
|
@@ -7,16 +7,10 @@ import { SparklesIcon } from './SparklesIcon';
|
|
|
7
7
|
import { hasUnsavedCommentContent } from '../utils/commentContent';
|
|
8
8
|
import { useSkillReferenceAutocomplete } from '../hooks/useSkillReferenceAutocomplete';
|
|
9
9
|
import { HumanOnlySkillNotice, SkillReferenceMenu } from './SkillReferenceMenu';
|
|
10
|
-
import type
|
|
11
|
-
import {
|
|
12
|
-
mentionTokenRanges,
|
|
13
|
-
mergeTokenRanges,
|
|
14
|
-
skillTokenRanges,
|
|
15
|
-
type ComposerTokenRange,
|
|
16
|
-
} from '../utils/composerTokens';
|
|
10
|
+
import { ComposerTokenTextarea, type ComposerMentionChips } from './ComposerTokenTextarea';
|
|
17
11
|
import { useMentionAutocomplete } from '../hooks/useMentionAutocomplete';
|
|
18
12
|
import { MentionAutocompleteMenu, mentionActiveOptionId } from './MentionAutocomplete';
|
|
19
|
-
import type {
|
|
13
|
+
import type { MentionSource } from '../utils/mentions';
|
|
20
14
|
import {
|
|
21
15
|
hasPrimaryCoarsePointer,
|
|
22
16
|
shouldUseExpandedComposer,
|
|
@@ -24,6 +18,8 @@ import {
|
|
|
24
18
|
type VisibleViewportBounds,
|
|
25
19
|
} from '../hooks/useViewportEnvironment';
|
|
26
20
|
|
|
21
|
+
export type { ComposerMentionChips } from './ComposerTokenTextarea';
|
|
22
|
+
|
|
27
23
|
export interface CommentAskAIContext {
|
|
28
24
|
kind: 'general' | 'selection';
|
|
29
25
|
label?: string;
|
|
@@ -714,7 +710,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
714
710
|
onSelect={skillAc.select}
|
|
715
711
|
/>
|
|
716
712
|
)}
|
|
717
|
-
<
|
|
713
|
+
<ComposerTokenTextarea
|
|
718
714
|
textareaRef={focusOnMountRef}
|
|
719
715
|
value={text}
|
|
720
716
|
onChange={(e) => { setText(e.target.value); readComposerCaret(); }}
|
|
@@ -730,6 +726,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
730
726
|
activeOptionId={activeComposerOptionId}
|
|
731
727
|
/>
|
|
732
728
|
<HumanOnlySkillNotice skills={skillAc.humanOnlyReferences} />
|
|
729
|
+
{mentionSource?.renderNotice?.(mentionChipPeople)}
|
|
733
730
|
<MentionAutocompleteMenu
|
|
734
731
|
id={mentionListboxId}
|
|
735
732
|
menu={mentionAc.menu}
|
|
@@ -867,7 +864,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
867
864
|
onSelect={skillAc.select}
|
|
868
865
|
/>
|
|
869
866
|
)}
|
|
870
|
-
<
|
|
867
|
+
<ComposerTokenTextarea
|
|
871
868
|
textareaRef={focusOnMountRef}
|
|
872
869
|
value={text}
|
|
873
870
|
onChange={(e) => { setText(e.target.value); readComposerCaret(); }}
|
|
@@ -883,6 +880,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
883
880
|
activeOptionId={activeComposerOptionId}
|
|
884
881
|
/>
|
|
885
882
|
<HumanOnlySkillNotice skills={skillAc.humanOnlyReferences} />
|
|
883
|
+
{mentionSource?.renderNotice?.(mentionChipPeople)}
|
|
886
884
|
<MentionAutocompleteMenu
|
|
887
885
|
id={mentionListboxId}
|
|
888
886
|
menu={mentionAc.menu}
|
|
@@ -930,345 +928,6 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
930
928
|
);
|
|
931
929
|
};
|
|
932
930
|
|
|
933
|
-
// ---------------------------------------------------------------------------
|
|
934
|
-
// Composer textarea with skill-reference token highlighting
|
|
935
|
-
// ---------------------------------------------------------------------------
|
|
936
|
-
|
|
937
|
-
/** Classes shared by the textarea and its highlight mirror — font, size,
|
|
938
|
-
* and box metrics MUST stay identical or the overlay drifts out of alignment. */
|
|
939
|
-
const COMPOSER_TEXT_CLASSES = 'w-full bg-transparent text-sm px-1 py-0.5';
|
|
940
|
-
|
|
941
|
-
/** Per-composer memo for the overlay gutter sync below. */
|
|
942
|
-
interface OverlayGutterState {
|
|
943
|
-
/** The overlay the applied value belongs to (a remount invalidates it). */
|
|
944
|
-
overlay: HTMLElement | null;
|
|
945
|
-
/** Last `offsetWidth - clientWidth` seen on the textarea. */
|
|
946
|
-
raw: number;
|
|
947
|
-
/** Extra right padding currently written inline on the overlay, in px. */
|
|
948
|
-
applied: number;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
const cssPx = (value: string): number => {
|
|
952
|
-
const n = parseFloat(value);
|
|
953
|
-
return Number.isFinite(n) ? n : 0;
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Keep the mirror's text column exactly as wide as the textarea's (#1525).
|
|
958
|
-
*
|
|
959
|
-
* A layout-consuming vertical scrollbar (Windows/Linux classic scrollbars; the
|
|
960
|
-
* app's `scrollbar-width: thin` makes them ~12px) narrows the textarea's
|
|
961
|
-
* content box once the composer hits its max height — but the overlay is
|
|
962
|
-
* `overflow: hidden` and keeps its full width, so from the first line that
|
|
963
|
-
* wraps differently the painted glyphs drift away from the native caret, which
|
|
964
|
-
* is the one thing the textarea itself still draws. `scrollbar-gutter: stable`
|
|
965
|
-
* cannot fix this: it is ignored on an `overflow: hidden` box. Mirroring the
|
|
966
|
-
* measured scrollbar width onto the overlay's right padding makes both boxes
|
|
967
|
-
* wrap identically again (and stops the overlay painting under the scrollbar).
|
|
968
|
-
*
|
|
969
|
-
* Overlay scrollbars (macOS, iOS, any touch surface) consume no layout, so the
|
|
970
|
-
* measured width is 0 — and then this writes NOTHING at all, leaving the
|
|
971
|
-
* class-driven padding untouched exactly as before the fix.
|
|
972
|
-
*/
|
|
973
|
-
function syncOverlayGutter(
|
|
974
|
-
el: HTMLTextAreaElement,
|
|
975
|
-
overlay: HTMLElement,
|
|
976
|
-
state: OverlayGutterState,
|
|
977
|
-
raw: number,
|
|
978
|
-
): void {
|
|
979
|
-
// Nothing measurable changed since the last sync — the common case.
|
|
980
|
-
if (overlay === state.overlay && raw === state.raw) return;
|
|
981
|
-
state.overlay = overlay;
|
|
982
|
-
state.raw = raw;
|
|
983
|
-
|
|
984
|
-
const clear = () => {
|
|
985
|
-
if (state.applied === 0) return; // never wrote anything; leave the DOM alone
|
|
986
|
-
overlay.style.removeProperty('padding-right');
|
|
987
|
-
state.applied = 0;
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
const style = getComputedStyle(el);
|
|
991
|
-
// `offsetWidth - clientWidth` is scrollbar + left/right borders. The borders
|
|
992
|
-
// are shared with the overlay (both boxes are border-box), so only the
|
|
993
|
-
// scrollbar part is the extra gutter the mirror is missing.
|
|
994
|
-
const borders = cssPx(style.borderLeftWidth) + cssPx(style.borderRightWidth);
|
|
995
|
-
const scrollbar = Math.max(0, raw - borders);
|
|
996
|
-
if (scrollbar === 0) {
|
|
997
|
-
clear();
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
// Base padding is read from the textarea (never the overlay, whose computed
|
|
1001
|
-
// value may already include a gutter we wrote), so the two can never drift
|
|
1002
|
-
// apart if COMPOSER_TEXT_CLASSES changes.
|
|
1003
|
-
const base = parseFloat(style.paddingRight);
|
|
1004
|
-
if (!Number.isFinite(base)) {
|
|
1005
|
-
clear(); // unmeasurable: fall back to the pre-fix behavior
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
overlay.style.paddingRight = `${base + scrollbar}px`;
|
|
1009
|
-
state.applied = scrollbar;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* The default chip look: the theme's primary at a wash, one shade stronger
|
|
1014
|
-
* than a skill reference's so the two token kinds in one overlay read as
|
|
1015
|
-
* siblings rather than the same thing. Deliberately no ring: every class here
|
|
1016
|
-
* is one the package already emitted, so a host build's CSS (and the portable
|
|
1017
|
-
* guide viewer's) is byte-identical to 0.43.2. A host that wants a pill adds
|
|
1018
|
-
* `box-shadow: 0 0 0 Npx <background>` through `tokenClassName` — paint, not
|
|
1019
|
-
* layout, per the metric rule above.
|
|
1020
|
-
*/
|
|
1021
|
-
const MENTION_CHIP_CLASSES = 'text-primary bg-primary/15 rounded-[3px]';
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* One token's span in the highlight overlay.
|
|
1025
|
-
*
|
|
1026
|
-
* METRIC RULE, load-bearing for every token kind: a span may change COLOR,
|
|
1027
|
-
* BACKGROUND, BORDER-RADIUS, BOX-SHADOW and TEXT-DECORATION only. Anything
|
|
1028
|
-
* that moves a glyph — padding, margin, border width, font-weight,
|
|
1029
|
-
* letter-spacing, font-size — would shift the overlay's text off the
|
|
1030
|
-
* textarea's own layout and drift the caret away from the painted glyphs. A
|
|
1031
|
-
* pill's breathing room is faked with a paint-only `box-shadow` ring in the
|
|
1032
|
-
* chip's own background color.
|
|
1033
|
-
*/
|
|
1034
|
-
function renderTokenSpan(
|
|
1035
|
-
range: ComposerTokenRange,
|
|
1036
|
-
text: string,
|
|
1037
|
-
mentionTokenClassName?: string,
|
|
1038
|
-
): React.ReactNode {
|
|
1039
|
-
if (range.kind === 'mention') {
|
|
1040
|
-
// `data-mention-token` / `data-mention-kind` are the host's styling hook;
|
|
1041
|
-
// `tokenClassName` is appended verbatim and is the host's to keep
|
|
1042
|
-
// metric-safe (see the rule above).
|
|
1043
|
-
return (
|
|
1044
|
-
<span
|
|
1045
|
-
key={`mention-${range.start}`}
|
|
1046
|
-
data-mention-token={range.person.id}
|
|
1047
|
-
data-mention-kind={range.person.kind}
|
|
1048
|
-
className={`${MENTION_CHIP_CLASSES}${
|
|
1049
|
-
mentionTokenClassName ? ` ${mentionTokenClassName}` : ''
|
|
1050
|
-
}`}
|
|
1051
|
-
>
|
|
1052
|
-
{text}
|
|
1053
|
-
</span>
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
// Human-only tokens carry a quiet dotted underline as their inline marker
|
|
1057
|
-
// (text-decoration never affects glyph layout, so overlay alignment is
|
|
1058
|
-
// safe). The accessible explanation lives in HumanOnlySkillNotice below
|
|
1059
|
-
// the textarea — this overlay is aria-hidden.
|
|
1060
|
-
return (
|
|
1061
|
-
<span
|
|
1062
|
-
key={`skill-${range.start}`}
|
|
1063
|
-
data-skill-ref-token={range.skill.entry.name}
|
|
1064
|
-
data-skill-ref-human-only={range.skill.entry.humanOnly ? 'true' : undefined}
|
|
1065
|
-
className={`text-primary bg-primary/10 rounded-[3px] ${
|
|
1066
|
-
range.skill.entry.humanOnly
|
|
1067
|
-
? 'underline decoration-dotted decoration-primary/60 underline-offset-2'
|
|
1068
|
-
: ''
|
|
1069
|
-
}`}
|
|
1070
|
-
>
|
|
1071
|
-
{text}
|
|
1072
|
-
</span>
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
/** The mention half of the highlight layer. Null → no mention source at all. */
|
|
1077
|
-
export interface ComposerMentionChips {
|
|
1078
|
-
/** The people whose `@Label` token still survives in the text. */
|
|
1079
|
-
readonly people: readonly MentionPerson[];
|
|
1080
|
-
/** Host class appended to each chip (see `MentionSource.tokenClassName`). */
|
|
1081
|
-
readonly tokenClassName?: string;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
interface ComposerTextareaProps {
|
|
1085
|
-
value: string;
|
|
1086
|
-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
1087
|
-
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
1088
|
-
/** Caret observer (skill autocomplete). */
|
|
1089
|
-
onSelectCaret: () => void;
|
|
1090
|
-
placeholder: string;
|
|
1091
|
-
/** Mode-specific min/max height classes. */
|
|
1092
|
-
sizeClassName: string;
|
|
1093
|
-
textareaRef: (el: HTMLTextAreaElement | null) => void;
|
|
1094
|
-
/** Positioned skill-reference occurrences to highlight. */
|
|
1095
|
-
tokens: SkillReferenceToken[];
|
|
1096
|
-
/** Off → no skill-reference contribution to the highlight layer. */
|
|
1097
|
-
skillReferences: boolean;
|
|
1098
|
-
/**
|
|
1099
|
-
* The mention contribution, or null with no `mentionSource`. Present (even
|
|
1100
|
-
* with nobody tagged yet) it turns the overlay on, so the first pick paints
|
|
1101
|
-
* a chip without swapping the textarea for a different element.
|
|
1102
|
-
*/
|
|
1103
|
-
mentionChips: ComposerMentionChips | null;
|
|
1104
|
-
/** ARIA relationship to the skill-reference listbox. */
|
|
1105
|
-
listboxId: string;
|
|
1106
|
-
listboxOpen: boolean;
|
|
1107
|
-
activeOptionId?: string;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* The composer's textarea. With NEITHER token source active this is exactly
|
|
1112
|
-
* the pre-feature `<textarea>`; with either one on, its tokens are painted by
|
|
1113
|
-
* a mirrored, aria-hidden overlay rendered BEHIND a transparent-text textarea
|
|
1114
|
-
* (a textarea cannot style substrings). ONE overlay serves both sources: two
|
|
1115
|
-
* mirrored layers could never stay pixel-aligned with each other, and only
|
|
1116
|
-
* one of them could own the scroll sync. The overlay shares the exact
|
|
1117
|
-
* font/padding/wrapping metrics and mirrors scroll position, and token spans
|
|
1118
|
-
* obey the metric rule on `renderTokenSpan` (paint only, never layout), so
|
|
1119
|
-
* the glyphs the browser lays out in the textarea and the glyphs the overlay
|
|
1120
|
-
* paints coincide. During IME composition the overlay hides and the textarea
|
|
1121
|
-
* text becomes visible again (`.pn-ref-composing`), keeping native
|
|
1122
|
-
* composition rendering (underlines, candidate highlights) intact.
|
|
1123
|
-
*/
|
|
1124
|
-
const ComposerTextarea: React.FC<ComposerTextareaProps> = ({
|
|
1125
|
-
value,
|
|
1126
|
-
onChange,
|
|
1127
|
-
onKeyDown,
|
|
1128
|
-
onSelectCaret,
|
|
1129
|
-
placeholder,
|
|
1130
|
-
sizeClassName,
|
|
1131
|
-
textareaRef,
|
|
1132
|
-
tokens,
|
|
1133
|
-
skillReferences,
|
|
1134
|
-
mentionChips,
|
|
1135
|
-
listboxId,
|
|
1136
|
-
listboxOpen,
|
|
1137
|
-
activeOptionId,
|
|
1138
|
-
}) => {
|
|
1139
|
-
const overlayRef = useRef<HTMLDivElement>(null);
|
|
1140
|
-
const [composing, setComposing] = useState(false);
|
|
1141
|
-
const gutterRef = useRef<OverlayGutterState>({ overlay: null, raw: 0, applied: 0 });
|
|
1142
|
-
|
|
1143
|
-
const syncScroll = useCallback((el: HTMLTextAreaElement) => {
|
|
1144
|
-
const overlay = overlayRef.current;
|
|
1145
|
-
if (!overlay) return;
|
|
1146
|
-
// Reads before writes so a sync costs at most one forced layout.
|
|
1147
|
-
const { scrollTop, scrollLeft } = el;
|
|
1148
|
-
const raw = el.offsetWidth - el.clientWidth;
|
|
1149
|
-
overlay.scrollTop = scrollTop;
|
|
1150
|
-
overlay.scrollLeft = scrollLeft;
|
|
1151
|
-
syncOverlayGutter(el, overlay, gutterRef.current, raw);
|
|
1152
|
-
}, []);
|
|
1153
|
-
|
|
1154
|
-
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1155
|
-
const attachRef = useCallback(
|
|
1156
|
-
(el: HTMLTextAreaElement | null) => {
|
|
1157
|
-
innerRef.current = el;
|
|
1158
|
-
textareaRef(el);
|
|
1159
|
-
},
|
|
1160
|
-
[textareaRef],
|
|
1161
|
-
);
|
|
1162
|
-
|
|
1163
|
-
// The overlay exists while EITHER source is active. `mentionChips` turns it
|
|
1164
|
-
// on for the whole life of a mention composer, not only once somebody is
|
|
1165
|
-
// tagged, so a pick never swaps the textarea element under the caret.
|
|
1166
|
-
const mentionPeople = mentionChips?.people;
|
|
1167
|
-
const highlight = skillReferences || mentionChips !== null;
|
|
1168
|
-
|
|
1169
|
-
// The one list of spans the overlay paints, from every active source.
|
|
1170
|
-
// Skill references are the earlier group, so they win a byte both claim.
|
|
1171
|
-
const ranges = useMemo(
|
|
1172
|
-
() =>
|
|
1173
|
-
mergeTokenRanges(value, [
|
|
1174
|
-
skillReferences ? skillTokenRanges(tokens) : [],
|
|
1175
|
-
mentionPeople ? mentionTokenRanges(value, mentionPeople) : [],
|
|
1176
|
-
]),
|
|
1177
|
-
[value, tokens, skillReferences, mentionPeople],
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
// Keep the mirror aligned when the value changes without a scroll event
|
|
1181
|
-
// (e.g. programmatic insertion moving the caret into a scrolled region).
|
|
1182
|
-
useEffect(() => {
|
|
1183
|
-
if (innerRef.current) syncScroll(innerRef.current);
|
|
1184
|
-
}, [value, syncScroll]);
|
|
1185
|
-
|
|
1186
|
-
// `syncScroll` otherwise only runs on scroll and on value change, so a
|
|
1187
|
-
// window/panel resize that grows or drops the textarea's scrollbar without a
|
|
1188
|
-
// keystroke would leave the gutter stale until the next one.
|
|
1189
|
-
useEffect(() => {
|
|
1190
|
-
const el = innerRef.current;
|
|
1191
|
-
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
1192
|
-
const observer = new ResizeObserver(() => {
|
|
1193
|
-
if (innerRef.current) syncScroll(innerRef.current);
|
|
1194
|
-
});
|
|
1195
|
-
observer.observe(el);
|
|
1196
|
-
return () => observer.disconnect();
|
|
1197
|
-
}, [highlight, syncScroll]);
|
|
1198
|
-
|
|
1199
|
-
if (!highlight) {
|
|
1200
|
-
return (
|
|
1201
|
-
<textarea
|
|
1202
|
-
data-pn-mobile-editable="true"
|
|
1203
|
-
// Absent any open menu these are all undefined, so the plain textarea
|
|
1204
|
-
// renders exactly the attributes it always did.
|
|
1205
|
-
aria-autocomplete={listboxOpen ? 'list' : undefined}
|
|
1206
|
-
aria-controls={listboxOpen ? listboxId : undefined}
|
|
1207
|
-
aria-activedescendant={activeOptionId}
|
|
1208
|
-
ref={attachRef}
|
|
1209
|
-
value={value}
|
|
1210
|
-
onChange={onChange}
|
|
1211
|
-
onKeyDown={onKeyDown}
|
|
1212
|
-
onSelect={onSelectCaret}
|
|
1213
|
-
placeholder={placeholder}
|
|
1214
|
-
className={`${COMPOSER_TEXT_CLASSES} placeholder:text-muted-foreground resize-none focus:outline-none ${sizeClassName}`}
|
|
1215
|
-
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
|
1216
|
-
/>
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const segments: React.ReactNode[] = [];
|
|
1221
|
-
let pos = 0;
|
|
1222
|
-
for (const range of ranges) {
|
|
1223
|
-
if (range.start > pos) segments.push(value.slice(pos, range.start));
|
|
1224
|
-
segments.push(
|
|
1225
|
-
renderTokenSpan(range, value.slice(range.start, range.end), mentionChips?.tokenClassName),
|
|
1226
|
-
);
|
|
1227
|
-
pos = range.end;
|
|
1228
|
-
}
|
|
1229
|
-
segments.push(value.slice(pos));
|
|
1230
|
-
|
|
1231
|
-
return (
|
|
1232
|
-
<div className="relative">
|
|
1233
|
-
<div
|
|
1234
|
-
ref={overlayRef}
|
|
1235
|
-
aria-hidden="true"
|
|
1236
|
-
// Names the SKILL-reference layer, so a skill composer's overlay is
|
|
1237
|
-
// byte-identical to the one it rendered before mentions existed; a
|
|
1238
|
-
// mentions-only overlay is found by its mirror attribute below.
|
|
1239
|
-
data-skill-ref-overlay={skillReferences ? 'true' : undefined}
|
|
1240
|
-
data-pn-mobile-editable-mirror="true"
|
|
1241
|
-
className={`${COMPOSER_TEXT_CLASSES} ${sizeClassName} pointer-events-none absolute inset-0 overflow-hidden whitespace-pre-wrap break-words`}
|
|
1242
|
-
style={composing ? { visibility: 'hidden' } : undefined}
|
|
1243
|
-
>
|
|
1244
|
-
{segments}
|
|
1245
|
-
{'\n'}
|
|
1246
|
-
</div>
|
|
1247
|
-
<textarea
|
|
1248
|
-
data-pn-mobile-editable="true"
|
|
1249
|
-
aria-label={placeholder}
|
|
1250
|
-
aria-autocomplete="list"
|
|
1251
|
-
aria-haspopup="listbox"
|
|
1252
|
-
aria-controls={listboxOpen ? listboxId : undefined}
|
|
1253
|
-
aria-owns={listboxOpen ? listboxId : undefined}
|
|
1254
|
-
aria-activedescendant={activeOptionId}
|
|
1255
|
-
ref={attachRef}
|
|
1256
|
-
value={value}
|
|
1257
|
-
onChange={onChange}
|
|
1258
|
-
onKeyDown={onKeyDown}
|
|
1259
|
-
onSelect={onSelectCaret}
|
|
1260
|
-
onScroll={(e) => syncScroll(e.currentTarget)}
|
|
1261
|
-
onCompositionStart={() => setComposing(true)}
|
|
1262
|
-
onCompositionEnd={() => setComposing(false)}
|
|
1263
|
-
placeholder={placeholder}
|
|
1264
|
-
className={`${COMPOSER_TEXT_CLASSES} placeholder:text-muted-foreground resize-none focus:outline-none relative pn-ref-input ${
|
|
1265
|
-
composing ? 'pn-ref-composing' : ''
|
|
1266
|
-
} ${sizeClassName}`}
|
|
1267
|
-
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
|
1268
|
-
/>
|
|
1269
|
-
</div>
|
|
1270
|
-
);
|
|
1271
|
-
};
|
|
1272
931
|
|
|
1273
932
|
// Icons
|
|
1274
933
|
|