@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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/components/advanced-text-area/AdvancedTextArea.svelte +58 -0
- package/dist/components/advanced-text-area/AdvancedTextArea.svelte.d.ts +12 -0
- package/dist/components/advanced-text-area/index.d.ts +1 -0
- package/dist/components/advanced-text-area/index.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/plain-text-editor/PlainTextEditor.svelte +133 -0
- package/dist/components/plain-text-editor/PlainTextEditor.svelte.d.ts +14 -0
- package/dist/components/plain-text-editor/index.d.ts +1 -0
- package/dist/components/plain-text-editor/index.js +1 -0
- package/dist/components/rich-text-editor/Icon.svelte +120 -0
- package/dist/components/rich-text-editor/Icon.svelte.d.ts +7 -0
- package/dist/components/rich-text-editor/RichTextEditor.svelte +427 -0
- package/dist/components/rich-text-editor/RichTextEditor.svelte.d.ts +18 -0
- package/dist/components/rich-text-editor/RichTextEditorLinkMenu.svelte +95 -0
- package/dist/components/rich-text-editor/RichTextEditorLinkMenu.svelte.d.ts +10 -0
- package/dist/components/rich-text-editor/RichTextEditorMenu.svelte +184 -0
- package/dist/components/rich-text-editor/RichTextEditorMenu.svelte.d.ts +19 -0
- package/dist/components/rich-text-editor/RichTextLink.d.ts +13 -0
- package/dist/components/rich-text-editor/RichTextLink.js +105 -0
- package/dist/components/rich-text-editor/Select.svelte +72 -0
- package/dist/components/rich-text-editor/Select.svelte.d.ts +13 -0
- package/dist/components/rich-text-editor/code.css +142 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadComponent.svelte +47 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadComponent.svelte.d.ts +4 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadNode.d.ts +45 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadNode.js +56 -0
- package/dist/components/rich-text-editor/index.d.ts +2 -0
- package/dist/components/rich-text-editor/index.js +1 -0
- package/dist/components/rich-text-editor/slash-menu/SuggestionSelect.svelte +88 -0
- package/dist/components/rich-text-editor/slash-menu/SuggestionSelect.svelte.d.ts +22 -0
- package/dist/components/rich-text-editor/slash-menu/index.d.ts +32 -0
- package/dist/components/rich-text-editor/slash-menu/index.js +168 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +1 -0
- 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,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>;
|