@zuilib/text-editor 0.4.0 → 0.6.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/styles.css CHANGED
@@ -11,21 +11,62 @@
11
11
  min-height: 100%;
12
12
  }
13
13
 
14
+ /*
15
+ * Text measure (opt in). `--zui-text-editor-measure` — a CSS length, unset
16
+ * by default — caps the text column. The pane's gutter grows from the base
17
+ * 2rem to centre that column, and `--zui-text-editor-bleed` is how far a
18
+ * full-width block extends past the column on each side. With the measure
19
+ * unset both resolve to the previous layout (2rem gutter, no bleed).
20
+ *
21
+ * The pane is a container so `cqw` measures *its* width in every
22
+ * descendant, whatever the descendant's own width; the outline sidebar is
23
+ * a sibling, so full-width blocks stop short of it.
24
+ */
14
25
  .zui-text-editor-main {
15
26
  position: relative;
16
27
  display: flex;
17
28
  flex-direction: column;
18
29
  flex: 1;
19
30
  min-width: 0;
31
+ container-type: inline-size;
32
+ --zui-text-editor-gutter: max(
33
+ 2rem,
34
+ calc((100cqw - var(--zui-text-editor-measure, 100cqw)) / 2)
35
+ );
36
+ --zui-text-editor-bleed: calc(var(--zui-text-editor-gutter) - 2rem);
20
37
  }
21
38
 
22
39
  .zui-text-editor-content {
23
40
  flex: 1;
24
41
  min-height: 100%;
25
- padding: 1.5rem 2rem;
42
+ padding: 1.5rem var(--zui-text-editor-gutter, 2rem);
26
43
  outline: none;
27
44
  }
28
45
 
46
+ /*
47
+ * Block width. Every top-level block sits in the text column and may bleed
48
+ * past it by `--zui-text-editor-block-bleed` (0 unless set). Tables and
49
+ * drawings default it to the pane's bleed (`full`); their `text` and
50
+ * `content` settings pin it to 0 — as an inline style on the table (see
51
+ * tableSettings.ts) and a class on the drawing canvas. Zero-specificity
52
+ * selectors, so one class rule overrides any block kind, e.g.
53
+ * `.zui-code { --zui-text-editor-block-bleed: var(--zui-text-editor-bleed) }`.
54
+ */
55
+ :where(.zui-text-editor-content > *) {
56
+ margin-inline: calc(-1 * var(--zui-text-editor-block-bleed, 0px));
57
+ }
58
+
59
+ :where(.zui-table, .zui-drawing) {
60
+ --zui-text-editor-block-bleed: var(--zui-text-editor-bleed, 0px);
61
+ }
62
+
63
+ :where(
64
+ .zui-drawing:has(> .is-text-width),
65
+ .zui-drawing:has(> .is-content-width)
66
+ ) {
67
+ --zui-text-editor-block-bleed: 0px;
68
+ }
69
+
29
70
  /* Outline (table of contents) sidebar */
30
71
  .zui-text-editor-outline {
31
72
  position: sticky;
@@ -115,7 +156,8 @@
115
156
 
116
157
  .zui-text-editor-fold {
117
158
  position: absolute;
118
- left: 0.45rem;
159
+ /* Just left of the text column; 0.45rem with the default gutter */
160
+ left: calc(var(--zui-text-editor-gutter, 2rem) - 1.55rem);
119
161
  display: inline-flex;
120
162
  align-items: center;
121
163
  justify-content: center;
@@ -155,7 +197,7 @@
155
197
  .zui-text-editor-placeholder {
156
198
  position: absolute;
157
199
  top: 1.5rem;
158
- left: 2rem;
200
+ left: var(--zui-text-editor-gutter, 2rem);
159
201
  pointer-events: none;
160
202
  user-select: none;
161
203
  }
@@ -209,7 +251,7 @@
209
251
  .zui-frontmatter {
210
252
  display: block;
211
253
  white-space: pre-wrap;
212
- margin: 0 0 1rem;
254
+ margin-block: 0 1rem;
213
255
  padding: 0.75rem 1rem;
214
256
  border: 1px solid currentColor;
215
257
  border-radius: 0.375rem;
@@ -272,11 +314,15 @@
272
314
  background: color-mix(in srgb, currentColor 15%, transparent);
273
315
  }
274
316
 
275
- /* Tables — artifact-style: rounded outer border, subtle header, tight rules */
317
+ /*
318
+ * Tables — artifact-style: rounded outer border, subtle header, tight rules.
319
+ * Width follows the block bleed above (inline margins come from the shared
320
+ * block rule); a `content` table sets `width: auto` inline instead.
321
+ */
276
322
  .zui-table {
277
- width: 100%;
278
- max-width: 100%;
279
- margin: 1rem 0;
323
+ width: calc(100% + 2 * var(--zui-text-editor-block-bleed, 0px));
324
+ max-width: calc(100% + 2 * var(--zui-text-editor-block-bleed, 0px));
325
+ margin-block: 1rem;
280
326
  border-collapse: separate;
281
327
  border-spacing: 0;
282
328
  border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
@@ -347,20 +393,28 @@
347
393
 
348
394
  /* Drawing canvas */
349
395
  .zui-drawing {
350
- margin: 1rem 0;
351
- }
352
-
353
- .zui-drawing-canvas.is-content-width {
354
- width: fit-content;
355
- max-width: 100%;
396
+ margin-block: 1rem;
356
397
  }
357
398
 
358
399
  .zui-drawing-canvas {
400
+ --zui-drawing-accent: #6965db;
359
401
  position: relative;
360
- border-radius: 0.75rem;
402
+ border: 1px solid color-mix(in srgb, currentColor 10%, transparent);
403
+ border-radius: 0.875rem;
361
404
  background: #ffffff;
362
405
  outline: none;
363
406
  overflow: hidden;
407
+ transition: box-shadow 0.18s ease, border-color 0.18s ease;
408
+ }
409
+
410
+ .zui-drawing-canvas.is-editable:focus-within {
411
+ border-color: color-mix(in srgb, var(--zui-drawing-accent) 45%, transparent);
412
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--zui-drawing-accent) 12%, transparent);
413
+ }
414
+
415
+ .zui-drawing-canvas.is-content-width {
416
+ width: fit-content;
417
+ max-width: 100%;
364
418
  }
365
419
 
366
420
  .zui-drawing-surface {
@@ -369,121 +423,242 @@
369
423
  max-width: 100%;
370
424
  touch-action: none;
371
425
  background-color: #ffffff;
372
- background-image: radial-gradient(
373
- circle,
374
- rgba(0, 0, 0, 0.07) 1px,
375
- transparent 1px
376
- );
426
+ background-image: radial-gradient(circle, rgba(0, 0, 0, 0.09) 1px, transparent 1px);
377
427
  background-size: 20px 20px;
378
428
  }
379
429
 
380
- /*
381
- * Dark mode (zui `.dark` class convention). The drawing surface, the color
382
- * swatches, and the inline text input are darkened Excalidraw-style with a
383
- * color-inversion filter, so stored shape colors stay theme-agnostic: the
384
- * default near-black stroke renders light on the dark canvas, and the
385
- * swatches preview exactly what will be drawn.
386
- */
387
- .dark .zui-drawing-canvas {
388
- background: #18181b;
389
- }
390
-
391
- .dark .zui-drawing-surface,
392
- .dark .zui-drawing-swatch,
393
- .dark .zui-drawing-text-input {
394
- filter: invert(93%) hue-rotate(180deg);
430
+ .zui-drawing-shape {
431
+ transition: filter 0.12s ease;
395
432
  }
396
433
 
397
- .dark .zui-drawing-toolbar-group {
398
- background: #232329;
399
- border-color: rgba(255, 255, 255, 0.1);
400
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
434
+ .zui-drawing-canvas.is-editable .zui-drawing-shape:hover:not(.is-selected) {
435
+ filter: drop-shadow(0 0 2.5px color-mix(in srgb, var(--zui-drawing-accent) 55%, transparent));
401
436
  }
402
437
 
403
- .dark .zui-drawing-tool {
404
- color: #e3e3e8;
438
+ .zui-drawing-handle {
439
+ filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.25));
405
440
  }
406
441
 
407
- .dark .zui-drawing-tool:hover {
408
- background: rgba(255, 255, 255, 0.08);
442
+ .zui-drawing-ghost {
443
+ opacity: 0.55;
444
+ transition: opacity 0.12s ease;
409
445
  }
410
446
 
411
- .dark .zui-drawing-tool.is-active {
412
- background: #45437a;
413
- color: #dcdaff;
447
+ .zui-drawing-ghost:hover {
448
+ opacity: 1;
414
449
  }
415
450
 
416
- .dark .zui-drawing-swatch-label {
417
- color: rgba(255, 255, 255, 0.55);
451
+ .zui-drawing-bind-target {
452
+ animation: zui-drawing-pulse 0.9s ease-in-out infinite alternate;
418
453
  }
419
454
 
420
- .dark .zui-drawing-resize {
421
- background: rgba(255, 255, 255, 0.2);
455
+ @keyframes zui-drawing-pulse {
456
+ from {
457
+ stroke-opacity: 0.55;
458
+ }
459
+ to {
460
+ stroke-opacity: 1;
461
+ }
422
462
  }
423
463
 
424
- .dark .zui-drawing-resize:hover {
425
- background: rgba(255, 255, 255, 0.35);
464
+ @keyframes zui-drawing-pop {
465
+ from {
466
+ opacity: 0;
467
+ transform: translateY(4px) scale(0.98);
468
+ }
469
+ to {
470
+ opacity: 1;
471
+ transform: translateY(0) scale(1);
472
+ }
426
473
  }
427
474
 
475
+ /* Floating toolbar */
428
476
  .zui-drawing-toolbar {
429
477
  position: absolute;
430
- top: 0.5rem;
431
- left: 0.5rem;
432
- right: 0.5rem;
478
+ top: 0.625rem;
479
+ left: 0.625rem;
480
+ right: 0.625rem;
433
481
  z-index: 5;
434
482
  display: flex;
435
483
  flex-wrap: wrap;
436
- align-items: center;
484
+ align-items: flex-start;
437
485
  gap: 0.5rem;
438
486
  pointer-events: none;
439
487
  }
440
488
 
441
- .zui-drawing-toolbar-group {
489
+ .zui-drawing-toolbar-group,
490
+ .zui-drawing-props {
442
491
  display: flex;
443
492
  align-items: center;
444
493
  gap: 0.125rem;
445
494
  padding: 0.25rem;
446
- border: 1px solid rgba(0, 0, 0, 0.08);
447
- border-radius: 0.5rem;
448
- background: #ffffff;
449
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
495
+ border: 1px solid rgba(0, 0, 0, 0.07);
496
+ border-radius: 0.625rem;
497
+ background: rgba(255, 255, 255, 0.88);
498
+ backdrop-filter: blur(10px);
499
+ -webkit-backdrop-filter: blur(10px);
500
+ box-shadow:
501
+ 0 1px 2px rgba(0, 0, 0, 0.05),
502
+ 0 6px 20px -8px rgba(0, 0, 0, 0.18);
450
503
  pointer-events: auto;
451
504
  }
452
505
 
453
- /* Canvas-level controls (width) sit at the far right of the toolbar row */
454
506
  .zui-drawing-toolbar-group-end {
455
507
  margin-left: auto;
456
508
  }
457
509
 
510
+ .zui-drawing-toolbar-divider {
511
+ width: 1px;
512
+ height: 1.125rem;
513
+ margin: 0 0.25rem;
514
+ background: rgba(0, 0, 0, 0.1);
515
+ }
516
+
458
517
  .zui-drawing-tool {
518
+ position: relative;
459
519
  display: inline-flex;
460
520
  align-items: center;
461
521
  justify-content: center;
462
- width: 1.75rem;
463
- height: 1.75rem;
522
+ width: 1.875rem;
523
+ height: 1.875rem;
464
524
  border: none;
465
- border-radius: 0.375rem;
525
+ border-radius: 0.5rem;
466
526
  background: transparent;
467
527
  color: #1e1e1e;
468
528
  cursor: pointer;
529
+ transition:
530
+ background-color 0.12s ease,
531
+ color 0.12s ease,
532
+ transform 0.12s ease;
469
533
  }
470
534
 
471
535
  .zui-drawing-tool:hover {
472
536
  background: rgba(0, 0, 0, 0.06);
473
537
  }
474
538
 
539
+ .zui-drawing-tool:active {
540
+ transform: scale(0.94);
541
+ }
542
+
475
543
  .zui-drawing-tool.is-active {
476
- background: #e0dfff;
544
+ background: color-mix(in srgb, var(--zui-drawing-accent) 16%, transparent);
477
545
  color: #4a47b1;
478
546
  }
479
547
 
548
+ .zui-drawing-tool.is-success {
549
+ color: #2f9e44;
550
+ }
551
+
480
552
  .zui-drawing-tool-danger {
481
553
  color: #e03131;
482
554
  }
483
555
 
556
+ .zui-drawing-tool-danger:hover {
557
+ background: rgba(224, 49, 49, 0.1);
558
+ }
559
+
560
+ .zui-drawing-tool-more {
561
+ width: 2.375rem;
562
+ }
563
+
564
+ .zui-drawing-caret {
565
+ position: absolute;
566
+ right: 0.3rem;
567
+ bottom: 0.3rem;
568
+ width: 0;
569
+ height: 0;
570
+ border-left: 4px solid transparent;
571
+ border-top: 4px solid currentColor;
572
+ opacity: 0.6;
573
+ }
574
+
575
+ .zui-drawing-more {
576
+ position: relative;
577
+ }
578
+
579
+ .zui-drawing-popover {
580
+ position: absolute;
581
+ top: calc(100% + 0.375rem);
582
+ left: 0;
583
+ z-index: 7;
584
+ display: flex;
585
+ flex-direction: column;
586
+ gap: 0.125rem;
587
+ min-width: 9.5rem;
588
+ padding: 0.25rem;
589
+ border: 1px solid rgba(0, 0, 0, 0.08);
590
+ border-radius: 0.625rem;
591
+ background: #ffffff;
592
+ box-shadow:
593
+ 0 1px 2px rgba(0, 0, 0, 0.06),
594
+ 0 10px 28px -8px rgba(0, 0, 0, 0.25);
595
+ animation: zui-drawing-pop 0.14s ease-out;
596
+ transform-origin: top left;
597
+ }
598
+
599
+ .zui-drawing-popover-item {
600
+ display: flex;
601
+ align-items: center;
602
+ gap: 0.5rem;
603
+ padding: 0.375rem 0.5rem;
604
+ border: none;
605
+ border-radius: 0.375rem;
606
+ background: transparent;
607
+ color: #1e1e1e;
608
+ font-size: 0.8125rem;
609
+ text-align: left;
610
+ cursor: pointer;
611
+ transition: background-color 0.12s ease;
612
+ }
613
+
614
+ .zui-drawing-popover-item:hover {
615
+ background: rgba(0, 0, 0, 0.06);
616
+ }
617
+
618
+ .zui-drawing-popover-item.is-active {
619
+ background: color-mix(in srgb, var(--zui-drawing-accent) 16%, transparent);
620
+ color: #4a47b1;
621
+ }
622
+
623
+ /* Contextual property bar: its own row under the tools */
624
+ .zui-drawing-props-host {
625
+ flex-basis: 100%;
626
+ display: flex;
627
+ opacity: 0;
628
+ transform: translateY(-4px);
629
+ pointer-events: none;
630
+ transition:
631
+ opacity 0.16s ease,
632
+ transform 0.16s ease;
633
+ }
634
+
635
+ .zui-drawing-props-host.is-visible {
636
+ opacity: 1;
637
+ transform: translateY(0);
638
+ pointer-events: auto;
639
+ }
640
+
641
+ .zui-drawing-props {
642
+ flex-wrap: wrap;
643
+ gap: 0.25rem;
644
+ padding: 0.25rem 0.375rem;
645
+ }
646
+
647
+ .zui-drawing-props-group {
648
+ display: flex;
649
+ align-items: center;
650
+ gap: 0.125rem;
651
+ }
652
+
653
+ .zui-drawing-props-group + .zui-drawing-props-group {
654
+ padding-left: 0.375rem;
655
+ border-left: 1px solid rgba(0, 0, 0, 0.1);
656
+ }
657
+
484
658
  .zui-drawing-swatch-label {
485
- margin: 0 0.25rem;
659
+ margin: 0 0.25rem 0 0.125rem;
486
660
  font-size: 0.6875rem;
661
+ letter-spacing: 0.02em;
487
662
  color: rgba(0, 0, 0, 0.5);
488
663
  user-select: none;
489
664
  }
@@ -491,58 +666,173 @@
491
666
  .zui-drawing-swatch {
492
667
  width: 1.125rem;
493
668
  height: 1.125rem;
494
- margin: 0 0.0625rem;
495
- border: 1px solid rgba(0, 0, 0, 0.15);
496
- border-radius: 0.3125rem;
497
- cursor: pointer;
669
+ margin: 0 0.125rem;
498
670
  padding: 0;
671
+ border: 1px solid rgba(0, 0, 0, 0.14);
672
+ border-radius: 999px;
673
+ cursor: pointer;
674
+ transition:
675
+ transform 0.12s ease,
676
+ box-shadow 0.12s ease;
677
+ }
678
+
679
+ .zui-drawing-swatch:hover {
680
+ transform: scale(1.12);
499
681
  }
500
682
 
501
683
  .zui-drawing-swatch.is-active {
502
- outline: 2px solid #6965db;
503
- outline-offset: 1px;
684
+ box-shadow:
685
+ 0 0 0 2px #ffffff,
686
+ 0 0 0 3.5px var(--zui-drawing-accent);
504
687
  }
505
688
 
506
689
  .zui-drawing-swatch.is-transparent {
507
690
  background-image: linear-gradient(
508
691
  45deg,
509
- transparent 46%,
510
- #e03131 46%,
511
- #e03131 54%,
512
- transparent 54%
692
+ transparent 44%,
693
+ #e03131 44%,
694
+ #e03131 56%,
695
+ transparent 56%
513
696
  );
514
697
  }
515
698
 
699
+ /* Empty state */
700
+ .zui-drawing-empty {
701
+ position: absolute;
702
+ inset: 0;
703
+ display: flex;
704
+ align-items: center;
705
+ justify-content: center;
706
+ padding-top: 2rem;
707
+ font-size: 0.8125rem;
708
+ color: rgba(0, 0, 0, 0.4);
709
+ pointer-events: none;
710
+ user-select: none;
711
+ }
712
+
713
+ /* Inline text editing */
714
+ .zui-drawing-overlay {
715
+ position: absolute;
716
+ top: 0;
717
+ left: 0;
718
+ right: 0;
719
+ bottom: 0;
720
+ transform-origin: top left;
721
+ pointer-events: none;
722
+ }
723
+
516
724
  .zui-drawing-text-input {
517
725
  position: absolute;
518
726
  z-index: 6;
519
- padding: 2px 4px;
520
- border: 1px dashed #6965db;
521
- border-radius: 0.25rem;
522
- background: rgba(255, 255, 255, 0.9);
523
- font-family: ui-sans-serif, system-ui, sans-serif;
524
- line-height: 1.35;
727
+ margin: 0;
728
+ padding: 0;
729
+ border: 0;
730
+ border-radius: 3px;
731
+ outline: 1.5px dashed var(--zui-drawing-accent, #6965db);
732
+ outline-offset: 3px;
733
+ background: rgba(255, 255, 255, 0.85);
734
+ color: inherit;
525
735
  resize: none;
526
- outline: none;
527
736
  overflow: hidden;
528
- white-space: pre;
737
+ box-sizing: content-box;
738
+ pointer-events: auto;
529
739
  }
530
740
 
741
+ .zui-drawing-text-input::placeholder {
742
+ color: currentColor;
743
+ opacity: 0.35;
744
+ }
745
+
746
+ /* Canvas height grip (bottom-right) */
531
747
  .zui-drawing-resize {
532
748
  position: absolute;
533
- bottom: 0.25rem;
534
- left: 50%;
535
- transform: translateX(-50%);
536
- width: 3rem;
537
- height: 0.375rem;
538
- border-radius: 999px;
539
- background: rgba(0, 0, 0, 0.15);
749
+ right: 0.375rem;
750
+ bottom: 0.375rem;
751
+ display: inline-flex;
752
+ align-items: center;
753
+ justify-content: center;
754
+ width: 1.375rem;
755
+ height: 1.375rem;
756
+ border-radius: 0.375rem;
757
+ color: #1e1e1e;
758
+ opacity: 0.35;
540
759
  cursor: ns-resize;
541
760
  touch-action: none;
761
+ transition:
762
+ opacity 0.12s ease,
763
+ background-color 0.12s ease;
542
764
  }
543
765
 
544
766
  .zui-drawing-resize:hover {
545
- background: rgba(0, 0, 0, 0.3);
767
+ opacity: 0.9;
768
+ background: rgba(0, 0, 0, 0.06);
769
+ }
770
+
771
+ /*
772
+ * Dark mode (zui `.dark` class convention). The drawing surface, the color
773
+ * swatches, and the inline text input are darkened with a color-inversion
774
+ * filter, so stored shape colors stay theme-agnostic: the default
775
+ * near-black stroke renders light on the dark canvas, and the swatches
776
+ * preview exactly what will be drawn.
777
+ */
778
+ .dark .zui-drawing-canvas {
779
+ background: #18181b;
780
+ border-color: rgba(255, 255, 255, 0.1);
781
+ }
782
+
783
+ .dark .zui-drawing-surface,
784
+ .dark .zui-drawing-swatch,
785
+ .dark .zui-drawing-text-input {
786
+ filter: invert(93%) hue-rotate(180deg);
787
+ }
788
+
789
+ .dark .zui-drawing-toolbar-group,
790
+ .dark .zui-drawing-props,
791
+ .dark .zui-drawing-popover {
792
+ background: rgba(35, 35, 41, 0.9);
793
+ border-color: rgba(255, 255, 255, 0.1);
794
+ box-shadow:
795
+ 0 1px 2px rgba(0, 0, 0, 0.3),
796
+ 0 8px 24px -8px rgba(0, 0, 0, 0.6);
797
+ }
798
+
799
+ .dark .zui-drawing-tool,
800
+ .dark .zui-drawing-popover-item,
801
+ .dark .zui-drawing-resize {
802
+ color: #e3e3e8;
803
+ }
804
+
805
+ .dark .zui-drawing-tool:hover,
806
+ .dark .zui-drawing-popover-item:hover,
807
+ .dark .zui-drawing-resize:hover {
808
+ background: rgba(255, 255, 255, 0.08);
809
+ }
810
+
811
+ .dark .zui-drawing-tool.is-active,
812
+ .dark .zui-drawing-popover-item.is-active {
813
+ background: #45437a;
814
+ color: #dcdaff;
815
+ }
816
+
817
+ .dark .zui-drawing-toolbar-divider,
818
+ .dark .zui-drawing-props-group + .zui-drawing-props-group {
819
+ border-color: rgba(255, 255, 255, 0.12);
820
+ background-color: rgba(255, 255, 255, 0.12);
821
+ }
822
+
823
+ .dark .zui-drawing-props-group + .zui-drawing-props-group {
824
+ background-color: transparent;
825
+ }
826
+
827
+ .dark .zui-drawing-swatch-label,
828
+ .dark .zui-drawing-empty {
829
+ color: rgba(255, 255, 255, 0.55);
830
+ }
831
+
832
+ .dark .zui-drawing-swatch.is-active {
833
+ box-shadow:
834
+ 0 0 0 2px #232329,
835
+ 0 0 0 3.5px #8f8bff;
546
836
  }
547
837
 
548
838
  .zui-text-editor-textarea {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuilib/text-editor",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "ZUI — A markdown editor component wrapping Lexical",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "tsup",
18
- "test": "node tests/roundtrip.mjs"
18
+ "test": "node tests/roundtrip.mjs && node --test tests/*.test.mjs"
19
19
  },
20
20
  "exports": {
21
21
  ".": {