@threadlabs/looma 0.29.2 → 0.29.3

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.
@@ -0,0 +1,47 @@
1
+ import type { EditorState } from "@tiptap/pm/state";
2
+ /** Host-owned non-mutating commands for the selected passage. */
3
+ export interface LoomaEditorSelectionAction {
4
+ id: string;
5
+ label: string;
6
+ }
7
+ /** Host action available at a collapsed caret inside an ephemeral editor range. */
8
+ export interface LoomaEditorRangeAction extends LoomaEditorSelectionAction {
9
+ from: number;
10
+ to: number;
11
+ }
12
+ /** Matching ranges are grouped into one control for each action ID. */
13
+ export interface LoomaEditorRangeActionDetail {
14
+ id: string;
15
+ ranges: {
16
+ from: number;
17
+ to: number;
18
+ }[];
19
+ }
20
+ /** Exact editor range captured before focus leaves for a host action. */
21
+ export interface LoomaEditorSelectionActionDetail {
22
+ id: string;
23
+ from: number;
24
+ to: number;
25
+ text: string;
26
+ before: string;
27
+ after: string;
28
+ }
29
+ /**
30
+ * The range actions whose range holds the collapsed text caret.
31
+ * @contract Ranges that are not whole numbers, are empty, or fall outside the document are ignored.
32
+ */
33
+ export declare function rangeActionsAtCaret(state: EditorState, actions: readonly LoomaEditorRangeAction[]): LoomaEditorRangeAction[];
34
+ /** One control per action ID: its label and each distinct range it covers at the caret, in order. */
35
+ export declare function groupRangeActions(actions: readonly LoomaEditorRangeAction[]): Array<{
36
+ id: string;
37
+ label: string;
38
+ detail: LoomaEditorRangeActionDetail;
39
+ }>;
40
+ /** Whether the selection is a passage that selection actions apply to: nonempty text, or everything. */
41
+ export declare function selectsPassage(state: EditorState): boolean;
42
+ /**
43
+ * A selection action's detail for the current passage: its range, its text, and up to 48 characters
44
+ * of context on each side, captured before focus leaves the editor.
45
+ * @failure Returns null when the selection is empty.
46
+ */
47
+ export declare function selectionActionDetail(state: EditorState, id: string): LoomaEditorSelectionActionDetail | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.29.2",
3
+ "version": "0.29.3",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -6,6 +6,7 @@ import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaCodeLanguages
6
6
  import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageSourceResolver, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
7
7
  import { type LoomaImageUploadDetail } from "../../editor/image-uploads.js";
8
8
  import { type LoomaResponder } from "../../editor/requests.js";
9
+ import { type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail } from "../../editor/host-actions.js";
9
10
  import { type LoomaLinkTarget } from "../../editor/link-editing.js";
10
11
  export type { LoomaImageUploadDetail } from "../../editor/image-uploads.js";
11
12
  export type { LoomaRequestInit, LoomaResponder } from "../../editor/requests.js";
@@ -17,33 +18,7 @@ export type { LoomaRequestInit, LoomaResponder } from "../../editor/requests.js"
17
18
  * set. Narrow viewports retain a dock; document commands/content are unchanged.
18
19
  */
19
20
  export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover" | "contextual";
20
- /** Host-owned non-mutating commands for the selected passage. */
21
- export interface LoomaEditorSelectionAction {
22
- id: string;
23
- label: string;
24
- }
25
- /** Host action available at a collapsed caret inside an ephemeral editor range. */
26
- export interface LoomaEditorRangeAction extends LoomaEditorSelectionAction {
27
- from: number;
28
- to: number;
29
- }
30
- /** Matching ranges are grouped into one control for each action ID. */
31
- export interface LoomaEditorRangeActionDetail {
32
- id: string;
33
- ranges: {
34
- from: number;
35
- to: number;
36
- }[];
37
- }
38
- /** Exact editor range captured before focus leaves for a host action. */
39
- export interface LoomaEditorSelectionActionDetail {
40
- id: string;
41
- from: number;
42
- to: number;
43
- text: string;
44
- before: string;
45
- after: string;
46
- }
21
+ export type { LoomaEditorRangeAction, LoomaEditorRangeActionDetail, LoomaEditorSelectionAction, LoomaEditorSelectionActionDetail, } from "../../editor/host-actions.js";
47
22
  export type { LoomaLinkTarget } from "../../editor/link-editing.js";
48
23
  /** The `slash-commands` request's detail: this editor's default commands. */
49
24
  export interface LoomaSlashCommandsDetail {
@@ -1751,6 +1751,66 @@ function createChipEditing(options) {
1751
1751
  };
1752
1752
  }
1753
1753
  //#endregion
1754
+ //#region src/editor/host-actions.ts
1755
+ /** Characters of context on each side of a selected passage. */
1756
+ var CONTEXT = 48;
1757
+ /**
1758
+ * The range actions whose range holds the collapsed text caret.
1759
+ * @contract Ranges that are not whole numbers, are empty, or fall outside the document are ignored.
1760
+ */
1761
+ function rangeActionsAtCaret(state, actions) {
1762
+ const selection = state.selection;
1763
+ if (!(selection instanceof TextSelection) || !selection.empty) return [];
1764
+ const size = state.doc.content.size;
1765
+ return actions.filter(({ from, to }) => Number.isSafeInteger(from) && Number.isSafeInteger(to) && from >= 0 && to > from && to <= size && from <= selection.from && selection.from <= to);
1766
+ }
1767
+ /** One control per action ID: its label and each distinct range it covers at the caret, in order. */
1768
+ function groupRangeActions(actions) {
1769
+ const groups = /* @__PURE__ */ new Map();
1770
+ for (const { id, label, from, to } of actions) {
1771
+ const group = groups.get(id) ?? {
1772
+ label,
1773
+ ranges: /* @__PURE__ */ new Map()
1774
+ };
1775
+ group.ranges.set(`${from}:${to}`, {
1776
+ from,
1777
+ to
1778
+ });
1779
+ groups.set(id, group);
1780
+ }
1781
+ return [...groups].map(([id, { label, ranges }]) => ({
1782
+ id,
1783
+ label,
1784
+ detail: {
1785
+ id,
1786
+ ranges: [...ranges.values()]
1787
+ }
1788
+ }));
1789
+ }
1790
+ /** Whether the selection is a passage that selection actions apply to: nonempty text, or everything. */
1791
+ function selectsPassage(state) {
1792
+ const selection = state.selection;
1793
+ return !selection.empty && (selection instanceof TextSelection || selection instanceof AllSelection);
1794
+ }
1795
+ /**
1796
+ * A selection action's detail for the current passage: its range, its text, and up to 48 characters
1797
+ * of context on each side, captured before focus leaves the editor.
1798
+ * @failure Returns null when the selection is empty.
1799
+ */
1800
+ function selectionActionDetail(state, id) {
1801
+ const { from, to } = state.selection;
1802
+ if (from === to) return null;
1803
+ const doc = state.doc;
1804
+ return {
1805
+ id,
1806
+ from,
1807
+ to,
1808
+ text: doc.textBetween(from, to, "\n"),
1809
+ before: doc.textBetween(Math.max(0, from - CONTEXT), from, "\n"),
1810
+ after: doc.textBetween(to, Math.min(doc.content.size, to + CONTEXT), "\n")
1811
+ };
1812
+ }
1813
+ //#endregion
1754
1814
  //#region src/editor/extensions/relative-links.ts
1755
1815
  /** Keep other origins and non-web schemes intact; same-origin links retain path, query, and fragment. */
1756
1816
  function siteRelativeHref(href, baseUrl) {
@@ -3456,11 +3516,7 @@ var LoomaEditor = defineComponent({
3456
3516
  let imagePopup = null;
3457
3517
  const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
3458
3518
  const formattingPickerOpen = () => linkState.value.surface === "popover" || tablePickerOpen.value || formatOpen.value;
3459
- const activeRangeActions = (instance) => {
3460
- const selection = instance.state.selection;
3461
- if (!(selection instanceof TextSelection) || !selection.empty) return [];
3462
- return props.rangeActions.filter((action) => Number.isSafeInteger(action.from) && Number.isSafeInteger(action.to) && action.from >= 0 && action.to > action.from && action.to <= instance.state.doc.content.size && action.from <= selection.from && selection.from <= action.to);
3463
- };
3519
+ const activeRangeActions = (instance) => rangeActionsAtCaret(instance.state, props.rangeActions);
3464
3520
  const formattingActive = () => formatting?.active() ?? false;
3465
3521
  const editorStateVersion = ref(0);
3466
3522
  const mobileToolbarMode = ref("formatting");
@@ -4514,40 +4570,22 @@ var LoomaEditor = defineComponent({
4514
4570
  onClick: run
4515
4571
  }, () => loomaIcon(icon));
4516
4572
  };
4517
- const renderRangeActions = (instance) => {
4518
- const groups = /* @__PURE__ */ new Map();
4519
- for (const action of activeRangeActions(instance)) {
4520
- const group = groups.get(action.id) ?? {
4521
- label: action.label,
4522
- ranges: /* @__PURE__ */ new Map()
4523
- };
4524
- group.ranges.set(`${action.from}:${action.to}`, {
4525
- from: action.from,
4526
- to: action.to
4527
- });
4528
- groups.set(action.id, group);
4573
+ const renderRangeActions = (instance) => groupRangeActions(activeRangeActions(instance)).map(({ id, label, detail }) => h(Button, {
4574
+ key: id,
4575
+ "aria-label": label,
4576
+ type: "button",
4577
+ variant: "ghost",
4578
+ tone: "neutral",
4579
+ size: "sm",
4580
+ onPointerdown: (event) => event.preventDefault(),
4581
+ onClick: () => {
4582
+ emit("rangeAction", detail);
4583
+ formattingPopup?.hide();
4529
4584
  }
4530
- return [...groups].map(([id, group]) => h(Button, {
4531
- key: id,
4532
- "aria-label": group.label,
4533
- type: "button",
4534
- variant: "ghost",
4535
- tone: "neutral",
4536
- size: "sm",
4537
- onPointerdown: (event) => event.preventDefault(),
4538
- onClick: () => {
4539
- emit("rangeAction", {
4540
- id,
4541
- ranges: [...group.ranges.values()]
4542
- });
4543
- formattingPopup?.hide();
4544
- }
4545
- }, () => group.label));
4546
- };
4585
+ }, () => label));
4547
4586
  const renderSelectionActions = (instance) => {
4548
- const selection = instance.state.selection;
4549
- if (selection.empty) return renderRangeActions(instance);
4550
- if (!(selection instanceof TextSelection || selection instanceof AllSelection)) return [];
4587
+ if (instance.state.selection.empty) return renderRangeActions(instance);
4588
+ if (!selectsPassage(instance.state)) return [];
4551
4589
  return props.selectionActions.map((action) => h(Button, {
4552
4590
  key: action.id,
4553
4591
  "aria-label": action.label,
@@ -4557,17 +4595,9 @@ var LoomaEditor = defineComponent({
4557
4595
  size: "sm",
4558
4596
  onPointerdown: (event) => event.preventDefault(),
4559
4597
  onClick: () => {
4560
- const { from, to } = instance.state.selection;
4561
- if (from === to) return;
4562
- const doc = instance.state.doc;
4563
- emit("selectionAction", {
4564
- id: action.id,
4565
- from,
4566
- to,
4567
- text: doc.textBetween(from, to, "\n"),
4568
- before: doc.textBetween(Math.max(0, from - 48), from, "\n"),
4569
- after: doc.textBetween(to, Math.min(doc.content.size, to + 48), "\n")
4570
- });
4598
+ const detail = selectionActionDetail(instance.state, action.id);
4599
+ if (!detail) return;
4600
+ emit("selectionAction", detail);
4571
4601
  formattingPopup?.hide();
4572
4602
  }
4573
4603
  }, () => action.label));