@signal9/era-ui 3.10.0 → 3.12.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 (52) hide show
  1. package/dist/apps/index.d.ts +19 -0
  2. package/dist/apps/index.js +19 -0
  3. package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +11 -0
  4. package/dist/apps/notes/editor/bubble-menu.svelte.js +106 -0
  5. package/dist/apps/notes/editor/bubble-toolbar.svelte +42 -0
  6. package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +22 -0
  7. package/dist/apps/notes/editor/extensions.d.ts +18 -0
  8. package/dist/apps/notes/editor/extensions.js +88 -0
  9. package/dist/apps/notes/editor/floating.d.ts +21 -0
  10. package/dist/apps/notes/editor/floating.js +44 -0
  11. package/dist/apps/notes/editor/link.d.ts +9 -0
  12. package/dist/apps/notes/editor/link.js +22 -0
  13. package/dist/apps/notes/editor/list-cleanup-rule.d.ts +14 -0
  14. package/dist/apps/notes/editor/list-cleanup-rule.js +47 -0
  15. package/dist/apps/notes/editor/slash-command.svelte.d.ts +22 -0
  16. package/dist/apps/notes/editor/slash-command.svelte.js +215 -0
  17. package/dist/apps/notes/editor/slash-menu.svelte +63 -0
  18. package/dist/apps/notes/editor/slash-menu.svelte.d.ts +23 -0
  19. package/dist/apps/notes/index.d.ts +11 -0
  20. package/dist/apps/notes/index.js +13 -0
  21. package/dist/apps/notes/markdown.d.ts +12 -0
  22. package/dist/apps/notes/markdown.js +165 -0
  23. package/dist/apps/notes/note-editor.svelte +456 -0
  24. package/dist/apps/notes/note-editor.svelte.d.ts +35 -0
  25. package/dist/apps/notes/notes-store.svelte.d.ts +64 -0
  26. package/dist/apps/notes/notes-store.svelte.js +234 -0
  27. package/dist/apps/notes/notes.svelte +377 -0
  28. package/dist/apps/notes/notes.svelte.d.ts +22 -0
  29. package/dist/apps/notes/tree.d.ts +70 -0
  30. package/dist/apps/notes/tree.js +185 -0
  31. package/dist/apps/notes/types.d.ts +49 -0
  32. package/dist/apps/notes/types.js +11 -0
  33. package/dist/docs/notes.md +82 -0
  34. package/dist/era-ui.css +1 -1
  35. package/dist/generated-docs/llms-full.txt +91 -0
  36. package/dist/generated-docs/llms.txt +1 -1
  37. package/dist/generated-docs/manifest.json +15 -0
  38. package/dist/generated-docs/notes.md +86 -0
  39. package/dist/os/window.svelte +6 -1
  40. package/dist/os/wm.svelte.d.ts +8 -1
  41. package/dist/os/wm.svelte.js +2 -1
  42. package/dist/ui/badge/badge.svelte.d.ts +17 -17
  43. package/dist/ui/bar/bar.svelte.d.ts +11 -11
  44. package/dist/ui/button/variants.d.ts +44 -44
  45. package/dist/ui/card/card.svelte.d.ts +20 -20
  46. package/dist/ui/chip/chip.svelte.d.ts +14 -14
  47. package/dist/ui/pane/pane-root.svelte.d.ts +1 -1
  48. package/dist/ui/pane/pane.svelte.d.ts +1 -1
  49. package/dist/ui/sheet/sheet-content.svelte.d.ts +14 -14
  50. package/dist/ui/skeleton/skeleton.svelte.d.ts +11 -11
  51. package/dist/utils/index.js +11 -0
  52. package/package.json +31 -5
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The apps layer — whole, opinionated applications built out of era primitives.
3
+ *
4
+ * `ui/` ships unopinionated pieces and `ai/` ships chat/agent composites; an app
5
+ * here is the finished thing: its own data model, its own persistence, its own
6
+ * keyboard map. You drop it into a window and it works. That also means an app
7
+ * makes product decisions a primitive never would, which is exactly why these
8
+ * live behind their own subpath instead of the root barrel.
9
+ *
10
+ * Apps are NOT re-exported from `@sig-nine/era-ui`. Each carries heavy,
11
+ * app-specific dependencies (Notes brings TipTap), and the same reasoning that
12
+ * isolates `ai/workflow` on its own subpath applies here: a consumer who never
13
+ * imports an app never pays for it. Import from `@sig-nine/era-ui/apps` (or a
14
+ * single app, e.g. `@sig-nine/era-ui/apps/notes`) and install that app's peers.
15
+ *
16
+ * Exports are flat and prefixed by app (`Notes`, `NotesStore`, `NoteEditor`) so
17
+ * a second app can land without a namespace churn.
18
+ */
19
+ export * from './notes/index.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The apps layer — whole, opinionated applications built out of era primitives.
3
+ *
4
+ * `ui/` ships unopinionated pieces and `ai/` ships chat/agent composites; an app
5
+ * here is the finished thing: its own data model, its own persistence, its own
6
+ * keyboard map. You drop it into a window and it works. That also means an app
7
+ * makes product decisions a primitive never would, which is exactly why these
8
+ * live behind their own subpath instead of the root barrel.
9
+ *
10
+ * Apps are NOT re-exported from `@sig-nine/era-ui`. Each carries heavy,
11
+ * app-specific dependencies (Notes brings TipTap), and the same reasoning that
12
+ * isolates `ai/workflow` on its own subpath applies here: a consumer who never
13
+ * imports an app never pays for it. Import from `@sig-nine/era-ui/apps` (or a
14
+ * single app, e.g. `@sig-nine/era-ui/apps/notes`) and install that app's peers.
15
+ *
16
+ * Exports are flat and prefixed by app (`Notes`, `NotesStore`, `NoteEditor`) so
17
+ * a second app can land without a namespace churn.
18
+ */
19
+ export * from './notes/index.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The selection ("bubble") toolbar.
3
+ *
4
+ * A plain ProseMirror plugin view rather than `@tiptap/extension-bubble-menu`:
5
+ * the official extension brings its own positioning engine and its own DOM, and
6
+ * both would have to be re-skinned anyway. This mounts an era `Bar` of `Button`s
7
+ * with `mount()` and drives it from `update()` — the same approach the slash
8
+ * menu takes, so both overlays share one placement helper and one set of tokens.
9
+ */
10
+ import { Extension } from '@tiptap/core';
11
+ export declare const BubbleMenu: Extension<any, any>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The selection ("bubble") toolbar.
3
+ *
4
+ * A plain ProseMirror plugin view rather than `@tiptap/extension-bubble-menu`:
5
+ * the official extension brings its own positioning engine and its own DOM, and
6
+ * both would have to be re-skinned anyway. This mounts an era `Bar` of `Button`s
7
+ * with `mount()` and drives it from `update()` — the same approach the slash
8
+ * menu takes, so both overlays share one placement helper and one set of tokens.
9
+ */
10
+ import { Extension } from '@tiptap/core';
11
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
12
+ import { mount, unmount } from 'svelte';
13
+ import Bold from '@lucide/svelte/icons/bold';
14
+ import Italic from '@lucide/svelte/icons/italic';
15
+ import Strikethrough from '@lucide/svelte/icons/strikethrough';
16
+ import Code from '@lucide/svelte/icons/code';
17
+ import Highlighter from '@lucide/svelte/icons/highlighter';
18
+ import LinkIcon from '@lucide/svelte/icons/link';
19
+ import BubbleToolbar from './bubble-toolbar.svelte';
20
+ import { createOverlayHost, placeOverSelection } from './floating.js';
21
+ import { toggleLink } from './link.js';
22
+ const buttons = [
23
+ {
24
+ mark: 'bold',
25
+ label: 'Bold',
26
+ icon: Bold,
27
+ command: (ed) => ed.chain().focus().toggleBold().run()
28
+ },
29
+ {
30
+ mark: 'italic',
31
+ label: 'Italic',
32
+ icon: Italic,
33
+ command: (ed) => ed.chain().focus().toggleItalic().run()
34
+ },
35
+ {
36
+ mark: 'strike',
37
+ label: 'Strikethrough',
38
+ icon: Strikethrough,
39
+ command: (ed) => ed.chain().focus().toggleStrike().run()
40
+ },
41
+ {
42
+ mark: 'code',
43
+ label: 'Code',
44
+ icon: Code,
45
+ command: (ed) => ed.chain().focus().toggleCode().run()
46
+ },
47
+ {
48
+ mark: 'highlight',
49
+ label: 'Highlight',
50
+ icon: Highlighter,
51
+ command: (ed) => ed.chain().focus().toggleHighlight().run()
52
+ },
53
+ { mark: 'link', label: 'Link', icon: LinkIcon, command: toggleLink }
54
+ ];
55
+ const items = buttons.map(({ mark, label, icon }) => ({ mark, label, icon }));
56
+ class BubbleMenuView {
57
+ #host;
58
+ #toolbar;
59
+ #editor;
60
+ #view = $state({
61
+ items,
62
+ active: {},
63
+ onCommand: (_mark) => { }
64
+ });
65
+ constructor(editor) {
66
+ this.#editor = editor;
67
+ this.#view.onCommand = (mark) => buttons.find((b) => b.mark === mark)?.command(editor);
68
+ this.#host = createOverlayHost();
69
+ this.#host.style.display = 'none';
70
+ document.body.append(this.#host);
71
+ this.#toolbar = mount(BubbleToolbar, { target: this.#host, props: this.#view });
72
+ }
73
+ update(view) {
74
+ const { state } = view;
75
+ const { selection } = state;
76
+ // Hide for a caret, an unfocused editor, a selection with no text (an image
77
+ // or table node), and inside a code block where marks do not apply.
78
+ if (selection.empty ||
79
+ !view.hasFocus() ||
80
+ !state.doc.textBetween(selection.from, selection.to).length ||
81
+ this.#editor.isActive('codeBlock')) {
82
+ this.#host.style.display = 'none';
83
+ return;
84
+ }
85
+ this.#view.active = Object.fromEntries(buttons.map((b) => [b.mark, this.#editor.isActive(b.mark)]));
86
+ // Measuring requires a laid-out box, so unhide before placing.
87
+ this.#host.style.display = 'block';
88
+ placeOverSelection(this.#host, view.coordsAtPos(selection.from), view.coordsAtPos(selection.to));
89
+ }
90
+ destroy() {
91
+ unmount(this.#toolbar);
92
+ this.#host.remove();
93
+ }
94
+ }
95
+ export const BubbleMenu = Extension.create({
96
+ name: 'notesBubbleMenu',
97
+ addProseMirrorPlugins() {
98
+ const editor = this.editor;
99
+ return [
100
+ new Plugin({
101
+ key: new PluginKey('notesBubbleMenu'),
102
+ view: () => new BubbleMenuView(editor)
103
+ })
104
+ ];
105
+ }
106
+ });
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The selection toolbar — an era `Bar` of latched icon `Button`s.
4
+ *
5
+ * `active` is Button's latched state (it emits `data-state="on"`), so a mark
6
+ * that is on reads as a held-down key on every surface, including bevel where
7
+ * the whole affordance is depth rather than tone.
8
+ */
9
+ import type { Component } from 'svelte';
10
+ import type { IconProps } from '@lucide/svelte';
11
+ import { Bar } from '../../../ui/bar/index.js';
12
+ import { Button } from '../../../ui/button/index.js';
13
+
14
+ interface Props {
15
+ items: Array<{ mark: string; label: string; icon: Component<IconProps> }>;
16
+ /** Which marks are active at the current selection. */
17
+ active: Record<string, boolean>;
18
+ onCommand: (mark: string) => void;
19
+ }
20
+
21
+ let { items, active, onCommand }: Props = $props();
22
+ </script>
23
+
24
+ <Bar size="sm" class="w-max bg-elevated shadow-lg glass-blur">
25
+ {#each items as item (item.mark)}
26
+ {@const Icon = item.icon}
27
+ <Button
28
+ icon
29
+ size="icon"
30
+ active={active[item.mark] ?? false}
31
+ aria-label={item.label}
32
+ title={item.label}
33
+ onmousedown={(e: MouseEvent) => {
34
+ // Taking focus would collapse the very selection the command formats.
35
+ e.preventDefault();
36
+ onCommand(item.mark);
37
+ }}
38
+ >
39
+ <Icon />
40
+ </Button>
41
+ {/each}
42
+ </Bar>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The selection toolbar — an era `Bar` of latched icon `Button`s.
3
+ *
4
+ * `active` is Button's latched state (it emits `data-state="on"`), so a mark
5
+ * that is on reads as a held-down key on every surface, including bevel where
6
+ * the whole affordance is depth rather than tone.
7
+ */
8
+ import type { Component } from 'svelte';
9
+ import type { IconProps } from '@lucide/svelte';
10
+ interface Props {
11
+ items: Array<{
12
+ mark: string;
13
+ label: string;
14
+ icon: Component<IconProps>;
15
+ }>;
16
+ /** Which marks are active at the current selection. */
17
+ active: Record<string, boolean>;
18
+ onCommand: (mark: string) => void;
19
+ }
20
+ declare const BubbleToolbar: Component<Props, {}, "">;
21
+ type BubbleToolbar = ReturnType<typeof BubbleToolbar>;
22
+ export default BubbleToolbar;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The Notes editor's extension set.
3
+ *
4
+ * Ported from the term/web notes app with two deliberate changes:
5
+ *
6
+ * - The slash menu and bubble toolbar are era components (see the sibling
7
+ * `.svelte.ts` files) instead of hand-built DOM, so the editor chrome tracks
8
+ * the density / surface / corners / motion / font axes like the rest of the
9
+ * library.
10
+ * - `@tiptap/extension-mathematics` is NOT included. It needs KaTeX's stylesheet
11
+ * and font files to render anything but raw markup, and that is a large,
12
+ * single-feature dependency to force on every consumer of this subpath. Add it
13
+ * in your own extension list (and import `katex/dist/katex.min.css`) if you
14
+ * want it — `noteExtensions` is a plain array, so spreading and extending it
15
+ * is the supported path.
16
+ */
17
+ import { type Extensions } from '@tiptap/core';
18
+ export declare const noteExtensions: Extensions;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The Notes editor's extension set.
3
+ *
4
+ * Ported from the term/web notes app with two deliberate changes:
5
+ *
6
+ * - The slash menu and bubble toolbar are era components (see the sibling
7
+ * `.svelte.ts` files) instead of hand-built DOM, so the editor chrome tracks
8
+ * the density / surface / corners / motion / font axes like the rest of the
9
+ * library.
10
+ * - `@tiptap/extension-mathematics` is NOT included. It needs KaTeX's stylesheet
11
+ * and font files to render anything but raw markup, and that is a large,
12
+ * single-feature dependency to force on every consumer of this subpath. Add it
13
+ * in your own extension list (and import `katex/dist/katex.min.css`) if you
14
+ * want it — `noteExtensions` is a plain array, so spreading and extending it
15
+ * is the supported path.
16
+ */
17
+ import { Extension } from '@tiptap/core';
18
+ import StarterKit from '@tiptap/starter-kit';
19
+ import { TaskList } from '@tiptap/extension-task-list';
20
+ import { TaskItem } from '@tiptap/extension-task-item';
21
+ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
22
+ import Placeholder from '@tiptap/extension-placeholder';
23
+ import Typography from '@tiptap/extension-typography';
24
+ import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table';
25
+ import { Highlight } from '@tiptap/extension-highlight';
26
+ import { Details, DetailsSummary, DetailsContent } from '@tiptap/extension-details';
27
+ import Image from '@tiptap/extension-image';
28
+ import { UniqueID } from '@tiptap/extension-unique-id';
29
+ import { Markdown } from 'tiptap-markdown';
30
+ import { common, createLowlight } from 'lowlight';
31
+ import { ListCleanupRule } from './list-cleanup-rule.js';
32
+ import { SlashCommand } from './slash-command.svelte.js';
33
+ import { BubbleMenu } from './bubble-menu.svelte.js';
34
+ import { toggleLink } from './link.js';
35
+ const LinkShortcut = Extension.create({
36
+ name: 'linkShortcut',
37
+ addKeyboardShortcuts() {
38
+ return {
39
+ 'Mod-k': ({ editor }) => {
40
+ toggleLink(editor);
41
+ return true;
42
+ }
43
+ };
44
+ }
45
+ });
46
+ const lowlight = createLowlight(common);
47
+ export const noteExtensions = [
48
+ StarterKit.configure({
49
+ heading: { levels: [1, 2, 3] },
50
+ // Replaced by the lowlight variant below.
51
+ codeBlock: false,
52
+ dropcursor: { color: 'var(--color-8)', width: 2 }
53
+ }),
54
+ CodeBlockLowlight.configure({ lowlight }),
55
+ TaskList.configure({
56
+ HTMLAttributes: { class: 'task-list' }
57
+ }),
58
+ TaskItem.configure({
59
+ nested: true,
60
+ HTMLAttributes: { class: 'task-item' },
61
+ a11y: {
62
+ checkboxLabel: (_node, checked) => (checked ? 'Task completed' : 'Task not completed')
63
+ }
64
+ }),
65
+ // Tables
66
+ Table.configure({ resizable: false, HTMLAttributes: { class: 'note-table' } }),
67
+ TableRow,
68
+ TableCell,
69
+ TableHeader,
70
+ // Inline formatting
71
+ Highlight.configure({ multicolor: false }),
72
+ // Block types
73
+ Details,
74
+ DetailsSummary,
75
+ DetailsContent,
76
+ Image.configure({ inline: true, allowBase64: true }),
77
+ // Utility
78
+ Placeholder.configure({ placeholder: 'Type / for commands…' }),
79
+ Typography,
80
+ // Stable ids on headings are what `tree.ts` addresses sections by, so
81
+ // section retrieval survives edits above the heading.
82
+ UniqueID.configure({ types: ['heading', 'paragraph', 'listItem', 'taskItem'] }),
83
+ Markdown.configure({ html: false }),
84
+ ListCleanupRule,
85
+ SlashCommand,
86
+ LinkShortcut,
87
+ BubbleMenu
88
+ ];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Placement for the two editor overlays (slash menu, bubble toolbar).
3
+ *
4
+ * They are mounted at the document root rather than inside the editor so a
5
+ * scrolling or `overflow: hidden` editor pane can never clip them — which means
6
+ * they are positioned in viewport coordinates and have to keep themselves on
7
+ * screen. ProseMirror hands out viewport rects already, so `position: fixed` is
8
+ * the natural fit and no scroll-offset bookkeeping is needed.
9
+ */
10
+ /** Create the fixed, root-level host element an overlay component mounts into. */
11
+ export declare function createOverlayHost(): HTMLDivElement;
12
+ /** Anchor below `rect`, flipping above when the bottom edge would run off. */
13
+ export declare function placeBelow(host: HTMLElement, rect: DOMRect, gap?: number): void;
14
+ /** Centre over a selection, flipping below when there is no room above. */
15
+ export declare function placeOverSelection(host: HTMLElement, start: {
16
+ top: number;
17
+ left: number;
18
+ bottom: number;
19
+ }, end: {
20
+ right: number;
21
+ }, gap?: number): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Placement for the two editor overlays (slash menu, bubble toolbar).
3
+ *
4
+ * They are mounted at the document root rather than inside the editor so a
5
+ * scrolling or `overflow: hidden` editor pane can never clip them — which means
6
+ * they are positioned in viewport coordinates and have to keep themselves on
7
+ * screen. ProseMirror hands out viewport rects already, so `position: fixed` is
8
+ * the natural fit and no scroll-offset bookkeeping is needed.
9
+ */
10
+ const EDGE = 4;
11
+ /** Create the fixed, root-level host element an overlay component mounts into. */
12
+ export function createOverlayHost() {
13
+ const host = document.createElement('div');
14
+ // z-50 is the bits-ui portal band — the same layer menus and dialogs use, so
15
+ // editor chrome stacks consistently with the rest of the library.
16
+ host.style.cssText = 'position:fixed;top:0;left:0;z-index:50;';
17
+ return host;
18
+ }
19
+ /** Anchor below `rect`, flipping above when the bottom edge would run off. */
20
+ export function placeBelow(host, rect, gap = EDGE) {
21
+ const box = host.getBoundingClientRect();
22
+ const left = clamp(rect.left, EDGE, window.innerWidth - box.width - EDGE);
23
+ let top = rect.bottom + gap;
24
+ if (top + box.height > window.innerHeight - EDGE) {
25
+ const above = rect.top - box.height - gap;
26
+ top = above >= EDGE ? above : window.innerHeight - box.height - EDGE;
27
+ }
28
+ host.style.left = `${Math.max(EDGE, left)}px`;
29
+ host.style.top = `${Math.max(EDGE, top)}px`;
30
+ }
31
+ /** Centre over a selection, flipping below when there is no room above. */
32
+ export function placeOverSelection(host, start, end, gap = 6) {
33
+ const box = host.getBoundingClientRect();
34
+ const centerX = (start.left + end.right) / 2;
35
+ const left = clamp(centerX - box.width / 2, EDGE, window.innerWidth - box.width - EDGE);
36
+ let top = start.top - box.height - gap;
37
+ if (top < EDGE)
38
+ top = start.bottom + gap;
39
+ host.style.left = `${Math.max(EDGE, left)}px`;
40
+ host.style.top = `${Math.max(EDGE, top)}px`;
41
+ }
42
+ function clamp(value, min, max) {
43
+ return Math.min(Math.max(value, min), Math.max(min, max));
44
+ }
@@ -0,0 +1,9 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ /**
3
+ * The one link behaviour, shared by the bubble toolbar and Mod-K.
4
+ *
5
+ * Three cases, in order: an existing link is removed; a selection that is
6
+ * already a bare URL links to itself (the common paste-then-select gesture);
7
+ * anything else asks for a target.
8
+ */
9
+ export declare function toggleLink(editor: Editor): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The one link behaviour, shared by the bubble toolbar and Mod-K.
3
+ *
4
+ * Three cases, in order: an existing link is removed; a selection that is
5
+ * already a bare URL links to itself (the common paste-then-select gesture);
6
+ * anything else asks for a target.
7
+ */
8
+ export function toggleLink(editor) {
9
+ if (editor.isActive('link')) {
10
+ editor.chain().focus().unsetLink().run();
11
+ return;
12
+ }
13
+ const { from, to } = editor.state.selection;
14
+ const selected = editor.state.doc.textBetween(from, to);
15
+ if (/^https?:\/\/\S+$/.test(selected)) {
16
+ editor.chain().focus().setLink({ href: selected }).run();
17
+ return;
18
+ }
19
+ const url = globalThis.prompt?.('Enter URL:');
20
+ if (url)
21
+ editor.chain().focus().setLink({ href: url }).run();
22
+ }
@@ -0,0 +1,14 @@
1
+ import { Extension } from '@tiptap/core';
2
+ /**
3
+ * Strips redundant list prefixes typed INSIDE a list item.
4
+ *
5
+ * TipTap turns a leading `1. ` into an ordered list. The user then presses
6
+ * Enter and — as humans do — types `2. `, which is now literal text inside an
7
+ * already-numbered item: "2. 2. Walk the dog". Same for `- ` inside a bullet
8
+ * list.
9
+ *
10
+ * These rules fire only when the cursor is genuinely inside a list of the
11
+ * matching kind, so the ordinary wrapping rules that CREATE lists are left
12
+ * alone.
13
+ */
14
+ export declare const ListCleanupRule: Extension<any, any>;
@@ -0,0 +1,47 @@
1
+ import { Extension, InputRule } from '@tiptap/core';
2
+ /**
3
+ * Strips redundant list prefixes typed INSIDE a list item.
4
+ *
5
+ * TipTap turns a leading `1. ` into an ordered list. The user then presses
6
+ * Enter and — as humans do — types `2. `, which is now literal text inside an
7
+ * already-numbered item: "2. 2. Walk the dog". Same for `- ` inside a bullet
8
+ * list.
9
+ *
10
+ * These rules fire only when the cursor is genuinely inside a list of the
11
+ * matching kind, so the ordinary wrapping rules that CREATE lists are left
12
+ * alone.
13
+ */
14
+ export const ListCleanupRule = Extension.create({
15
+ name: 'listCleanupRule',
16
+ addInputRules() {
17
+ return [
18
+ // "N. " typed while already in an ordered list
19
+ new InputRule({
20
+ find: /^(\d+)\.\s$/,
21
+ handler: ({ state, range }) => {
22
+ const { $from } = state.selection;
23
+ for (let depth = $from.depth; depth > 0; depth--) {
24
+ if ($from.node(depth).type.name === 'orderedList') {
25
+ state.apply(state.tr.delete(range.from, range.to));
26
+ return;
27
+ }
28
+ }
29
+ // Not in an ordered list — leave it to the wrapping rule.
30
+ }
31
+ }),
32
+ // "- " or "* " typed while already in a bullet list
33
+ new InputRule({
34
+ find: /^[-*]\s$/,
35
+ handler: ({ state, range }) => {
36
+ const { $from } = state.selection;
37
+ for (let depth = $from.depth; depth > 0; depth--) {
38
+ if ($from.node(depth).type.name === 'bulletList') {
39
+ state.apply(state.tr.delete(range.from, range.to));
40
+ return;
41
+ }
42
+ }
43
+ }
44
+ })
45
+ ];
46
+ }
47
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The `/` slash-command menu.
3
+ *
4
+ * TipTap's Suggestion plugin is imperative — it hands you a `render()` object
5
+ * with `onStart`/`onUpdate`/`onKeyDown`/`onExit` and expects you to produce DOM.
6
+ * Rather than hand-build that DOM (and hand-maintain a second set of colours and
7
+ * spacings that would drift from the library the moment a token moved), this
8
+ * mounts a real Svelte component with `mount()` and feeds it a `$state` props
9
+ * object. The panel is therefore styled with era utilities like everything else
10
+ * and follows every axis automatically.
11
+ */
12
+ import { Extension, type Editor, type Range } from '@tiptap/core';
13
+ import { type Component } from 'svelte';
14
+ import type { IconProps } from '@lucide/svelte';
15
+ export interface SlashCommandItem {
16
+ title: string;
17
+ description: string;
18
+ icon: Component<IconProps>;
19
+ command: (editor: Editor, range: Range) => void;
20
+ }
21
+ export declare const slashItems: SlashCommandItem[];
22
+ export declare const SlashCommand: Extension<any, any>;