@placeholderco/placeholder-ui 2.2.5 → 2.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/dist/app.css CHANGED
@@ -172,3 +172,91 @@ a {
172
172
  a:hover {
173
173
  color: var(--ui-link-hover-color);
174
174
  }
175
+
176
+ /* ---- Buttons in rich text ----
177
+ The TipTap toolbar's Button turns a link into a call-to-action:
178
+ `<a class="editor-button editor-button-accent editor-button-lg" href="...">`.
179
+ The classes live here rather than in the editor so the saved HTML looks the
180
+ same wherever it is rendered. The look follows the Button component. */
181
+ .editor-button {
182
+ --editor-button-fg: var(--ui-primary-contrast);
183
+ --editor-button-bg: var(--ui-primary);
184
+ --editor-button-border: transparent;
185
+ --editor-button-hover-fg: var(--ui-primary-contrast);
186
+ --editor-button-hover-bg: var(--ui-accent-dark);
187
+ --editor-button-hover-border: transparent;
188
+ display: inline-block;
189
+ padding: 0.5rem 1rem;
190
+ border: 0;
191
+ border-radius: 6px;
192
+ background-color: var(--editor-button-bg);
193
+ color: var(--editor-button-fg);
194
+ font-size: 0.875rem;
195
+ font-weight: 600;
196
+ line-height: 1.125rem;
197
+ letter-spacing: 0.05rem;
198
+ text-decoration: none;
199
+ transition:
200
+ color 0.15s ease-in-out,
201
+ background-color 0.15s ease-in-out,
202
+ border-color 0.15s ease-in-out;
203
+ }
204
+
205
+ .editor-button:hover {
206
+ color: var(--editor-button-hover-fg);
207
+ background-color: var(--editor-button-hover-bg);
208
+ border-color: var(--editor-button-hover-border);
209
+ text-decoration: none;
210
+ }
211
+
212
+ .editor-button:focus-visible {
213
+ outline: solid 0.125rem var(--ui-accent-dark);
214
+ outline-offset: 2px;
215
+ }
216
+
217
+ .editor-button-accent {
218
+ --editor-button-fg: var(--ui-accent-contrast);
219
+ --editor-button-bg: var(--ui-accent);
220
+ --editor-button-hover-fg: var(--ui-primary);
221
+ --editor-button-hover-bg: var(--ui-tertiary);
222
+ }
223
+
224
+ .editor-button-tertiary {
225
+ --editor-button-fg: var(--ui-primary);
226
+ --editor-button-bg: var(--ui-tertiary);
227
+ --editor-button-hover-fg: var(--ui-primary);
228
+ --editor-button-hover-bg: var(--ui-accent);
229
+ }
230
+
231
+ .editor-button-primary-outline,
232
+ .editor-button-accent-outline {
233
+ border: 1px solid var(--editor-button-border);
234
+ --editor-button-bg: transparent;
235
+ --editor-button-hover-bg: transparent;
236
+ }
237
+
238
+ .editor-button-primary-outline {
239
+ --editor-button-fg: var(--ui-primary);
240
+ --editor-button-border: var(--ui-primary);
241
+ --editor-button-hover-fg: var(--ui-accent-dark);
242
+ --editor-button-hover-border: var(--ui-accent-dark);
243
+ }
244
+
245
+ .editor-button-accent-outline {
246
+ --editor-button-fg: var(--ui-accent);
247
+ --editor-button-border: var(--ui-accent);
248
+ --editor-button-hover-fg: var(--ui-tertiary);
249
+ --editor-button-hover-border: var(--ui-tertiary);
250
+ }
251
+
252
+ .editor-button-sm {
253
+ padding: 0.375rem 0.75rem;
254
+ font-size: 0.8125rem;
255
+ line-height: 1.0625rem;
256
+ }
257
+
258
+ .editor-button-lg {
259
+ padding: 0.625rem 1.25rem;
260
+ font-size: 1rem;
261
+ line-height: 1.25rem;
262
+ }
@@ -71,6 +71,6 @@ export interface FileTreeProps {
71
71
  onChange?: (nodes: FileTreeNode[]) => void;
72
72
  class?: string;
73
73
  }
74
- declare const FileTree: import("svelte").Component<FileTreeProps, {}, "selectedId" | "nodes">;
74
+ declare const FileTree: import("svelte").Component<FileTreeProps, {}, "nodes" | "selectedId">;
75
75
  type FileTree = ReturnType<typeof FileTree>;
76
76
  export default FileTree;
@@ -15,7 +15,14 @@
15
15
  import { TextStyle } from '@tiptap/extension-text-style';
16
16
  import Collaboration from '@tiptap/extension-collaboration';
17
17
  import { CollaborationCaret } from '@tiptap/extension-collaboration-caret';
18
- import { Alert, CodeBlockEditor, Div, Span, type CodeBlocksOptions } from './extensions/index.js';
18
+ import {
19
+ Alert,
20
+ ClassAttribute,
21
+ CodeBlockEditor,
22
+ Div,
23
+ Span,
24
+ type CodeBlocksOptions
25
+ } from './extensions/index.js';
19
26
  import type { CollaborationConfig } from './collaboration.js';
20
27
  import FormGroup from '../../form/FormGroup.svelte';
21
28
  import LinkActionMenu from './LinkActionMenu.svelte';
@@ -31,6 +38,7 @@
31
38
  import UnderlineButton from './toolbar/UnderlineButton.svelte';
32
39
  import ColorButton from './toolbar/ColorButton.svelte';
33
40
  import LinkButton from './toolbar/LinkButton.svelte';
41
+ import ButtonLinkButton from './toolbar/ButtonLinkButton.svelte';
34
42
  import SuperscriptButton from './toolbar/SuperscriptButton.svelte';
35
43
  import SubscriptButton from './toolbar/SubscriptButton.svelte';
36
44
  import AlignLeftButton from './toolbar/AlignLeftButton.svelte';
@@ -298,6 +306,8 @@
298
306
  Alert,
299
307
  TabHandler,
300
308
  TableKit.configure({ table: { resizable: true } }),
309
+ // Keep `class` on standard nodes/marks so classes in loaded HTML survive
310
+ ClassAttribute,
301
311
  ...customExtensions
302
312
  ];
303
313
 
@@ -485,6 +495,7 @@
485
495
  <div class="toolbar-divider"></div>
486
496
 
487
497
  <div class="toolbar-group">
498
+ <ButtonLinkButton editor={editorState.editor} />
488
499
  <AlertButton editor={editorState.editor} />
489
500
  </div>
490
501
 
@@ -629,6 +640,19 @@
629
640
  background: rgba(var(--ui-accent-rgbc), 0.1);
630
641
  }
631
642
 
643
+ /* Buttons keep the app.css button look instead of the link colours above. */
644
+ .editor-content :global(.ProseMirror a.editor-link.editor-button) {
645
+ color: var(--editor-button-fg);
646
+ background-color: var(--editor-button-bg);
647
+ text-decoration: none;
648
+ }
649
+
650
+ .editor-content :global(.ProseMirror a.editor-link.editor-button:hover),
651
+ :global(.dark) .editor-content :global(.ProseMirror a.editor-link.editor-button:hover) {
652
+ color: var(--editor-button-hover-fg);
653
+ background-color: var(--editor-button-hover-bg);
654
+ }
655
+
632
656
  /* Ensure the editor remains clickable while links show the action menu. */
633
657
  .editor-content :global(.ProseMirror) {
634
658
  pointer-events: auto;
@@ -15,7 +15,14 @@
15
15
  import { TextStyle } from '@tiptap/extension-text-style';
16
16
  import Collaboration from '@tiptap/extension-collaboration';
17
17
  import { CollaborationCaret } from '@tiptap/extension-collaboration-caret';
18
- import { Alert, CodeBlockEditor, Div, Span, type CodeBlocksOptions } from './extensions/index.js';
18
+ import {
19
+ Alert,
20
+ ClassAttribute,
21
+ CodeBlockEditor,
22
+ Div,
23
+ Span,
24
+ type CodeBlocksOptions
25
+ } from './extensions/index.js';
19
26
  import type { CollaborationConfig } from './collaboration.js';
20
27
  import FormGroup from '../../form/FormGroup.svelte';
21
28
  import LinkActionMenu from './LinkActionMenu.svelte';
@@ -263,6 +270,8 @@
263
270
  Alert,
264
271
  TabHandler,
265
272
  TableKit.configure({ table: { resizable: true } }),
273
+ // Keep `class` on standard nodes/marks so classes in loaded HTML survive
274
+ ClassAttribute,
266
275
  ...customExtensions
267
276
  ];
268
277
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Call-to-action buttons in TipTap content.
3
+ *
4
+ * A button is an ordinary link whose `class` attribute carries `editor-button`
5
+ * plus a variant and, optionally, a size class. The styles live in `app.css`
6
+ * (global, built on the `--ui-*` tokens) so the saved HTML looks the same in
7
+ * the editor and wherever the consumer renders it.
8
+ */
9
+ export declare const editorButtonVariants: readonly ["primary", "accent", "tertiary", "primary-outline", "accent-outline"];
10
+ export type EditorButtonVariant = (typeof editorButtonVariants)[number];
11
+ export declare const editorButtonSizes: readonly ["sm", "md", "lg"];
12
+ export type EditorButtonSize = (typeof editorButtonSizes)[number];
13
+ export interface EditorButtonStyle {
14
+ variant: EditorButtonVariant;
15
+ size: EditorButtonSize;
16
+ }
17
+ export declare const EDITOR_BUTTON_CLASS = "editor-button";
18
+ /** The link `class` value for a button style, e.g. `editor-button editor-button-accent editor-button-lg`. */
19
+ export declare function editorButtonClass(style: EditorButtonStyle): string;
20
+ /** The button style encoded in a link's `class`, or `null` when the link is not a button. */
21
+ export declare function parseEditorButtonClass(className: string | null | undefined): EditorButtonStyle | null;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Call-to-action buttons in TipTap content.
3
+ *
4
+ * A button is an ordinary link whose `class` attribute carries `editor-button`
5
+ * plus a variant and, optionally, a size class. The styles live in `app.css`
6
+ * (global, built on the `--ui-*` tokens) so the saved HTML looks the same in
7
+ * the editor and wherever the consumer renders it.
8
+ */
9
+ export const editorButtonVariants = [
10
+ 'primary',
11
+ 'accent',
12
+ 'tertiary',
13
+ 'primary-outline',
14
+ 'accent-outline'
15
+ ];
16
+ export const editorButtonSizes = ['sm', 'md', 'lg'];
17
+ export const EDITOR_BUTTON_CLASS = 'editor-button';
18
+ const CLASS_PREFIX = `${EDITOR_BUTTON_CLASS}-`;
19
+ /** The link `class` value for a button style, e.g. `editor-button editor-button-accent editor-button-lg`. */
20
+ export function editorButtonClass(style) {
21
+ const classes = [EDITOR_BUTTON_CLASS, `${CLASS_PREFIX}${style.variant}`];
22
+ if (style.size !== 'md')
23
+ classes.push(`${CLASS_PREFIX}${style.size}`);
24
+ return classes.join(' ');
25
+ }
26
+ /** The button style encoded in a link's `class`, or `null` when the link is not a button. */
27
+ export function parseEditorButtonClass(className) {
28
+ const tokens = (className ?? '').split(/\s+/).filter(Boolean);
29
+ if (!tokens.includes(EDITOR_BUTTON_CLASS))
30
+ return null;
31
+ let variant = 'primary';
32
+ let size = 'md';
33
+ for (const token of tokens) {
34
+ if (!token.startsWith(CLASS_PREFIX))
35
+ continue;
36
+ const suffix = token.slice(CLASS_PREFIX.length);
37
+ if (editorButtonVariants.includes(suffix)) {
38
+ variant = suffix;
39
+ }
40
+ else if (editorButtonSizes.includes(suffix)) {
41
+ size = suffix;
42
+ }
43
+ }
44
+ return { variant, size };
45
+ }
@@ -0,0 +1,16 @@
1
+ import { Extension } from '@tiptap/core';
2
+ /**
3
+ * Node and mark types that keep their `class` attribute by default. Types that
4
+ * already manage `class` themselves (div, span, alert, link) are left alone.
5
+ */
6
+ export declare const defaultClassAttributeTypes: readonly ["paragraph", "heading", "blockquote", "bulletList", "orderedList", "listItem", "codeBlock", "horizontalRule", "image", "table", "tableRow", "tableHeader", "tableCell", "bold", "italic", "strike", "code", "underline", "subscript", "superscript", "textStyle"];
7
+ export interface ClassAttributeOptions {
8
+ /** Node and mark type names that should preserve `class` (default: all standard types) */
9
+ types: string[];
10
+ }
11
+ /**
12
+ * Preserves the `class` attribute on standard nodes and marks so classes in
13
+ * loaded or pasted HTML survive the round-trip through the editor instead of
14
+ * being stripped by the schema.
15
+ */
16
+ export declare const ClassAttribute: Extension<ClassAttributeOptions, any>;
@@ -0,0 +1,59 @@
1
+ import { Extension } from '@tiptap/core';
2
+ /**
3
+ * Node and mark types that keep their `class` attribute by default. Types that
4
+ * already manage `class` themselves (div, span, alert, link) are left alone.
5
+ */
6
+ export const defaultClassAttributeTypes = [
7
+ 'paragraph',
8
+ 'heading',
9
+ 'blockquote',
10
+ 'bulletList',
11
+ 'orderedList',
12
+ 'listItem',
13
+ 'codeBlock',
14
+ 'horizontalRule',
15
+ 'image',
16
+ 'table',
17
+ 'tableRow',
18
+ 'tableHeader',
19
+ 'tableCell',
20
+ 'bold',
21
+ 'italic',
22
+ 'strike',
23
+ 'code',
24
+ 'underline',
25
+ 'subscript',
26
+ 'superscript',
27
+ 'textStyle'
28
+ ];
29
+ /**
30
+ * Preserves the `class` attribute on standard nodes and marks so classes in
31
+ * loaded or pasted HTML survive the round-trip through the editor instead of
32
+ * being stripped by the schema.
33
+ */
34
+ export const ClassAttribute = Extension.create({
35
+ name: 'classAttribute',
36
+ addOptions() {
37
+ return {
38
+ types: [...defaultClassAttributeTypes]
39
+ };
40
+ },
41
+ addGlobalAttributes() {
42
+ return [
43
+ {
44
+ types: this.options.types,
45
+ attributes: {
46
+ class: {
47
+ default: null,
48
+ parseHTML: (element) => element.getAttribute('class') || null,
49
+ renderHTML: (attributes) => {
50
+ if (!attributes.class)
51
+ return {};
52
+ return { class: attributes.class };
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ];
58
+ }
59
+ });
@@ -1,6 +1,8 @@
1
1
  export { Alert } from './Alert.js';
2
2
  export { Div } from './Div.js';
3
3
  export { Span } from './Span.js';
4
+ export { ClassAttribute, defaultClassAttributeTypes } from './ClassAttribute.js';
5
+ export type { ClassAttributeOptions } from './ClassAttribute.js';
4
6
  export { CodeBlockEditor } from './CodeBlockEditor.js';
5
7
  export type { CodeBlockEditorOptions, CodeBlocksOptions } from './CodeBlockEditor.js';
6
8
  export { codeBlockLanguages, resolveCodeBlockLanguage } from './codeBlockLanguages.js';
@@ -1,5 +1,6 @@
1
1
  export { Alert } from './Alert.js';
2
2
  export { Div } from './Div.js';
3
3
  export { Span } from './Span.js';
4
+ export { ClassAttribute, defaultClassAttributeTypes } from './ClassAttribute.js';
4
5
  export { CodeBlockEditor } from './CodeBlockEditor.js';
5
6
  export { codeBlockLanguages, resolveCodeBlockLanguage } from './codeBlockLanguages.js';
@@ -58,7 +58,7 @@
58
58
  <div style="position: relative; display: inline-block;">
59
59
  <ToolbarButton
60
60
  icon={iconAlertTriangle}
61
- title="Insert Alert"
61
+ title="Alert"
62
62
  active={showDialog}
63
63
  onclick={toggleDialog}
64
64
  />
@@ -66,7 +66,7 @@
66
66
  {#if showDialog}
67
67
  <div class="alert-dialog" bind:this={dialogElement}>
68
68
  <div class="dialog-header">
69
- <h4>Insert Alert</h4>
69
+ <h4>Alert</h4>
70
70
  </div>
71
71
 
72
72
  <div class="dialog-body">
@@ -0,0 +1,427 @@
1
+ <script lang="ts">
2
+ import type { Editor, JSONContent } from '@tiptap/core';
3
+ import ToolbarButton from './ToolbarButton.svelte';
4
+ import { iconButton } from '../../../icon/index.js';
5
+ import {
6
+ editorButtonClass,
7
+ parseEditorButtonClass,
8
+ type EditorButtonSize,
9
+ type EditorButtonVariant
10
+ } from '../buttonLink.js';
11
+
12
+ interface Props {
13
+ editor: Editor;
14
+ }
15
+
16
+ let { editor }: Props = $props();
17
+
18
+ let showDialog = $state(false);
19
+ let label = $state('');
20
+ let url = $state('');
21
+ let variant = $state<EditorButtonVariant>('primary');
22
+ let size = $state<EditorButtonSize>('md');
23
+ let openInNewTab = $state(false);
24
+ /** The dialog opened on an existing link, which Apply rewrites in place. */
25
+ let editing = $state(false);
26
+ let labelInputElement: HTMLInputElement | undefined = $state();
27
+ let urlInputElement: HTMLInputElement | undefined = $state();
28
+
29
+ const variants: { value: EditorButtonVariant; label: string }[] = [
30
+ { value: 'primary', label: 'Primary' },
31
+ { value: 'accent', label: 'Accent' },
32
+ { value: 'tertiary', label: 'Tertiary' },
33
+ { value: 'primary-outline', label: 'Primary outline' },
34
+ { value: 'accent-outline', label: 'Accent outline' }
35
+ ];
36
+
37
+ const sizes: { value: EditorButtonSize; label: string }[] = [
38
+ { value: 'sm', label: 'Small' },
39
+ { value: 'md', label: 'Medium' },
40
+ { value: 'lg', label: 'Large' }
41
+ ];
42
+
43
+ const isButton = $derived(parseEditorButtonClass(editor.getAttributes('link').class) !== null);
44
+
45
+ function selectionText(): string {
46
+ const { from, to } = editor.state.selection;
47
+ return editor.state.doc.textBetween(from, to, ' ');
48
+ }
49
+
50
+ const toggleDialog = () => {
51
+ if (showDialog) {
52
+ closeDialog();
53
+ return;
54
+ }
55
+
56
+ editing = editor.isActive('link');
57
+ if (editing) {
58
+ // Work on the whole link, whatever part of it the cursor is in.
59
+ editor.chain().extendMarkRange('link').run();
60
+ }
61
+
62
+ const attrs = editor.getAttributes('link') as {
63
+ href?: string;
64
+ target?: string | null;
65
+ class?: string | null;
66
+ };
67
+ const style = parseEditorButtonClass(attrs.class);
68
+
69
+ url = attrs.href ?? '';
70
+ openInNewTab = attrs.target === '_blank';
71
+ variant = style?.variant ?? 'primary';
72
+ size = style?.size ?? 'md';
73
+ label = selectionText();
74
+ showDialog = true;
75
+
76
+ setTimeout(() => {
77
+ const input = label ? urlInputElement : labelInputElement;
78
+ input?.focus();
79
+ input?.select();
80
+ }, 0);
81
+ };
82
+
83
+ const applyButton = () => {
84
+ const href = url.trim();
85
+ if (!href) {
86
+ urlInputElement?.focus();
87
+ return;
88
+ }
89
+
90
+ const text = label.trim() || href;
91
+ const attrs = {
92
+ href,
93
+ target: openInNewTab ? '_blank' : null,
94
+ rel: openInNewTab ? 'noopener noreferrer' : null,
95
+ class: editorButtonClass({ variant, size })
96
+ };
97
+ const content: JSONContent[] = [{ type: 'text', text, marks: [{ type: 'link', attrs }] }];
98
+ // A button typed in at the cursor gets a space after it, so what is typed
99
+ // next does not extend the button.
100
+ if (!editing && editor.state.selection.empty) {
101
+ content.push({ type: 'text', text: ' ' });
102
+ }
103
+
104
+ const chain = editor.chain().focus();
105
+ if (editing) chain.extendMarkRange('link');
106
+ chain.insertContent(content).run();
107
+
108
+ closeDialog();
109
+ };
110
+
111
+ const removeButton = () => {
112
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
113
+ closeDialog();
114
+ };
115
+
116
+ const closeDialog = () => {
117
+ showDialog = false;
118
+ editing = false;
119
+ label = '';
120
+ url = '';
121
+ variant = 'primary';
122
+ size = 'md';
123
+ openInNewTab = false;
124
+ };
125
+
126
+ const handleKeydown = (event: KeyboardEvent) => {
127
+ if (event.key === 'Enter') {
128
+ event.preventDefault();
129
+ applyButton();
130
+ } else if (event.key === 'Escape') {
131
+ event.preventDefault();
132
+ closeDialog();
133
+ editor.commands.focus();
134
+ }
135
+ };
136
+ </script>
137
+
138
+ <div style="position: relative; display: inline-block;">
139
+ <ToolbarButton icon={iconButton} title="Button" active={isButton} onclick={toggleDialog} />
140
+
141
+ {#if showDialog}
142
+ <div class="button-dialog" role="dialog" aria-label="Button">
143
+ <div class="dialog-header">
144
+ <h4>{editing ? 'Edit button' : 'Insert button'}</h4>
145
+ </div>
146
+
147
+ <div class="dialog-body">
148
+ <div class="form-group">
149
+ <label for="button-link-label">Text</label>
150
+ <input
151
+ id="button-link-label"
152
+ type="text"
153
+ bind:this={labelInputElement}
154
+ bind:value={label}
155
+ onkeydown={handleKeydown}
156
+ placeholder="Button text"
157
+ class="form-input"
158
+ />
159
+ </div>
160
+
161
+ <div class="form-group">
162
+ <label for="button-link-url">URL</label>
163
+ <input
164
+ id="button-link-url"
165
+ type="text"
166
+ bind:this={urlInputElement}
167
+ bind:value={url}
168
+ onkeydown={handleKeydown}
169
+ placeholder="Enter URL"
170
+ class="form-input"
171
+ />
172
+ </div>
173
+
174
+ <div class="form-group">
175
+ <span class="group-label" id="button-link-style">Style</span>
176
+ <div class="option-row" role="group" aria-labelledby="button-link-style">
177
+ {#each variants as option (option.value)}
178
+ <button
179
+ class="editor-button editor-button-{option.value} editor-button-sm option-button"
180
+ class:selected={variant === option.value}
181
+ onclick={() => (variant = option.value)}
182
+ type="button"
183
+ >
184
+ {option.label}
185
+ </button>
186
+ {/each}
187
+ </div>
188
+ </div>
189
+
190
+ <div class="form-group">
191
+ <span class="group-label" id="button-link-size">Size</span>
192
+ <div class="option-row" role="group" aria-labelledby="button-link-size">
193
+ {#each sizes as option (option.value)}
194
+ <button
195
+ class="size-button"
196
+ class:active={size === option.value}
197
+ onclick={() => (size = option.value)}
198
+ type="button"
199
+ >
200
+ {option.label}
201
+ </button>
202
+ {/each}
203
+ </div>
204
+ </div>
205
+
206
+ <label class="checkbox-label">
207
+ <input type="checkbox" bind:checked={openInNewTab} class="checkbox" />
208
+ <span>Open in new tab</span>
209
+ </label>
210
+ </div>
211
+
212
+ <div class="dialog-footer">
213
+ {#if editing}
214
+ <button class="btn btn-remove" onclick={removeButton} type="button">Remove</button>
215
+ {/if}
216
+ <span class="footer-spacer"></span>
217
+ <button class="btn btn-secondary" onclick={closeDialog} type="button">Cancel</button>
218
+ <button class="btn btn-primary" onclick={applyButton} type="button">
219
+ {editing ? 'Apply' : 'Insert'}
220
+ </button>
221
+ </div>
222
+ </div>
223
+ {/if}
224
+ </div>
225
+
226
+ <style>
227
+ .button-dialog {
228
+ position: absolute;
229
+ top: 100%;
230
+ left: 0;
231
+ margin-top: 4px;
232
+ display: flex;
233
+ flex-direction: column;
234
+ background: var(--paper-body-bg);
235
+ border: 1px solid var(--border-color);
236
+ border-radius: 6px;
237
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
238
+ z-index: 1000;
239
+ width: 360px;
240
+ max-width: calc(100vw - 32px);
241
+ }
242
+
243
+ :global(.dark) .button-dialog {
244
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
245
+ }
246
+
247
+ .dialog-header {
248
+ padding: 12px 16px;
249
+ border-bottom: 1px solid var(--border-color);
250
+ }
251
+
252
+ .dialog-header h4 {
253
+ margin: 0;
254
+ font-size: 14px;
255
+ font-weight: 600;
256
+ color: var(--text-color);
257
+ }
258
+
259
+ .dialog-body {
260
+ padding: 16px;
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 14px;
264
+ }
265
+
266
+ .form-group {
267
+ display: flex;
268
+ flex-direction: column;
269
+ gap: 8px;
270
+ }
271
+
272
+ .form-group label,
273
+ .group-label {
274
+ font-size: 12px;
275
+ font-weight: 600;
276
+ color: var(--text-color);
277
+ }
278
+
279
+ .form-input {
280
+ padding: 8px 12px;
281
+ border: 1px solid var(--border-color);
282
+ border-radius: 4px;
283
+ background: var(--input-bg);
284
+ color: var(--text-color);
285
+ font-size: 13px;
286
+ font-family: inherit;
287
+ }
288
+
289
+ .form-input::placeholder {
290
+ color: var(--placeholder-color);
291
+ }
292
+
293
+ .form-input:focus {
294
+ outline: none;
295
+ border-color: var(--ui-primary);
296
+ }
297
+
298
+ :global(.dark) .form-input:focus {
299
+ border-color: var(--ui-accent);
300
+ }
301
+
302
+ .option-row {
303
+ display: flex;
304
+ flex-wrap: wrap;
305
+ gap: 8px;
306
+ }
307
+
308
+ /* The style choices are drawn with the real button classes from app.css; the
309
+ ring marks the selected one. */
310
+ .option-button {
311
+ cursor: pointer;
312
+ font-family: inherit;
313
+ }
314
+
315
+ .option-button.selected {
316
+ outline: 2px solid var(--ui-primary);
317
+ outline-offset: 2px;
318
+ }
319
+
320
+ :global(.dark) .option-button.selected {
321
+ outline-color: var(--ui-accent);
322
+ }
323
+
324
+ .size-button {
325
+ padding: 6px 12px;
326
+ border: 1px solid var(--border-color);
327
+ border-radius: 4px;
328
+ background: var(--paper-body-bg);
329
+ color: var(--text-color);
330
+ font-size: 13px;
331
+ font-family: inherit;
332
+ cursor: pointer;
333
+ transition: all 0.15s ease;
334
+ }
335
+
336
+ .size-button:hover {
337
+ background: var(--page-bg);
338
+ }
339
+
340
+ .size-button.active {
341
+ background: var(--page-bg);
342
+ border-color: var(--ui-primary);
343
+ font-weight: 600;
344
+ }
345
+
346
+ :global(.dark) .size-button.active {
347
+ border-color: var(--ui-accent);
348
+ }
349
+
350
+ .checkbox-label {
351
+ display: flex;
352
+ align-items: center;
353
+ gap: 6px;
354
+ font-size: 13px;
355
+ color: var(--text-color);
356
+ cursor: pointer;
357
+ user-select: none;
358
+ }
359
+
360
+ .checkbox {
361
+ width: 16px;
362
+ height: 16px;
363
+ cursor: pointer;
364
+ accent-color: var(--ui-primary);
365
+ }
366
+
367
+ :global(.dark) .checkbox {
368
+ accent-color: var(--ui-accent);
369
+ }
370
+
371
+ .dialog-footer {
372
+ padding: 12px 16px;
373
+ border-top: 1px solid var(--border-color);
374
+ display: flex;
375
+ gap: 8px;
376
+ align-items: center;
377
+ }
378
+
379
+ .footer-spacer {
380
+ flex: 1;
381
+ }
382
+
383
+ .btn {
384
+ padding: 6px 16px;
385
+ border: none;
386
+ border-radius: 4px;
387
+ font-size: 13px;
388
+ font-weight: 500;
389
+ font-family: inherit;
390
+ cursor: pointer;
391
+ transition: all 0.15s ease;
392
+ }
393
+
394
+ .btn-secondary {
395
+ background: var(--page-bg);
396
+ color: var(--text-color);
397
+ border: 1px solid var(--border-color);
398
+ }
399
+
400
+ .btn-secondary:hover {
401
+ background: var(--border-color);
402
+ }
403
+
404
+ .btn-primary {
405
+ background: var(--ui-primary);
406
+ color: var(--off-white);
407
+ }
408
+
409
+ .btn-primary:hover {
410
+ opacity: 0.9;
411
+ }
412
+
413
+ :global(.dark) .btn-primary {
414
+ background: var(--ui-accent);
415
+ color: var(--ui-primary);
416
+ }
417
+
418
+ .btn-remove {
419
+ background: transparent;
420
+ color: var(--danger-text);
421
+ border: 1px solid var(--border-color);
422
+ }
423
+
424
+ .btn-remove:hover {
425
+ background: var(--danger-bg-subtle);
426
+ }
427
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ interface Props {
3
+ editor: Editor;
4
+ }
5
+ declare const ButtonLinkButton: import("svelte").Component<Props, {}, "">;
6
+ type ButtonLinkButton = ReturnType<typeof ButtonLinkButton>;
7
+ export default ButtonLinkButton;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-button"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 8a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M8 12h8" /></svg>
@@ -19,6 +19,7 @@ export { default as iconAt } from './at.svg?raw';
19
19
  export { default as iconBell } from './bell.svg?raw';
20
20
  export { default as iconBold } from './bold.svg?raw';
21
21
  export { default as iconBookmark } from './bookmark.svg?raw';
22
+ export { default as iconButton } from './button.svg?raw';
22
23
  export { default as iconCalendar } from './calendar.svg?raw';
23
24
  export { default as iconCamera } from './camera.svg?raw';
24
25
  export { default as iconChartBar } from './chart-bar.svg?raw';
@@ -19,6 +19,7 @@ export { default as iconAt } from './at.svg?raw';
19
19
  export { default as iconBell } from './bell.svg?raw';
20
20
  export { default as iconBold } from './bold.svg?raw';
21
21
  export { default as iconBookmark } from './bookmark.svg?raw';
22
+ export { default as iconButton } from './button.svg?raw';
22
23
  export { default as iconCalendar } from './calendar.svg?raw';
23
24
  export { default as iconCamera } from './camera.svg?raw';
24
25
  export { default as iconChartBar } from './chart-bar.svg?raw';
package/dist/index.d.ts CHANGED
@@ -114,9 +114,11 @@ export type { TipTapProps, DocumentUploadResult } from './editors/tiptap/TipTap.
114
114
  export { default as TipTapBubble } from './editors/tiptap/TipTapBubble.svelte';
115
115
  export type { TipTapBubbleProps } from './editors/tiptap/TipTapBubble.svelte';
116
116
  export type { CollaborationConfig, CollaborationUser } from './editors/tiptap/collaboration.js';
117
- export { CodeBlockEditor, codeBlockLanguages, resolveCodeBlockLanguage } from './editors/tiptap/extensions/index.js';
118
- export type { CodeBlocksOptions, CodeBlockEditorOptions, CodeBlockLanguageDef, CodeBlockLanguageId } from './editors/tiptap/extensions/index.js';
117
+ export { ClassAttribute, defaultClassAttributeTypes, CodeBlockEditor, codeBlockLanguages, resolveCodeBlockLanguage } from './editors/tiptap/extensions/index.js';
118
+ export type { ClassAttributeOptions, CodeBlocksOptions, CodeBlockEditorOptions, CodeBlockLanguageDef, CodeBlockLanguageId } from './editors/tiptap/extensions/index.js';
119
119
  export type { AnyExtension } from '@tiptap/core';
120
+ export { editorButtonVariants, editorButtonSizes, editorButtonClass, parseEditorButtonClass } from './editors/tiptap/buttonLink.js';
121
+ export type { EditorButtonVariant, EditorButtonSize, EditorButtonStyle } from './editors/tiptap/buttonLink.js';
120
122
  export { default as CodeMirror } from './editors/CodeMirror.svelte';
121
123
  export type { CodeMirrorProps } from './editors/CodeMirror.svelte';
122
124
  export { default as GitCard } from './cards/GitCard.svelte';
package/dist/index.js CHANGED
@@ -56,7 +56,8 @@ export { default as ColorPicker } from './form/ColorPicker.svelte';
56
56
  export { parseColor, parseHex, toHex, formatColor, rgbToHsv, hsvToRgb, rgbToHsl } from './form/ColorPicker.svelte';
57
57
  export { default as TipTap } from './editors/tiptap/TipTap.svelte';
58
58
  export { default as TipTapBubble } from './editors/tiptap/TipTapBubble.svelte';
59
- export { CodeBlockEditor, codeBlockLanguages, resolveCodeBlockLanguage } from './editors/tiptap/extensions/index.js';
59
+ export { ClassAttribute, defaultClassAttributeTypes, CodeBlockEditor, codeBlockLanguages, resolveCodeBlockLanguage } from './editors/tiptap/extensions/index.js';
60
+ export { editorButtonVariants, editorButtonSizes, editorButtonClass, parseEditorButtonClass } from './editors/tiptap/buttonLink.js';
60
61
  export { default as CodeMirror } from './editors/CodeMirror.svelte';
61
62
  export { default as GitCard } from './cards/GitCard.svelte';
62
63
  export * from './icon/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@placeholderco/placeholder-ui",
3
- "version": "2.2.5",
3
+ "version": "2.4.0",
4
4
  "description": "A modern, customizable Svelte 5 UI component library with comprehensive theming support. Re-brand it with a single config object.",
5
5
  "license": "MIT",
6
6
  "author": "Placeholderco",