@foxui/text 0.4.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/dist/components/advanced-text-area/AdvancedTextArea.svelte +58 -0
  4. package/dist/components/advanced-text-area/AdvancedTextArea.svelte.d.ts +12 -0
  5. package/dist/components/advanced-text-area/index.d.ts +1 -0
  6. package/dist/components/advanced-text-area/index.js +1 -0
  7. package/dist/components/index.d.ts +3 -0
  8. package/dist/components/index.js +3 -0
  9. package/dist/components/plain-text-editor/PlainTextEditor.svelte +133 -0
  10. package/dist/components/plain-text-editor/PlainTextEditor.svelte.d.ts +14 -0
  11. package/dist/components/plain-text-editor/index.d.ts +1 -0
  12. package/dist/components/plain-text-editor/index.js +1 -0
  13. package/dist/components/rich-text-editor/Icon.svelte +120 -0
  14. package/dist/components/rich-text-editor/Icon.svelte.d.ts +7 -0
  15. package/dist/components/rich-text-editor/RichTextEditor.svelte +427 -0
  16. package/dist/components/rich-text-editor/RichTextEditor.svelte.d.ts +18 -0
  17. package/dist/components/rich-text-editor/RichTextEditorLinkMenu.svelte +95 -0
  18. package/dist/components/rich-text-editor/RichTextEditorLinkMenu.svelte.d.ts +10 -0
  19. package/dist/components/rich-text-editor/RichTextEditorMenu.svelte +184 -0
  20. package/dist/components/rich-text-editor/RichTextEditorMenu.svelte.d.ts +19 -0
  21. package/dist/components/rich-text-editor/RichTextLink.d.ts +13 -0
  22. package/dist/components/rich-text-editor/RichTextLink.js +105 -0
  23. package/dist/components/rich-text-editor/Select.svelte +72 -0
  24. package/dist/components/rich-text-editor/Select.svelte.d.ts +13 -0
  25. package/dist/components/rich-text-editor/code.css +142 -0
  26. package/dist/components/rich-text-editor/image-upload/ImageUploadComponent.svelte +47 -0
  27. package/dist/components/rich-text-editor/image-upload/ImageUploadComponent.svelte.d.ts +4 -0
  28. package/dist/components/rich-text-editor/image-upload/ImageUploadNode.d.ts +45 -0
  29. package/dist/components/rich-text-editor/image-upload/ImageUploadNode.js +56 -0
  30. package/dist/components/rich-text-editor/index.d.ts +2 -0
  31. package/dist/components/rich-text-editor/index.js +1 -0
  32. package/dist/components/rich-text-editor/slash-menu/SuggestionSelect.svelte +88 -0
  33. package/dist/components/rich-text-editor/slash-menu/SuggestionSelect.svelte.d.ts +22 -0
  34. package/dist/components/rich-text-editor/slash-menu/index.d.ts +32 -0
  35. package/dist/components/rich-text-editor/slash-menu/index.js +168 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/types.d.ts +1 -0
  39. package/package.json +93 -0
@@ -0,0 +1,184 @@
1
+ <script lang="ts">
2
+ import { cn, Toggle, toggleVariants, Tooltip } from '@foxui/core';
3
+ import type { Editor } from '@tiptap/core';
4
+ import Select from './Select.svelte';
5
+ import type { RichTextTypes } from '.';
6
+
7
+ let {
8
+ editor,
9
+ isBold,
10
+ isImage,
11
+ isItalic,
12
+ isUnderline,
13
+ isStrikethrough,
14
+ isLink,
15
+ clickedLink,
16
+ selectedType = $bindable('paragraph'),
17
+ ref = $bindable(null),
18
+ processImageFile,
19
+ switchTo
20
+ }: {
21
+ editor: Editor | null;
22
+ isBold: boolean;
23
+ isImage: boolean;
24
+ isItalic: boolean;
25
+ isUnderline: boolean;
26
+ isStrikethrough: boolean;
27
+ isLink: boolean;
28
+ clickedLink: () => void;
29
+ selectedType: RichTextTypes;
30
+ ref: HTMLElement | null;
31
+ processImageFile: (file: File, input: HTMLInputElement) => void;
32
+ switchTo: (value: RichTextTypes) => void;
33
+ } = $props();
34
+
35
+ $inspect(isBold);
36
+
37
+ function handleFileProcess(event: Event) {
38
+ const input = event.target as HTMLInputElement;
39
+ if (!input.files?.length) return;
40
+ const file = input.files[0];
41
+ if (!file || !file.type.startsWith('image/')) return;
42
+ processImageFile(file, input);
43
+ }
44
+
45
+ let fileInput = $state<HTMLInputElement | null>(null);
46
+
47
+ </script>
48
+
49
+ <div
50
+ bind:this={ref}
51
+ class="bg-base-50 dark:bg-base-900 border-base-500/20 dark:border-base-700/20 relative hidden w-fit rounded-2xl border px-1 py-1 shadow-lg backdrop-blur-sm"
52
+ >
53
+ <Select
54
+ onValueChange={(value) => {
55
+ switchTo(value as RichTextTypes);
56
+ }}
57
+ type="single"
58
+ items={[
59
+ { value: 'paragraph', label: 'Text' },
60
+ { value: 'heading-1', label: 'Heading 1' },
61
+ { value: 'heading-2', label: 'Heading 2' },
62
+ { value: 'heading-3', label: 'Heading 3' },
63
+ { value: 'blockquote', label: 'Blockquote' },
64
+ { value: 'code', label: 'Code Block' },
65
+ { value: 'bullet-list', label: 'Bullet List' },
66
+ { value: 'ordered-list', label: 'Ordered List' }
67
+ ]}
68
+ bind:value={selectedType}
69
+ />
70
+ <!-- <Tooltip withContext text="Bold" delayDuration={0}>
71
+ {#snippet child({ props })} -->
72
+ <Toggle
73
+ size="sm"
74
+ onclick={() => editor?.chain().focus().toggleBold().run()}
75
+ bind:pressed={() => isBold, (bold) => {}}
76
+ >
77
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
78
+ <path
79
+ fill-rule="evenodd"
80
+ d="M5.246 3.744a.75.75 0 0 1 .75-.75h7.125a4.875 4.875 0 0 1 3.346 8.422 5.25 5.25 0 0 1-2.97 9.58h-7.5a.75.75 0 0 1-.75-.75V3.744Zm7.125 6.75a2.625 2.625 0 0 0 0-5.25H8.246v5.25h4.125Zm-4.125 2.251v6h4.5a3 3 0 0 0 0-6h-4.5Z"
81
+ clip-rule="evenodd"
82
+ />
83
+ </svg>
84
+
85
+ <span class="sr-only">Bold</span>
86
+ </Toggle>
87
+ <!-- {/snippet}
88
+ </Tooltip> -->
89
+ <Toggle
90
+ size="sm"
91
+ onclick={() => editor?.chain().focus().toggleItalic().run()}
92
+ bind:pressed={() => isItalic, (italic) => {}}
93
+ >
94
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
95
+ <path
96
+ fill-rule="evenodd"
97
+ d="M10.497 3.744a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-3.275l-5.357 15.002h2.632a.75.75 0 1 1 0 1.5h-7.5a.75.75 0 1 1 0-1.5h3.275l5.357-15.002h-2.632a.75.75 0 0 1-.75-.75Z"
98
+ clip-rule="evenodd"
99
+ />
100
+ </svg>
101
+
102
+ <span class="sr-only">Italic</span>
103
+ </Toggle>
104
+
105
+ <Toggle
106
+ size="sm"
107
+ onclick={() => editor?.chain().focus().toggleUnderline().run()}
108
+ bind:pressed={() => isUnderline, (underline) => {}}
109
+ >
110
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
111
+ <path
112
+ fill-rule="evenodd"
113
+ d="M5.995 2.994a.75.75 0 0 1 .75.75v7.5a5.25 5.25 0 1 0 10.5 0v-7.5a.75.75 0 0 1 1.5 0v7.5a6.75 6.75 0 1 1-13.5 0v-7.5a.75.75 0 0 1 .75-.75Zm-3 17.252a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5h-16.5a.75.75 0 0 1-.75-.75Z"
114
+ clip-rule="evenodd"
115
+ />
116
+ </svg>
117
+
118
+ <span class="sr-only">Underline</span>
119
+ </Toggle>
120
+
121
+ <Toggle
122
+ size="sm"
123
+ onclick={() => editor?.chain().focus().toggleStrike().run()}
124
+ bind:pressed={() => isStrikethrough, (strikethrough) => {}}
125
+ >
126
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
127
+ <path
128
+ fill-rule="evenodd"
129
+ d="M9.657 4.728c-1.086.385-1.766 1.057-1.979 1.85-.214.8.046 1.733.81 2.616.746.862 1.93 1.612 3.388 2.003.07.019.14.037.21.053h8.163a.75.75 0 0 1 0 1.5h-8.24a.66.66 0 0 1-.02 0H3.75a.75.75 0 0 1 0-1.5h4.78a7.108 7.108 0 0 1-1.175-1.074C6.372 9.042 5.849 7.61 6.229 6.19c.377-1.408 1.528-2.38 2.927-2.876 1.402-.497 3.127-.55 4.855-.086A8.937 8.937 0 0 1 16.94 4.6a.75.75 0 0 1-.881 1.215 7.437 7.437 0 0 0-2.436-1.14c-1.473-.394-2.885-.331-3.966.052Zm6.533 9.632a.75.75 0 0 1 1.03.25c.592.974.846 2.094.55 3.2-.378 1.408-1.529 2.38-2.927 2.876-1.402.497-3.127.55-4.855.087-1.712-.46-3.168-1.354-4.134-2.47a.75.75 0 0 1 1.134-.982c.746.862 1.93 1.612 3.388 2.003 1.473.394 2.884.331 3.966-.052 1.085-.384 1.766-1.056 1.978-1.85.169-.628.046-1.33-.381-2.032a.75.75 0 0 1 .25-1.03Z"
130
+ clip-rule="evenodd"
131
+ />
132
+ </svg>
133
+
134
+ <span class="sr-only">Strikethrough</span>
135
+ </Toggle>
136
+
137
+ <Toggle
138
+ size="sm"
139
+ onclick={() => {
140
+ clickedLink();
141
+ }}
142
+ bind:pressed={() => isLink, (link) => {}}
143
+ >
144
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
145
+ <path
146
+ fill-rule="evenodd"
147
+ d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z"
148
+ clip-rule="evenodd"
149
+ />
150
+ </svg>
151
+
152
+ <span class="sr-only">Link</span>
153
+ </Toggle>
154
+
155
+ <!-- <Toggle
156
+ size="sm"
157
+ onclick={() => {
158
+ fileInput?.click();
159
+ }}
160
+ bind:pressed={() => isImage, (image) => {}}
161
+ >
162
+ <svg
163
+ xmlns="http://www.w3.org/2000/svg"
164
+ viewBox="0 0 24 24"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ stroke-width="2"
168
+ stroke-linecap="round"
169
+ stroke-linejoin="round"
170
+ ><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path
171
+ d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"
172
+ /></svg
173
+ >
174
+ </Toggle> -->
175
+
176
+ <input
177
+ type="file"
178
+ accept="image/*"
179
+ class="hidden"
180
+ onchange={handleFileProcess}
181
+ tabindex="-1"
182
+ bind:this={fileInput}
183
+ />
184
+ </div>
@@ -0,0 +1,19 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ import type { RichTextTypes } from '.';
3
+ type $$ComponentProps = {
4
+ editor: Editor | null;
5
+ isBold: boolean;
6
+ isImage: boolean;
7
+ isItalic: boolean;
8
+ isUnderline: boolean;
9
+ isStrikethrough: boolean;
10
+ isLink: boolean;
11
+ clickedLink: () => void;
12
+ selectedType: RichTextTypes;
13
+ ref: HTMLElement | null;
14
+ processImageFile: (file: File, input: HTMLInputElement) => void;
15
+ switchTo: (value: RichTextTypes) => void;
16
+ };
17
+ declare const RichTextEditorMenu: import("svelte").Component<$$ComponentProps, {}, "ref" | "selectedType">;
18
+ type RichTextEditorMenu = ReturnType<typeof RichTextEditorMenu>;
19
+ export default RichTextEditorMenu;
@@ -0,0 +1,13 @@
1
+ import type { LinkOptions } from '@tiptap/extension-link';
2
+ /**
3
+ * The options available to customize the `RichTextLink` extension.
4
+ */
5
+ type RichTextLinkOptions = LinkOptions;
6
+ /**
7
+ * Custom extension that extends the built-in `Link` extension to add additional input/paste rules
8
+ * for converting the Markdown link syntax (i.e. `[Doist](https://doist.com)`) into links, and also
9
+ * adds support for the `title` attribute.
10
+ */
11
+ declare const RichTextLink: import("@tiptap/core").Mark<LinkOptions, any>;
12
+ export { RichTextLink };
13
+ export type { RichTextLinkOptions };
@@ -0,0 +1,105 @@
1
+ // from https://github.com/Doist/typist/blob/main/src/extensions/rich-text/rich-text-link.ts
2
+ import { InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core';
3
+ import { Link } from '@tiptap/extension-link';
4
+ /**
5
+ * The input regex for Markdown links with title support, and multiple quotation marks (required
6
+ * in case the `Typography` extension is being included).
7
+ */
8
+ const inputRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)$/i;
9
+ /**
10
+ * The paste regex for Markdown links with title support, and multiple quotation marks (required
11
+ * in case the `Typography` extension is being included).
12
+ */
13
+ const pasteRegex = /(?:^|\s)\[([^\]]*)?\]\((\S+)(?: ["“](.+)["”])?\)/gi;
14
+ /**
15
+ * Input rule built specifically for the `Link` extension, which ignores the auto-linked URL in
16
+ * parentheses (e.g., `(https://doist.dev)`).
17
+ *
18
+ * @see https://github.com/ueberdosis/tiptap/discussions/1865
19
+ */
20
+ function linkInputRule(config) {
21
+ const defaultMarkInputRule = markInputRule(config);
22
+ return new InputRule({
23
+ find: config.find,
24
+ handler(props) {
25
+ const { tr } = props.state;
26
+ defaultMarkInputRule.handler(props);
27
+ tr.setMeta('preventAutolink', true);
28
+ }
29
+ });
30
+ }
31
+ /**
32
+ * Paste rule built specifically for the `Link` extension, which ignores the auto-linked URL in
33
+ * parentheses (e.g., `(https://doist.dev)`). This extension was inspired from the multiple
34
+ * implementations found in a Tiptap discussion at GitHub.
35
+ *
36
+ * @see https://github.com/ueberdosis/tiptap/discussions/1865
37
+ */
38
+ function linkPasteRule(config) {
39
+ const defaultMarkPasteRule = markPasteRule(config);
40
+ return new PasteRule({
41
+ find: config.find,
42
+ handler(props) {
43
+ const { tr } = props.state;
44
+ defaultMarkPasteRule.handler(props);
45
+ tr.setMeta('preventAutolink', true);
46
+ }
47
+ });
48
+ }
49
+ /**
50
+ * Custom extension that extends the built-in `Link` extension to add additional input/paste rules
51
+ * for converting the Markdown link syntax (i.e. `[Doist](https://doist.com)`) into links, and also
52
+ * adds support for the `title` attribute.
53
+ */
54
+ const RichTextLink = Link.extend({
55
+ inclusive: false,
56
+ addOptions() {
57
+ return {
58
+ ...this.parent?.(),
59
+ openOnClick: 'whenNotEditable'
60
+ };
61
+ },
62
+ addAttributes() {
63
+ return {
64
+ ...this.parent?.(),
65
+ title: {
66
+ default: null
67
+ }
68
+ };
69
+ },
70
+ addInputRules() {
71
+ return [
72
+ linkInputRule({
73
+ find: inputRegex,
74
+ type: this.type,
75
+ // We need to use `pop()` to remove the last capture groups from the match to
76
+ // satisfy Tiptap's `markPasteRule` expectation of having the content as the last
77
+ // capture group in the match (this makes the attribute order important)
78
+ getAttributes(match) {
79
+ return {
80
+ title: match.pop()?.trim(),
81
+ href: match.pop()?.trim()
82
+ };
83
+ }
84
+ })
85
+ ];
86
+ },
87
+ addPasteRules() {
88
+ return [
89
+ linkPasteRule({
90
+ find: pasteRegex,
91
+ type: this.type,
92
+ // We need to use `pop()` to remove the last capture groups from the match to
93
+ // satisfy Tiptap's `markInputRule` expectation of having the content as the last
94
+ // capture group in the match (this makes the attribute order important)
95
+ getAttributes(match) {
96
+ return {
97
+ title: match.pop()?.trim(),
98
+ href: match.pop()?.trim()
99
+ };
100
+ }
101
+ })
102
+ ];
103
+ }
104
+ });
105
+ export { RichTextLink };
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import { Button, cn, toggleVariants } from '@foxui/core';
3
+ import { Select, type WithoutChildren } from 'bits-ui';
4
+ import Icon from './Icon.svelte';
5
+
6
+ type Props = WithoutChildren<Select.RootProps> & {
7
+ placeholder?: string;
8
+ items: { value: string; label: string; disabled?: boolean }[];
9
+ contentProps?: WithoutChildren<Select.ContentProps>;
10
+ };
11
+
12
+ let { value = $bindable(), items, contentProps, placeholder, ...restProps }: Props = $props();
13
+ </script>
14
+
15
+ <Select.Root bind:value={value as never} {...restProps}>
16
+ <Select.Trigger>
17
+ {#snippet child({ props })}
18
+ <button {...props} class={cn(toggleVariants({ size: 'sm' }), 'gap-1')}>
19
+ {#if value}
20
+ <Icon name={value as any} />
21
+ {:else}
22
+ <span class="size-3.5">?</span>
23
+ {/if}
24
+
25
+ <svg
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ fill="none"
28
+ viewBox="0 0 24 24"
29
+ stroke-width="1.5"
30
+ stroke="currentColor"
31
+ class="!size-2.5"
32
+ >
33
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
34
+ </svg>
35
+ </button>
36
+ {/snippet}
37
+ </Select.Trigger>
38
+ <Select.Portal>
39
+ <Select.Content
40
+ {...contentProps}
41
+ class={cn(
42
+ 'bg-base-50/50 border-base-500/20 overflow-hidden rounded-2xl border shadow-lg backdrop-blur-xl',
43
+ 'dark:bg-base-900/50 dark:border-base-500/10',
44
+ 'motion-safe:animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
45
+ contentProps?.class
46
+ )}
47
+ sideOffset={6}
48
+ >
49
+ <Select.ScrollUpButton>up</Select.ScrollUpButton>
50
+ <Select.Viewport class="divide-base-300/30 dark:divide-base-800 divide-y text-sm">
51
+ {#each items as { value, label, disabled } (value)}
52
+ <Select.Item {value} {label} {disabled}>
53
+ {#snippet children({ selected })}
54
+ <div
55
+ class={cn(
56
+ 'text-base-900 dark:text-base-200 group relative isolate flex min-w-28 cursor-pointer items-center gap-3 px-3 py-2 font-medium [&_svg]:size-3.5',
57
+ selected
58
+ ? 'text-accent-700 dark:text-accent-400 bg-accent-500/10'
59
+ : 'hover:bg-accent-500/10'
60
+ )}
61
+ >
62
+ <Icon name={value as any} />
63
+ {label}
64
+ </div>
65
+ {/snippet}
66
+ </Select.Item>
67
+ {/each}
68
+ </Select.Viewport>
69
+ <Select.ScrollDownButton>down</Select.ScrollDownButton>
70
+ </Select.Content>
71
+ </Select.Portal>
72
+ </Select.Root>
@@ -0,0 +1,13 @@
1
+ import { Select, type WithoutChildren } from 'bits-ui';
2
+ type Props = WithoutChildren<Select.RootProps> & {
3
+ placeholder?: string;
4
+ items: {
5
+ value: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ }[];
9
+ contentProps?: WithoutChildren<Select.ContentProps>;
10
+ };
11
+ declare const Select: import("svelte").Component<Props, {}, "value">;
12
+ type Select = ReturnType<typeof Select>;
13
+ export default Select;
@@ -0,0 +1,142 @@
1
+ /* Basic editor styles */
2
+ .tiptap {
3
+ :first-child {
4
+ margin-top: 0;
5
+ }
6
+
7
+ pre {
8
+ background: var(--color-base-200);
9
+ border-radius: 1rem;
10
+ color: var(--color-base-900);
11
+ font-family: 'JetBrainsMono', monospace;
12
+ border: 1px solid var(--color-base-300);
13
+ margin: 1.5rem 0;
14
+ padding: 0.75rem 1rem;
15
+
16
+ code {
17
+ background: none;
18
+ color: inherit;
19
+ font-size: 0.8rem;
20
+ padding: 0;
21
+ }
22
+
23
+ /* Code styling */
24
+ .hljs-comment,
25
+ .hljs-quote {
26
+ color: var(--color-base-500);
27
+ }
28
+
29
+ .hljs-variable,
30
+ .hljs-template-variable,
31
+ .hljs-attribute,
32
+ .hljs-tag,
33
+ .hljs-regexp,
34
+ .hljs-link,
35
+ .hljs-name,
36
+ .hljs-selector-id,
37
+ .hljs-selector-class {
38
+ color: var(--color-accent-600);
39
+ }
40
+
41
+ .hljs-number,
42
+ .hljs-meta,
43
+ .hljs-built_in,
44
+ .hljs-builtin-name,
45
+ .hljs-literal,
46
+ .hljs-type,
47
+ .hljs-params {
48
+ color: oklch(from var(--color-accent-600) l c calc(h + 30));
49
+ }
50
+
51
+ .hljs-string,
52
+ .hljs-symbol,
53
+ .hljs-bullet {
54
+ color: oklch(from var(--color-accent-600) l c calc(h + 60));
55
+ }
56
+
57
+ .hljs-title,
58
+ .hljs-section {
59
+ color: oklch(from var(--color-accent-700) l c calc(h + 15));
60
+ }
61
+
62
+ .hljs-keyword,
63
+ .hljs-selector-tag {
64
+ color: oklch(from var(--color-accent-700) l c calc(h + 45));
65
+ }
66
+
67
+ .hljs-emphasis {
68
+ font-style: italic;
69
+ }
70
+
71
+ .hljs-strong {
72
+ font-weight: 700;
73
+ }
74
+ }
75
+ }
76
+
77
+ .dark .tiptap {
78
+ pre {
79
+ background: var(--color-base-900);
80
+ color: var(--color-base-200);
81
+ border: 1px solid var(--color-base-800);
82
+
83
+ code {
84
+ background: none;
85
+ color: inherit;
86
+ font-size: 0.8rem;
87
+ padding: 0;
88
+ }
89
+
90
+ /* Code styling */
91
+ .hljs-comment,
92
+ .hljs-quote {
93
+ color: var(--color-base-400);
94
+ }
95
+
96
+ .hljs-variable,
97
+ .hljs-template-variable,
98
+ .hljs-attribute,
99
+ .hljs-tag,
100
+ .hljs-regexp,
101
+ .hljs-link,
102
+ .hljs-name,
103
+ .hljs-selector-id,
104
+ .hljs-selector-class {
105
+ color: var(--color-accent-400);
106
+ }
107
+
108
+ .hljs-number,
109
+ .hljs-meta,
110
+ .hljs-built_in,
111
+ .hljs-builtin-name,
112
+ .hljs-literal,
113
+ .hljs-type,
114
+ .hljs-params {
115
+ color: oklch(from var(--color-accent-400) l c calc(h + 30));
116
+ }
117
+
118
+ .hljs-string,
119
+ .hljs-symbol,
120
+ .hljs-bullet {
121
+ color: oklch(from var(--color-accent-400) l c calc(h + 60));
122
+ }
123
+
124
+ .hljs-title,
125
+ .hljs-section {
126
+ color: oklch(from var(--color-accent-300) l c calc(h + 15));
127
+ }
128
+
129
+ .hljs-keyword,
130
+ .hljs-selector-tag {
131
+ color: oklch(from var(--color-accent-300) l c calc(h + 45));
132
+ }
133
+
134
+ .hljs-emphasis {
135
+ font-style: italic;
136
+ }
137
+
138
+ .hljs-strong {
139
+ font-weight: 700;
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { NodeViewProps } from '@tiptap/core';
3
+ import { onMount } from 'svelte';
4
+ import { NodeViewWrapper } from 'svelte-tiptap';
5
+
6
+ let props: NodeViewProps = $props();
7
+
8
+ let progress = $state(0);
9
+
10
+ const handleUpload = async (files?: File[]) => {
11
+ console.log(props.node, props.node.attrs);
12
+
13
+ const url = await props.extension.options.upload(files, (event) => {
14
+ console.log('progress', event);
15
+ progress = event.progress;
16
+ });
17
+
18
+ if (url) {
19
+ const pos = props.getPos();
20
+ const filename = files?.[0]?.name.replace(/\.[^/.]+$/, '') || 'unknown';
21
+
22
+ props.deleteNode();
23
+ props.editor
24
+ .chain()
25
+ .focus()
26
+ .insertContentAt(pos, [
27
+ {
28
+ type: 'image',
29
+ attrs: { src: props.node.attrs.preview, alt: filename, title: filename }
30
+ }
31
+ ])
32
+ .run();
33
+ }
34
+ };
35
+
36
+ onMount(() => {
37
+ console.log('node', props);
38
+ handleUpload();
39
+ });
40
+ </script>
41
+
42
+ <NodeViewWrapper class="relative">
43
+ <img src={props.node.attrs.preview} alt="Upload preview" class="" />
44
+ <div class="absolute left-0 right-0 bottom-0 h-1 bg-accent-500/30">
45
+ <div class="h-full bg-accent-500 transition-all duration-300" style="width: {progress * 100}%"></div>
46
+ </div>
47
+ </NodeViewWrapper>
@@ -0,0 +1,4 @@
1
+ import type { NodeViewProps } from '@tiptap/core';
2
+ declare const ImageUploadComponent: import("svelte").Component<NodeViewProps, {}, "">;
3
+ type ImageUploadComponent = ReturnType<typeof ImageUploadComponent>;
4
+ export default ImageUploadComponent;
@@ -0,0 +1,45 @@
1
+ import { Node } from '@tiptap/core';
2
+ export type UploadFunction = (file: File, onProgress?: (event: {
3
+ progress: number;
4
+ }) => void, abortSignal?: AbortSignal) => Promise<string>;
5
+ export interface ImageUploadNodeOptions {
6
+ /**
7
+ * Acceptable file types for upload.
8
+ * @default 'image/*'
9
+ */
10
+ accept?: string;
11
+ /**
12
+ * Maximum number of files that can be uploaded.
13
+ * @default 1
14
+ */
15
+ limit?: number;
16
+ /**
17
+ * Maximum file size in bytes (0 for unlimited).
18
+ * @default 0
19
+ */
20
+ maxSize?: number;
21
+ /**
22
+ * Preview image URL.
23
+ */
24
+ preview?: string;
25
+ /**
26
+ * Function to handle the upload process.
27
+ */
28
+ upload?: UploadFunction;
29
+ /**
30
+ * Callback for upload errors.
31
+ */
32
+ onError?: (error: Error) => void;
33
+ /**
34
+ * Callback for successful uploads.
35
+ */
36
+ onSuccess?: (url: string) => void;
37
+ }
38
+ declare module '@tiptap/core' {
39
+ interface Commands<ReturnType> {
40
+ imageUpload: {
41
+ setImageUploadNode: (options?: ImageUploadNodeOptions) => ReturnType;
42
+ };
43
+ }
44
+ }
45
+ export declare const ImageUploadNode: Node<ImageUploadNodeOptions, any>;