@mythrantic/svelte-rich-text 1.1.0 → 1.2.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 (23) hide show
  1. package/README.md +2 -0
  2. package/dist/components/ValiantRichText/editor.css +38 -0
  3. package/dist/components/ValiantRichText/headless/components/AudioPlaceHolder.svelte +36 -1
  4. package/dist/components/ValiantRichText/headless/components/ImagePlaceholder.svelte +36 -1
  5. package/dist/components/ValiantRichText/headless/components/VideoPlaceholder.svelte +36 -1
  6. package/dist/components/ValiantRichText/headless/components/toolbar/FontSize.svelte +11 -30
  7. package/dist/components/ValiantRichText/headless/components/toolbar/QuickColors.svelte +21 -39
  8. package/dist/components/ValiantRichText/headless/components/toolbar/SearchAndReplace.svelte +3 -2
  9. package/dist/components/ValiantRichText/headless/components/toolbar/ToolbarDropdown.svelte +177 -0
  10. package/dist/components/ValiantRichText/headless/components/toolbar/ToolbarDropdown.svelte.d.ts +29 -0
  11. package/dist/components/ValiantRichText/headless/editor.svelte +61 -34
  12. package/dist/components/ValiantRichText/headless/editor.svelte.d.ts +7 -0
  13. package/dist/components/ValiantRichText/headless/style.css +183 -42
  14. package/dist/components/ValiantRichText/headless/toolbar.svelte +140 -9
  15. package/dist/components/ValiantRichText/themes/default-dark.css +70 -0
  16. package/dist/components/ValiantRichText/themes/default-light.css +68 -0
  17. package/dist/components/ValiantRichText/themes/inherit.css +67 -0
  18. package/dist/components/ValiantRichText/themes/modern-dark.css +69 -0
  19. package/dist/components/ValiantRichText/themes/modern-light.css +67 -0
  20. package/dist/components/ValiantRichText/themes/professional-dark.css +69 -0
  21. package/dist/components/ValiantRichText/themes/professional-light.css +67 -0
  22. package/dist/components/ValiantRichText/types.d.ts +3 -0
  23. package/package.json +3 -3
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Open in Coder](https://coder.valiantlynx.com/open-in-coder.svg)](https://coder.valiantlynx.com/templates/docker/workspace?param.git_repo=git@github.com:mythrantic/svelte-rich-text.git)
2
+
1
3
  # Valiant Rich Text Svelte Component
2
4
 
3
5
  ![valiantlynx logo](./static/valiantlynx.jpg)
@@ -1,5 +1,43 @@
1
1
  /* Base TipTap Editor Styles with Light/Dark Theme Support */
2
2
 
3
+ /* Default fallback theme */
4
+ :root {
5
+ --color-background: #ffffff;
6
+ --color-foreground: #0f172a;
7
+ --color-muted: #f1f5f9;
8
+ --color-muted-foreground: #64748b;
9
+ --color-primary: #ea580c;
10
+ --color-primary-light: #f97316;
11
+ --color-primary-dark: #c2410c;
12
+ --color-secondary: #f1f5f9;
13
+ --color-secondary-foreground: #0f172a;
14
+ --color-accent: #ea580c;
15
+ --color-accent-light: #f97316;
16
+ --color-accent-dark: #c2410c;
17
+ --color-destructive: #dc2626;
18
+ --color-input: #e2e8f0;
19
+ --color-border: #e2e8f0;
20
+ --blockquote-color: #475569;
21
+ --blockquote-border: #ea580c;
22
+ --border-color: #e2e8f0;
23
+ --border-color-hover: #cbd5e1;
24
+ --code-bg: #f1f5f9;
25
+ --codeblock-bg: #1e293b;
26
+ --table-border: #e2e8f0;
27
+ --table-bg-selected: #f0f9ff;
28
+ --table-bg-hover: #f8fafc;
29
+ --highlight-color: #fef3c7;
30
+ --highlight-border: #fbbf24;
31
+ --search-result-bg: #fef08a;
32
+ --search-result-current-bg: #fbbf24;
33
+ --task-completed-color: #94a3b8;
34
+ }
35
+
36
+ .valiant-editor {
37
+ background-color: var(--color-background);
38
+ color: var(--color-foreground);
39
+ }
40
+
3
41
  .tiptap :first-child {
4
42
  margin-top: 0;
5
43
  }
@@ -6,12 +6,40 @@
6
6
  const { editor }: NodeViewProps = $props();
7
7
  import Audio from '@lucide/svelte/icons/audio-lines';
8
8
 
9
+ let fileInput = $state<HTMLInputElement | undefined>();
10
+
9
11
  function handleClick() {
10
- const audioUrl = prompt('Please enter the audio URL');
12
+ // Try to open file picker first
13
+ if (fileInput) {
14
+ fileInput.click();
15
+ } else {
16
+ // Fallback to URL input
17
+ promptForUrl();
18
+ }
19
+ }
20
+
21
+ function promptForUrl() {
22
+ const audioUrl = prompt('Enter audio URL or select a file');
11
23
  if (audioUrl) {
12
24
  editor.chain().focus().setAudio(audioUrl).run();
13
25
  }
14
26
  }
27
+
28
+ function handleFileSelect(e: Event) {
29
+ const input = e.target as HTMLInputElement;
30
+ const file = input.files?.[0];
31
+
32
+ if (file) {
33
+ const reader = new FileReader();
34
+ reader.onload = (event) => {
35
+ const src = event.target?.result as string;
36
+ if (src) {
37
+ editor.chain().focus().setAudio(src).run();
38
+ }
39
+ };
40
+ reader.readAsDataURL(file);
41
+ }
42
+ }
15
43
  </script>
16
44
 
17
45
  <NodeViewWrapper>
@@ -22,5 +50,12 @@
22
50
  title="Insert an audio"
23
51
  onClick={handleClick}
24
52
  />
53
+ <input
54
+ bind:this={fileInput}
55
+ type="file"
56
+ accept="audio/*"
57
+ onchange={handleFileSelect}
58
+ style="display: none;"
59
+ />
25
60
  {/if}
26
61
  </NodeViewWrapper>
@@ -6,12 +6,40 @@
6
6
  const { editor }: NodeViewProps = $props();
7
7
  import Image from '@lucide/svelte/icons/image';
8
8
 
9
+ let fileInput = $state<HTMLInputElement | undefined>();
10
+
9
11
  function handleClick() {
10
- const imageUrl = prompt('Please enter the image URL');
12
+ // Try to open file picker first
13
+ if (fileInput) {
14
+ fileInput.click();
15
+ } else {
16
+ // Fallback to URL input
17
+ promptForUrl();
18
+ }
19
+ }
20
+
21
+ function promptForUrl() {
22
+ const imageUrl = prompt('Enter image URL or select a file');
11
23
  if (imageUrl) {
12
24
  editor.chain().focus().setImage({ src: imageUrl }).run();
13
25
  }
14
26
  }
27
+
28
+ function handleFileSelect(e: Event) {
29
+ const input = e.target as HTMLInputElement;
30
+ const file = input.files?.[0];
31
+
32
+ if (file) {
33
+ const reader = new FileReader();
34
+ reader.onload = (event) => {
35
+ const src = event.target?.result as string;
36
+ if (src) {
37
+ editor.chain().focus().setImage({ src }).run();
38
+ }
39
+ };
40
+ reader.readAsDataURL(file);
41
+ }
42
+ }
15
43
  </script>
16
44
 
17
45
  <NodeViewWrapper>
@@ -22,5 +50,12 @@
22
50
  title="Insert an image"
23
51
  onClick={handleClick}
24
52
  />
53
+ <input
54
+ bind:this={fileInput}
55
+ type="file"
56
+ accept="image/*"
57
+ onchange={handleFileSelect}
58
+ style="display: none;"
59
+ />
25
60
  {/if}
26
61
  </NodeViewWrapper>
@@ -6,12 +6,40 @@
6
6
  const { editor }: NodeViewProps = $props();
7
7
  import Video from '@lucide/svelte/icons/video';
8
8
 
9
+ let fileInput = $state<HTMLInputElement | undefined>();
10
+
9
11
  function handleClick() {
10
- const videoUrl = prompt('Please enter the video URL');
12
+ // Try to open file picker first
13
+ if (fileInput) {
14
+ fileInput.click();
15
+ } else {
16
+ // Fallback to URL input
17
+ promptForUrl();
18
+ }
19
+ }
20
+
21
+ function promptForUrl() {
22
+ const videoUrl = prompt('Enter video URL or select a file');
11
23
  if (videoUrl) {
12
24
  editor.chain().focus().setVideo(videoUrl).run();
13
25
  }
14
26
  }
27
+
28
+ function handleFileSelect(e: Event) {
29
+ const input = e.target as HTMLInputElement;
30
+ const file = input.files?.[0];
31
+
32
+ if (file) {
33
+ const reader = new FileReader();
34
+ reader.onload = (event) => {
35
+ const src = event.target?.result as string;
36
+ if (src) {
37
+ editor.chain().focus().setVideo(src).run();
38
+ }
39
+ };
40
+ reader.readAsDataURL(file);
41
+ }
42
+ }
15
43
  </script>
16
44
 
17
45
  <NodeViewWrapper>
@@ -22,5 +50,12 @@
22
50
  title="Insert a video"
23
51
  onClick={handleClick}
24
52
  />
53
+ <input
54
+ bind:this={fileInput}
55
+ type="file"
56
+ accept="video/*"
57
+ onchange={handleFileSelect}
58
+ style="display: none;"
59
+ />
25
60
  {/if}
26
61
  </NodeViewWrapper>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Editor } from '@tiptap/core';
3
+ import ToolbarDropdown from './ToolbarDropdown.svelte';
3
4
 
4
5
  interface Props {
5
6
  editor: Editor;
@@ -17,36 +18,16 @@
17
18
  ];
18
19
 
19
20
  let currentSize = $derived.by(() => editor.getAttributes('textStyle').fontSize || '');
21
+
22
+ function handleFontSizeChange(value: string) {
23
+ editor.chain().focus().setFontSize(value).run();
24
+ }
20
25
  </script>
21
26
 
22
- <select
27
+ <ToolbarDropdown
28
+ options={FONT_SIZE}
23
29
  value={currentSize}
24
- onchange={(e) => {
25
- editor
26
- .chain()
27
- .focus()
28
- .setFontSize((e.target as HTMLSelectElement).value)
29
- .run();
30
- }}
31
- title="Font Size"
32
- >
33
- {#each FONT_SIZE as fontSize (fontSize)}
34
- <option value={fontSize.value} label={fontSize.label.split(' ')[0]}></option>
35
- {/each}
36
- </select>
37
-
38
- <style>
39
- select {
40
- display: inline-flex;
41
- align-items: center;
42
- justify-content: center;
43
- border: none;
44
- background-color: var(--edra-button-bg-color);
45
- border-radius: var(--edra-button-border-radius);
46
- cursor: pointer;
47
- transition: background-color 0.2s ease-in-out;
48
- padding: var(--edra-button-padding);
49
- min-width: fit;
50
- min-height: full;
51
- }
52
- </style>
30
+ label="Font Size"
31
+ placeholder="Size"
32
+ onchange={handleFontSizeChange}
33
+ />
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Editor } from '@tiptap/core';
3
+ import ToolbarDropdown from './ToolbarDropdown.svelte';
3
4
 
4
5
  interface Props {
5
6
  editor: Editor;
@@ -21,50 +22,31 @@
21
22
 
22
23
  const currentColor = $derived.by(() => editor.getAttributes('textStyle').color ?? '');
23
24
  const currentHighlight = $derived.by(() => editor.getAttributes('highlight').color ?? '');
25
+
26
+ function handleColorChange(color: string) {
27
+ editor.chain().focus().setColor(color).run();
28
+ }
29
+
30
+ function handleHighlightChange(color: string) {
31
+ editor.chain().focus().setHighlight({ color }).run();
32
+ }
24
33
  </script>
25
34
 
26
- <select
35
+ <ToolbarDropdown
36
+ options={colors}
27
37
  value={currentColor}
28
- onchange={(e) => {
29
- const color = (e.target as HTMLSelectElement).value;
30
- editor.chain().focus().setColor(color).run();
31
- }}
32
- style={`color: ${currentColor}`}
33
- title="Text Color"
34
- >
35
- <option value="" label="Default"></option>
36
- {#each colors as color (color)}
37
- <option value={color.value} label={color.label}></option>
38
- {/each}
39
- </select>
38
+ label="Text Color"
39
+ placeholder="Color"
40
+ onchange={handleColorChange}
41
+ />
40
42
 
41
- <select
43
+ <ToolbarDropdown
44
+ options={colors}
42
45
  value={currentHighlight}
43
- onchange={(e) => {
44
- const color = (e.target as HTMLSelectElement).value;
45
- editor.chain().focus().setHighlight({ color }).run();
46
- }}
47
- style={`background-color: ${currentHighlight}50`}
48
- title="Hightlight Color"
49
- >
50
- <option value="" label="Default"></option>
51
- {#each colors as color (color)}
52
- <option value={color.value} label={color.label}>A</option>
53
- {/each}
54
- </select>
46
+ label="Highlight Color"
47
+ placeholder="Highlight"
48
+ onchange={handleHighlightChange}
49
+ />
55
50
 
56
51
  <style>
57
- select {
58
- display: inline-flex;
59
- align-items: center;
60
- justify-content: center;
61
- border: none;
62
- background-color: var(--edra-button-bg-color);
63
- border-radius: var(--edra-button-border-radius);
64
- cursor: pointer;
65
- transition: background-color 0.2s ease-in-out;
66
- padding: var(--edra-button-padding);
67
- min-width: fit;
68
- min-height: var(--edra-button-size);
69
- }
70
52
  </style>
@@ -65,19 +65,20 @@
65
65
 
66
66
  <div class="edra-search-and-replace">
67
67
  <button
68
- class="edra-command-button"
68
+ class="edra-command-button edra-filter-button"
69
69
  onclick={() => {
70
70
  show = !show;
71
71
  clear();
72
72
  updateSearchTerm();
73
73
  }}
74
- title={show ? 'Go Back' : 'Search and Replace'}
74
+ title={show ? 'Go Back' : 'Filter and Search'}
75
75
  >
76
76
  {#if show}
77
77
  <ArrowLeft class="edra-toolbar-icon" />
78
78
  {:else}
79
79
  <Search class="edra-toolbar-icon" />
80
80
  {/if}
81
+ <span class="edra-filter-label">Filter</span>
81
82
  </button>
82
83
  {#if show}
83
84
  <div class="edra-search-and-replace-content">
@@ -0,0 +1,177 @@
1
+ <script lang="ts">
2
+ import ChevronDown from '@lucide/svelte/icons/chevron-down';
3
+
4
+ interface Option {
5
+ label: string;
6
+ value: string;
7
+ icon?: any;
8
+ }
9
+
10
+ interface Props {
11
+ options: Option[];
12
+ value?: string;
13
+ placeholder?: string;
14
+ label?: string;
15
+ onchange?: (value: string) => void;
16
+ icon?: any;
17
+ }
18
+
19
+ let { options, value, placeholder = 'Select...', label, onchange, icon: Icon }: Props = $props();
20
+
21
+ let isOpen = $state(false);
22
+ let container: HTMLDivElement;
23
+ let selectedOption = $derived(options.find((opt) => opt.value === value));
24
+
25
+ function handleSelect(val: string) {
26
+ onchange?.(val);
27
+ isOpen = false;
28
+ }
29
+
30
+ function handleClickOutside(e: MouseEvent) {
31
+ const target = e.target as HTMLElement;
32
+ if (container && !container.contains(target)) {
33
+ isOpen = false;
34
+ }
35
+ }
36
+
37
+ $effect(() => {
38
+ if (isOpen) {
39
+ document.addEventListener('mousedown', handleClickOutside);
40
+ return () => document.removeEventListener('mousedown', handleClickOutside);
41
+ }
42
+ });
43
+ </script>
44
+
45
+ <div class="toolbar-dropdown-container" bind:this={container}>
46
+ <button
47
+ class="toolbar-dropdown-trigger"
48
+ onclick={() => (isOpen = !isOpen)}
49
+ title={label}
50
+ aria-label={label}
51
+ aria-haspopup="listbox"
52
+ aria-expanded={isOpen}
53
+ >
54
+ {#if Icon}
55
+ <svelte:component this={Icon} size={16} />
56
+ {/if}
57
+ <span class="toolbar-dropdown-label">
58
+ {selectedOption?.label || placeholder}
59
+ </span>
60
+ <ChevronDown size={14} style={isOpen ? 'transform: rotate(180deg)' : ''} class="chevron" />
61
+ </button>
62
+
63
+ {#if isOpen}
64
+ <div class="toolbar-dropdown-content" role="listbox">
65
+ {#each options as option (option.value)}
66
+ <button
67
+ class="toolbar-dropdown-item"
68
+ class:active={option.value === value}
69
+ onclick={() => handleSelect(option.value)}
70
+ role="option"
71
+ aria-selected={option.value === value}
72
+ >
73
+ {#if option.icon}
74
+ <svelte:component this={option.icon} size={14} />
75
+ {/if}
76
+ <span>{option.label}</span>
77
+ </button>
78
+ {/each}
79
+ </div>
80
+ {/if}
81
+ </div>
82
+
83
+ <style>
84
+ .toolbar-dropdown-container {
85
+ position: relative;
86
+ display: inline-flex;
87
+ align-items: center;
88
+ }
89
+
90
+ .toolbar-dropdown-trigger {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ gap: 0.375rem;
95
+ border: 1px solid transparent;
96
+ background-color: transparent;
97
+ border-radius: 0.375rem;
98
+ cursor: pointer;
99
+ transition: all 0.15s ease-in-out;
100
+ padding: 0.375rem 0.5rem;
101
+ min-height: 2.5rem;
102
+ color: var(--edra-icon-color);
103
+ font-size: 0.875rem;
104
+ font-weight: 500;
105
+ white-space: nowrap;
106
+ flex-shrink: 0;
107
+ }
108
+
109
+ .toolbar-dropdown-trigger:hover {
110
+ background-color: var(--color-muted);
111
+ }
112
+
113
+ .toolbar-dropdown-trigger:active {
114
+ background-color: var(--color-muted);
115
+ }
116
+
117
+ .chevron {
118
+ transition: transform 0.2s ease-in-out;
119
+ }
120
+
121
+ .chevron.rotated {
122
+ transform: rotate(180deg);
123
+ }
124
+
125
+ .toolbar-dropdown-label {
126
+ max-width: 6rem;
127
+ overflow: hidden;
128
+ text-overflow: ellipsis;
129
+ }
130
+
131
+ .toolbar-dropdown-content {
132
+ position: absolute;
133
+ top: calc(100% + 0.25rem);
134
+ left: 0;
135
+ z-index: 50;
136
+ min-width: -moz-max-content;
137
+ min-width: max-content;
138
+ background-color: var(--color-background);
139
+ border: 1px solid var(--color-border);
140
+ border-radius: 0.5rem;
141
+ box-shadow:
142
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
143
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
144
+ padding: 0.375rem;
145
+ display: flex;
146
+ flex-direction: column;
147
+ max-height: 20rem;
148
+ overflow-y: auto;
149
+ }
150
+
151
+ .toolbar-dropdown-item {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 0.5rem;
155
+ padding: 0.5rem 0.75rem;
156
+ background-color: transparent;
157
+ border: none;
158
+ border-radius: 0.375rem;
159
+ cursor: pointer;
160
+ color: var(--color-foreground);
161
+ font-size: 0.875rem;
162
+ transition: all 0.15s ease-in-out;
163
+ text-align: left;
164
+ min-width: 6rem;
165
+ font-weight: 400;
166
+ }
167
+
168
+ .toolbar-dropdown-item:hover {
169
+ background-color: var(--color-muted);
170
+ }
171
+
172
+ .toolbar-dropdown-item.active {
173
+ background-color: var(--color-muted);
174
+ font-weight: 600;
175
+ color: var(--color-primary);
176
+ }
177
+ </style>
@@ -0,0 +1,29 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const ToolbarDropdown: $$__sveltets_2_IsomorphicComponent<{
15
+ options: {
16
+ label: string;
17
+ value: string;
18
+ icon?: any;
19
+ }[];
20
+ value?: string;
21
+ placeholder?: string;
22
+ label?: string;
23
+ onchange?: (value: string) => void;
24
+ icon?: any;
25
+ }, {
26
+ [evt: string]: CustomEvent<any>;
27
+ }, {}, {}, "">;
28
+ type ToolbarDropdown = InstanceType<typeof ToolbarDropdown>;
29
+ export default ToolbarDropdown;