@panphora/bevel 0.2.1 → 0.3.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.
@@ -6,12 +6,15 @@ import { type ChangeDetails } from '../internal/controllable-state.js';
6
6
  import { Button } from './button.js';
7
7
  export declare const wrapCompartment: Compartment;
8
8
  export declare const darkCompartment: Compartment;
9
+ export declare const readOnlyCompartment: Compartment;
9
10
  /** iOS zooms the page when a focused editable is under 16px, contenteditable included. */
10
11
  export declare function coarsePointer(): boolean;
11
12
  /** Wrapping is remembered per device. A blocked or absent store falls back to the pointer. */
12
13
  export declare function storedWrap(coarse: boolean): boolean;
13
14
  export declare function rememberWrap(on: boolean): boolean;
14
15
  export declare const wrapExtension: (on: boolean) => Extension;
16
+ /** Read-only still takes focus, so the keyboard can move, select and search; only edits are refused. */
17
+ export declare const readOnlyExtension: (on: boolean) => Extension;
15
18
  export declare const bevelHighlight: HighlightStyle;
16
19
  /** One theme serves both modes: the tokens are light-dark() and the dark flag is a facet. */
17
20
  export declare function bevelEditorTheme(coarse: boolean): Extension;
@@ -22,8 +25,10 @@ export interface CodeEditorExtensionOptions {
22
25
  onSave: () => void;
23
26
  onDocChange: () => void;
24
27
  onFindChange: (open: boolean) => void;
28
+ readOnly: boolean;
29
+ onFind: () => void;
25
30
  }
26
- export declare function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange }: CodeEditorExtensionOptions): Extension[];
31
+ export declare function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange, readOnly, onFind }: CodeEditorExtensionOptions): Extension[];
27
32
  export type CodeEditorAction = 'undo' | 'redo' | 'find' | 'outdent' | 'indent' | 'wrap' | 'fold' | 'suggest';
28
33
  interface CodeEditorState {
29
34
  view: EditorView | null;
@@ -36,6 +41,8 @@ interface CodeEditorState {
36
41
  /** The host clears the flag through this once its own write has landed. `save()` never clears it. */
37
42
  setDirty: (next: boolean | ((previous: boolean) => boolean), details: ChangeDetails) => void;
38
43
  findOpen: boolean;
44
+ findRequest: number;
45
+ readOnly: boolean;
39
46
  extensions: Extension[];
40
47
  root: RefObject<HTMLDivElement | null>;
41
48
  save: () => void;
@@ -52,11 +59,13 @@ export interface CodeEditorProps extends Omit<ComponentProps<'div'>, 'onChange'>
52
59
  wrap?: boolean;
53
60
  defaultWrap?: boolean;
54
61
  onWrapChange?: (wrap: boolean, details: ChangeDetails) => void;
62
+ /** Keeps the editor focusable and selectable, and refuses edits. */
63
+ readOnly?: boolean;
55
64
  /** The mounted view, and null when it is torn down. The tab drives live sync through it. */
56
65
  onViewChange?: (view: EditorView | null) => void;
57
66
  extensions?: Extension[];
58
67
  }
59
- declare function Root({ doc, onSave, dirty, defaultDirty, onDirtyChange, wrap, defaultWrap, onWrapChange, onViewChange, extensions, ref, children, ...props }: CodeEditorProps): import("react").JSX.Element;
68
+ declare function Root({ doc, onSave, dirty, defaultDirty, onDirtyChange, wrap, defaultWrap, onWrapChange, readOnly, onViewChange, extensions, ref, onKeyDown, children, ...props }: CodeEditorProps): import("react").JSX.Element;
60
69
  declare function Dirty({ children, hidden, ...props }: ComponentProps<'i'>): import("react").JSX.Element;
61
70
  export interface CodeEditorToolProps extends ComponentProps<'button'> {
62
71
  action?: CodeEditorAction;
@@ -64,6 +64,7 @@ const WRAP_STORAGE_KEY = "bevel.code-editor.wrap";
64
64
  const FONT = "var(--bevel-font-mono)";
65
65
  const wrapCompartment = new Compartment();
66
66
  const darkCompartment = new Compartment();
67
+ const readOnlyCompartment = new Compartment();
67
68
  function coarsePointer() {
68
69
  if (typeof window === "undefined") return false;
69
70
  return window.matchMedia("(pointer: coarse)").matches || navigator.maxTouchPoints > 0;
@@ -122,6 +123,7 @@ const hangingIndent = ViewPlugin.fromClass(class {
122
123
  }
123
124
  }, { decorations: (plugin) => plugin.decorations });
124
125
  const wrapExtension = (on) => on ? [EditorView.lineWrapping, hangingIndent] : [];
126
+ const readOnlyExtension = (on) => on ? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ inputmode: "none" })] : [];
125
127
  const bevelHighlight = HighlightStyle.define([
126
128
  { tag: [t.comment, t.lineComment, t.blockComment, t.docComment], color: "var(--bevel-syn-comment)", fontStyle: "italic" },
127
129
  { tag: [t.tagName, t.standard(t.tagName)], color: "var(--bevel-syn-tag)", fontWeight: "500" },
@@ -382,7 +384,7 @@ function findPanel(coarse) {
382
384
  };
383
385
  };
384
386
  }
385
- function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange }) {
387
+ function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange, readOnly, onFind }) {
386
388
  return [
387
389
  lineNumbers(),
388
390
  highlightActiveLineGutter(),
@@ -415,6 +417,7 @@ function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFi
415
417
  ...coarse ? [] : [rectangularSelection(), crosshairCursor()],
416
418
  wrapCompartment.of(wrapExtension(wrapped)),
417
419
  darkCompartment.of(EditorView.darkTheme.of(dark)),
420
+ readOnlyCompartment.of(readOnlyExtension(readOnly)),
418
421
  bevelEditorTheme(coarse),
419
422
  EditorView.contentAttributes.of({
420
423
  autocorrect: "off",
@@ -424,6 +427,7 @@ function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFi
424
427
  "aria-label": "Source"
425
428
  }),
426
429
  Prec.highest(keymap.of([
430
+ { key: "Mod-f", preventDefault: true, run: () => (onFind(), true) },
427
431
  { key: "Mod-s", preventDefault: true, run: () => (onSave(), true) },
428
432
  { key: "Mod-Shift-s", preventDefault: true, run: () => (onSave(), true) },
429
433
  { key: "Mod-]", preventDefault: true, run: indentMore },
@@ -502,7 +506,7 @@ const StatusPart = nativePart("div", "bevel-r-code-editor__status");
502
506
  const MetaPart = nativePart("span", "bevel-r-code-editor__meta");
503
507
  const BarPart = nativePart("div", "bevel-r-code-editor__bar");
504
508
  const BarToolsPart = nativePart("div", "bevel-r-code-editor__bar-tools");
505
- function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, defaultWrap, onWrapChange, onViewChange, extensions, ref, children, ...props }) {
509
+ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, defaultWrap, onWrapChange, readOnly = false, onViewChange, extensions, ref, onKeyDown, children, ...props }) {
506
510
  const [view, setView] = useState(null);
507
511
  const viewRef = useRef(null);
508
512
  const root = useRef(null);
@@ -512,6 +516,7 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
512
516
  const [isDirty, setDirty] = useControllableState({ value: dirty, defaultValue: defaultDirty, onChange: onDirtyChange });
513
517
  const [wrapped, setWrap] = useControllableState({ value: wrap, defaultValue: initialWrap, onChange: onWrapChange });
514
518
  const [findOpen, setFindOpen] = useState(false);
519
+ const [findRequest, setFindRequest] = useState(0);
515
520
  const { mode } = useBevel();
516
521
  const dark = useResolvedDark(mode);
517
522
  const latest = useRef({ onSave, onViewChange, wrapped });
@@ -527,6 +532,12 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
527
532
  setView(next);
528
533
  latest.current.onViewChange?.(next);
529
534
  }, []);
535
+ const requestFind = useCallback(() => {
536
+ const current = viewRef.current;
537
+ if (!current) return;
538
+ openSearchPanel(current);
539
+ setFindRequest((count) => count + 1);
540
+ }, []);
530
541
  const run = useCallback((action) => {
531
542
  const current = viewRef.current;
532
543
  if (!current) return;
@@ -540,20 +551,22 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
540
551
  if (searchPanelOpen(current.state)) {
541
552
  closeSearchPanel(current);
542
553
  current.focus();
543
- } else openSearchPanel(current);
554
+ } else requestFind();
544
555
  return;
545
556
  } else if (action === "wrap") {
546
557
  setWrap(!latest.current.wrapped, { reason: "toolbar" });
547
558
  }
548
559
  current.focus();
549
- }, [setWrap]);
560
+ }, [setWrap, requestFind]);
550
561
  const mounted = useMemo(() => codeEditorExtensions({
551
562
  coarse,
552
563
  wrapped: initialWrap,
553
564
  dark,
565
+ readOnly,
554
566
  onSave: save,
555
567
  onDocChange: () => setDirty(true, { reason: "edit" }),
556
- onFindChange: setFindOpen
568
+ onFindChange: setFindOpen,
569
+ onFind: requestFind
557
570
  }), []);
558
571
  useEffect(() => {
559
572
  view?.dispatch({ effects: wrapCompartment.reconfigure(wrapExtension(wrapped)) });
@@ -561,6 +574,9 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
561
574
  useEffect(() => {
562
575
  view?.dispatch({ effects: darkCompartment.reconfigure(EditorView.darkTheme.of(dark)) });
563
576
  }, [view, dark]);
577
+ useEffect(() => {
578
+ view?.dispatch({ effects: readOnlyCompartment.reconfigure(readOnlyExtension(readOnly)) });
579
+ }, [view, readOnly]);
564
580
  const settled = useRef(false);
565
581
  useEffect(() => {
566
582
  if (!settled.current) {
@@ -579,12 +595,20 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
579
595
  dirty: isDirty,
580
596
  setDirty,
581
597
  findOpen,
598
+ findRequest,
599
+ readOnly,
582
600
  extensions: extensions ? [...mounted, ...extensions] : mounted,
583
601
  root,
584
602
  save,
585
603
  run
586
- }), [view, register, doc, coarse, dark, wrapped, isDirty, setDirty, findOpen, extensions, mounted, save, run]);
587
- return /* @__PURE__ */ jsx(CodeEditorContext, { value: context, children: /* @__PURE__ */ jsx(RootPart, { ...props, ref: merged, children }) });
604
+ }), [view, register, doc, coarse, dark, wrapped, isDirty, setDirty, findOpen, findRequest, readOnly, extensions, mounted, save, run]);
605
+ return /* @__PURE__ */ jsx(CodeEditorContext, { value: context, children: /* @__PURE__ */ jsx(RootPart, { ...props, ref: merged, onKeyDown: (event) => {
606
+ onKeyDown?.(event);
607
+ if (event.defaultPrevented || event.altKey || event.shiftKey || !(event.metaKey || event.ctrlKey)) return;
608
+ if (event.key.toLowerCase() !== "f") return;
609
+ event.preventDefault();
610
+ requestFind();
611
+ }, children }) });
588
612
  }
589
613
  function Dirty({ children, hidden, ...props }) {
590
614
  const { dirty } = useCodeEditor();
@@ -687,6 +711,8 @@ export {
687
711
  coarsePointer,
688
712
  codeEditorExtensions,
689
713
  darkCompartment,
714
+ readOnlyCompartment,
715
+ readOnlyExtension,
690
716
  rememberWrap,
691
717
  storedWrap,
692
718
  useCodeEditor,
@@ -7,7 +7,7 @@ const distributions = {
7
7
  richclay: ["richclay.js", "richclay.css", "LICENSE", "THIRD-PARTY-NOTICES.md", "vendor/squire.js", "vendor/squire.provenance.json", "vendor/SQUIRE-LICENSE", "vendor/purify.min.js", "vendor/DOMPURIFY-LICENSE", "vendor/DOMPURIFY-LICENSE-MPL"],
8
8
  quickcrop: ["quickcrop.js", "quickcrop.css", "LICENSE"],
9
9
  hypercms: ["dist/hypercms.min.js", "LICENSE"],
10
- "clay-mutation": ["src/lib/mutation.js", "src/lib/extension-noise.js", "src/lib/region-policy.js", "src/lib/region-capabilities.js", "src/lib/root-attrs.js", "src/lib/user-gesture.js", "src/lib/ephemeral.js", "LICENSE"]
10
+ "clay-mutation": ["src/lib/mutation.js", "src/lib/extension-noise.js", "src/lib/region-policy.js", "src/lib/region-capabilities.js", "src/lib/root-attrs.js", "src/lib/user-gesture.js", "src/lib/ephemeral.js", "src/lib/derived.js", "src/vendor/sap-ownership.vendor.js", "src/core/boot-phase.js", "LICENSE"]
11
11
  };
12
12
  const packageNames = { hypercms: "@panphora/hyper-cms", "clay-mutation": "@panphora/clayjs" };
13
13
  const [name, source] = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/bevel",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Beveled controls in an editorial page. One stylesheet, one script, no build.",
5
5
  "license": "MIT-0",
6
6
  "author": "David Miranda",
package/react/README.md CHANGED
@@ -225,7 +225,7 @@ Each row links to a real example compiled against the installed package. Each co
225
225
  | persistent-action-banner | `@panphora/bevel/react/persistent-action-banner` | ActionBanner (Root, Dismiss, Body, Icon, Actions, Observation, Meta, Head, Count, Progress, Status) | [Example](../tests/react/consumer/catalog-cases.tsx#L105) |
226
226
  | save-status-indicator | `@panphora/bevel/react/save-status-indicator` | SaveStatus; SavedNotification | [Example](../tests/react/consumer/catalog-cases.tsx#L106) |
227
227
  | code-block | `@panphora/bevel/react/code-block` | CodeBlock (Root, Code, Copy, Wrap, Feedback, Header, Name, Language, Meta) | [Example](../tests/react/consumer/catalog-cases.tsx#L108) |
228
- | code-editor | `@panphora/bevel/react/code-editor` | CodeEditor (Root, Tab, Name, Host, Dirty, Spacer, Toolbar, Tool, Save, Surface, Status, Meta, KeyboardBar, BarTools). CodeMirror 6 and its grammars are optional peers, so this entry is not re-exported from `@panphora/bevel/react` and a consumer that never opens it pays nothing. Give the root a definite height. | [Example](../tests/react/consumer/catalog-cases.tsx#L107) |
228
+ | code-editor | `@panphora/bevel/react/code-editor` | CodeEditor (Root, Tab, Name, Host, Dirty, Spacer, Toolbar, Tool, Save, Surface, Status, Meta, KeyboardBar, BarTools). CodeMirror 6 and its grammars are optional peers, so this entry is not re-exported from `@panphora/bevel/react` and a consumer that never opens it pays nothing. Give the root a definite height. `readOnly` keeps the editor focusable and selectable; `findRequest` in `useCodeEditor()` increases on every find request, for hosts that draw their own find field. | [Example](../tests/react/consumer/catalog-cases.tsx#L107) |
229
229
  | diff-view | `@panphora/bevel/react/diff-view` | DiffView (Root, Body, Line, Count, Layout, Hunk, Header, Controls, Footer) | [Example](../tests/react/consumer/catalog-cases.tsx#L109) |
230
230
  | activity-feed | `@panphora/bevel/react/activity-feed` | ActivityFeed (Root, Groups, More, Day, List, Item, Head, Detail, Avatar) | [Example](../tests/react/consumer/catalog-cases.tsx#L110) |
231
231
  | stat-tiles | `@panphora/bevel/react/stat-tiles` | StatTile (Root, Label, Value, Delta, Spark, Description); StatTiles | [Example](../tests/react/consumer/catalog-cases.tsx#L111) |
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "richclay",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "source": "Explicitly supplied local distribution; hashes identify the exact tested bytes, not a published release claim.",
5
5
  "files": {
6
- "richclay.js": "3efdc37689e640e0c3fddcef41617243dca186679a8f799cde16db7aa245f2c7",
6
+ "richclay.js": "5434545ce62960fc27611edae1ef808f29dfe31f4e8bd8c8553c4eb118d83c0c",
7
7
  "richclay.css": "fc44ab9e71f8b66950fbda5a535774c7acb97d7e54aff2521ea4122d21422d47",
8
8
  "LICENSE": "5312a8ef6ff9a844697b8b112612467efd12957e8afba098ce600dec0f8396c4",
9
9
  "THIRD-PARTY-NOTICES.md": "3659529614b62297b8336ac7f6189ef9f67425911265bff1cde8d136df21562b",
@@ -1786,13 +1786,16 @@ var RichClayBundle = (() => {
1786
1786
  return clone.innerHTML;
1787
1787
  }
1788
1788
  var instances = /* @__PURE__ */ new WeakMap();
1789
- var autoInitWindows = /* @__PURE__ */ new WeakSet();
1790
- var watchedWindows = /* @__PURE__ */ new WeakSet();
1789
+ var autoInitWindows = /* @__PURE__ */ new WeakMap();
1790
+ var watchedWindows = /* @__PURE__ */ new WeakMap();
1791
1791
  var globalRegistry = createDefaultRegistry();
1792
1792
  var KEEP_FOCUS = Symbol("richclay-keep-focus");
1793
1793
  var ROOT_TRANSFER_EVENTS = ["cut", "paste", "drop"];
1794
1794
  var MODIFIER_ORDER = ["Alt", "Ctrl", "Meta", "Shift"];
1795
1795
  var dialogSeq = 0;
1796
+ function isManagedByClayJS(win) {
1797
+ return typeof win.clay?.mode === "string";
1798
+ }
1796
1799
  var defaultOptions = {
1797
1800
  toolbar: "standard",
1798
1801
  toolbarContainer: null,
@@ -1886,23 +1889,36 @@ var RichClayBundle = (() => {
1886
1889
  return elements.map((element) => new _RichClay(element, options));
1887
1890
  }
1888
1891
  static autoInit(win = typeof window !== "undefined" ? window : void 0) {
1889
- if (!win || !win.document || autoInitWindows.has(win)) return;
1890
- autoInitWindows.add(win);
1892
+ if (!win || !win.document) return () => {
1893
+ };
1894
+ const existing = autoInitWindows.get(win);
1895
+ if (existing) return existing;
1896
+ let cancelled = false;
1897
+ let stopWatching = null;
1891
1898
  const run = () => {
1899
+ if (cancelled || isManagedByClayJS(win)) return;
1892
1900
  if (shouldUseHyperclay({}, win) && isHyperclayEditMode(win)) {
1893
1901
  _RichClay.init();
1894
- _RichClay.watch(win);
1902
+ stopWatching = _RichClay.watch(win);
1895
1903
  }
1896
1904
  };
1905
+ const stop = () => {
1906
+ cancelled = true;
1907
+ stopWatching?.();
1908
+ };
1909
+ autoInitWindows.set(win, stop);
1897
1910
  if (win.document.readyState === "loading") {
1898
1911
  win.document.addEventListener("DOMContentLoaded", run, { once: true });
1899
1912
  } else {
1900
1913
  run();
1901
1914
  }
1915
+ return stop;
1902
1916
  }
1903
1917
  static watch(win = typeof window !== "undefined" ? window : void 0, options = {}) {
1904
- if (!win || !win.document || watchedWindows.has(win)) return;
1905
- watchedWindows.add(win);
1918
+ if (!win || !win.document) return () => {
1919
+ };
1920
+ const existing = watchedWindows.get(win);
1921
+ if (existing) return existing;
1906
1922
  const mount = (element) => {
1907
1923
  if (!isMountable(element)) return;
1908
1924
  if (!instances.has(element) && !conflictsWithExistingEditor(element)) new _RichClay(element, options);
@@ -1931,12 +1947,21 @@ var RichClayBundle = (() => {
1931
1947
  });
1932
1948
  });
1933
1949
  });
1950
+ let stopped = false;
1951
+ const stop = () => {
1952
+ if (stopped) return;
1953
+ stopped = true;
1954
+ observer.disconnect();
1955
+ if (watchedWindows.get(win) === stop) watchedWindows.delete(win);
1956
+ };
1934
1957
  observer.observe(win.document.documentElement || win.document, {
1935
1958
  childList: true,
1936
1959
  subtree: true,
1937
1960
  attributes: true,
1938
1961
  attributeFilter: ["editable", "clay-editable", "data-richclay", "richclay"]
1939
1962
  });
1963
+ watchedWindows.set(win, stop);
1964
+ return stop;
1940
1965
  }
1941
1966
  static registerButton(def) {
1942
1967
  validateButton(def);