@threadlabs/looma 0.16.0 → 0.16.2

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/vue/UiIcon.vue CHANGED
@@ -281,6 +281,10 @@ const shapes = computed(() =>
281
281
  tag: 'path',
282
282
  d: 'M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18',
283
283
  }],
284
+ tag: [{
285
+ tag: 'path',
286
+ d: 'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z',
287
+ }, { tag: 'circle', cx: '7.5', cy: '7.5', r: '.5', fill: 'currentColor' }],
284
288
  trash: [
285
289
  { tag: 'path', d: 'M10 11v6' },
286
290
  { tag: 'path', d: 'M14 11v6' },
@@ -24,6 +24,6 @@ var UiBadge_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE
24
24
  return openBlock(), createElementBlock("span", mergeProps({ "data-component": "ui-badge" }, _ctx.$attrs, { "data-ui-badge-state": hostState.value || void 0 }), [renderSlot(_ctx.$slots, "default", {}, void 0, true)], 16, _hoisted_1);
25
25
  };
26
26
  }
27
- }), [["__scopeId", "data-v-3485e390"]]);
27
+ }), [["__scopeId", "data-v-bea7b3f9"]]);
28
28
  //#endregion
29
29
  export { UiBadge_default as t };
@@ -879,6 +879,16 @@ var UiIcon_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
879
879
  tag: "path",
880
880
  d: "M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"
881
881
  }],
882
+ tag: [{
883
+ tag: "path",
884
+ d: "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"
885
+ }, {
886
+ tag: "circle",
887
+ cx: "7.5",
888
+ cy: "7.5",
889
+ r: ".5",
890
+ fill: "currentColor"
891
+ }],
882
892
  trash: [
883
893
  {
884
894
  tag: "path",
@@ -1042,6 +1052,6 @@ var UiIcon_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
1042
1052
  }), 128))]))], 16);
1043
1053
  };
1044
1054
  }
1045
- }), [["__scopeId", "data-v-50d3f581"]]);
1055
+ }), [["__scopeId", "data-v-988c6eae"]]);
1046
1056
  //#endregion
1047
1057
  export { UiIcon_default as t };
@@ -92,7 +92,7 @@
92
92
  syntax: "*";
93
93
  inherits: false;
94
94
  }
95
- [data-component~="ui-icon"][data-v-50d3f581] {
95
+ [data-component~="ui-icon"][data-v-988c6eae] {
96
96
  /* Hooks do not inherit, so the svg reads the stroke from the root. A Looma component that
97
97
  sizes the icons it renders sets --_ui-default-icon-*, which the icon's own hooks override. */
98
98
  --_ui-icon-stroke-width: var(--ui-icon-stroke-width, var(--_ui-default-icon-stroke-width));
@@ -102,14 +102,14 @@
102
102
  inline-size: var(--ui-icon-size, var(--_ui-default-icon-size, 1em));
103
103
  block-size: var(--ui-icon-size, var(--_ui-default-icon-size, 1em));
104
104
  }
105
- svg[data-v-50d3f581] {
105
+ svg[data-v-988c6eae] {
106
106
  inline-size: 100%;
107
107
  block-size: 100%;
108
108
  stroke-width: var(--_ui-icon-stroke-width, 2);
109
109
  }
110
110
 
111
111
  /* A component that sets its display still honours the hidden attribute on its root. */
112
- [data-component~="ui-icon"][hidden][data-v-50d3f581] {
112
+ [data-component~="ui-icon"][hidden][data-v-988c6eae] {
113
113
  display: none;
114
114
  }
115
115
 
@@ -314,11 +314,11 @@ svg[data-v-50d3f581] {
314
314
  syntax: "*";
315
315
  inherits: false;
316
316
  }
317
- [data-component~="ui-badge"][data-v-3485e390] {
317
+ [data-component~="ui-badge"][data-v-bea7b3f9] {
318
318
  --_badge-surface: var(--ui-surface-subtle);
319
319
  --_badge-text: var(--ui-text-secondary);
320
- /* Every tone's edge is its fill, so it is unseen until forced colors draw it. */
321
- --_badge-border: var(--ui-surface-subtle);
320
+ /* Sit between the soft fill and its stronger theme ink. */
321
+ --_badge-border: color-mix(in srgb, var(--_badge-surface) 75%, var(--_badge-text) 25%);
322
322
  /* --ui-badge-color derives a wash and an ink from one colour, between the tone and the explicit
323
323
  surface, text, and border hooks. Each value is invalid, so the tone shows, while it is unset. The
324
324
  mixes keep 4.5:1 text contrast for any colour, in a light theme or a dark one. */
@@ -326,7 +326,11 @@ svg[data-v-50d3f581] {
326
326
  --_badge-color-wash: color-mix(in oklab, var(--_ui-badge-color) 15%, var(--ui-surface-default));
327
327
  --_badge-color-ink: color-mix(in oklab, var(--_ui-badge-color) 35%, var(--ui-text-primary));
328
328
  --_badge-color-surface: var(--_badge-color-wash);
329
- --_badge-color-border: var(--_badge-color-wash);
329
+ --_badge-color-border: color-mix(
330
+ in srgb,
331
+ var(--_badge-color-wash) 75%,
332
+ var(--_badge-color-ink) 25%
333
+ );
330
334
  --_badge-color-text: var(--_badge-color-ink);
331
335
  --_badge-outline-surface: var(
332
336
  --ui-badge-surface,
@@ -354,7 +358,7 @@ svg[data-v-50d3f581] {
354
358
  * Trimming applies to block containers; an inline-block is one and, like the inline-flex it
355
359
  * replaces, still sizes to its label rather than filling its container. */
356
360
  @supports (text-box-trim: trim-both) {
357
- [data-component~="ui-badge"][data-v-3485e390] {
361
+ [data-component~="ui-badge"][data-v-bea7b3f9] {
358
362
  display: inline-block;
359
363
  min-block-size: var(--ui-badge-min-block-size, 1.5rem);
360
364
  text-align: center;
@@ -363,65 +367,60 @@ svg[data-v-50d3f581] {
363
367
  text-box-edge: cap alphabetic;
364
368
  }
365
369
  }
366
- [data-component~="ui-badge"][data-ui-badge-state~="tone=accent"][data-v-3485e390] {
370
+ [data-component~="ui-badge"][data-ui-badge-state~="tone=accent"][data-v-bea7b3f9] {
367
371
  --_badge-surface: var(--ui-accent-strong-surface, var(--ui-accent-solid));
368
372
  --_badge-border: var(--ui-accent-strong-surface, var(--ui-accent-solid));
369
373
  --_badge-text: var(--ui-accent-on-strong, var(--ui-text-on-accent));
370
374
  }
371
- [data-component~="ui-badge"][data-ui-badge-state~="tone=danger"][data-v-3485e390] {
375
+ [data-component~="ui-badge"][data-ui-badge-state~="tone=danger"][data-v-bea7b3f9] {
372
376
  --_badge-surface: var(--ui-danger-strong-surface, var(--ui-danger-solid));
373
377
  --_badge-border: var(--ui-danger-strong-surface, var(--ui-danger-solid));
374
378
  --_badge-text: var(--ui-danger-on-strong, var(--ui-text-on-accent));
375
379
  }
376
- [data-component~="ui-badge"][data-ui-badge-state~="tone=success"][data-v-3485e390] {
380
+ [data-component~="ui-badge"][data-ui-badge-state~="tone=success"][data-v-bea7b3f9] {
377
381
  --_badge-surface: var(--ui-success-strong-surface, var(--ui-success-solid));
378
382
  --_badge-border: var(--ui-success-strong-surface, var(--ui-success-solid));
379
383
  --_badge-text: var(--ui-success-on-strong, var(--ui-text-on-accent));
380
384
  }
381
- [data-component~="ui-badge"][data-ui-badge-state~="tone=warning"][data-v-3485e390] {
385
+ [data-component~="ui-badge"][data-ui-badge-state~="tone=warning"][data-v-bea7b3f9] {
382
386
  --_badge-surface: var(--ui-warning-strong-surface, var(--ui-warning-solid));
383
387
  --_badge-border: var(--ui-warning-strong-surface, var(--ui-warning-solid));
384
388
  --_badge-text: var(--ui-warning-on-strong, var(--ui-text-on-accent));
385
389
  }
386
- [data-component~="ui-badge"][data-ui-badge-state~="tone=info"][data-v-3485e390] {
390
+ [data-component~="ui-badge"][data-ui-badge-state~="tone=info"][data-v-bea7b3f9] {
387
391
  --_badge-surface: var(--ui-info-strong-surface, var(--ui-info-solid));
388
392
  --_badge-border: var(--ui-info-strong-surface, var(--ui-info-solid));
389
393
  --_badge-text: var(--ui-info-on-strong, var(--ui-text-on-accent));
390
394
  }
391
- [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=accent"][data-v-3485e390] {
395
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=accent"][data-v-bea7b3f9] {
392
396
  --_badge-surface: var(--ui-accent-soft);
393
- --_badge-border: var(--ui-accent-soft);
394
397
  --_badge-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
395
398
  }
396
- [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=danger"][data-v-3485e390] {
399
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=danger"][data-v-bea7b3f9] {
397
400
  --_badge-surface: var(--ui-danger-soft);
398
- --_badge-border: var(--ui-danger-soft);
399
401
  --_badge-text: var(--ui-danger-subtle-text, var(--ui-danger-solid));
400
402
  }
401
- [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=success"][data-v-3485e390] {
403
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=success"][data-v-bea7b3f9] {
402
404
  --_badge-surface: var(--ui-success-soft);
403
- --_badge-border: var(--ui-success-soft);
404
405
  --_badge-text: var(--ui-success-subtle-text, var(--ui-success-solid));
405
406
  }
406
- [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=warning"][data-v-3485e390] {
407
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=warning"][data-v-bea7b3f9] {
407
408
  --_badge-surface: var(--ui-warning-soft);
408
- --_badge-border: var(--ui-warning-soft);
409
409
  --_badge-text: var(--ui-warning-subtle-text, var(--ui-warning-solid));
410
410
  }
411
- [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=info"][data-v-3485e390] {
411
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=subtle"][data-ui-badge-state~="tone=info"][data-v-bea7b3f9] {
412
412
  --_badge-surface: var(--ui-info-soft);
413
- --_badge-border: var(--ui-info-soft);
414
413
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
415
414
  }
416
415
 
417
416
  /* A solid colour fills with the ink. Its text is the page surface, mixed at 0% only so that it too is
418
417
  invalid while the hook is unset. */
419
- [data-component~="ui-badge"][data-ui-badge-state~="variant=solid"][data-v-3485e390] {
418
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=solid"][data-v-bea7b3f9] {
420
419
  --_badge-color-surface: var(--_badge-color-ink);
421
420
  --_badge-color-border: var(--_badge-color-ink);
422
421
  --_badge-color-text: color-mix(in oklab, var(--_ui-badge-color) 0%, var(--ui-surface-default));
423
422
  }
424
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-v-3485e390] {
423
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-v-bea7b3f9] {
425
424
  --_badge-surface: var(--ui-surface-default);
426
425
  --_badge-border: var(--ui-border-strong);
427
426
  --_badge-text: var(--ui-text-secondary);
@@ -429,23 +428,23 @@ svg[data-v-50d3f581] {
429
428
  --_badge-color-surface: initial;
430
429
  --_badge-color-border: var(--_badge-color-ink);
431
430
  }
432
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=accent"][data-v-3485e390] {
431
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=accent"][data-v-bea7b3f9] {
433
432
  --_badge-border: var(--ui-accent-solid);
434
433
  --_badge-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
435
434
  }
436
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=danger"][data-v-3485e390] {
435
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=danger"][data-v-bea7b3f9] {
437
436
  --_badge-border: var(--ui-danger-solid);
438
437
  --_badge-text: var(--ui-danger-subtle-text, var(--ui-danger-solid));
439
438
  }
440
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=success"][data-v-3485e390] {
439
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=success"][data-v-bea7b3f9] {
441
440
  --_badge-border: var(--ui-success-solid);
442
441
  --_badge-text: var(--ui-success-subtle-text, var(--ui-success-solid));
443
442
  }
444
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=warning"][data-v-3485e390] {
443
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=warning"][data-v-bea7b3f9] {
445
444
  --_badge-border: var(--ui-warning-solid);
446
445
  --_badge-text: var(--ui-warning-subtle-text, var(--ui-warning-solid));
447
446
  }
448
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=info"][data-v-3485e390] {
447
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="tone=info"][data-v-bea7b3f9] {
449
448
  --_badge-border: var(--ui-info-solid);
450
449
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
451
450
  }
@@ -453,7 +452,7 @@ svg[data-v-50d3f581] {
453
452
  /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
454
453
  outline to follow: the edge is the fill. The end padding grows by the point's depth so the
455
454
  label never runs into it. */
456
- [data-component~="ui-badge"][data-ui-badge-state~="shape=tag"][data-v-3485e390] {
455
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9] {
457
456
  --_badge-point: var(--ui-badge-point, var(--ui-space-2));
458
457
 
459
458
  border-color: transparent;
@@ -467,7 +466,7 @@ svg[data-v-50d3f581] {
467
466
  0 100%
468
467
  );
469
468
  }
470
- [data-component~="ui-badge"][data-ui-badge-state~="shape=tag"][data-v-3485e390]:dir(rtl) {
469
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9]:dir(rtl) {
471
470
  padding-inline-end: var(--ui-badge-padding-inline, var(--ui-space-2));
472
471
  padding-inline-start: calc(
473
472
  var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point)
@@ -477,12 +476,12 @@ svg[data-v-50d3f581] {
477
476
 
478
477
  /* The tag clips its point, including the native border. Draw the outer shape in the border
479
478
  colour and an inset inner shape in the surface colour to keep a visible diagonal edge. */
480
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-3485e390] {
479
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9] {
481
480
  position: relative;
482
481
  isolation: isolate;
483
482
  background: var(--ui-badge-border, var(--_badge-color-border, var(--_badge-border)));
484
483
  }
485
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-3485e390]::before {
484
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9]::before {
486
485
  content: "";
487
486
  position: absolute;
488
487
  inset: 1px;
@@ -497,22 +496,22 @@ svg[data-v-50d3f581] {
497
496
  );
498
497
  pointer-events: none;
499
498
  }
500
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-3485e390]:dir(rtl)::before {
499
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9]:dir(rtl)::before {
501
500
  clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
502
501
  }
503
502
  @media (forced-colors: active) {
504
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-3485e390] {
503
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9] {
505
504
  forced-color-adjust: none;
506
505
  background: CanvasText;
507
506
  color: CanvasText;
508
507
  }
509
- [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-3485e390]::before {
508
+ [data-component~="ui-badge"][data-ui-badge-state~="variant=outline"][data-ui-badge-state~="shape=tag"][data-v-bea7b3f9]::before {
510
509
  background: Canvas;
511
510
  }
512
511
  }
513
512
 
514
513
  /* A component that sets its display still honours the hidden attribute on its root. */
515
- [data-component~="ui-badge"][hidden][data-v-3485e390] {
514
+ [data-component~="ui-badge"][hidden][data-v-bea7b3f9] {
516
515
  display: none;
517
516
  }
518
517
 
@@ -8577,6 +8576,7 @@ html[data-ui-input-modality="touch"]
8577
8576
 
8578
8577
  .looma-editor__table-picker-popover,
8579
8578
  .looma-editor__link-popover,
8579
+ .looma-editor__chip-popover,
8580
8580
  .looma-editor__table-toolbar-shell,
8581
8581
  .looma-editor__table-menu-shell {
8582
8582
  z-index: var(--ui-editor-popover-z-index, 300);
@@ -8593,6 +8593,55 @@ html[data-ui-input-modality="touch"]
8593
8593
  inline-size: min(22rem, calc(100vw - 2rem));
8594
8594
  }
8595
8595
 
8596
+ .looma-editor__chip-popover {
8597
+ inline-size: min(15rem, calc(100vw - 2rem));
8598
+ }
8599
+
8600
+ .looma-editor__chip-form {
8601
+ display: flex;
8602
+ flex-direction: column;
8603
+ gap: var(--ui-space-3);
8604
+ }
8605
+
8606
+ .looma-editor__chip-form input {
8607
+ box-sizing: border-box;
8608
+ width: 100%;
8609
+ min-height: var(--ui-control-min-block-size);
8610
+ padding: var(--ui-space-2);
8611
+ border: 1px solid var(--ui-control-border);
8612
+ border-radius: var(--ui-field-radius, var(--ui-radius-md));
8613
+ background: var(--ui-control-surface);
8614
+ color: var(--ui-text-primary);
8615
+ font: inherit;
8616
+ }
8617
+
8618
+ .looma-editor__chip-colors {
8619
+ display: flex;
8620
+ justify-content: space-between;
8621
+ gap: var(--ui-space-1);
8622
+ }
8623
+
8624
+ .looma-editor__chip-color {
8625
+ display: grid;
8626
+ place-items: center;
8627
+ width: 1.65rem;
8628
+ height: 1.65rem;
8629
+ padding: 0;
8630
+ border: 1px solid var(--_chip-border);
8631
+ border-radius: 50%;
8632
+ background: var(--_chip-surface);
8633
+ color: var(--_chip-text);
8634
+ font: inherit;
8635
+ font-size: var(--ui-font-size-xs);
8636
+ cursor: pointer;
8637
+ }
8638
+
8639
+ .looma-editor__chip-form :is(input, button):focus-visible,
8640
+ .looma-editor .ProseMirror [data-looma-chip]:focus-visible {
8641
+ outline: 2px solid var(--ui-focus-ring);
8642
+ outline-offset: 2px;
8643
+ }
8644
+
8596
8645
  .looma-editor__link-form,
8597
8646
  .looma-editor__link-form label {
8598
8647
  display: flex;
@@ -8741,6 +8790,59 @@ html[data-ui-input-modality="touch"]
8741
8790
  white-space: nowrap;
8742
8791
  }
8743
8792
 
8793
+ .looma-editor .ProseMirror [data-looma-chip],
8794
+ .looma-editor__chip-color {
8795
+ --_chip-tone-surface: var(--ui-surface-subtle);
8796
+ --_chip-tone-text: var(--ui-text-secondary);
8797
+ --_chip-surface: var(--ui-editor-chip-surface, var(--_chip-tone-surface));
8798
+ --_chip-text: var(--ui-editor-chip-text, var(--_chip-tone-text));
8799
+ --_chip-border: var(--ui-editor-chip-border, color-mix(in srgb, var(--_chip-surface) 75%, var(--_chip-text) 25%));
8800
+ }
8801
+
8802
+ .looma-editor .ProseMirror [data-looma-chip][data-color="blue"],
8803
+ .looma-editor__chip-color[data-color="blue"] {
8804
+ --_chip-tone-surface: var(--ui-info-soft);
8805
+ --_chip-tone-text: var(--ui-info-subtle-text);
8806
+ }
8807
+
8808
+ .looma-editor .ProseMirror [data-looma-chip][data-color="green"],
8809
+ .looma-editor__chip-color[data-color="green"] {
8810
+ --_chip-tone-surface: var(--ui-success-soft);
8811
+ --_chip-tone-text: var(--ui-success-subtle-text);
8812
+ }
8813
+
8814
+ .looma-editor .ProseMirror [data-looma-chip][data-color="yellow"],
8815
+ .looma-editor__chip-color[data-color="yellow"] {
8816
+ --_chip-tone-surface: var(--ui-warning-soft);
8817
+ --_chip-tone-text: var(--ui-warning-subtle-text);
8818
+ }
8819
+
8820
+ .looma-editor .ProseMirror [data-looma-chip][data-color="red"],
8821
+ .looma-editor__chip-color[data-color="red"] {
8822
+ --_chip-tone-surface: var(--ui-danger-soft);
8823
+ --_chip-tone-text: var(--ui-danger-subtle-text);
8824
+ }
8825
+
8826
+ .looma-editor .ProseMirror [data-looma-chip][data-color="purple"],
8827
+ .looma-editor__chip-color[data-color="purple"] {
8828
+ --_chip-tone-surface: var(--ui-accent-soft);
8829
+ --_chip-tone-text: var(--ui-accent-subtle-text);
8830
+ }
8831
+
8832
+ .looma-editor .ProseMirror [data-looma-chip] {
8833
+ padding: 0.08em 0.45em;
8834
+ border: 1px solid var(--_chip-border);
8835
+ border-radius: var(--ui-radius-round);
8836
+ background: var(--_chip-surface);
8837
+ color: var(--_chip-text);
8838
+ font-size: var(--ui-editor-chip-font-size, var(--ui-font-size-sm));
8839
+ font-weight: var(--ui-font-medium);
8840
+ line-height: var(--ui-line-height-tight);
8841
+ -webkit-box-decoration-break: clone;
8842
+ box-decoration-break: clone;
8843
+ cursor: pointer;
8844
+ }
8845
+
8744
8846
  .ProseMirror .tableWrapper {
8745
8847
  max-width: 100%;
8746
8848
  overflow-x: auto;
@@ -1,5 +1,5 @@
1
1
  import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
2
- import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
2
+ import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
3
3
  import { EditorInsertTableGrid, EditorMentionMenu, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Menu, MenuItem, Popover, Tooltip } from "@threadlabs/looma/vue";
4
4
  import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
5
5
  import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
@@ -353,6 +353,11 @@ var LoomaEditor = defineComponent({
353
353
  const linkText = ref("");
354
354
  const linkNewTab = ref(true);
355
355
  const linkError = ref("");
356
+ const chipAnchorId = `looma-editor-chip-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
357
+ const chipInput = ref(null);
358
+ const chipOpen = ref(false);
359
+ const chipLabel = ref("");
360
+ const chipColor = ref("neutral");
356
361
  let linkSelection = null;
357
362
  const toolbarScope = `looma-editor-tool-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
358
363
  const tooltipFor = ref("");
@@ -435,8 +440,43 @@ var LoomaEditor = defineComponent({
435
440
  menuStyle: {},
436
441
  capabilities: { ...EMPTY_CAPABILITIES }
437
442
  });
443
+ const openChipEditor = (instance, position) => {
444
+ if (!props.editable) return;
445
+ const node = instance.state.doc.nodeAt(position);
446
+ const element = instance.view.nodeDOM(position);
447
+ if (node?.type.name !== "loomaChip" || !(element instanceof HTMLElement)) return;
448
+ root.value?.querySelector(`#${chipAnchorId}`)?.removeAttribute("id");
449
+ element.id = chipAnchorId;
450
+ chipLabel.value = String(node.attrs.label ?? "");
451
+ chipColor.value = normalizeLoomaChipColor(node.attrs.color);
452
+ chipOpen.value = true;
453
+ nextTick(() => chipInput.value?.focus());
454
+ };
455
+ const changeChip = (changes) => {
456
+ const instance = editor.value;
457
+ const element = root.value?.querySelector(`#${chipAnchorId}`);
458
+ if (!instance || !element) {
459
+ chipOpen.value = false;
460
+ return;
461
+ }
462
+ const position = instance.view.posAtDOM(element, 0);
463
+ const node = instance.state.doc.nodeAt(position);
464
+ if (node?.type.name !== "loomaChip") {
465
+ chipOpen.value = false;
466
+ return;
467
+ }
468
+ instance.view.dispatch(instance.state.tr.setNodeMarkup(position, void 0, {
469
+ ...node.attrs,
470
+ ...changes
471
+ }));
472
+ };
473
+ const closeChip = (focusChip = false) => {
474
+ chipOpen.value = false;
475
+ if (focusChip) root.value?.querySelector(`#${chipAnchorId}`)?.focus();
476
+ };
438
477
  const slashExtension = createLoomaSlashCommandExtension({
439
478
  onOpenImagePicker: () => fileInput.value?.click(),
479
+ onOpenChipEditor: openChipEditor,
440
480
  onStateChange: (state) => {
441
481
  Object.assign(slash, state);
442
482
  }
@@ -602,7 +642,13 @@ var LoomaEditor = defineComponent({
602
642
  instance.view.dispatch(transaction);
603
643
  instance.commands.focus();
604
644
  };
605
- watch(() => props.editable, (editable) => editor.value?.setEditable(editable));
645
+ watch(() => props.editable, (editable) => {
646
+ editor.value?.setEditable(editable);
647
+ root.value?.querySelectorAll("[data-looma-chip]").forEach((chip) => {
648
+ chip.setAttribute("aria-disabled", String(!editable));
649
+ });
650
+ if (!editable) chipOpen.value = false;
651
+ });
606
652
  watch(() => props.label, (label) => {
607
653
  editor.value?.setOptions({ editorProps: { attributes: {
608
654
  role: "textbox",
@@ -796,6 +842,18 @@ var LoomaEditor = defineComponent({
796
842
  trigger
797
843
  });
798
844
  };
845
+ const chipElementForEvent = (event) => {
846
+ const element = event.target instanceof Element ? event.target.closest("[data-looma-chip]") : null;
847
+ return element && root.value?.contains(element) ? element : null;
848
+ };
849
+ const onChipClickCapture = (event) => {
850
+ const chip = chipElementForEvent(event);
851
+ if (chip && editor.value && props.editable) {
852
+ event.preventDefault();
853
+ event.stopPropagation();
854
+ openChipEditor(editor.value, editor.value.view.posAtDOM(chip, 0));
855
+ }
856
+ };
799
857
  const onImageClick = (event) => {
800
858
  const element = imageElementForEvent(event);
801
859
  if (element && !props.editable) activateImage(element, "pointer");
@@ -806,6 +864,12 @@ var LoomaEditor = defineComponent({
806
864
  };
807
865
  const onImageKeyDown = (event) => {
808
866
  if (event.key !== "Enter" && event.key !== " ") return;
867
+ const chip = chipElementForEvent(event);
868
+ if (chip && editor.value && props.editable) {
869
+ event.preventDefault();
870
+ openChipEditor(editor.value, editor.value.view.posAtDOM(chip, 0));
871
+ return;
872
+ }
809
873
  const element = imageElementForEvent(event);
810
874
  if (!element) return;
811
875
  event.preventDefault();
@@ -1149,6 +1213,7 @@ var LoomaEditor = defineComponent({
1149
1213
  }, 100);
1150
1214
  },
1151
1215
  onDrop,
1216
+ onClickCapture: onChipClickCapture,
1152
1217
  onClick: onImageClick,
1153
1218
  onDblclick: onImageDoubleClick,
1154
1219
  onKeydown: onImageKeyDown
@@ -1213,6 +1278,54 @@ var LoomaEditor = defineComponent({
1213
1278
  class: "looma-editor__drop-overlay",
1214
1279
  "aria-hidden": "true"
1215
1280
  }, [loomaIcon("image"), h("span", "Drop image to upload")]) : null,
1281
+ h(Popover, {
1282
+ class: "looma-editor__chip-popover",
1283
+ open: chipOpen.value,
1284
+ for: chipAnchorId,
1285
+ placement: mobile.value ? "top-start" : "bottom-start",
1286
+ onClose: () => {
1287
+ chipOpen.value = false;
1288
+ }
1289
+ }, () => h("div", {
1290
+ class: "looma-editor__chip-form",
1291
+ role: "dialog",
1292
+ "aria-label": "Edit chip"
1293
+ }, [h("input", {
1294
+ ref: chipInput,
1295
+ type: "text",
1296
+ value: chipLabel.value,
1297
+ placeholder: "Set a label",
1298
+ "aria-label": "Chip text",
1299
+ onInput: (event) => {
1300
+ chipLabel.value = event.target.value;
1301
+ changeChip({ label: chipLabel.value });
1302
+ },
1303
+ onKeydown: (event) => {
1304
+ if (event.key === "Enter" || event.key === "Escape") {
1305
+ event.preventDefault();
1306
+ closeChip(true);
1307
+ }
1308
+ }
1309
+ }), h("div", {
1310
+ class: "looma-editor__chip-colors",
1311
+ role: "group",
1312
+ "aria-label": "Chip color"
1313
+ }, LOOMA_CHIP_COLORS.map((color) => h("button", {
1314
+ type: "button",
1315
+ class: "looma-editor__chip-color",
1316
+ "data-color": color,
1317
+ "aria-label": `${color[0].toUpperCase()}${color.slice(1)} chip`,
1318
+ "aria-pressed": chipColor.value === color,
1319
+ onClick: () => {
1320
+ if (chipColor.value === color) {
1321
+ chipInput.value?.focus();
1322
+ return;
1323
+ }
1324
+ chipColor.value = color;
1325
+ changeChip({ color });
1326
+ chipInput.value?.focus();
1327
+ }
1328
+ }, chipColor.value === color ? "✓" : "")))])),
1216
1329
  h(Popover, {
1217
1330
  class: "looma-editor__link-popover",
1218
1331
  open: linkOpen.value,