@sveltia/ui 0.78.3 → 0.79.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.
Files changed (181) hide show
  1. package/dist/components/alert/alert-content.svelte +51 -0
  2. package/dist/components/alert/alert-content.svelte.d.ts +39 -0
  3. package/dist/components/alert/alert.d.ts +3 -0
  4. package/dist/components/alert/alert.js +21 -0
  5. package/dist/components/alert/alert.svelte +6 -30
  6. package/dist/components/alert/infobar.svelte +7 -21
  7. package/dist/components/alert/infobar.svelte.d.ts +2 -2
  8. package/dist/components/button/button.svelte +11 -11
  9. package/dist/components/button/button.svelte.d.ts +2 -2
  10. package/dist/components/button/select-button.svelte +1 -1
  11. package/dist/components/button/select-button.svelte.d.ts +2 -2
  12. package/dist/components/button/split-button.svelte +1 -1
  13. package/dist/components/button/split-button.svelte.d.ts +3 -3
  14. package/dist/components/calendar/calendar.d.ts +2 -0
  15. package/dist/components/calendar/calendar.js +34 -0
  16. package/dist/components/calendar/calendar.svelte +49 -5
  17. package/dist/components/checkbox/checkbox.svelte +11 -4
  18. package/dist/components/checkbox/checkbox.svelte.d.ts +2 -2
  19. package/dist/components/dialog/alert-dialog.svelte +1 -1
  20. package/dist/components/dialog/alert-dialog.svelte.d.ts +2 -2
  21. package/dist/components/dialog/confirmation-dialog.svelte +1 -1
  22. package/dist/components/dialog/confirmation-dialog.svelte.d.ts +2 -2
  23. package/dist/components/dialog/dialog.svelte +8 -18
  24. package/dist/components/dialog/dialog.svelte.d.ts +2 -2
  25. package/dist/components/dialog/modal-close-button.svelte +50 -0
  26. package/dist/components/dialog/modal-close-button.svelte.d.ts +43 -0
  27. package/dist/components/dialog/prompt-dialog.svelte +1 -1
  28. package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -4
  29. package/dist/components/disclosure/disclosure.svelte +1 -1
  30. package/dist/components/drawer/drawer.svelte +12 -34
  31. package/dist/components/drawer/drawer.svelte.d.ts +1 -1
  32. package/dist/components/emoji/caret.d.ts +1 -1
  33. package/dist/components/emoji/caret.js +1 -1
  34. package/dist/components/emoji/emoji-suggestions.svelte +1 -1
  35. package/dist/components/emoji/emoji-suggestions.svelte.d.ts +3 -3
  36. package/dist/components/emoji/emoji.d.ts +2 -2
  37. package/dist/components/emoji/emoji.js +1 -1
  38. package/dist/components/emoji/position.d.ts +2 -2
  39. package/dist/components/emoji/position.js +1 -1
  40. package/dist/components/grid/grid-body.svelte +6 -2
  41. package/dist/components/grid/grid-cell.svelte +3 -9
  42. package/dist/components/grid/grid-col-header.svelte +3 -9
  43. package/dist/components/grid/grid-foot.svelte +4 -11
  44. package/dist/components/grid/grid-head.svelte +4 -11
  45. package/dist/components/grid/grid-row-header.svelte +3 -10
  46. package/dist/components/grid/grid-row.svelte +3 -16
  47. package/dist/components/listbox/listbox.svelte +1 -1
  48. package/dist/components/listbox/listbox.svelte.d.ts +1 -1
  49. package/dist/components/listbox/option.svelte +1 -1
  50. package/dist/components/listbox/option.svelte.d.ts +2 -2
  51. package/dist/components/menu/menu-button.svelte +2 -2
  52. package/dist/components/menu/menu-button.svelte.d.ts +4 -4
  53. package/dist/components/menu/menu-item-checkable.svelte +71 -0
  54. package/dist/components/menu/menu-item-checkable.svelte.d.ts +25 -0
  55. package/dist/components/menu/menu-item-checkbox.svelte +3 -36
  56. package/dist/components/menu/menu-item-checkbox.svelte.d.ts +3 -3
  57. package/dist/components/menu/menu-item-group.svelte +8 -5
  58. package/dist/components/menu/menu-item-group.svelte.d.ts +9 -1
  59. package/dist/components/menu/menu-item-radio.svelte +3 -36
  60. package/dist/components/menu/menu-item-radio.svelte.d.ts +3 -3
  61. package/dist/components/menu/menu-item.svelte +22 -6
  62. package/dist/components/menu/menu-item.svelte.d.ts +4 -4
  63. package/dist/components/radio/radio.svelte +12 -1
  64. package/dist/components/resizable-pane/resizable-handle.svelte +14 -18
  65. package/dist/components/resizable-pane/resizable-pane-group.svelte +9 -6
  66. package/dist/components/resizable-pane/resizable-pane.svelte +4 -4
  67. package/dist/components/resizable-pane/resizable-pane.svelte.d.ts +7 -6
  68. package/dist/components/resizable-pane/sizing.d.ts +5 -4
  69. package/dist/components/resizable-pane/sizing.js +84 -25
  70. package/dist/components/scroll/infinite-scroll.svelte +16 -21
  71. package/dist/components/select/combobox.svelte +43 -52
  72. package/dist/components/select/combobox.svelte.d.ts +2 -2
  73. package/dist/components/select/select-tags.svelte +7 -1
  74. package/dist/components/select/select.svelte +2 -22
  75. package/dist/components/select/select.svelte.d.ts +1 -1
  76. package/dist/components/slider/slider.d.ts +6 -0
  77. package/dist/components/slider/slider.js +55 -2
  78. package/dist/components/slider/slider.svelte +63 -32
  79. package/dist/components/table/table-cell.svelte +3 -9
  80. package/dist/components/table/table-col-header.svelte +3 -9
  81. package/dist/components/table/table-foot.svelte +4 -11
  82. package/dist/components/table/table-head.svelte +4 -11
  83. package/dist/components/table/table-part.svelte +69 -0
  84. package/dist/components/table/table-part.svelte.d.ts +48 -0
  85. package/dist/components/table/table-row-header.svelte +3 -10
  86. package/dist/components/table/table-row.svelte +3 -10
  87. package/dist/components/tabs/tab-list.svelte +12 -0
  88. package/dist/components/tabs/tab.svelte +1 -1
  89. package/dist/components/tabs/tab.svelte.d.ts +2 -2
  90. package/dist/components/text-editor/constants.d.ts +5 -5
  91. package/dist/components/text-editor/constants.js +1 -1
  92. package/dist/components/text-editor/core.d.ts +4 -5
  93. package/dist/components/text-editor/core.js +376 -273
  94. package/dist/components/text-editor/emoji-autocomplete.svelte +1 -1
  95. package/dist/components/text-editor/lexical-root.svelte +1 -1
  96. package/dist/components/text-editor/raw-markdown.d.ts +6 -33
  97. package/dist/components/text-editor/raw-markdown.js +63 -8
  98. package/dist/components/text-editor/shiki/facade.d.ts +1 -1
  99. package/dist/components/text-editor/shiki/facade.js +63 -48
  100. package/dist/components/text-editor/shiki/generated.d.ts +1 -1
  101. package/dist/components/text-editor/shiki/generated.js +1 -1
  102. package/dist/components/text-editor/shiki/highlighter.d.ts +6 -1
  103. package/dist/components/text-editor/shiki/highlighter.js +3 -4
  104. package/dist/components/text-editor/shiki/loader.d.ts +1 -1
  105. package/dist/components/text-editor/shiki/loader.js +1 -1
  106. package/dist/components/text-editor/shiki/version.d.ts +1 -1
  107. package/dist/components/text-editor/shiki/version.js +1 -1
  108. package/dist/components/text-editor/store.svelte.d.ts +1 -1
  109. package/dist/components/text-editor/store.svelte.js +5 -2
  110. package/dist/components/text-editor/text-editor.svelte +2 -2
  111. package/dist/components/text-editor/text-editor.svelte.d.ts +3 -3
  112. package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -16
  113. package/dist/components/text-editor/toolbar/format-text-button.svelte +7 -31
  114. package/dist/components/text-editor/toolbar/format-text-button.svelte.d.ts +1 -1
  115. package/dist/components/text-editor/toolbar/insert-component.d.ts +3 -0
  116. package/dist/components/text-editor/toolbar/insert-component.js +34 -0
  117. package/dist/components/text-editor/toolbar/insert-item-button.svelte +4 -24
  118. package/dist/components/text-editor/toolbar/insert-item-button.svelte.d.ts +1 -1
  119. package/dist/components/text-editor/toolbar/insert-link-button.svelte +9 -25
  120. package/dist/components/text-editor/toolbar/insert-menu-button.svelte +4 -24
  121. package/dist/components/text-editor/toolbar/insert-menu-button.svelte.d.ts +1 -1
  122. package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +1 -1
  123. package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +31 -35
  124. package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte.d.ts +1 -1
  125. package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte +0 -10
  126. package/dist/components/text-editor/toolbar/toolbar-wrapper.svelte.d.ts +1 -11
  127. package/dist/components/text-editor/transformers/table.d.ts +0 -1
  128. package/dist/components/text-editor/transformers/table.js +1 -2
  129. package/dist/components/text-field/emoji-autocomplete.svelte +1 -1
  130. package/dist/components/text-field/masked-input.svelte +179 -0
  131. package/dist/components/text-field/masked-input.svelte.d.ts +42 -0
  132. package/dist/components/text-field/number-input.js +7 -1
  133. package/dist/components/text-field/number-input.svelte +13 -5
  134. package/dist/components/text-field/number-input.svelte.d.ts +3 -3
  135. package/dist/components/text-field/password-input.svelte +3 -116
  136. package/dist/components/text-field/password-input.svelte.d.ts +3 -3
  137. package/dist/components/text-field/search-bar.svelte +1 -1
  138. package/dist/components/text-field/search-bar.svelte.d.ts +3 -3
  139. package/dist/components/text-field/secret-input.svelte +3 -119
  140. package/dist/components/text-field/secret-input.svelte.d.ts +3 -3
  141. package/dist/components/text-field/text-area.svelte +1 -1
  142. package/dist/components/text-field/text-area.svelte.d.ts +3 -3
  143. package/dist/components/text-field/text-input.svelte +1 -1
  144. package/dist/components/text-field/text-input.svelte.d.ts +3 -3
  145. package/dist/components/toast/toast.svelte +11 -20
  146. package/dist/components/toast/toast.svelte.d.ts +1 -1
  147. package/dist/components/tree/tree-item.svelte +1 -1
  148. package/dist/components/tree/tree-item.svelte.d.ts +1 -1
  149. package/dist/components/tree/tree.svelte +1 -1
  150. package/dist/components/tree/tree.svelte.d.ts +1 -1
  151. package/dist/components/util/font-links.svelte +1 -0
  152. package/dist/components/util/modal.svelte +37 -1
  153. package/dist/components/util/modal.svelte.d.ts +1 -1
  154. package/dist/components/util/popup.svelte +2 -5
  155. package/dist/components/util/popup.svelte.d.ts +2 -2
  156. package/dist/components/util/visibility-observer.svelte +1 -0
  157. package/dist/locales/ar.yaml +1 -1
  158. package/dist/locales/sk.yaml +136 -0
  159. package/dist/services/group.svelte.d.ts +10 -1
  160. package/dist/services/group.svelte.js +361 -251
  161. package/dist/services/navigation.d.ts +19 -0
  162. package/dist/services/navigation.js +84 -0
  163. package/dist/services/popup-position.d.ts +90 -0
  164. package/dist/services/popup-position.js +275 -0
  165. package/dist/services/popup.svelte.d.ts +16 -17
  166. package/dist/services/popup.svelte.js +25 -169
  167. package/dist/services/scroll.d.ts +1 -0
  168. package/dist/services/scroll.js +17 -0
  169. package/dist/services/select.svelte.d.ts +2 -2
  170. package/dist/services/select.svelte.js +1 -1
  171. package/dist/services/text.d.ts +2 -0
  172. package/dist/services/text.js +28 -0
  173. package/dist/services/tree.svelte.d.ts +3 -11
  174. package/dist/services/tree.svelte.js +35 -77
  175. package/dist/services/type-ahead.d.ts +0 -4
  176. package/dist/services/type-ahead.js +3 -1
  177. package/dist/services/widget.d.ts +8 -0
  178. package/dist/services/widget.js +39 -0
  179. package/dist/shiki-engine.js +15 -15
  180. package/dist/styles/mixins.scss +126 -0
  181. package/package.json +38 -34
@@ -24,7 +24,7 @@
24
24
  import EmojiSuggestions from '../emoji/emoji-suggestions.svelte';
25
25
 
26
26
  /**
27
- * @import { EmojiAnchorRect, EmojiEntry, EmojiTrigger, TextEditorStore } from '../../typedefs';
27
+ * @import { EmojiAnchorRect, EmojiEntry, EmojiTrigger, TextEditorStore } from '../../typedefs.js';
28
28
  */
29
29
 
30
30
  /**
@@ -6,7 +6,7 @@
6
6
 
7
7
  /**
8
8
  * @import { Snippet } from 'svelte';
9
- * @import { TextEditorStore } from '../../typedefs';
9
+ * @import { TextEditorStore } from '../../typedefs.js';
10
10
  */
11
11
 
12
12
  /**
@@ -1,33 +1,4 @@
1
- /**
2
- * @import {
3
- * TextEditorBlockType,
4
- * TextEditorComponent,
5
- * TextEditorFormatType,
6
- * } from '../../typedefs';
7
- */
8
- /**
9
- * Current value and selection of the `<textarea>`.
10
- * @typedef {object} RawTextState
11
- * @property {string} value Value.
12
- * @property {number} start Selection start.
13
- * @property {number} end Selection end.
14
- */
15
- /**
16
- * Edit to apply to the `<textarea>`: replace the text between `start` and `end` with `text`, then
17
- * select the text between `selectionStart` and `selectionEnd`, which are offsets in the new value.
18
- * @typedef {object} RawTextEdit
19
- * @property {number} start Start of the range to replace.
20
- * @property {number} end End of the range to replace.
21
- * @property {string} text Replacement text.
22
- * @property {number} selectionStart New selection start.
23
- * @property {number} selectionEnd New selection end.
24
- */
25
- /**
26
- * Markdown markers for the inline formats.
27
- * @type {Record<TextEditorFormatType, string>}
28
- */
29
- export const INLINE_MARKERS: Record<TextEditorFormatType, string>;
30
- export function getBlockType({ value, start }: RawTextState): TextEditorBlockType;
1
+ export function getBlockType(state: RawTextState): TextEditorBlockType;
31
2
  export function toggleInlineFormat({ value, start, end }: RawTextState, type: TextEditorFormatType): RawTextEdit;
32
3
  export function setBlockType(state: RawTextState, type: TextEditorBlockType): RawTextEdit;
33
4
  export function insertLink({ value, start, end }: RawTextState, { url, text }: {
@@ -41,6 +12,8 @@ export function getRawTextState({ value, selectionStart, selectionEnd }: HTMLTex
41
12
  export function focusTextArea(textArea: HTMLTextAreaElement): Promise<boolean>;
42
13
  export function isRawTextEditable({ readOnly, disabled }: HTMLTextAreaElement): boolean;
43
14
  export function applyRawTextEdit(textArea: HTMLTextAreaElement, { start, end, text, selectionStart, selectionEnd }: RawTextEdit): Promise<void>;
15
+ export function editRawText(textArea: HTMLTextAreaElement | undefined, getEdit: (state: RawTextState) => RawTextEdit): Promise<void>;
16
+ export function registerRawTextShortcut(textArea: HTMLTextAreaElement, key: string, handler: () => void): () => void;
44
17
  export function insertComponent(textArea: HTMLTextAreaElement, component: TextEditorComponent): Promise<void>;
45
18
  /**
46
19
  * Current value and selection of the `<textarea>`.
@@ -85,6 +58,6 @@ export type RawTextEdit = {
85
58
  */
86
59
  selectionEnd: number;
87
60
  };
88
- import type { TextEditorFormatType } from '../../typedefs';
89
- import type { TextEditorBlockType } from '../../typedefs';
90
- import type { TextEditorComponent } from '../../typedefs';
61
+ import type { TextEditorBlockType } from '../../typedefs.js';
62
+ import type { TextEditorFormatType } from '../../typedefs.js';
63
+ import type { TextEditorComponent } from '../../typedefs.js';
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Markdown editing helpers for the plain text mode of the text editor, where the toolbar buttons
3
3
  * edit the Markdown source in the `<textarea>` instead of Lexical nodes. Apart from
4
- * {@link getRawTextState}, {@link applyRawTextEdit} and {@link insertComponent}, everything here is
5
- * pure: a function takes the current value and selection, and returns the edit to apply.
4
+ * {@link getRawTextState}, {@link applyRawTextEdit}, {@link editRawText},
5
+ * {@link registerRawTextShortcut} and {@link insertComponent}, everything here is pure: a function
6
+ * takes the current value and selection, and returns the edit to apply.
6
7
  */
7
8
 
9
+ import { isMac, matchesShortcuts } from '@sveltia/utils/events';
8
10
  import { getComponentMarkdown } from './core.js';
9
11
 
10
12
  /**
@@ -12,7 +14,7 @@ import { getComponentMarkdown } from './core.js';
12
14
  * TextEditorBlockType,
13
15
  * TextEditorComponent,
14
16
  * TextEditorFormatType,
15
- * } from '../../typedefs';
17
+ * } from '../../typedefs.js';
16
18
  */
17
19
 
18
20
  /**
@@ -38,7 +40,7 @@ import { getComponentMarkdown } from './core.js';
38
40
  * Markdown markers for the inline formats.
39
41
  * @type {Record<TextEditorFormatType, string>}
40
42
  */
41
- export const INLINE_MARKERS = {
43
+ const INLINE_MARKERS = {
42
44
  bold: '**',
43
45
  italic: '_',
44
46
  strikethrough: '~~',
@@ -66,6 +68,13 @@ const getLineRange = ({ value, start, end }) => {
66
68
  return { lineStart, lineEnd: lineEndIndex === -1 ? value.length : lineEndIndex };
67
69
  };
68
70
 
71
+ /**
72
+ * Get the index of the line where the selection starts.
73
+ * @param {RawTextState} state Current state.
74
+ * @returns {number} Line index.
75
+ */
76
+ const getStartLineIndex = ({ value, start }) => value.slice(0, start).split('\n').length - 1;
77
+
69
78
  /**
70
79
  * Find the fenced code block the given line is in.
71
80
  * @param {string[]} lines All the lines.
@@ -104,9 +113,9 @@ const findCodeBlock = (lines, lineIndex) => {
104
113
  * @param {RawTextState} state Current state.
105
114
  * @returns {TextEditorBlockType} Block type.
106
115
  */
107
- export const getBlockType = ({ value, start }) => {
108
- const lines = value.split('\n');
109
- const lineIndex = value.slice(0, start).split('\n').length - 1;
116
+ export const getBlockType = (state) => {
117
+ const lines = state.value.split('\n');
118
+ const lineIndex = getStartLineIndex(state);
110
119
  const line = lines[lineIndex];
111
120
 
112
121
  if (findCodeBlock(lines, lineIndex)) {
@@ -205,7 +214,7 @@ export const toggleInlineFormat = ({ value, start, end }, type) => {
205
214
  export const setBlockType = (state, type) => {
206
215
  const { value, start, end } = state;
207
216
  const lines = value.split('\n');
208
- const firstIndex = value.slice(0, start).split('\n').length - 1;
217
+ const firstIndex = getStartLineIndex(state);
209
218
  const codeBlock = findCodeBlock(lines, firstIndex);
210
219
 
211
220
  // Leave a code block by removing its fences, then change the type of the selected lines
@@ -476,6 +485,52 @@ export const applyRawTextEdit = async (
476
485
  textArea.setSelectionRange(selectionStart, selectionEnd);
477
486
  };
478
487
 
488
+ /**
489
+ * Apply the edit made by the given function from the current state of the `<textarea>`, and focus
490
+ * it. Nothing happens if there’s no `<textarea>`.
491
+ * @param {HTMLTextAreaElement | undefined} textArea `<textarea>` element.
492
+ * @param {(state: RawTextState) => RawTextEdit} getEdit Function to make the edit from the current
493
+ * state.
494
+ * @returns {Promise<void>} Nothing.
495
+ */
496
+ export const editRawText = async (textArea, getEdit) => {
497
+ if (!textArea) {
498
+ return;
499
+ }
500
+
501
+ await applyRawTextEdit(textArea, getEdit(getRawTextState(textArea)));
502
+ };
503
+
504
+ /**
505
+ * Handle the given keyboard shortcut, Accel+{key}, on the `<textarea>` while it’s editable. The
506
+ * rich text editor handles its shortcuts on its own.
507
+ * @param {HTMLTextAreaElement} textArea `<textarea>` element.
508
+ * @param {string} key Key to combine with the Accel key, like `B`.
509
+ * @param {() => void} handler Function to call when the shortcut is pressed.
510
+ * @returns {() => void} Function to stop handling the shortcut.
511
+ */
512
+ export const registerRawTextShortcut = (textArea, key, handler) => {
513
+ /**
514
+ * Handle the `keydown` event.
515
+ * @param {KeyboardEvent} event `keydown` event.
516
+ */
517
+ const onKeyDown = (event) => {
518
+ if (
519
+ isRawTextEditable(textArea) &&
520
+ matchesShortcuts(event, isMac() ? `Meta+${key}` : `Ctrl+${key}`)
521
+ ) {
522
+ event.preventDefault();
523
+ handler();
524
+ }
525
+ };
526
+
527
+ textArea.addEventListener('keydown', onKeyDown);
528
+
529
+ return () => {
530
+ textArea.removeEventListener('keydown', onKeyDown);
531
+ };
532
+ };
533
+
479
534
  /**
480
535
  * Insert the Markdown of a new instance of the given editor component into the `<textarea>`. A
481
536
  * component matched by a single-line pattern is inserted inline at the caret; anything else is
@@ -18,4 +18,4 @@ import type { LexicalEditor } from 'lexical';
18
18
  import type { NodeKey } from 'lexical';
19
19
  import type { CodeNode } from '@lexical/code-core';
20
20
  import type { LexicalNode } from 'lexical';
21
- import type { HighlightedToken } from '../../../typedefs';
21
+ import type { HighlightedToken } from '../../../typedefs.js';
@@ -29,7 +29,7 @@ import { getCodeTheme } from './theme.js';
29
29
  /**
30
30
  * @import { CodeNode } from '@lexical/code-core';
31
31
  * @import { LexicalEditor, LexicalNode, NodeKey } from 'lexical';
32
- * @import { HighlightedToken } from '../../../typedefs';
32
+ * @import { HighlightedToken } from '../../../typedefs.js';
33
33
  */
34
34
 
35
35
  /**
@@ -242,15 +242,48 @@ const resolvePayload = async (kind, id, load) => {
242
242
  return payload;
243
243
  };
244
244
 
245
+ /**
246
+ * Load a grammar or theme into the highlighter once, then refresh the given code node. The
247
+ * configured loader and the highlighter share the method name for each kind.
248
+ * @param {'lang' | 'theme'} kind Payload kind.
249
+ * @param {string} id Language or theme ID.
250
+ * @param {LexicalEditor} [editor] Editor instance to refresh once the payload is loaded.
251
+ * @param {NodeKey} [codeNodeKey] Key of the code node to refresh.
252
+ * @returns {Promise<void>} Promise resolving when the payload is loaded.
253
+ */
254
+ const loadAsset = (kind, id, editor, codeNodeKey) => {
255
+ const method = kind === 'lang' ? 'loadLanguage' : 'loadTheme';
256
+
257
+ return loadOnce(
258
+ `${kind}/${id}`,
259
+ async () => {
260
+ const payload = await resolvePayload(kind, id, () => getCodeHighlighterLoaders()[method](id));
261
+
262
+ if (payload) {
263
+ await highlighter[method](payload);
264
+ }
265
+ },
266
+ editor,
267
+ codeNodeKey,
268
+ );
269
+ };
270
+
271
+ /**
272
+ * Find the metadata of the given language.
273
+ * @param {string} language Language identifier or alias, like `ts`.
274
+ * @returns {(typeof LANGUAGES)[number] | undefined} Language metadata, or `undefined` when the
275
+ * language is unknown.
276
+ */
277
+ const findLanguage = (language) =>
278
+ LANGUAGES.find(({ id, aliases }) => id === language || aliases?.includes(language));
279
+
245
280
  /**
246
281
  * Resolve a language alias to its canonical identifier.
247
282
  * @param {string} language Language identifier or alias, like `ts`.
248
283
  * @returns {string} Canonical identifier, like `typescript`. Returns the input unchanged when the
249
284
  * language is unknown.
250
285
  */
251
- export const normalizeCodeLanguage = (language) =>
252
- LANGUAGES.find(({ id, aliases }) => id === language || aliases?.includes(language))?.id ??
253
- language;
286
+ export const normalizeCodeLanguage = (language) => findLanguage(language)?.id ?? language;
254
287
 
255
288
  /**
256
289
  * Load the grammar for the given language, then refresh the given code node.
@@ -267,26 +300,13 @@ export const loadCodeLanguage = (language, editor, codeNodeKey) => {
267
300
  return undefined;
268
301
  }
269
302
 
270
- const info = LANGUAGES.find(({ id: langId, aliases }) => langId === id || aliases?.includes(id));
303
+ const info = findLanguage(id);
271
304
 
272
305
  if (!info) {
273
306
  return undefined;
274
307
  }
275
308
 
276
- return loadOnce(
277
- `lang/${info.id}`,
278
- async () => {
279
- const payload = await resolvePayload('lang', info.id, () =>
280
- getCodeHighlighterLoaders().loadLanguage(info.id),
281
- );
282
-
283
- if (payload) {
284
- await highlighter.loadLanguage(payload);
285
- }
286
- },
287
- editor,
288
- codeNodeKey,
289
- );
309
+ return loadAsset('lang', info.id, editor, codeNodeKey);
290
310
  };
291
311
 
292
312
  /**
@@ -319,20 +339,7 @@ export const loadCodeTheme = (theme, editor, codeNodeKey) => {
319
339
  return undefined;
320
340
  }
321
341
 
322
- return loadOnce(
323
- `theme/${theme}`,
324
- async () => {
325
- const payload = await resolvePayload('theme', theme, () =>
326
- getCodeHighlighterLoaders().loadTheme(theme),
327
- );
328
-
329
- if (payload) {
330
- await highlighter.loadTheme(payload);
331
- }
332
- },
333
- editor,
334
- codeNodeKey,
335
- );
342
+ return loadAsset('theme', theme, editor, codeNodeKey);
336
343
  };
337
344
 
338
345
  /**
@@ -419,6 +426,24 @@ export const getHighlightNodes = (codeNode, language) => {
419
426
  return mapTokensToLexicalStructure(tokens, !!diffedLanguage);
420
427
  };
421
428
 
429
+ /**
430
+ * Get the theme to highlight a snippet of code with, if it can be highlighted synchronously: the
431
+ * engine, the grammar of the given language and the theme are all loaded.
432
+ * @param {string} id Canonical language identifier.
433
+ * @param {string} [theme] Shiki theme ID. Defaults to the one matching the app’s appearance.
434
+ * @returns {string | undefined} Theme ID, or `undefined` when the code cannot be highlighted yet.
435
+ */
436
+ const getHighlightTheme = (id, theme) => {
437
+ if (!highlighter || isPlainLanguage(id) || !isCodeLanguageLoaded(id)) {
438
+ return undefined;
439
+ }
440
+
441
+ // Resolved only once the cheap checks pass, so bailing out early costs nothing
442
+ const resolvedTheme = theme ?? getCodeTheme();
443
+
444
+ return isCodeThemeLoaded(resolvedTheme) ? resolvedTheme : undefined;
445
+ };
446
+
422
447
  /**
423
448
  * Highlight a snippet of code as an HTML string.
424
449
  *
@@ -436,16 +461,11 @@ export const getHighlightNodes = (codeNode, language) => {
436
461
  * yet.
437
462
  */
438
463
  export const highlightCodeToHTML = (code, language, { theme } = {}) => {
464
+ // Unlike tokens, the HTML is highlighted with the language a `diff-*` language wraps
439
465
  const id = normalizeCodeLanguage(getDiffedLanguage(language) ?? language);
466
+ const resolvedTheme = getHighlightTheme(id, theme);
440
467
 
441
- if (!highlighter || isPlainLanguage(id) || !isCodeLanguageLoaded(id)) {
442
- return undefined;
443
- }
444
-
445
- // Resolved only once the cheap checks pass, so bailing out early costs nothing
446
- const resolvedTheme = theme ?? getCodeTheme();
447
-
448
- if (!isCodeThemeLoaded(resolvedTheme)) {
468
+ if (resolvedTheme === undefined) {
449
469
  return undefined;
450
470
  }
451
471
 
@@ -471,14 +491,9 @@ export const highlightCodeToHTML = (code, language, { theme } = {}) => {
471
491
  */
472
492
  export const highlightCodeToTokens = (code, language, { theme } = {}) => {
473
493
  const id = normalizeCodeLanguage(language);
494
+ const resolvedTheme = getHighlightTheme(id, theme);
474
495
 
475
- if (!highlighter || isPlainLanguage(id) || !isCodeLanguageLoaded(id)) {
476
- return undefined;
477
- }
478
-
479
- const resolvedTheme = theme ?? getCodeTheme();
480
-
481
- if (!isCodeThemeLoaded(resolvedTheme)) {
496
+ if (resolvedTheme === undefined) {
482
497
  return undefined;
483
498
  }
484
499
 
@@ -3,7 +3,7 @@
3
3
  * version bundled into `dist/shiki-engine.js`, because grammar and theme payloads are only
4
4
  * guaranteed to be compatible with the matching engine.
5
5
  */
6
- export const SHIKI_VERSION: "4.4.3";
6
+ export const SHIKI_VERSION: "4.5.0";
7
7
  /**
8
8
  * Available syntax highlighting languages, sorted by display name.
9
9
  * @type {{ id: string, name: string, aliases?: string[] }[]}
@@ -5,7 +5,7 @@
5
5
  * version bundled into `dist/shiki-engine.js`, because grammar and theme payloads are only
6
6
  * guaranteed to be compatible with the matching engine.
7
7
  */
8
- export const SHIKI_VERSION = "4.4.3";
8
+ export const SHIKI_VERSION = "4.5.0";
9
9
 
10
10
  /**
11
11
  * Available syntax highlighting languages, sorted by display name.
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @import { CodeNode as CodeNodeType } from '@lexical/code-core';
3
+ * @import { LexicalEditor, LexicalNode, NodeKey } from 'lexical';
4
+ * @import { CodeTokenizer, CodeTransformState } from '../../../typedefs.js';
5
+ */
1
6
  /**
2
7
  * Default tokenizer backed by Shiki.
3
8
  *
@@ -7,5 +12,5 @@
7
12
  */
8
13
  export const shikiTokenizer: CodeTokenizer;
9
14
  export function registerCodeHighlighting(editor: LexicalEditor, tokenizer?: CodeTokenizer): () => void;
10
- import type { CodeTokenizer } from '../../../typedefs';
15
+ import type { CodeTokenizer } from '../../../typedefs.js';
11
16
  import type { LexicalEditor } from 'lexical';
@@ -44,15 +44,14 @@ import {
44
44
  loadCodeTheme,
45
45
  loadEngine,
46
46
  } from './facade.js';
47
+ import { CODE_THEME_LIGHT } from './theme.js';
47
48
 
48
49
  /**
49
50
  * @import { CodeNode as CodeNodeType } from '@lexical/code-core';
50
51
  * @import { LexicalEditor, LexicalNode, NodeKey } from 'lexical';
51
- * @import { CodeTokenizer, CodeTransformState } from '../../../typedefs';
52
+ * @import { CodeTokenizer, CodeTransformState } from '../../../typedefs.js';
52
53
  */
53
54
 
54
- const DEFAULT_CODE_THEME = 'github-light';
55
-
56
55
  /**
57
56
  * Default tokenizer backed by Shiki.
58
57
  *
@@ -62,7 +61,7 @@ const DEFAULT_CODE_THEME = 'github-light';
62
61
  */
63
62
  export const shikiTokenizer = {
64
63
  defaultLanguage: DEFAULT_CODE_LANGUAGE,
65
- defaultTheme: DEFAULT_CODE_THEME,
64
+ defaultTheme: CODE_THEME_LIGHT,
66
65
  /**
67
66
  * Convert a code node’s content into Lexical nodes.
68
67
  * @param {CodeNodeType} codeNode Code node to tokenize.
@@ -3,4 +3,4 @@ export function getLanguageURL(id: string): string;
3
3
  export function getThemeURL(id: string): string;
4
4
  export function setCodeHighlighterLoaders(overrides: Partial<CodeHighlighterLoaders>): void;
5
5
  export function getCodeHighlighterLoaders(): CodeHighlighterLoaders;
6
- import type { CodeHighlighterLoaders } from '../../../typedefs';
6
+ import type { CodeHighlighterLoaders } from '../../../typedefs.js';
@@ -2,7 +2,7 @@ import { SHIKI_VERSION } from './generated.js';
2
2
  import { UI_VERSION } from './version.js';
3
3
 
4
4
  /**
5
- * @import { CodeHighlighterLoaders } from '../../../typedefs';
5
+ * @import { CodeHighlighterLoaders } from '../../../typedefs.js';
6
6
  */
7
7
 
8
8
  const SHIKI_CDN_BASE_URL = 'https://unpkg.com/@shikijs';
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
3
3
  */
4
- export const UI_VERSION: "0.78.3";
4
+ export const UI_VERSION: "0.79.0";
@@ -3,4 +3,4 @@
3
3
  /**
4
4
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
5
5
  */
6
- export const UI_VERSION = '0.78.3';
6
+ export const UI_VERSION = '0.79.0';
@@ -1,2 +1,2 @@
1
1
  export function createEditorStore(): TextEditorStore;
2
- import type { TextEditorStore } from '../../typedefs';
2
+ import type { TextEditorStore } from '../../typedefs.js';
@@ -4,7 +4,7 @@ import { convertMarkdownToLexical } from './core.js';
4
4
  /**
5
5
  * @import { LexicalEditor } from 'lexical';
6
6
  * @import { Transformer } from '@lexical/markdown';
7
- * @import { TextEditorConfig, TextEditorSelectionState, TextEditorStore } from '../../typedefs';
7
+ * @import { TextEditorConfig, TextEditorSelectionState, TextEditorStore } from '../../typedefs.js';
8
8
  */
9
9
 
10
10
  /**
@@ -86,7 +86,10 @@ export const createEditorStore = () => {
86
86
  enabledTransformers,
87
87
  );
88
88
 
89
- lastImport = { source: originalValue, exported };
89
+ // Skip an import superseded by a newer one, which sets its own value
90
+ if (exported !== undefined) {
91
+ lastImport = { source: originalValue, exported };
92
+ }
90
93
  } catch (ex) {
91
94
  // Go through the setter, so the editor actually falls back to the plain text mode
92
95
  setConverterError(true);
@@ -20,8 +20,8 @@
20
20
 
21
21
  /**
22
22
  * @import { Snippet } from 'svelte';
23
- * @import { TextEditorComponent, TextEditorMode, TextEditorNodeType } from '../../typedefs';
24
- * @import { HighlightedToken } from '../../typedefs';
23
+ * @import { TextEditorComponent, TextEditorMode, TextEditorNodeType } from '../../typedefs.js';
24
+ * @import { HighlightedToken } from '../../typedefs.js';
25
25
  */
26
26
 
27
27
  /**
@@ -186,7 +186,7 @@ type Props = {
186
186
  */
187
187
  children?: Snippet<[]> | undefined;
188
188
  };
189
- import type { TextEditorMode } from '../../typedefs';
190
- import type { TextEditorNodeType } from '../../typedefs';
191
- import type { TextEditorComponent } from '../../typedefs';
189
+ import type { TextEditorMode } from '../../typedefs.js';
190
+ import type { TextEditorNodeType } from '../../typedefs.js';
191
+ import type { TextEditorComponent } from '../../typedefs.js';
192
192
  import type { Snippet } from 'svelte';
@@ -9,7 +9,8 @@
9
9
  import { LANGUAGES } from '../shiki/generated.js';
10
10
 
11
11
  /**
12
- * @import { TextEditorStore } from '../../../typedefs';
12
+ * @import { CodeNode } from '@lexical/code-core';
13
+ * @import { TextEditorStore } from '../../../typedefs.js';
13
14
  */
14
15
 
15
16
  /**
@@ -46,15 +47,31 @@
46
47
  )?.key ?? 'plain',
47
48
  );
48
49
 
50
+ /**
51
+ * Get the code block the switcher targets. A code editor has exactly one code block, and its
52
+ * `blockNodeKey` is still unset the first time the switcher is used, before the editor has ever
53
+ * been focused. Call this within `editor.read()` or `editor.update()`.
54
+ * @returns {CodeNode | null} Code node, or `null` if the selection is not in a code block.
55
+ */
56
+ const getCodeNode = () => {
57
+ const { blockNodeKey } = editorStore.selection;
58
+
59
+ const node = editorStore.config.isCodeEditor
60
+ ? getRoot().getChildren()[0]
61
+ : blockNodeKey
62
+ ? getNodeByKey(blockNodeKey)
63
+ : null;
64
+
65
+ return isCodeNode(node) ? node : null;
66
+ };
67
+
49
68
  $effect(() => {
50
69
  void editorStore.selection.blockNodeKey;
51
70
 
52
71
  editorStore.editor?.read(() => {
53
- const node = editorStore.config.isCodeEditor
54
- ? getRoot().getChildren()[0]
55
- : getNodeByKey(/** @type {string} */ (editorStore.selection.blockNodeKey));
72
+ const node = getCodeNode();
56
73
 
57
- if (isCodeNode(node)) {
74
+ if (node) {
58
75
  // The highlighter gives a block without a language the default one as it’s transformed,
59
76
  // so the fallbacks here are only for the unexpected
60
77
  /* v8 ignore next */
@@ -77,19 +94,10 @@
77
94
  await loadCodeHighlighter(lang);
78
95
 
79
96
  editorStore.editor.update(() => {
80
- // Resolve the target the same way the effect above does. A code editor has exactly one code
81
- // block, and its `blockNodeKey` is still unset the first time the switcher is used, before
82
- // the editor has ever been focused.
83
97
  // https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx#L713
84
- const { blockNodeKey } = editorStore.selection;
85
-
86
- const node = editorStore.config.isCodeEditor
87
- ? getRoot().getChildren()[0]
88
- : blockNodeKey
89
- ? getNodeByKey(blockNodeKey)
90
- : null;
98
+ const node = getCodeNode();
91
99
 
92
- if (isCodeNode(node)) {
100
+ if (node) {
93
101
  node.setLanguage(lang);
94
102
  selectedLanguage = lang;
95
103
  }
@@ -1,21 +1,15 @@
1
1
  <script>
2
2
  import { _ } from '@sveltia/i18n';
3
- import { isMac, matchesShortcuts } from '@sveltia/utils/events';
4
3
  import { FORMAT_TEXT_COMMAND } from 'lexical';
5
4
  import { getContext } from 'svelte';
6
5
  import Button from '../../button/button.svelte';
7
6
  import Icon from '../../icon/icon.svelte';
8
7
  import { AVAILABLE_BUTTONS } from '../constants.js';
9
8
  import { focusEditor } from '../core.js';
10
- import {
11
- applyRawTextEdit,
12
- getRawTextState,
13
- isRawTextEditable,
14
- toggleInlineFormat,
15
- } from '../raw-markdown.js';
9
+ import { editRawText, registerRawTextShortcut, toggleInlineFormat } from '../raw-markdown.js';
16
10
 
17
11
  /**
18
- * @import { TextEditorFormatType, TextEditorStore } from '../../../typedefs';
12
+ * @import { TextEditorFormatType, TextEditorStore } from '../../../typedefs.js';
19
13
  */
20
14
 
21
15
  /**
@@ -50,10 +44,7 @@
50
44
 
51
45
  if (!useRichText) {
52
46
  // The button is only enabled while the `<textarea>` is there
53
- /* v8 ignore else */
54
- if (textArea) {
55
- applyRawTextEdit(textArea, toggleInlineFormat(getRawTextState(textArea), type));
56
- }
47
+ editRawText(textArea, (state) => toggleInlineFormat(state, type));
57
48
 
58
49
  return;
59
50
  }
@@ -74,25 +65,10 @@
74
65
  return undefined;
75
66
  }
76
67
 
77
- /**
78
- * Handle the keyboard shortcut in the plain text mode.
79
- * @param {KeyboardEvent} event `keydown` event.
80
- */
81
- const onKeyDown = (event) => {
82
- if (
83
- isRawTextEditable(textArea) &&
84
- matchesShortcuts(event, isMac() ? `Meta+${key}` : `Ctrl+${key}`)
85
- ) {
86
- event.preventDefault();
87
- format();
88
- }
89
- };
90
-
91
- textArea.addEventListener('keydown', onKeyDown);
92
-
93
- return () => {
94
- textArea.removeEventListener('keydown', onKeyDown);
95
- };
68
+ // Handle the keyboard shortcut in the plain text mode
69
+ return registerRawTextShortcut(textArea, key, () => {
70
+ format();
71
+ });
96
72
  });
97
73
  </script>
98
74
 
@@ -15,4 +15,4 @@ type Props = {
15
15
  */
16
16
  type: TextEditorFormatType;
17
17
  };
18
- import type { TextEditorFormatType } from '../../../typedefs';
18
+ import type { TextEditorFormatType } from '../../../typedefs.js';
@@ -0,0 +1,3 @@
1
+ export function insertEditorComponent(editorStore: TextEditorStore, component: TextEditorComponent): void;
2
+ import type { TextEditorStore } from '../../../typedefs.js';
3
+ import type { TextEditorComponent } from '../../../typedefs.js';