@threadlabs/looma 0.17.1 → 0.17.2

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.
@@ -9200,6 +9200,12 @@ function setActiveTableCellBackground(editor, backgroundColor) {
9200
9200
  * Default Tiptap extension preset for Looma editor.
9201
9201
  * Uses the Vanilla JS Tiptap API; apps provide @tiptap/core and Looma ships the preset extensions.
9202
9202
  */
9203
+ var LoomaCodeBlock = CodeBlockLowlight.extend({ addInputRules() {
9204
+ return [...this.parent?.() ?? [], textblockTypeInputRule({
9205
+ find: /^```$/,
9206
+ type: this.type
9207
+ })];
9208
+ } });
9203
9209
  var StoredOnlyHighlight = Highlight.extend({
9204
9210
  parseHTML: () => [],
9205
9211
  addKeyboardShortcuts: () => ({}),
@@ -9243,9 +9249,13 @@ function getLoomaTableExtensions() {
9243
9249
  * can be replaced without coupling UI chrome to an application directory.
9244
9250
  * Use with `new Editor({ extensions: getDefaultEditorExtensions(), ... })` or a
9245
9251
  * framework's Tiptap editor hook.
9252
+ * @contract Code blocks accept three backticks in an empty paragraph as an
9253
+ * immediate typing shortcut. An optional node view changes presentation only;
9254
+ * the code and explicit language stay in the document schema.
9246
9255
  */
9247
9256
  function getDefaultEditorExtensions(options = {}) {
9248
- const { placeholder = "Type “/” for commands, or start writing…", linkOpenOnClick = false, imageInline = false, mention = createLoomaMentionExtension(), disableHighlight = false, codeLanguages } = options;
9257
+ const { placeholder = "Type “/” for commands, or start writing…", linkOpenOnClick = false, imageInline = false, mention = createLoomaMentionExtension(), disableHighlight = false, codeLanguages, codeBlockNodeView } = options;
9258
+ const codeBlock = LoomaCodeBlock.configure({ lowlight: createLowlight(codeLanguages) });
9249
9259
  return [
9250
9260
  Document,
9251
9261
  Paragraph,
@@ -9283,7 +9293,7 @@ function getDefaultEditorExtensions(options = {}) {
9283
9293
  (disableHighlight ? StoredOnlyHighlight : Highlight).configure({ multicolor: false }),
9284
9294
  Code,
9285
9295
  LoomaSmartPaste,
9286
- CodeBlockLowlight.configure({ lowlight: createLowlight(codeLanguages) }),
9296
+ codeBlockNodeView ? codeBlock.extend({ addNodeView: () => codeBlockNodeView }) : codeBlock,
9287
9297
  Typography,
9288
9298
  Placeholder.configure({
9289
9299
  placeholder: ({ node }) => node.type.name === "paragraph" ? placeholder : "",
@@ -2,7 +2,7 @@
2
2
  * Default Tiptap extension preset for Looma editor.
3
3
  * Uses the Vanilla JS Tiptap API; apps provide @tiptap/core and Looma ships the preset extensions.
4
4
  */
5
- import { type AnyExtension } from "@tiptap/core";
5
+ import { type AnyExtension, type NodeViewRenderer } from "@tiptap/core";
6
6
  import { createLowlight } from "lowlight";
7
7
  /** Grammars supplied by the host; importing a language remains the host's choice. */
8
8
  export type LoomaCodeLanguages = NonNullable<Parameters<typeof createLowlight>[0]>;
@@ -29,6 +29,8 @@ export interface DefaultEditorExtensionsOptions {
29
29
  disableHighlight?: boolean;
30
30
  /** Code block grammars to register. None are loaded by default. */
31
31
  codeLanguages?: LoomaCodeLanguages;
32
+ /** Optional presentation for a code block; the Vue editor supplies its language control. */
33
+ codeBlockNodeView?: NodeViewRenderer;
32
34
  }
33
35
  /**
34
36
  * Complete Looma table schema as one Tiptap extension.
@@ -50,5 +52,8 @@ export declare function getLoomaTableExtensions(): AnyExtension[];
50
52
  * can be replaced without coupling UI chrome to an application directory.
51
53
  * Use with `new Editor({ extensions: getDefaultEditorExtensions(), ... })` or a
52
54
  * framework's Tiptap editor hook.
55
+ * @contract Code blocks accept three backticks in an empty paragraph as an
56
+ * immediate typing shortcut. An optional node view changes presentation only;
57
+ * the code and explicit language stay in the document schema.
53
58
  */
54
59
  export declare function getDefaultEditorExtensions(options?: DefaultEditorExtensionsOptions): AnyExtension[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -8661,6 +8661,20 @@ html[data-ui-input-modality="touch"]
8661
8661
  font-size: var(--ui-font-size-sm);
8662
8662
  }
8663
8663
 
8664
+ .looma-editor .ProseMirror pre.looma-editor__code-block--has-language {
8665
+ position: relative;
8666
+ padding-block-start: calc(var(--ui-space-4) + 2rem);
8667
+ }
8668
+
8669
+ .looma-editor__code-language {
8670
+ position: absolute;
8671
+ inset-block-start: var(--ui-space-2);
8672
+ inset-inline-end: var(--ui-space-2);
8673
+ inline-size: min(8.5rem, calc(100% - 2 * var(--ui-space-2)));
8674
+ z-index: 1;
8675
+ font-family: var(--ui-font-family-sans, sans-serif);
8676
+ }
8677
+
8664
8678
  .looma-editor .ProseMirror code {
8665
8679
  padding: 0.1em 0.3em;
8666
8680
  border-radius: var(--ui-radius-sm);
@@ -0,0 +1,29 @@
1
+ import type { LoomaCodeLanguages } from "@threadlabs/looma/editor";
2
+ /** Per-block language control; the document stores only the code and explicit language. */
3
+ export declare function createCodeBlockView(languages: LoomaCodeLanguages, isEditable: () => boolean): import("vue").DefineComponent<{
4
+ decorations: readonly import("@tiptap/core").DecorationWithType[];
5
+ selected: boolean;
6
+ updateAttributes: (attributes: Record<string, any>) => void;
7
+ deleteNode: () => void;
8
+ node: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[0];
9
+ view: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[1];
10
+ getPos: () => number;
11
+ innerDecorations: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[4];
12
+ editor: import("@tiptap/core").Editor;
13
+ extension: import("@tiptap/core").Node;
14
+ HTMLAttributes: Record<string, any>;
15
+ }, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
16
+ [key: string]: any;
17
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
18
+ decorations: readonly import("@tiptap/core").DecorationWithType[];
19
+ selected: boolean;
20
+ updateAttributes: (attributes: Record<string, any>) => void;
21
+ deleteNode: () => void;
22
+ node: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[0];
23
+ view: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[1];
24
+ getPos: () => number;
25
+ innerDecorations: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[4];
26
+ editor: import("@tiptap/core").Editor;
27
+ extension: import("@tiptap/core").Node;
28
+ HTMLAttributes: Record<string, any>;
29
+ }> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,10 +1,12 @@
1
- import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
1
+ import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
2
2
  import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
3
- import { EditorInsertTableGrid, EditorMentionMenu, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Menu, MenuItem, Popover, Tooltip } from "@threadlabs/looma/vue";
4
- import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
3
+ import { Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Menu, MenuItem, Popover, Tooltip } from "@threadlabs/looma/vue";
4
+ import { BubbleMenu, EditorContent, NodeViewContent, NodeViewWrapper, VueNodeViewRenderer, nodeViewProps, useEditor } from "@tiptap/vue-3";
5
5
  import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
6
6
  import { Extension } from "@tiptap/core";
7
7
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
+ import { closeHistory } from "@tiptap/pm/history";
9
+ import { createLowlight } from "lowlight";
8
10
  export * from "@threadlabs/looma/editor";
9
11
  //#region src/vue/editor/primitives.ts
10
12
  /** Each Vue editor component's element tag, for tooling that enumerates them. */
@@ -178,6 +180,69 @@ function imageUploadContent(image) {
178
180
  };
179
181
  }
180
182
  //#endregion
183
+ //#region src/vue/editor/code-block-view.ts
184
+ /** Per-block language control; the document stores only the code and explicit language. */
185
+ function createCodeBlockView(languages, isEditable) {
186
+ const lowlight = createLowlight(languages);
187
+ const names = Object.keys(languages).sort();
188
+ return defineComponent({
189
+ name: "LoomaCodeBlockView",
190
+ props: nodeViewProps,
191
+ setup(props) {
192
+ const selected = computed(() => {
193
+ const language = props.node.attrs.language;
194
+ return typeof language === "string" && language ? language : "auto";
195
+ });
196
+ const detected = computed(() => {
197
+ if (!props.node.textContent.trim()) return null;
198
+ const language = lowlight.highlightAuto(props.node.textContent).data?.language;
199
+ return typeof language === "string" ? language : null;
200
+ });
201
+ const options = computed(() => [
202
+ {
203
+ id: "auto",
204
+ value: "auto",
205
+ label: detected.value ? `Auto (${detected.value.toUpperCase()})` : "Auto"
206
+ },
207
+ ...selected.value !== "auto" && !Object.hasOwn(languages, selected.value) ? [{
208
+ id: selected.value,
209
+ value: selected.value,
210
+ label: `${selected.value.toUpperCase()} (unavailable)`
211
+ }] : [],
212
+ ...names.map((name) => ({
213
+ id: name,
214
+ value: name,
215
+ label: name.toUpperCase()
216
+ }))
217
+ ]);
218
+ const choose = (detail) => {
219
+ if (detail.kind !== "selection" || !detail.value) return;
220
+ const language = detail.value === "auto" ? null : detail.value;
221
+ if (language !== null && !Object.hasOwn(languages, language)) return;
222
+ props.editor.view.dispatch(closeHistory(props.editor.state.tr));
223
+ props.updateAttributes({ language });
224
+ props.editor.view.dispatch(closeHistory(props.editor.state.tr));
225
+ };
226
+ const showControl = () => names.length > 0 && isEditable() && props.editor.isEditable;
227
+ return () => h(NodeViewWrapper, {
228
+ as: "pre",
229
+ class: showControl() ? "looma-editor__code-block--has-language" : void 0
230
+ }, () => [showControl() ? h("div", {
231
+ class: "looma-editor__code-language",
232
+ contenteditable: "false"
233
+ }, [h(Combobox, {
234
+ label: "Code language",
235
+ labelVisibility: "sr-only",
236
+ size: "sm",
237
+ disclosure: true,
238
+ filter: "label",
239
+ value: selected.value,
240
+ onValueChange: choose
241
+ }, () => options.value.map((option) => h("option", { value: option.value }, option.label)))]) : null, h(NodeViewContent, { as: "code" })]);
242
+ }
243
+ });
244
+ }
245
+ //#endregion
181
246
  //#region src/vue/editor/LoomaEditor.ts
182
247
  var EMPTY_DOCUMENT = {
183
248
  type: "doc",
@@ -518,7 +583,8 @@ var LoomaEditor = defineComponent({
518
583
  placeholder: props.placeholder,
519
584
  mention: mentionExtension ?? false,
520
585
  disableHighlight: props.disableHighlight,
521
- codeLanguages: props.codeLanguages
586
+ codeLanguages: props.codeLanguages,
587
+ codeBlockNodeView: VueNodeViewRenderer(createCodeBlockView(props.codeLanguages, () => props.editable))
522
588
  }),
523
589
  imageDelivery.extension,
524
590
  slashExtension,