paris 0.18.1 → 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,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
+ }
@@ -0,0 +1,223 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import { useState } from 'react';
3
+ import { FixedToolbar } from './FixedToolbar';
4
+ import { FloatingToolbar } from './FloatingToolbar';
5
+ import { MarkdownEditor } from './MarkdownEditor';
6
+
7
+ const meta: Meta<typeof MarkdownEditor> = {
8
+ title: 'Content/MarkdownEditor',
9
+ component: MarkdownEditor,
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ size: {
13
+ control: 'select',
14
+ options: ['paragraphLarge', 'paragraphMedium', 'paragraphSmall', 'paragraphXSmall', 'paragraphXXSmall'],
15
+ },
16
+ status: {
17
+ control: 'select',
18
+ options: ['default', 'error', 'success'],
19
+ },
20
+ },
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof MarkdownEditor>;
25
+
26
+ const sampleMarkdown = `# Getting Started
27
+
28
+ This is a **WYSIWYG** markdown editor built on *Tiptap*.
29
+
30
+ ## Features
31
+
32
+ - Bold, italic, and ~~strikethrough~~
33
+ - [Links](https://paris.slingshot.fm) and \`inline code\`
34
+ - Headings, blockquotes, and horizontal rules
35
+
36
+ > Blockquotes render with Paris styling.
37
+
38
+ ### Task Lists
39
+
40
+ - [x] Set up Tiptap
41
+ - [x] Add markdown serialization
42
+ - [ ] Style with Paris tokens
43
+
44
+ \`\`\`typescript
45
+ const [md, setMd] = useState('');
46
+ \`\`\`
47
+
48
+ ---
49
+
50
+ That's it! The editor outputs **markdown** on every change.`;
51
+
52
+ /**
53
+ * Default editor with both FixedToolbar and FloatingToolbar.
54
+ * All features enabled.
55
+ */
56
+ export const Default: Story = {
57
+ render: (args) => {
58
+ const [value, setValue] = useState(sampleMarkdown);
59
+ return (
60
+ <div style={{ maxWidth: 700 }}>
61
+ <MarkdownEditor {...args} value={value} onChange={setValue}>
62
+ <FixedToolbar />
63
+ <FloatingToolbar />
64
+ </MarkdownEditor>
65
+ <details style={{ marginTop: 16 }}>
66
+ <summary
67
+ style={{ cursor: 'pointer', fontSize: 12, color: 'var(--pte-new-colors-contentTertiary)' }}
68
+ >
69
+ Markdown output
70
+ </summary>
71
+ <pre
72
+ style={{
73
+ marginTop: 8,
74
+ padding: 12,
75
+ fontSize: 12,
76
+ background: 'var(--pte-new-colors-backgroundSecondary)',
77
+ borderRadius: 8,
78
+ overflow: 'auto',
79
+ whiteSpace: 'pre-wrap',
80
+ }}
81
+ >
82
+ {value}
83
+ </pre>
84
+ </details>
85
+ </div>
86
+ );
87
+ },
88
+ args: {
89
+ placeholder: 'Start writing...',
90
+ },
91
+ };
92
+
93
+ /**
94
+ * Editor with only the FloatingToolbar (appears on text selection).
95
+ * Clean editing surface without a fixed toolbar.
96
+ */
97
+ export const FloatingOnly: Story = {
98
+ render: (args) => {
99
+ const [value, setValue] = useState('Select some text to see the floating toolbar.');
100
+ return (
101
+ <div style={{ maxWidth: 700 }}>
102
+ <MarkdownEditor {...args} value={value} onChange={setValue}>
103
+ <FloatingToolbar />
104
+ </MarkdownEditor>
105
+ </div>
106
+ );
107
+ },
108
+ args: {
109
+ placeholder: 'Start writing...',
110
+ },
111
+ };
112
+
113
+ /**
114
+ * Editor with only the FixedToolbar.
115
+ */
116
+ export const FixedOnly: Story = {
117
+ render: (args) => {
118
+ const [value, setValue] = useState('');
119
+ return (
120
+ <div style={{ maxWidth: 700 }}>
121
+ <MarkdownEditor {...args} value={value} onChange={setValue}>
122
+ <FixedToolbar />
123
+ </MarkdownEditor>
124
+ </div>
125
+ );
126
+ },
127
+ args: {
128
+ placeholder: 'Start writing...',
129
+ },
130
+ };
131
+
132
+ /**
133
+ * Editor with a limited feature set — only bold, italic, heading, and link.
134
+ * Toolbar only shows buttons for enabled features.
135
+ */
136
+ export const LimitedFeatures: Story = {
137
+ render: (args) => {
138
+ const [value, setValue] = useState('Only **bold**, *italic*, headings, and links are available.');
139
+ return (
140
+ <div style={{ maxWidth: 700 }}>
141
+ <MarkdownEditor {...args} value={value} onChange={setValue}>
142
+ <FixedToolbar />
143
+ <FloatingToolbar />
144
+ </MarkdownEditor>
145
+ </div>
146
+ );
147
+ },
148
+ args: {
149
+ features: ['bold', 'italic', 'heading', 'link'],
150
+ placeholder: 'Limited formatting...',
151
+ },
152
+ };
153
+
154
+ /**
155
+ * Read-only editor with pre-filled content.
156
+ * The editor is not editable — useful for preview modes.
157
+ */
158
+ export const ReadOnly: Story = {
159
+ render: (args) => {
160
+ return (
161
+ <div style={{ maxWidth: 700 }}>
162
+ <MarkdownEditor {...args} value={sampleMarkdown} editable={false} />
163
+ </div>
164
+ );
165
+ },
166
+ };
167
+
168
+ /**
169
+ * Editor with error status — shows error border styling.
170
+ */
171
+ export const ErrorStatus: Story = {
172
+ render: (args) => {
173
+ const [value, setValue] = useState('');
174
+ return (
175
+ <div style={{ maxWidth: 700 }}>
176
+ <MarkdownEditor {...args} value={value} onChange={setValue} status="error">
177
+ <FixedToolbar />
178
+ </MarkdownEditor>
179
+ </div>
180
+ );
181
+ },
182
+ args: {
183
+ placeholder: 'This field has an error...',
184
+ },
185
+ };
186
+
187
+ /**
188
+ * Editor with no toolbar — users rely on keyboard shortcuts and
189
+ * markdown input rules (e.g., type `## ` for heading 2).
190
+ */
191
+ export const NoToolbar: Story = {
192
+ render: (args) => {
193
+ const [value, setValue] = useState('');
194
+ return (
195
+ <div style={{ maxWidth: 700 }}>
196
+ <MarkdownEditor {...args} value={value} onChange={setValue} />
197
+ </div>
198
+ );
199
+ },
200
+ args: {
201
+ placeholder: 'Type markdown shortcuts: # heading, **bold**, - list item...',
202
+ },
203
+ };
204
+
205
+ /**
206
+ * Editor with custom placeholder text.
207
+ */
208
+ export const WithPlaceholder: Story = {
209
+ render: (args) => {
210
+ const [value, setValue] = useState('');
211
+ return (
212
+ <div style={{ maxWidth: 700 }}>
213
+ <MarkdownEditor {...args} value={value} onChange={setValue}>
214
+ <FixedToolbar />
215
+ </MarkdownEditor>
216
+ </div>
217
+ );
218
+ },
219
+ args: {
220
+ placeholder: 'Write your thoughts here...',
221
+ size: 'paragraphMedium',
222
+ },
223
+ };