paris 0.18.0 → 0.19.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.
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+
3
+ import type { FC } from 'react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import styles from './LinkPopover.module.scss';
6
+ import { useMarkdownEditorContext } from './MarkdownEditorContext';
7
+
8
+ export type LinkPopoverProps = {
9
+ /** Callback when the popover should close. */
10
+ onClose: () => void;
11
+ };
12
+
13
+ /**
14
+ * An inline popover for inserting/editing links.
15
+ * Reads the current link state from the editor and allows
16
+ * setting href and target attributes.
17
+ */
18
+ export const LinkPopover: FC<LinkPopoverProps> = ({ onClose }) => {
19
+ const { editor } = useMarkdownEditorContext();
20
+ const inputRef = useRef<HTMLInputElement>(null);
21
+ const popoverRef = useRef<HTMLDivElement>(null);
22
+
23
+ // Read existing link attributes if cursor is on a link
24
+ const existingHref = editor?.getAttributes('link')?.href ?? '';
25
+ const existingTarget = editor?.getAttributes('link')?.target ?? '_blank';
26
+
27
+ const [url, setUrl] = useState(existingHref);
28
+ const [openInNewTab, setOpenInNewTab] = useState(existingTarget === '_blank');
29
+
30
+ // Focus the input on mount
31
+ useEffect(() => {
32
+ inputRef.current?.focus();
33
+ inputRef.current?.select();
34
+ }, []);
35
+
36
+ // Close on click outside
37
+ useEffect(() => {
38
+ const handleClickOutside = (e: MouseEvent) => {
39
+ if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
40
+ onClose();
41
+ }
42
+ };
43
+ // Use requestAnimationFrame to skip the current event that triggered the popover
44
+ const raf = requestAnimationFrame(() => {
45
+ document.addEventListener('mousedown', handleClickOutside);
46
+ });
47
+ return () => {
48
+ cancelAnimationFrame(raf);
49
+ document.removeEventListener('mousedown', handleClickOutside);
50
+ };
51
+ }, [onClose]);
52
+
53
+ // Close on Escape
54
+ useEffect(() => {
55
+ const handleKeyDown = (e: KeyboardEvent) => {
56
+ if (e.key === 'Escape') {
57
+ onClose();
58
+ editor?.chain().focus().run();
59
+ }
60
+ };
61
+ document.addEventListener('keydown', handleKeyDown);
62
+ return () => document.removeEventListener('keydown', handleKeyDown);
63
+ }, [onClose, editor]);
64
+
65
+ const handleApply = useCallback(() => {
66
+ if (!editor) return;
67
+
68
+ if (!url) {
69
+ editor.chain().focus().unsetLink().run();
70
+ } else {
71
+ editor
72
+ .chain()
73
+ .focus()
74
+ .extendMarkRange('link')
75
+ .setLink({
76
+ href: url,
77
+ target: openInNewTab ? '_blank' : null,
78
+ })
79
+ .run();
80
+ }
81
+ onClose();
82
+ }, [editor, url, openInNewTab, onClose]);
83
+
84
+ const handleRemove = useCallback(() => {
85
+ if (!editor) return;
86
+ editor.chain().focus().unsetLink().run();
87
+ onClose();
88
+ }, [editor, onClose]);
89
+
90
+ const handleKeyDown = useCallback(
91
+ (e: React.KeyboardEvent) => {
92
+ if (e.key === 'Enter') {
93
+ e.preventDefault();
94
+ handleApply();
95
+ }
96
+ },
97
+ [handleApply],
98
+ );
99
+
100
+ return (
101
+ <div ref={popoverRef} className={styles.popover}>
102
+ <div className={styles.label}>Link URL</div>
103
+ <div className={styles.inputRow}>
104
+ <input
105
+ ref={inputRef}
106
+ type="url"
107
+ className={styles.input}
108
+ placeholder="https://..."
109
+ value={url}
110
+ onChange={(e) => setUrl(e.target.value)}
111
+ onKeyDown={handleKeyDown}
112
+ />
113
+ <button
114
+ type="button"
115
+ className={styles.applyButton}
116
+ onMouseDown={(e) => e.preventDefault()}
117
+ onClick={handleApply}
118
+ >
119
+ Apply
120
+ </button>
121
+ </div>
122
+ <div className={styles.footer}>
123
+ <label className={styles.checkboxLabel}>
124
+ <input type="checkbox" checked={openInNewTab} onChange={(e) => setOpenInNewTab(e.target.checked)} />
125
+ Open in new tab
126
+ </label>
127
+ {existingHref && (
128
+ <button type="button" className={styles.removeButton} onClick={handleRemove}>
129
+ Remove link
130
+ </button>
131
+ )}
132
+ </div>
133
+ </div>
134
+ );
135
+ };
@@ -0,0 +1,405 @@
1
+ /* ── MarkdownEditor styles ────────────────────────────────────
2
+ * Uses Paris design tokens (--pte-*) for consistent theming.
3
+ * Editor content styles mirror Markdown.module.scss but target
4
+ * native HTML elements rendered by Tiptap (not CSS module classes).
5
+ * ──────────────────────────────────────────────────────────── */
6
+
7
+ .root {
8
+ display: flex;
9
+ flex-direction: column;
10
+ width: 100%;
11
+ }
12
+
13
+ // ── Editor container (input-like wrapper) ─────────────────
14
+ .editorContainer {
15
+ display: flex;
16
+ flex-direction: column;
17
+ border-radius: var(--pte-borders-radius-rectangle);
18
+ background-color: var(--pte-new-colors-inputFill);
19
+ border: 1px solid var(--pte-new-colors-inputFill);
20
+ transition: var(--pte-animations-interaction);
21
+
22
+ &:focus-within {
23
+ outline: none;
24
+ border-color: var(--pte-new-colors-inputBorderFocus);
25
+ background-color: var(--pte-new-colors-inputFillFocus);
26
+ }
27
+
28
+ &[data-status='error'] {
29
+ border-color: var(--pte-new-colors-inputBorderNegative);
30
+ background-color: var(--pte-new-colors-inputFillNegative);
31
+ color: var(--pte-new-colors-contentNegative);
32
+
33
+ &:focus-within {
34
+ color: var(--pte-new-colors-contentPrimary);
35
+ }
36
+ }
37
+
38
+ &[data-status='success'] {
39
+ border-color: var(--pte-new-colors-backgroundPositive);
40
+ }
41
+
42
+ &[data-disabled='true'] {
43
+ background-color: var(--pte-new-colors-inputFillDisabled);
44
+ border-color: var(--pte-new-colors-inputFillDisabled);
45
+ color: var(--pte-new-colors-contentDisabled);
46
+ pointer-events: none;
47
+ cursor: default;
48
+
49
+ &:focus-within {
50
+ border-color: var(--pte-new-colors-inputFillDisabled) !important;
51
+ }
52
+ }
53
+ }
54
+
55
+ // ── Editor content area ───────────────────────────────────
56
+ // Targets native HTML elements rendered by Tiptap (not CSS module classes).
57
+ // Mirrors the visual output of the read-only Markdown component.
58
+ .editorContent {
59
+ padding: 12px 16px;
60
+ min-height: 120px;
61
+ outline: none;
62
+ line-height: 1.7;
63
+ color: var(--pte-new-colors-contentPrimary);
64
+ font-size: var(--markdown-base-font-size, 14px);
65
+
66
+ // Tiptap root element
67
+ :global(.tiptap) {
68
+ outline: none;
69
+ min-height: inherit;
70
+
71
+ > *:first-child {
72
+ margin-top: 0;
73
+ }
74
+
75
+ > *:last-child {
76
+ margin-bottom: 0;
77
+ }
78
+ }
79
+
80
+ // ── Placeholder ──────────────────────────────────────
81
+ :global(.tiptap p.is-editor-empty:first-child::before) {
82
+ content: attr(data-placeholder);
83
+ color: var(--pte-new-colors-contentTertiary);
84
+ pointer-events: none;
85
+ float: left;
86
+ height: 0;
87
+ }
88
+
89
+ // ── Headings ─────────────────────────────────────────
90
+ h1,
91
+ h2,
92
+ h3,
93
+ h4,
94
+ h5,
95
+ h6 {
96
+ margin-top: 1.5em;
97
+ margin-bottom: 0.6em;
98
+ color: var(--pte-new-colors-contentPrimary);
99
+ line-height: 1.3;
100
+
101
+ &:first-child {
102
+ margin-top: 0;
103
+ }
104
+ }
105
+
106
+ h1 {
107
+ font-size: var(--pte-new-typography-styles-headingLarge-fontSize, 32px);
108
+ font-weight: var(--pte-new-typography-styles-headingLarge-fontWeight, 700);
109
+ }
110
+
111
+ h2 {
112
+ font-size: var(--pte-new-typography-styles-headingMedium-fontSize, 24px);
113
+ font-weight: var(--pte-new-typography-styles-headingMedium-fontWeight, 700);
114
+ }
115
+
116
+ h3 {
117
+ font-size: var(--pte-new-typography-styles-headingSmall-fontSize, 20px);
118
+ font-weight: var(--pte-new-typography-styles-headingSmall-fontWeight, 700);
119
+ }
120
+
121
+ h4 {
122
+ font-size: var(--pte-new-typography-styles-headingXSmall-fontSize, 18px);
123
+ font-weight: var(--pte-new-typography-styles-headingXSmall-fontWeight, 600);
124
+ }
125
+
126
+ h5 {
127
+ font-size: var(--pte-new-typography-styles-headingXXSmall-fontSize, 16px);
128
+ font-weight: var(--pte-new-typography-styles-headingXXSmall-fontWeight, 600);
129
+ }
130
+
131
+ h6 {
132
+ font-size: var(--pte-new-typography-styles-labelMedium-fontSize, 14px);
133
+ font-weight: var(--pte-new-typography-styles-labelMedium-fontWeight, 600);
134
+ text-transform: uppercase;
135
+ letter-spacing: 0.05em;
136
+ }
137
+
138
+ // ── Paragraphs ───────────────────────────────────────
139
+ p {
140
+ margin-bottom: 1em;
141
+ line-height: 1.7;
142
+
143
+ &:last-child {
144
+ margin-bottom: 0;
145
+ }
146
+ }
147
+
148
+ // ── Inline marks ─────────────────────────────────────
149
+ strong {
150
+ font-weight: 600;
151
+ }
152
+
153
+ em {
154
+ font-style: italic;
155
+ }
156
+
157
+ s {
158
+ text-decoration: line-through;
159
+ text-decoration-color: var(--pte-new-colors-contentTertiary);
160
+ }
161
+
162
+ // ── Links ────────────────────────────────────────────
163
+ a {
164
+ color: var(--pte-new-colors-contentAccent);
165
+ text-decoration: underline;
166
+ text-underline-offset: 2px;
167
+ cursor: pointer;
168
+
169
+ &:hover {
170
+ text-decoration-color: var(--pte-new-colors-contentAccent);
171
+ }
172
+ }
173
+
174
+ // ── Images ───────────────────────────────────────────
175
+ img {
176
+ max-width: 100%;
177
+ height: auto;
178
+ border-radius: var(--pte-new-borders-radius-rounded);
179
+ border: 1px solid var(--pte-new-colors-borderSubtle);
180
+ margin: 1em 0;
181
+ display: block;
182
+ }
183
+
184
+ // ── Blockquotes ──────────────────────────────────────
185
+ blockquote {
186
+ border-left: 3px solid var(--pte-new-colors-borderMedium);
187
+ padding: 8px 16px;
188
+ margin: 1em 0;
189
+ background-color: var(--pte-new-colors-backgroundSecondary);
190
+ border-radius: 0 var(--pte-new-borders-radius-rounded, 8px)
191
+ var(--pte-new-borders-radius-rounded, 8px) 0;
192
+ color: var(--pte-new-colors-contentSecondary);
193
+
194
+ blockquote {
195
+ margin: 0.5em 0;
196
+ }
197
+
198
+ p {
199
+ margin-bottom: 0.5em;
200
+
201
+ &:last-child {
202
+ margin-bottom: 0;
203
+ }
204
+ }
205
+ }
206
+
207
+ // ── Horizontal rules ─────────────────────────────────
208
+ hr {
209
+ border: none;
210
+ border-top: 1px solid var(--pte-new-colors-borderMedium);
211
+ margin: 1.5em 0;
212
+ }
213
+
214
+ // ── Lists ────────────────────────────────────────────
215
+ ul,
216
+ ol {
217
+ margin: 0.5em 0 1em;
218
+ padding-left: 1.5em;
219
+ }
220
+
221
+ ul {
222
+ list-style-type: disc;
223
+
224
+ ul {
225
+ list-style-type: circle;
226
+ margin: 0.25em 0;
227
+
228
+ ul {
229
+ list-style-type: square;
230
+ }
231
+ }
232
+ }
233
+
234
+ ol {
235
+ list-style-type: decimal;
236
+
237
+ ol {
238
+ list-style-type: lower-alpha;
239
+ margin: 0.25em 0;
240
+
241
+ ol {
242
+ list-style-type: lower-roman;
243
+ }
244
+ }
245
+ }
246
+
247
+ li {
248
+ margin-bottom: 0.25em;
249
+ line-height: 1.7;
250
+
251
+ p {
252
+ margin-bottom: 0.25em;
253
+ }
254
+ }
255
+
256
+ // ── Task lists (Tiptap v3 uses data-checked on li) ────
257
+ ul[data-type='taskList'] {
258
+ list-style: none;
259
+ padding-left: 0;
260
+ }
261
+
262
+ li[data-checked] {
263
+ display: flex;
264
+ flex-direction: row;
265
+ align-items: baseline;
266
+ gap: 8px;
267
+
268
+ > label {
269
+ flex-shrink: 0;
270
+
271
+ input[type='checkbox'] {
272
+ appearance: none;
273
+ width: 14px;
274
+ height: 14px;
275
+ border: 2px solid var(--pte-new-colors-contentTertiary);
276
+ border-radius: var(--pte-borders-radius-rectangle);
277
+ background: transparent;
278
+ cursor: pointer;
279
+ position: relative;
280
+ top: 2px;
281
+ transition: var(--pte-animations-interaction);
282
+
283
+ &:checked {
284
+ // Matches the Paris Checkbox: filled SVG covers the full 14x14 area
285
+ &::after {
286
+ content: '';
287
+ position: absolute;
288
+ inset: -2px;
289
+ background-color: var(--pte-new-colors-contentPrimary);
290
+ mask-image: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.333374 0.333252V13.6666H13.6667V0.333252H0.333374ZM6.00004 10.3999L2.26672 6.66658L3.66671 5.26658L5.93339 7.53325L10.2 3.26658L11.6001 4.66659L6.00004 10.3999Z' fill='black'/%3E%3C/svg%3E");
291
+ mask-size: contain;
292
+ mask-repeat: no-repeat;
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ > div {
299
+ flex: 1;
300
+ }
301
+ }
302
+
303
+ // ── Inline code ──────────────────────────────────────
304
+ code {
305
+ font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
306
+ font-size: 0.875em;
307
+ padding: 2px 6px;
308
+ border-radius: 6px;
309
+ background-color: var(--pte-new-colors-backgroundSecondary);
310
+ border: 1px solid var(--pte-new-colors-borderSubtle);
311
+ color: var(--pte-new-colors-contentPrimary);
312
+ word-break: break-word;
313
+ }
314
+
315
+ // ── Code blocks ──────────────────────────────────────
316
+ pre {
317
+ margin: 1em 0;
318
+ padding: 0;
319
+ background: transparent;
320
+ overflow: visible;
321
+
322
+ code {
323
+ display: block;
324
+ padding: 16px;
325
+ overflow-x: auto;
326
+ font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
327
+ font-size: 13px;
328
+ line-height: 1.6;
329
+ tab-size: 4;
330
+ white-space: pre;
331
+ border-radius: var(--pte-new-borders-radius-rounded, 8px);
332
+ background-color: var(--pte-new-colors-backgroundSecondary);
333
+ border: 1px solid var(--pte-new-colors-borderSubtle);
334
+ color: var(--pte-new-colors-contentPrimary);
335
+
336
+ &::-webkit-scrollbar {
337
+ height: 6px;
338
+ }
339
+
340
+ &::-webkit-scrollbar-track {
341
+ background: transparent;
342
+ }
343
+
344
+ &::-webkit-scrollbar-thumb {
345
+ background-color: var(--pte-new-colors-borderMedium);
346
+ border-radius: 3px;
347
+ }
348
+ }
349
+ }
350
+
351
+ // ── Tables ───────────────────────────────────────────
352
+ table {
353
+ width: 100%;
354
+ border-collapse: collapse;
355
+ margin: 1em 0;
356
+ font-size: var(--pte-new-typography-paragraphSmall-fontSize, 13px);
357
+ border-radius: var(--pte-new-borders-radius-rounded, 8px);
358
+ border: 1px solid var(--pte-new-colors-borderSubtle);
359
+ overflow: hidden;
360
+ }
361
+
362
+ thead {
363
+ background-color: var(--pte-new-colors-backgroundSecondary);
364
+ }
365
+
366
+ th {
367
+ padding: 10px 16px;
368
+ text-align: left;
369
+ border-bottom: 1px solid var(--pte-new-colors-borderMedium);
370
+ white-space: nowrap;
371
+ color: var(--pte-new-colors-contentSecondary);
372
+ font-size: var(--pte-new-typography-styles-labelXSmall-fontSize, 11px);
373
+ font-weight: 600;
374
+ text-transform: uppercase;
375
+ letter-spacing: 0.05em;
376
+ }
377
+
378
+ td {
379
+ padding: 10px 16px;
380
+ border-bottom: 1px solid var(--pte-new-colors-borderSubtle);
381
+ color: var(--pte-new-colors-contentPrimary);
382
+ }
383
+
384
+ tr:last-child td {
385
+ border-bottom: none;
386
+ }
387
+
388
+ tbody tr {
389
+ transition: background-color 0.15s ease;
390
+
391
+ &:hover {
392
+ background-color: var(--pte-new-colors-backgroundSecondary);
393
+ }
394
+ }
395
+
396
+ // ── Tiptap table selection states ────────────────────
397
+ :global(.selectedCell) {
398
+ background-color: var(--pte-new-colors-backgroundAccentSubtle, rgba(99, 102, 241, 0.1));
399
+ }
400
+
401
+ // ── Selection highlight ──────────────────────────────
402
+ ::selection {
403
+ background-color: var(--pte-new-colors-backgroundAccentSubtle, rgba(99, 102, 241, 0.2));
404
+ }
405
+ }