@particle-academy/fancy-code 0.16.0 → 0.17.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.
- package/CHANGELOG.md +38 -1
- package/dist/index.cjs +50 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -7
- package/dist/index.d.ts +47 -7
- package/dist/index.js +50 -10
- package/dist/index.js.map +1 -1
- package/docs/CodeEditor.md +16 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.17.0] - 2026-11-13
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Releases need a lab-run certificate.** `publish.yml` gains a read-only
|
|
22
|
+
`lab-certificate` job that every publishing job needs: a tag is published only
|
|
23
|
+
when the fancy-labs lab has certified that exact version and commit. Consumers
|
|
24
|
+
do nothing; this changes how releases are cut, not what the package does.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **The editing textarea of `CodeEditor`, `FileViewer` and `MarkdownEditor`
|
|
29
|
+
shows a focus ring.** It was `outline-none` with nothing in its place, so a
|
|
30
|
+
keyboard user could not see where focus was. It now carries the kit ring
|
|
31
|
+
(`focus-visible:ring-2 ring-inset`, `--fancy-focus` / `--fancy-focus-dark`) on
|
|
32
|
+
the textarea itself. Consumers do nothing.
|
|
33
|
+
- **`FileViewer` (and any `readOnly` editor) no longer traps keyboard focus.**
|
|
34
|
+
Its key handler called `preventDefault` on every key except copy and
|
|
35
|
+
select-all, Tab included, so focus could never leave and the caret could not
|
|
36
|
+
move. Read-only now intercepts nothing; the textarea's `readOnly` attribute
|
|
37
|
+
already refuses edits.
|
|
38
|
+
- **Editable `CodeEditor` and `MarkdownEditor` no longer trap keyboard focus
|
|
39
|
+
(WCAG 2.1.2).** Tab always inserted indentation, so tabbing in meant never
|
|
40
|
+
tabbing out. Tab / Shift+Tab now indent and dedent only once you have edited
|
|
41
|
+
in the current focus session (typed, pasted, pressed Enter); until then Tab
|
|
42
|
+
moves focus on. Escape disarms, so the next Tab leaves; blur disarms too.
|
|
43
|
+
**Behaviour change:** a Tab pressed straight after focusing the editor, with
|
|
44
|
+
no edit yet, now moves focus instead of indenting. Nothing to do unless you
|
|
45
|
+
relied on that; type first, and Tab indents as before.
|
|
46
|
+
- **`CodeEditor`, `FileViewer` and `MarkdownEditor` can be named, and are named
|
|
47
|
+
by default** (axe `label`, critical, lab run 601, K28). The `<textarea>` a
|
|
48
|
+
person types into had no accessible name and none of the three accepted one.
|
|
49
|
+
Each now takes `aria-label`, `aria-labelledby` and `aria-describedby`, and
|
|
50
|
+
puts them on that textarea, not on a wrapper. Defaults: "Code editor",
|
|
51
|
+
the `filename` (else "File contents") for `FileViewer`, and "Markdown
|
|
52
|
+
editor". A `aria-labelledby` wins and suppresses the default label. Nothing
|
|
53
|
+
to do on upgrade; pass a name that says what the field is for, where you can.
|
|
54
|
+
|
|
17
55
|
## [0.16.0] - 2026-11-02
|
|
18
56
|
|
|
19
57
|
### Added
|
|
@@ -197,7 +235,6 @@ renders exactly as before.
|
|
|
197
235
|
|
|
198
236
|
These are the kit 0.5 platform floors, applied across every package at once so a consumer never has to resolve a mix. **No API changed, nothing was removed, nothing was renamed** — only what the package requires.
|
|
199
237
|
|
|
200
|
-
|
|
201
238
|
## [0.9.1] — 2026-07-28
|
|
202
239
|
|
|
203
240
|
### Fixed
|
package/dist/index.cjs
CHANGED
|
@@ -42,7 +42,8 @@ function CodeEditorPanel({ className }) {
|
|
|
42
42
|
_diffAnnotations: diffAnnotations,
|
|
43
43
|
_maxHeight,
|
|
44
44
|
_minHeight,
|
|
45
|
-
_highlightLines: highlightLines
|
|
45
|
+
_highlightLines: highlightLines,
|
|
46
|
+
_aria: aria
|
|
46
47
|
} = useCodeEditor();
|
|
47
48
|
const lineNumberElements = react.useMemo(() => {
|
|
48
49
|
if (!engine || !lineNumbers) return null;
|
|
@@ -132,6 +133,7 @@ function CodeEditorPanel({ className }) {
|
|
|
132
133
|
themeColors,
|
|
133
134
|
handleKeyDown,
|
|
134
135
|
handleInput,
|
|
136
|
+
handleBlur,
|
|
135
137
|
handleScroll,
|
|
136
138
|
handleSelect,
|
|
137
139
|
scrollTop,
|
|
@@ -204,8 +206,12 @@ function CodeEditorPanel({ className }) {
|
|
|
204
206
|
"textarea",
|
|
205
207
|
{
|
|
206
208
|
ref: textareaRef,
|
|
209
|
+
"aria-label": aria?.["aria-label"],
|
|
210
|
+
"aria-labelledby": aria?.["aria-labelledby"],
|
|
211
|
+
"aria-describedby": aria?.["aria-describedby"],
|
|
207
212
|
className: reactFancy.cn(
|
|
208
|
-
"relative m-0 block resize-none border-none bg-transparent p-2.5 text-[13px] leading-[1.5] text-transparent
|
|
213
|
+
"relative m-0 block resize-none border-none bg-transparent p-2.5 text-[13px] leading-[1.5] text-transparent",
|
|
214
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--fancy-focus,var(--color-blue-500))] dark:focus-visible:ring-[color:var(--fancy-focus-dark,var(--color-blue-400))]",
|
|
209
215
|
wordWrap ? "w-full" : "min-w-full"
|
|
210
216
|
),
|
|
211
217
|
style: {
|
|
@@ -222,6 +228,7 @@ function CodeEditorPanel({ className }) {
|
|
|
222
228
|
readOnly,
|
|
223
229
|
onKeyDown: handleKeyDown,
|
|
224
230
|
onInput: handleInput,
|
|
231
|
+
onBlur: handleBlur,
|
|
225
232
|
onScroll: handleScroll,
|
|
226
233
|
onSelect: handleSelect,
|
|
227
234
|
onClick: handleSelect
|
|
@@ -483,6 +490,7 @@ function useEditorEngine({
|
|
|
483
490
|
const [scrollTop, setScrollTop] = react.useState(0);
|
|
484
491
|
const [scrollLeft, setScrollLeft] = react.useState(0);
|
|
485
492
|
const [activeLine, setActiveLine] = react.useState(1);
|
|
493
|
+
const tabArmedRef = react.useRef(false);
|
|
486
494
|
const themeColors = react.useMemo(() => {
|
|
487
495
|
const def = getTheme(theme);
|
|
488
496
|
return def?.colors ?? DEFAULT_COLORS;
|
|
@@ -543,6 +551,7 @@ function useEditorEngine({
|
|
|
543
551
|
const handleInput = react.useCallback(() => {
|
|
544
552
|
const ta = textareaRef.current;
|
|
545
553
|
if (!ta) return;
|
|
554
|
+
tabArmedRef.current = true;
|
|
546
555
|
onChangeRef.current?.(ta.value);
|
|
547
556
|
autoResize();
|
|
548
557
|
updateCursorInfo();
|
|
@@ -578,14 +587,19 @@ function useEditorEngine({
|
|
|
578
587
|
},
|
|
579
588
|
[updateCursorInfo]
|
|
580
589
|
);
|
|
590
|
+
const handleBlur = react.useCallback(() => {
|
|
591
|
+
tabArmedRef.current = false;
|
|
592
|
+
}, []);
|
|
581
593
|
const handleKeyDown = react.useCallback(
|
|
582
594
|
(e) => {
|
|
583
595
|
if (readOnly) {
|
|
584
|
-
if (e.key === "c" && (e.metaKey || e.ctrlKey)) return;
|
|
585
|
-
if (e.key === "a" && (e.metaKey || e.ctrlKey)) return;
|
|
586
|
-
e.preventDefault();
|
|
587
596
|
return;
|
|
588
597
|
}
|
|
598
|
+
if (e.key === "Escape") {
|
|
599
|
+
tabArmedRef.current = false;
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (e.key === "Tab" && !tabArmedRef.current) return;
|
|
589
603
|
if (e.key === "Tab") {
|
|
590
604
|
e.preventDefault();
|
|
591
605
|
const ta = e.currentTarget;
|
|
@@ -634,6 +648,7 @@ function useEditorEngine({
|
|
|
634
648
|
}
|
|
635
649
|
if (e.key === "Enter") {
|
|
636
650
|
e.preventDefault();
|
|
651
|
+
tabArmedRef.current = true;
|
|
637
652
|
const ta = e.currentTarget;
|
|
638
653
|
const start = ta.selectionStart;
|
|
639
654
|
const end = ta.selectionEnd;
|
|
@@ -662,6 +677,7 @@ function useEditorEngine({
|
|
|
662
677
|
themeColors,
|
|
663
678
|
handleKeyDown,
|
|
664
679
|
handleInput,
|
|
680
|
+
handleBlur,
|
|
665
681
|
handleScroll,
|
|
666
682
|
handleSelect,
|
|
667
683
|
revealLine,
|
|
@@ -702,8 +718,19 @@ function CodeEditorRoot({
|
|
|
702
718
|
cursorLine,
|
|
703
719
|
cursorColumn,
|
|
704
720
|
diffBase,
|
|
705
|
-
highlightLines
|
|
721
|
+
highlightLines,
|
|
722
|
+
"aria-label": ariaLabel,
|
|
723
|
+
"aria-labelledby": ariaLabelledBy,
|
|
724
|
+
"aria-describedby": ariaDescribedBy
|
|
706
725
|
}) {
|
|
726
|
+
const aria = react.useMemo(
|
|
727
|
+
() => ({
|
|
728
|
+
"aria-label": ariaLabelledBy ? void 0 : ariaLabel ?? "Code editor",
|
|
729
|
+
"aria-labelledby": ariaLabelledBy,
|
|
730
|
+
"aria-describedby": ariaDescribedBy
|
|
731
|
+
}),
|
|
732
|
+
[ariaLabel, ariaLabelledBy, ariaDescribedBy]
|
|
733
|
+
);
|
|
707
734
|
const [currentValue, setCurrentValue] = reactFancy.useControllableState(valueProp, defaultValue, onChange);
|
|
708
735
|
const diffAnnotations = react.useMemo(
|
|
709
736
|
() => diffBase == null ? null : fancyFileCommons.annotateLines(diffBase, currentValue),
|
|
@@ -796,9 +823,10 @@ function CodeEditorRoot({
|
|
|
796
823
|
_engineReturn: engineReturn,
|
|
797
824
|
_minHeight: minHeight,
|
|
798
825
|
_maxHeight: maxHeight,
|
|
799
|
-
_highlightLines: highlightLines
|
|
826
|
+
_highlightLines: highlightLines,
|
|
827
|
+
_aria: aria
|
|
800
828
|
}),
|
|
801
|
-
[engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight, highlightLines]
|
|
829
|
+
[engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight, highlightLines, aria]
|
|
802
830
|
);
|
|
803
831
|
return /* @__PURE__ */ jsxRuntime.jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
804
832
|
"div",
|
|
@@ -834,7 +862,10 @@ function MarkdownEditor({
|
|
|
834
862
|
minHeight,
|
|
835
863
|
maxHeight,
|
|
836
864
|
className,
|
|
837
|
-
renderPreview
|
|
865
|
+
renderPreview,
|
|
866
|
+
"aria-label": ariaLabel,
|
|
867
|
+
"aria-labelledby": ariaLabelledBy,
|
|
868
|
+
"aria-describedby": ariaDescribedBy
|
|
838
869
|
}) {
|
|
839
870
|
const [content, setContent] = reactFancy.useControllableState(value, defaultValue, onValueChange);
|
|
840
871
|
const showEditor = mode !== "preview";
|
|
@@ -859,6 +890,9 @@ function MarkdownEditor({
|
|
|
859
890
|
placeholder,
|
|
860
891
|
minHeight,
|
|
861
892
|
maxHeight,
|
|
893
|
+
"aria-label": ariaLabel ?? "Markdown editor",
|
|
894
|
+
"aria-labelledby": ariaLabelledBy,
|
|
895
|
+
"aria-describedby": ariaDescribedBy,
|
|
862
896
|
children: /* @__PURE__ */ jsxRuntime.jsx(CodeEditor.Panel, {})
|
|
863
897
|
}
|
|
864
898
|
) }),
|
|
@@ -960,7 +994,10 @@ function FileViewer({
|
|
|
960
994
|
frameProps,
|
|
961
995
|
document,
|
|
962
996
|
className,
|
|
963
|
-
style
|
|
997
|
+
style,
|
|
998
|
+
"aria-label": ariaLabel,
|
|
999
|
+
"aria-labelledby": ariaLabelledBy,
|
|
1000
|
+
"aria-describedby": ariaDescribedBy
|
|
964
1001
|
}) {
|
|
965
1002
|
const resolved = fileKind ?? resolveFileKind({ filename, mime });
|
|
966
1003
|
if (resolved.kind === "document" && view === "rendered" && readOnly && line == null) {
|
|
@@ -1054,6 +1091,9 @@ function FileViewer({
|
|
|
1054
1091
|
cursorLine: line,
|
|
1055
1092
|
cursorColumn: column,
|
|
1056
1093
|
highlightLines: highlightLines ?? (line != null ? [line] : void 0),
|
|
1094
|
+
"aria-label": ariaLabel ?? filename ?? "File contents",
|
|
1095
|
+
"aria-labelledby": ariaLabelledBy,
|
|
1096
|
+
"aria-describedby": ariaDescribedBy,
|
|
1057
1097
|
children: /* @__PURE__ */ jsxRuntime.jsx(CodeEditor.Panel, {})
|
|
1058
1098
|
}
|
|
1059
1099
|
)
|