@railway/inkwell 1.2.0 → 1.3.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/styles.css +127 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@railway/inkwell",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Inkwell is a Markdown editor and renderer for React with an extensible plugin system.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/src/styles.css CHANGED
@@ -4,6 +4,19 @@
4
4
  * consumer can drop in and either keep or override. Light by default,
5
5
  * dark via `prefers-color-scheme: dark`. No saturated brand colors.
6
6
  *
7
+ * Specificity strategy:
8
+ * Visual-chrome defaults (color, background, border, padding, typography)
9
+ * are wrapped in `:where()` so they carry 0,0,0 specificity. Any single-class
10
+ * consumer rule — Tailwind utilities, `classNames` slot styling, `components`
11
+ * overrides on the renderer — wins automatically by specificity tie-break.
12
+ * No `!important`, no descendant scoping, no inline-style hacks needed.
13
+ *
14
+ * Layout-critical rules (position, z-index, structural width/height/overflow,
15
+ * picker flip math) deliberately stay at normal specificity. Consumers should
16
+ * not silently break editor or picker geometry by adding a class — when they
17
+ * want to override layout, they can with descendant or `!important` rules
18
+ * applied intentionally.
19
+ *
7
20
  * Import with: `import "@railway/inkwell/styles.css";`
8
21
  */
9
22
 
@@ -72,14 +85,16 @@
72
85
 
73
86
  /* ── Editor surface ──────────────────────────────────────────────── */
74
87
 
88
+ /* Wrapper position is structural — plugin pickers, bubble menus, and the
89
+ character-count overlay all absolutely position against it. Stays at
90
+ normal specificity. */
75
91
  .inkwell-editor-wrapper {
76
92
  position: relative;
77
93
  }
78
94
 
79
- /* Visual-chrome defaults (padding, border, background, type) are wrapped in
80
- `:where()` so they carry 0,0,0 specificity. Any single-class consumer rule
81
- on `.inkwell-editor` (Tailwind utilities, `classNames.editor`, etc.) wins
82
- automatically — no `!important` or selector gymnastics needed.
95
+ /* Visual-chrome defaults on the editable surface live inside `:where()`
96
+ so a single-class consumer rule (Tailwind utilities, `classNames.editor`,
97
+ etc.) wins automatically.
83
98
 
84
99
  Container size (min-height, max-height, height) is deliberately NOT
85
100
  defaulted here. Inkwell sits inside the consumer's layout and the right
@@ -88,10 +103,12 @@
88
103
  container. Set it on the editor via `styles.editor`, `classNames.editor`,
89
104
  or your own CSS. */
90
105
  :where(.inkwell-editor) {
106
+ outline: none;
91
107
  padding: 1rem 1.25rem;
92
108
  border: 1px solid var(--inkwell-border);
93
109
  border-radius: var(--inkwell-radius);
94
110
  background: var(--inkwell-bg);
111
+ color: var(--inkwell-text);
95
112
  line-height: 1.6;
96
113
  font-size: 0.95rem;
97
114
  transition: border-color 0.15s ease;
@@ -99,27 +116,28 @@
99
116
  :where(.inkwell-editor:focus-within) {
100
117
  border-color: var(--inkwell-border-strong);
101
118
  }
102
- .inkwell-editor {
103
- outline: none;
104
- color: var(--inkwell-text);
105
- }
106
119
 
120
+ /* `position: relative` on paragraphs is structural — Slate decorations and
121
+ inline children position against it. Margin is theming and goes through
122
+ `:where()` so a consumer paragraph-spacing utility wins. */
107
123
  .inkwell-editor p {
108
- margin: 0;
109
124
  position: relative;
110
125
  }
111
- .inkwell-editor strong {
126
+ :where(.inkwell-editor p) {
127
+ margin: 0;
128
+ }
129
+ :where(.inkwell-editor strong) {
112
130
  font-weight: 600;
113
131
  color: var(--inkwell-text);
114
132
  }
115
- .inkwell-editor em {
133
+ :where(.inkwell-editor em) {
116
134
  font-style: italic;
117
135
  }
118
- .inkwell-editor del {
136
+ :where(.inkwell-editor del) {
119
137
  text-decoration: line-through;
120
138
  color: var(--inkwell-text-muted);
121
139
  }
122
- .inkwell-editor code {
140
+ :where(.inkwell-editor code) {
123
141
  background: var(--inkwell-code-bg);
124
142
  color: var(--inkwell-code-fg);
125
143
  padding: 0.1em 0.35em;
@@ -128,38 +146,38 @@
128
146
  font-size: 0.85em;
129
147
  }
130
148
 
131
- .inkwell-editor-blockquote {
149
+ :where(.inkwell-editor-blockquote) {
132
150
  border-left: 3px solid var(--inkwell-border-strong);
133
151
  padding-left: 0.85em;
134
152
  margin: 0.5em 0;
135
153
  color: var(--inkwell-text-muted);
136
154
  }
137
155
 
138
- .inkwell-editor-heading {
156
+ :where(.inkwell-editor-heading) {
139
157
  font-weight: 600;
140
158
  line-height: 1.3;
141
159
  color: var(--inkwell-text);
142
160
  }
143
- .inkwell-editor-heading-1 {
161
+ :where(.inkwell-editor-heading-1) {
144
162
  font-size: 1.75em;
145
163
  }
146
- .inkwell-editor-heading-2 {
164
+ :where(.inkwell-editor-heading-2) {
147
165
  font-size: 1.4em;
148
166
  }
149
- .inkwell-editor-heading-3 {
167
+ :where(.inkwell-editor-heading-3) {
150
168
  font-size: 1.2em;
151
169
  }
152
- .inkwell-editor-heading-4 {
170
+ :where(.inkwell-editor-heading-4) {
153
171
  font-size: 1em;
154
172
  }
155
- .inkwell-editor-heading-5 {
173
+ :where(.inkwell-editor-heading-5) {
156
174
  font-size: 0.9em;
157
175
  }
158
- .inkwell-editor-heading-6 {
176
+ :where(.inkwell-editor-heading-6) {
159
177
  font-size: 0.8em;
160
178
  }
161
179
 
162
- .inkwell-editor-image {
180
+ :where(.inkwell-editor-image) {
163
181
  margin: 0.75em 0;
164
182
  border-radius: var(--inkwell-radius);
165
183
  overflow: hidden;
@@ -168,10 +186,14 @@
168
186
  border-color 0.15s ease,
169
187
  box-shadow 0.15s ease;
170
188
  }
171
- .inkwell-editor-image[data-selected] {
189
+ :where(.inkwell-editor-image[data-selected]) {
172
190
  border-color: var(--inkwell-accent);
173
191
  box-shadow: 0 0 0 3px var(--inkwell-accent-soft);
174
192
  }
193
+ /* Image fitting stays structural — `max-width: 100%` and `height: auto`
194
+ are the only sensible defaults for keeping aspect ratio inside a
195
+ variable-width editor. Consumers who want a different sizing model
196
+ should opt in with a descendant or higher-specificity rule. */
175
197
  .inkwell-editor-image img {
176
198
  display: block;
177
199
  max-width: 100%;
@@ -185,44 +207,53 @@
185
207
  limit is a soft hint — typing past it is allowed; the count then
186
208
  turns red and the wrapper picks up `.inkwell-editor-over-limit`,
187
209
  which paints a soft red halo on the editor surface so it's visually
188
- obvious the document is over budget. */
210
+ obvious the document is over budget.
211
+
212
+ Positioning stays at normal specificity — the overlay only works
213
+ from the top-right corner of the wrapper. Chrome (color, font-weight,
214
+ etc.) lives below in `:where()`. */
189
215
  .inkwell-editor-character-count {
190
216
  position: absolute;
191
217
  top: 0.5rem;
192
218
  right: 0.5rem;
193
219
  z-index: 10;
220
+ pointer-events: none;
221
+ user-select: none;
222
+ }
223
+ :where(.inkwell-editor-character-count) {
194
224
  padding: 0.1rem 0.4rem;
195
225
  font-size: 0.72rem;
196
226
  font-variant-numeric: tabular-nums;
197
227
  color: var(--inkwell-text-dim);
198
228
  background: var(--inkwell-bg);
199
229
  border-radius: calc(var(--inkwell-radius) - 2px);
200
- pointer-events: none;
201
- user-select: none;
202
230
  }
203
- .inkwell-editor-character-count-over {
231
+ :where(.inkwell-editor-character-count-over) {
204
232
  color: var(--inkwell-danger);
205
233
  font-weight: 500;
206
234
  }
207
- .inkwell-editor-wrapper.inkwell-editor-over-limit .inkwell-editor {
235
+ :where(.inkwell-editor-wrapper.inkwell-editor-over-limit .inkwell-editor) {
208
236
  border-color: var(--inkwell-danger-soft);
209
237
  box-shadow: 0 0 0 3px var(--inkwell-danger-soft);
210
238
  }
211
239
 
212
- .inkwell-editor-backtick,
213
- .inkwell-editor-marker {
240
+ :where(.inkwell-editor-backtick),
241
+ :where(.inkwell-editor-marker) {
214
242
  color: var(--inkwell-text-dim);
215
243
  }
216
- .inkwell-editor .inkwell-editor-code-fence {
244
+ :where(.inkwell-editor .inkwell-editor-code-fence) {
217
245
  color: var(--inkwell-text-dim);
218
246
  }
219
- .inkwell-editor .inkwell-editor-code-line,
220
- .inkwell-editor .inkwell-editor-code-fence,
221
- .inkwell-renderer pre code {
247
+ :where(.inkwell-editor .inkwell-editor-code-line),
248
+ :where(.inkwell-editor .inkwell-editor-code-fence),
249
+ :where(.inkwell-renderer pre code) {
222
250
  font-family: var(--inkwell-font-mono);
223
251
  font-size: 0.85em;
224
252
  line-height: 1.55;
225
253
  }
254
+ /* Wrapping behavior for code lines stays structural — Slate emits one
255
+ contenteditable line per node and depends on `white-space: pre-wrap`
256
+ for correct caret placement. */
226
257
  .inkwell-editor .inkwell-editor-code-line {
227
258
  white-space: pre-wrap;
228
259
  word-wrap: break-word;
@@ -241,11 +272,13 @@
241
272
  }
242
273
  }
243
274
 
275
+ /* Container positioning is layout-critical — the bubble menu is JS-
276
+ positioned against the selection. Keep at normal specificity. */
244
277
  .inkwell-plugin-bubble-menu-container {
245
278
  position: absolute;
246
279
  z-index: 9999;
247
280
  }
248
- .inkwell-plugin-bubble-menu-inner {
281
+ :where(.inkwell-plugin-bubble-menu-inner) {
249
282
  display: flex;
250
283
  gap: 2px;
251
284
  background: var(--inkwell-bg-elevated);
@@ -255,7 +288,7 @@
255
288
  box-shadow: 0 6px 20px hsla(220, 20%, 10%, 0.12);
256
289
  animation: inkwell-fade-in 0.12s ease-out;
257
290
  }
258
- .inkwell-plugin-bubble-menu-btn {
291
+ :where(.inkwell-plugin-bubble-menu-btn) {
259
292
  display: flex;
260
293
  align-items: center;
261
294
  justify-content: center;
@@ -270,35 +303,38 @@
270
303
  background 0.1s ease,
271
304
  color 0.1s ease;
272
305
  }
273
- .inkwell-plugin-bubble-menu-btn:hover {
306
+ :where(.inkwell-plugin-bubble-menu-btn:hover) {
274
307
  background: var(--inkwell-bg-subtle);
275
308
  color: var(--inkwell-text);
276
309
  }
277
- .inkwell-plugin-bubble-menu-btn-active {
310
+ :where(.inkwell-plugin-bubble-menu-btn-active) {
278
311
  background: var(--inkwell-bg-subtle);
279
312
  color: var(--inkwell-text);
280
313
  }
281
- .inkwell-plugin-bubble-menu-item-bold {
314
+ :where(.inkwell-plugin-bubble-menu-item-bold) {
282
315
  font-weight: 700;
283
316
  font-size: 14px;
284
317
  }
285
- .inkwell-plugin-bubble-menu-item-italic {
318
+ :where(.inkwell-plugin-bubble-menu-item-italic) {
286
319
  font-style: italic;
287
320
  font-size: 14px;
288
321
  font-family: Georgia, "Times New Roman", serif;
289
322
  }
290
- .inkwell-plugin-bubble-menu-item-strike {
323
+ :where(.inkwell-plugin-bubble-menu-item-strike) {
291
324
  text-decoration: line-through;
292
325
  font-size: 14px;
293
326
  }
294
327
 
295
328
  /* ── Shared plugin picker (snippets, mentions, etc.) ─────────────── */
296
329
 
330
+ /* Popup positioning is layout-critical — the picker reads
331
+ `popupEl.offsetParent` for flip math, so it must remain absolutely
332
+ positioned inside the editor wrapper. */
297
333
  .inkwell-plugin-picker-popup {
298
334
  position: absolute;
299
335
  z-index: 1001;
300
336
  }
301
- .inkwell-plugin-picker {
337
+ :where(.inkwell-plugin-picker) {
302
338
  background: var(--inkwell-bg-elevated);
303
339
  border: 1px solid var(--inkwell-border);
304
340
  border-radius: var(--inkwell-radius);
@@ -307,7 +343,7 @@
307
343
  max-width: 320px;
308
344
  box-shadow: 0 6px 24px hsla(220, 20%, 10%, 0.14);
309
345
  }
310
- .inkwell-plugin-picker-search {
346
+ :where(.inkwell-plugin-picker-search) {
311
347
  width: 100%;
312
348
  padding: 7px 10px;
313
349
  background: var(--inkwell-bg);
@@ -317,9 +353,12 @@
317
353
  font-size: 0.85rem;
318
354
  outline: none;
319
355
  }
320
- .inkwell-plugin-picker-search::placeholder {
356
+ :where(.inkwell-plugin-picker-search::placeholder) {
321
357
  color: var(--inkwell-text-dim);
322
358
  }
359
+ /* List `max-height` is structural — prevents the picker from blowing out
360
+ the viewport with long item lists. Scroll behavior and scrollbar
361
+ styling stay alongside it for a single source of truth. */
323
362
  .inkwell-plugin-picker-list {
324
363
  max-height: 240px;
325
364
  overflow-y: auto;
@@ -342,27 +381,27 @@
342
381
  .inkwell-plugin-picker-list::-webkit-scrollbar-thumb:hover {
343
382
  background-color: var(--inkwell-text-dim);
344
383
  }
345
- .inkwell-plugin-picker-item {
384
+ :where(.inkwell-plugin-picker-item) {
346
385
  padding: 7px 10px;
347
386
  cursor: pointer;
348
387
  transition: background 0.1s ease;
349
388
  }
350
- .inkwell-plugin-picker-item:hover,
351
- .inkwell-plugin-picker-item-active {
389
+ :where(.inkwell-plugin-picker-item:hover),
390
+ :where(.inkwell-plugin-picker-item-active) {
352
391
  background: var(--inkwell-bg-subtle);
353
392
  }
354
- .inkwell-plugin-picker-title {
393
+ :where(.inkwell-plugin-picker-title) {
355
394
  font-size: 0.85rem;
356
395
  font-weight: 500;
357
396
  color: var(--inkwell-text);
358
397
  margin-right: 0.5rem;
359
398
  }
360
- .inkwell-plugin-picker-subtitle {
399
+ :where(.inkwell-plugin-picker-subtitle) {
361
400
  font-size: 0.75rem;
362
401
  color: var(--inkwell-text-muted);
363
402
  margin-top: 2px;
364
403
  }
365
- .inkwell-plugin-picker-preview {
404
+ :where(.inkwell-plugin-picker-preview) {
366
405
  font-size: 0.75rem;
367
406
  color: var(--inkwell-text-muted);
368
407
  margin-top: 2px;
@@ -370,14 +409,14 @@
370
409
  text-overflow: ellipsis;
371
410
  white-space: nowrap;
372
411
  }
373
- .inkwell-plugin-picker-empty {
412
+ :where(.inkwell-plugin-picker-empty) {
374
413
  padding: 12px;
375
414
  text-align: center;
376
415
  color: var(--inkwell-text-dim);
377
416
  font-size: 0.85rem;
378
417
  }
379
418
 
380
- .inkwell-plugin-slash-commands-execute {
419
+ :where(.inkwell-plugin-slash-commands-execute) {
381
420
  padding: 8px 10px;
382
421
  color: var(--inkwell-text);
383
422
  font-size: 0.85rem;
@@ -387,57 +426,58 @@
387
426
 
388
427
  /* ── Renderer ────────────────────────────────────────────────────── */
389
428
 
390
- /* Layout defaults on the renderer follow the same low-specificity pattern
391
- as the editor a single-class consumer rule wins automatically. */
429
+ /* Every rule below targets a rendered HTML element (`a`, `h1`, `p`, ...)
430
+ that consumers customize through the `components` prop on
431
+ `<InkwellRenderer />` or by adding a class on the rendered element. All
432
+ chrome lives inside `:where()` so those overrides win without
433
+ `!important`. */
392
434
  :where(.inkwell-renderer) {
435
+ color: var(--inkwell-text);
393
436
  line-height: 1.65;
394
437
  font-size: 0.95rem;
395
438
  }
396
- .inkwell-renderer {
397
- color: var(--inkwell-text);
398
- }
399
- .inkwell-renderer :first-child {
439
+ :where(.inkwell-renderer :first-child) {
400
440
  margin-top: 0;
401
441
  }
402
- .inkwell-renderer h1 {
442
+ :where(.inkwell-renderer h1) {
403
443
  font-size: 1.75em;
404
444
  font-weight: 600;
405
445
  margin: 0.67em 0;
406
446
  }
407
- .inkwell-renderer h2 {
447
+ :where(.inkwell-renderer h2) {
408
448
  font-size: 1.4em;
409
449
  font-weight: 600;
410
450
  margin: 0.75em 0;
411
451
  }
412
- .inkwell-renderer h3 {
452
+ :where(.inkwell-renderer h3) {
413
453
  font-size: 1.2em;
414
454
  font-weight: 600;
415
455
  margin: 0.8em 0;
416
456
  }
417
- .inkwell-renderer p {
457
+ :where(.inkwell-renderer p) {
418
458
  margin: 0.5em 0;
419
459
  }
420
- .inkwell-renderer blockquote {
460
+ :where(.inkwell-renderer blockquote) {
421
461
  border-left: 3px solid var(--inkwell-border-strong);
422
462
  padding-left: 0.85em;
423
463
  margin: 1em 0;
424
464
  color: var(--inkwell-text-muted);
425
465
  }
426
- .inkwell-renderer ul,
427
- .inkwell-renderer ol {
466
+ :where(.inkwell-renderer ul),
467
+ :where(.inkwell-renderer ol) {
428
468
  padding-left: 1.5em;
429
469
  margin: 1em 0;
430
470
  }
431
- .inkwell-renderer ul {
471
+ :where(.inkwell-renderer ul) {
432
472
  list-style: disc;
433
473
  }
434
- .inkwell-renderer ol {
474
+ :where(.inkwell-renderer ol) {
435
475
  list-style: decimal;
436
476
  }
437
- .inkwell-renderer li {
477
+ :where(.inkwell-renderer li) {
438
478
  margin: 0.25em 0;
439
479
  }
440
- .inkwell-renderer code {
480
+ :where(.inkwell-renderer code) {
441
481
  background: var(--inkwell-code-bg);
442
482
  color: var(--inkwell-code-fg);
443
483
  padding: 0.1em 0.35em;
@@ -445,13 +485,21 @@
445
485
  font-family: var(--inkwell-font-mono);
446
486
  font-size: 0.85em;
447
487
  }
488
+ /* Code-block wrapper position is structural — the copy button absolutely
489
+ positions inside it. */
448
490
  .inkwell-renderer-code-block {
449
491
  position: relative;
450
492
  }
493
+ /* Copy button positioning is layout-critical (top-right inside the code
494
+ block); its chrome lives in `:where()` so consumers can restyle without
495
+ breaking placement. */
451
496
  .inkwell-renderer-copy-btn {
452
497
  position: absolute;
453
498
  top: 0.5rem;
454
499
  right: 0.5rem;
500
+ z-index: 1;
501
+ }
502
+ :where(.inkwell-renderer-copy-btn) {
455
503
  display: flex;
456
504
  align-items: center;
457
505
  justify-content: center;
@@ -467,49 +515,48 @@
467
515
  opacity 0.15s ease,
468
516
  color 0.15s ease,
469
517
  background 0.15s ease;
470
- z-index: 1;
471
518
  }
472
- .inkwell-renderer-code-block:hover .inkwell-renderer-copy-btn {
519
+ :where(.inkwell-renderer-code-block:hover .inkwell-renderer-copy-btn) {
473
520
  opacity: 1;
474
521
  }
475
- .inkwell-renderer-copy-btn:hover {
522
+ :where(.inkwell-renderer-copy-btn:hover) {
476
523
  background: var(--inkwell-bg-subtle);
477
524
  color: var(--inkwell-text);
478
525
  }
479
- .inkwell-renderer pre {
526
+ :where(.inkwell-renderer pre) {
480
527
  margin: 1em 0;
481
528
  border-radius: var(--inkwell-radius);
482
529
  overflow: auto;
483
530
  border: 1px solid var(--inkwell-border);
484
531
  background: var(--inkwell-bg-subtle);
485
532
  }
486
- .inkwell-renderer pre code {
533
+ :where(.inkwell-renderer pre code) {
487
534
  display: block;
488
535
  padding: 0.85em 1em;
489
536
  background: transparent;
490
537
  color: var(--inkwell-text);
491
538
  font-size: 0.82em;
492
539
  }
493
- .inkwell-renderer a {
540
+ :where(.inkwell-renderer a) {
494
541
  color: var(--inkwell-accent);
495
542
  text-decoration: underline;
496
543
  text-underline-offset: 2px;
497
544
  }
498
- .inkwell-renderer hr {
545
+ :where(.inkwell-renderer hr) {
499
546
  border: none;
500
547
  border-top: 1px solid var(--inkwell-border);
501
548
  margin: 2em 0;
502
549
  }
503
- .inkwell-renderer strong {
550
+ :where(.inkwell-renderer strong) {
504
551
  font-weight: 600;
505
552
  }
506
- .inkwell-renderer em {
553
+ :where(.inkwell-renderer em) {
507
554
  font-style: italic;
508
555
  }
509
- .inkwell-renderer del {
556
+ :where(.inkwell-renderer del) {
510
557
  text-decoration: line-through;
511
558
  }
512
- .inkwell-renderer img {
559
+ :where(.inkwell-renderer img) {
513
560
  max-width: 100%;
514
561
  height: auto;
515
562
  border-radius: var(--inkwell-radius);