@threadlabs/looma 0.23.24 → 0.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.23.24",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -177,6 +177,7 @@
177
177
  :scope[data-ui-button-state~="variant=card"] {
178
178
  display: grid;
179
179
  grid-template-columns: auto minmax(0, 1fr) auto;
180
+ align-content: center;
180
181
  align-items: start;
181
182
  gap: var(--ui-space-3);
182
183
  inline-size: 100%;
package/vue/UiButton.vue CHANGED
@@ -1020,6 +1020,7 @@ function concat<T extends unknown[]>(...values: T): ConcatResult<T> {
1020
1020
  [data-component~="ui-button"][data-ui-button-state~="variant=card"] {
1021
1021
  display: grid;
1022
1022
  grid-template-columns: auto minmax(0, 1fr) auto;
1023
+ align-content: center;
1023
1024
  align-items: start;
1024
1025
  gap: var(--ui-space-3);
1025
1026
  inline-size: 100%;
@@ -755,6 +755,6 @@ var UiButton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PUR
755
755
  }]]);
756
756
  };
757
757
  }
758
- }), [["__scopeId", "data-v-e0bdc48b"]]);
758
+ }), [["__scopeId", "data-v-ecae2439"]]);
759
759
  //#endregion
760
760
  export { UiButton_default as t };
@@ -824,7 +824,7 @@ svg[data-v-1012d0f2],
824
824
  syntax: "*";
825
825
  inherits: false;
826
826
  }
827
- [data-component~="ui-button"][data-v-e0bdc48b] {
827
+ [data-component~="ui-button"][data-v-ecae2439] {
828
828
  --_hue: var(--ui-accent);
829
829
  --_hue-hover: var(--ui-accent-hover);
830
830
  --_hue-active: var(--ui-accent-active);
@@ -843,15 +843,15 @@ svg[data-v-1012d0f2],
843
843
  vertical-align: middle;
844
844
  color: var(--_tone-text);
845
845
  }
846
- [data-component~="ui-button"][data-ui-button-state~="align=start"][data-v-e0bdc48b] {
846
+ [data-component~="ui-button"][data-ui-button-state~="align=start"][data-v-ecae2439] {
847
847
  justify-content: flex-start;
848
848
  text-align: start;
849
849
  }
850
- [data-component~="ui-button"][data-ui-button-state~="stretch"][data-v-e0bdc48b] {
850
+ [data-component~="ui-button"][data-ui-button-state~="stretch"][data-v-ecae2439] {
851
851
  display: flex;
852
852
  inline-size: 100%;
853
853
  }
854
- [data-component~="ui-button"][data-v-e0bdc48b] {
854
+ [data-component~="ui-button"][data-v-ecae2439] {
855
855
  appearance: none;
856
856
  min-block-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
857
857
  border-style: solid;
@@ -879,35 +879,35 @@ svg[data-v-1012d0f2],
879
879
  }
880
880
 
881
881
  /* An anchor keeps the boxed button look even when an app styles ordinary links. */
882
- [data-component~="ui-button"][data-v-e0bdc48b]:is(a) {
882
+ [data-component~="ui-button"][data-v-ecae2439]:is(a) {
883
883
  text-decoration: none;
884
884
  }
885
- .spinner-wrap[data-v-e0bdc48b] {
885
+ .spinner-wrap[data-v-ecae2439] {
886
886
  display: inline-flex;
887
887
  flex: none;
888
888
  margin-inline-end: var(--ui-space-2);
889
889
  vertical-align: middle;
890
890
  }
891
891
  @supports (text-box-trim: trim-both) {
892
- [data-component~="ui-button"][data-v-e0bdc48b] {
892
+ [data-component~="ui-button"][data-v-ecae2439] {
893
893
  text-box-trim: trim-both;
894
894
  text-box-edge: cap alphabetic;
895
895
  }
896
896
  }
897
- [data-component~="ui-button"][data-ui-button-state~="size=xs"][data-v-e0bdc48b] {
897
+ [data-component~="ui-button"][data-ui-button-state~="size=xs"][data-v-ecae2439] {
898
898
  min-block-size: 1.5rem;
899
899
  padding: calc(var(--ui-space-0-5) * 1.5) calc(var(--ui-space-1) * 1.25);
900
900
  font-size: var(--ui-font-size-xs);
901
901
  }
902
- [data-component~="ui-button"][data-ui-button-state~="shape=pill"][data-v-e0bdc48b] {
902
+ [data-component~="ui-button"][data-ui-button-state~="shape=pill"][data-v-ecae2439] {
903
903
  border-radius: var(--ui-button-radius, var(--ui-radius-round));
904
904
  }
905
- [data-component~="ui-button"][data-ui-button-state~="size=sm"][data-v-e0bdc48b] {
905
+ [data-component~="ui-button"][data-ui-button-state~="size=sm"][data-v-ecae2439] {
906
906
  min-block-size: var(--ui-control-size-sm);
907
907
  padding: var(--ui-space-1) var(--ui-space-2);
908
908
  font-size: var(--ui-font-size-sm);
909
909
  }
910
- [data-component~="ui-button"][data-ui-button-state~="size=lg"][data-v-e0bdc48b] {
910
+ [data-component~="ui-button"][data-ui-button-state~="size=lg"][data-v-ecae2439] {
911
911
  min-block-size: var(--ui-control-size-lg);
912
912
  padding: var(--ui-space-3) var(--ui-space-4);
913
913
  font-size: var(--ui-font-size-md);
@@ -915,9 +915,10 @@ svg[data-v-1012d0f2],
915
915
 
916
916
  /* Card actions inherit Button's palette and behavior; only their shared layout changes.
917
917
  The icon may contain any decorative composition. Empty icons remove their column. */
918
- [data-component~="ui-button"][data-ui-button-state~="variant=card"][data-v-e0bdc48b] {
918
+ [data-component~="ui-button"][data-ui-button-state~="variant=card"][data-v-ecae2439] {
919
919
  display: grid;
920
920
  grid-template-columns: auto minmax(0, 1fr) auto;
921
+ align-content: center;
921
922
  align-items: start;
922
923
  gap: var(--ui-space-3);
923
924
  inline-size: 100%;
@@ -927,23 +928,23 @@ svg[data-v-1012d0f2],
927
928
  box-shadow: none;
928
929
  background-image: none;
929
930
  }
930
- [data-component~="ui-button"][data-ui-button-state~="variant=card"][data-ui-button-state~="size=lg"][data-v-e0bdc48b] {
931
+ [data-component~="ui-button"][data-ui-button-state~="variant=card"][data-ui-button-state~="size=lg"][data-v-ecae2439] {
931
932
  min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-lg));
932
933
  }
933
- [data-component~="ui-button"][data-ui-button-state~="variant=card"][data-v-e0bdc48b]:has(.card-icon:empty) {
934
+ [data-component~="ui-button"][data-ui-button-state~="variant=card"][data-v-ecae2439]:has(.card-icon:empty) {
934
935
  grid-template-columns: minmax(0, 1fr) auto;
935
936
  }
936
- .card-icon[data-v-e0bdc48b] {
937
+ .card-icon[data-v-ecae2439] {
937
938
  display: flex;
938
939
  align-items: flex-start;
939
940
  }
940
- .card-icon[data-v-e0bdc48b]:empty {
941
+ .card-icon[data-v-ecae2439]:empty {
941
942
  display: none;
942
943
  }
943
- .card-content[data-v-e0bdc48b] {
944
+ .card-content[data-v-ecae2439] {
944
945
  min-inline-size: 0;
945
946
  }
946
- .card-action[data-v-e0bdc48b] {
947
+ .card-action[data-v-ecae2439] {
947
948
  display: flex;
948
949
  align-items: center;
949
950
  align-self: center;
@@ -951,29 +952,29 @@ svg[data-v-1012d0f2],
951
952
 
952
953
  /* A link is never :enabled, so interactive states test "not unavailable" instead: for a button that
953
954
  is exactly :enabled, at the same specificity, and it also matches a link that can be followed. */
954
- [data-component~="ui-button"][data-v-e0bdc48b]:hover:not(:disabled, [aria-disabled="true"]) {
955
+ [data-component~="ui-button"][data-v-ecae2439]:hover:not(:disabled, [aria-disabled="true"]) {
955
956
  border-color: var(--ui-button-hover-border, var(--_tone-hover));
956
957
  background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
957
958
  }
958
959
 
959
960
  /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
960
- [data-component~="ui-button"][data-v-e0bdc48b]:active:not(:disabled, [aria-disabled="true"]) {
961
+ [data-component~="ui-button"][data-v-ecae2439]:active:not(:disabled, [aria-disabled="true"]) {
961
962
  background-color: var(--ui-button-active-surface, oklch(from var(--_tone) l c h / 18%));
962
963
  background-image: none;
963
964
  box-shadow: var(--ui-pressed);
964
965
  }
965
- [data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-e0bdc48b],
966
- [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-e0bdc48b] {
966
+ [data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-ecae2439],
967
+ [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-ecae2439] {
967
968
  border-color: var(--_tone);
968
969
  background-color: var(--_tone);
969
970
  color: var(--_on-tone);
970
971
  box-shadow: var(--ui-button-solid-shadow, var(--ui-raised));
971
972
  }
972
- :is([data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-e0bdc48b], [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-e0bdc48b]):hover:not(:disabled, [aria-disabled="true"]) {
973
+ :is([data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-ecae2439], [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-ecae2439]):hover:not(:disabled, [aria-disabled="true"]) {
973
974
  background-color: var(--_tone-hover);
974
975
  border-color: var(--_tone-hover);
975
976
  }
976
- :is([data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-e0bdc48b], [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-e0bdc48b]):active:not(:disabled, [aria-disabled="true"]) {
977
+ :is([data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-ecae2439], [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-ecae2439]):active:not(:disabled, [aria-disabled="true"]) {
977
978
  background-color: var(--_tone-active);
978
979
  border-color: var(--_tone-active);
979
980
  background-image: none;
@@ -982,43 +983,43 @@ svg[data-v-1012d0f2],
982
983
 
983
984
  /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
984
985
  destructive secondary action, not a special case. */
985
- [data-component~="ui-button"][data-ui-button-state~="tone=neutral"][data-v-e0bdc48b] {
986
+ [data-component~="ui-button"][data-ui-button-state~="tone=neutral"][data-v-ecae2439] {
986
987
  --_hue: var(--ui-text);
987
988
  --_hue-hover: var(--ui-text);
988
989
  --_hue-active: var(--ui-text);
989
990
  --_hue-text: var(--ui-text);
990
991
  --_on-hue: var(--ui-surface);
991
992
  }
992
- [data-component~="ui-button"][data-ui-button-state~="tone=danger"][data-v-e0bdc48b],
993
- [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-e0bdc48b] {
993
+ [data-component~="ui-button"][data-ui-button-state~="tone=danger"][data-v-ecae2439],
994
+ [data-component~="ui-button"][data-ui-button-state~="variant=danger"][data-v-ecae2439] {
994
995
  --_hue: var(--ui-danger);
995
996
  --_hue-hover: var(--ui-danger-hover);
996
997
  --_hue-active: var(--ui-danger-hover);
997
998
  --_hue-text: var(--ui-danger-hover);
998
999
  --_on-hue: var(--ui-on-danger);
999
1000
  }
1000
- [data-component~="ui-button"][data-ui-button-state~="tone=success"][data-v-e0bdc48b] {
1001
+ [data-component~="ui-button"][data-ui-button-state~="tone=success"][data-v-ecae2439] {
1001
1002
  --_hue: var(--ui-success);
1002
1003
  --_hue-hover: color-mix(in srgb, var(--ui-success) 85%, #000);
1003
1004
  --_hue-active: color-mix(in srgb, var(--ui-success) 75%, #000);
1004
1005
  --_hue-text: color-mix(in srgb, var(--ui-success) 85%, #000);
1005
1006
  --_on-hue: var(--ui-surface);
1006
1007
  }
1007
- [data-component~="ui-button"][data-ui-button-state~="tone=warning"][data-v-e0bdc48b] {
1008
+ [data-component~="ui-button"][data-ui-button-state~="tone=warning"][data-v-ecae2439] {
1008
1009
  --_hue: var(--ui-warning);
1009
1010
  --_hue-hover: color-mix(in srgb, var(--ui-warning) 85%, #000);
1010
1011
  --_hue-active: color-mix(in srgb, var(--ui-warning) 75%, #000);
1011
1012
  --_hue-text: color-mix(in srgb, var(--ui-warning) 85%, #000);
1012
1013
  --_on-hue: var(--ui-surface);
1013
1014
  }
1014
- [data-component~="ui-button"][data-ui-button-state~="tone=info"][data-v-e0bdc48b] {
1015
+ [data-component~="ui-button"][data-ui-button-state~="tone=info"][data-v-ecae2439] {
1015
1016
  --_hue: var(--ui-info);
1016
1017
  --_hue-hover: color-mix(in srgb, var(--ui-info) 85%, #000);
1017
1018
  --_hue-active: color-mix(in srgb, var(--ui-info) 75%, #000);
1018
1019
  --_hue-text: color-mix(in srgb, var(--ui-info) 85%, #000);
1019
1020
  --_on-hue: var(--ui-surface);
1020
1021
  }
1021
- [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-e0bdc48b] {
1022
+ [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-ecae2439] {
1022
1023
  border-color: transparent;
1023
1024
  background: transparent;
1024
1025
  box-shadow: none;
@@ -1026,14 +1027,14 @@ svg[data-v-1012d0f2],
1026
1027
  }
1027
1028
 
1028
1029
  /* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
1029
- [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-e0bdc48b]:hover:not(:disabled, [aria-disabled="true"]) {
1030
+ [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-ecae2439]:hover:not(:disabled, [aria-disabled="true"]) {
1030
1031
  background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
1031
1032
  border-color: transparent;
1032
1033
  }
1033
1034
 
1034
1035
  /* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
1035
1036
  edge and a soft inner shadow. Lighter than hover, so an open menu's trigger never reads as heavy. */
1036
- [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-e0bdc48b]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
1037
+ [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-ecae2439]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
1037
1038
  background-color: var(
1038
1039
  --ui-button-ghost-active-surface,
1039
1040
  color-mix(in oklch, var(--ui-surface), var(--_tone) 3%)
@@ -1045,13 +1046,13 @@ svg[data-v-1012d0f2],
1045
1046
  }
1046
1047
 
1047
1048
  /* A soft halo that transitions in with the other box-shadow changes, layered on the raised shadow. */
1048
- [data-component~="ui-button"][data-v-e0bdc48b]:focus-visible {
1049
+ [data-component~="ui-button"][data-v-ecae2439]:focus-visible {
1049
1050
  outline: none;
1050
1051
  box-shadow:
1051
1052
  var(--ui-button-shadow, var(--ui-raised)),
1052
1053
  var(--ui-focus-halo);
1053
1054
  }
1054
- [data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-e0bdc48b]:focus-visible {
1055
+ [data-component~="ui-button"][data-ui-button-state~="variant=solid"][data-v-ecae2439]:focus-visible {
1055
1056
  box-shadow:
1056
1057
  var(--ui-button-solid-shadow, var(--ui-raised)),
1057
1058
  var(--ui-focus-halo);
@@ -1059,7 +1060,7 @@ svg[data-v-1012d0f2],
1059
1060
 
1060
1061
  /* A loading action stays visually active; only an explicitly disabled action takes the
1061
1062
  neutral disabled palette. Its flat surface and muted ink make the state unambiguous. */
1062
- [data-component~="ui-button"][data-v-e0bdc48b]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
1063
+ [data-component~="ui-button"][data-v-ecae2439]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
1063
1064
  border-color: var(--ui-disabled-border);
1064
1065
  background-color: var(--ui-disabled-surface);
1065
1066
  color: var(--ui-disabled-text);
@@ -1069,19 +1070,19 @@ svg[data-v-1012d0f2],
1069
1070
  background-image: none;
1070
1071
  box-shadow: none;
1071
1072
  }
1072
- :is([data-component~="ui-button"][data-ui-button-state~="loading"][data-v-e0bdc48b], [data-component~="ui-button"][data-ui-button-state~="pending"][data-v-e0bdc48b]):focus-visible {
1073
+ :is([data-component~="ui-button"][data-ui-button-state~="loading"][data-v-ecae2439], [data-component~="ui-button"][data-ui-button-state~="pending"][data-v-ecae2439]):focus-visible {
1073
1074
  box-shadow: var(--ui-focus-halo);
1074
1075
  }
1075
1076
 
1076
1077
  /* Ghost keeps no border, but gains the same neutral surface as other disabled buttons. */
1077
- [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-e0bdc48b]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
1078
+ [data-component~="ui-button"][data-ui-button-state~="variant=ghost"][data-v-ecae2439]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
1078
1079
  background-color: var(--ui-disabled-surface);
1079
1080
  border-color: transparent;
1080
1081
  }
1081
1082
 
1082
1083
  /* The size states outrank :host, so sm is named too, or a small button would stay small. */
1083
1084
  @media (pointer: coarse) {
1084
- [data-component~="ui-button"][data-v-e0bdc48b], [data-component~="ui-button"][data-ui-button-state~="size=sm"][data-v-e0bdc48b] {
1085
+ [data-component~="ui-button"][data-v-ecae2439], [data-component~="ui-button"][data-ui-button-state~="size=sm"][data-v-ecae2439] {
1085
1086
  min-block-size: var(--ui-control-min-block-size);
1086
1087
  }
1087
1088
  }
@@ -1092,15 +1093,15 @@ svg[data-v-1012d0f2],
1092
1093
  area. xs sits inside a line of text the same way, so it keeps its size and gains the area too.
1093
1094
  Only these need it: a hit area on every button would reach over its neighbours. */
1094
1095
  html[data-ui-input-modality="touch"]
1095
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b],
1096
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439],
1096
1097
  html[data-ui-input-modality="touch"]
1097
- [data-component~="ui-button"][data-ui-button-state~="size=xs"][data-v-e0bdc48b] {
1098
+ [data-component~="ui-button"][data-ui-button-state~="size=xs"][data-v-ecae2439] {
1098
1099
  position: relative;
1099
1100
  }
1100
1101
  html[data-ui-input-modality="touch"]
1101
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b]::after,
1102
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439]::after,
1102
1103
  html[data-ui-input-modality="touch"]
1103
- [data-component~="ui-button"][data-ui-button-state~="size=xs"][data-v-e0bdc48b]::after {
1104
+ [data-component~="ui-button"][data-ui-button-state~="size=xs"][data-v-ecae2439]::after {
1104
1105
  position: absolute;
1105
1106
  inset: 50% auto auto 50%;
1106
1107
  inline-size: max(100%, var(--ui-control-min-block-size));
@@ -1111,7 +1112,7 @@ html[data-ui-input-modality="touch"]
1111
1112
 
1112
1113
  /* Link: an inline text action. No box, padding, or minimum height (inline targets are exempt under
1113
1114
  WCAG 2.5.8); the inherited font; an underline that strengthens on hover; a visible focus ring. */
1114
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b] {
1115
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439] {
1115
1116
  display: inline;
1116
1117
  min-block-size: 0;
1117
1118
  padding: 0;
@@ -1129,21 +1130,21 @@ html[data-ui-input-modality="touch"]
1129
1130
  color var(--ui-motion-fast) var(--ui-motion-ease),
1130
1131
  text-decoration-color var(--ui-motion-fast) var(--ui-motion-ease);
1131
1132
  }
1132
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b]:hover:not(:disabled, [aria-disabled="true"]) {
1133
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439]:hover:not(:disabled, [aria-disabled="true"]) {
1133
1134
  color: var(--ui-button-link-hover-text, var(--ui-text-primary));
1134
1135
  text-decoration-color: currentColor;
1135
1136
  background: none;
1136
1137
  transform: none;
1137
1138
  }
1138
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b]:active:not(:disabled, [aria-disabled="true"]) {
1139
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439]:active:not(:disabled, [aria-disabled="true"]) {
1139
1140
  background: none;
1140
1141
  transform: none;
1141
1142
  }
1142
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
1143
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
1143
1144
  background: none;
1144
1145
  border-color: transparent;
1145
1146
  }
1146
- [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-e0bdc48b]:focus-visible {
1147
+ [data-component~="ui-button"][data-ui-button-state~="variant=link"][data-v-ecae2439]:focus-visible {
1147
1148
  box-shadow: none;
1148
1149
  /* Fallback keeps the ring if the token is missing: an unresolved var() would void the outline. */
1149
1150
  outline: var(--ui-focus-width) solid var(--ui-focus-ring);
@@ -1151,7 +1152,7 @@ html[data-ui-input-modality="touch"]
1151
1152
  }
1152
1153
 
1153
1154
  /* A component that sets its display still honours the hidden attribute on its root. */
1154
- [data-component~="ui-button"][hidden][data-v-e0bdc48b] {
1155
+ [data-component~="ui-button"][hidden][data-v-ecae2439] {
1155
1156
  display: none;
1156
1157
  }
1157
1158
 
@@ -1,5 +1,5 @@
1
1
  import "./looma-editor.css";
2
- import type { AnyExtension, Editor, JSONContent } from "@tiptap/core";
2
+ import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
3
3
  import { type PropType, type VNode } from "vue";
4
4
  import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaMentionProvider, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
5
5
  import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageRenditionErrorDetail } from "./image-delivery.js";
@@ -23,8 +23,9 @@ export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUplo
23
23
  /**
24
24
  * Chooses whether formatting controls follow a selection, occupy persistent
25
25
  * editor chrome, or open from an app-owned button (`popover`, with a text-only
26
- * bubble for selections). `contextual` exposes all commands at a focused caret
27
- * as well as a selection; it does not alter document commands or stored content.
26
+ * bubble for selections). Floating tools open on text selection or a held press
27
+ * and dismiss on Escape/caret movement. `contextual` includes the full command
28
+ * set. Narrow viewports retain a dock; document commands/content are unchanged.
28
29
  */
29
30
  export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover" | "contextual";
30
31
  /**
@@ -1,11 +1,11 @@
1
1
  import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
2
- import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig, resolveTableCellAt, shouldShowTextFormattingToolbar, siteRelativeHref } from "@threadlabs/looma/editor";
2
+ import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig, resolveTableCellAt, siteRelativeHref } from "@threadlabs/looma/editor";
3
3
  import { Button, Card, Checkbox, Cluster, Combobox, Disclosure, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, EditorToolbar as EditorToolbar$1, FormField, Icon, IconButton, Image, Input, InputGroup, List, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
4
+ import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
4
5
  import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
5
6
  import { closeHistory } from "@tiptap/pm/history";
6
7
  import { createLowlight } from "lowlight";
7
8
  import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
8
- import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule } from "@tiptap/core";
9
9
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
10
10
  import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
11
11
  export * from "@threadlabs/looma/editor";
@@ -1795,6 +1795,21 @@ var LoomaEditor = defineComponent({
1795
1795
  const mobileToolbarStyle = ref({});
1796
1796
  const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
1797
1797
  const editorFocused = ref(false);
1798
+ let dismissedFormattingSelection = null;
1799
+ let heldFormattingSelection = null;
1800
+ let formattingPopup = null;
1801
+ let heldPressTimer;
1802
+ let heldPress = null;
1803
+ const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
1804
+ const cancelHeldPress = () => {
1805
+ clearTimeout(heldPressTimer);
1806
+ heldPressTimer = void 0;
1807
+ heldPress = null;
1808
+ };
1809
+ const formattingSelectionActive = (instance) => {
1810
+ const selection = instance.state.selection;
1811
+ return selection instanceof TextSelection && !dismissedFormattingSelection?.eq(selection) && (!selection.empty || Boolean(heldFormattingSelection?.eq(selection)));
1812
+ };
1798
1813
  const editorStateVersion = ref(0);
1799
1814
  const mobileToolbarMode = ref("formatting");
1800
1815
  let dragLeaveTimer = null;
@@ -1995,6 +2010,12 @@ var LoomaEditor = defineComponent({
1995
2010
  onFocus: ({ editor: instance }) => rememberSelection(instance),
1996
2011
  onSelectionUpdate: ({ editor: instance }) => {
1997
2012
  rememberSelection(instance);
2013
+ if (floatingFormattingMode()) {
2014
+ const selection = instance.state.selection;
2015
+ if (!dismissedFormattingSelection?.eq(selection)) dismissedFormattingSelection = null;
2016
+ if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
2017
+ if (!formattingSelectionActive(instance)) formattingPopup?.hide();
2018
+ }
1998
2019
  if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
1999
2020
  },
2000
2021
  onTransaction: () => {
@@ -2442,7 +2463,10 @@ var LoomaEditor = defineComponent({
2442
2463
  editor.value?.commands.dismissLoomaSlashMenu();
2443
2464
  });
2444
2465
  watch(() => props.editable, (editable) => {
2466
+ cancelHeldPress();
2467
+ heldFormattingSelection = null;
2445
2468
  editor.value?.setEditable(editable);
2469
+ if (!editable) formattingPopup?.hide();
2446
2470
  if (editor.value) imageDelivery.reset(editor.value);
2447
2471
  root.value?.querySelectorAll("[data-looma-chip]").forEach((chip) => {
2448
2472
  chip.setAttribute("aria-disabled", String(!editable));
@@ -2638,10 +2662,12 @@ var LoomaEditor = defineComponent({
2638
2662
  updateMobileViewport();
2639
2663
  };
2640
2664
  const onEditorBlur = () => {
2665
+ cancelHeldPress();
2641
2666
  queueMicrotask(updateTableUi);
2642
2667
  updateCodeUi();
2643
2668
  setTimeout(() => {
2644
2669
  if (!tableInteractionActive && !editor.value?.isFocused) editorFocused.value = false;
2670
+ if (!editor.value?.isFocused && !formattingPopup?.popper.contains(document.activeElement)) heldFormattingSelection = null;
2645
2671
  }, 0);
2646
2672
  };
2647
2673
  const bindEditorUi = (instance) => {
@@ -2667,9 +2693,13 @@ var LoomaEditor = defineComponent({
2667
2693
  }, { immediate: true });
2668
2694
  const onViewportChange = (event) => {
2669
2695
  if (event?.type === "scroll" && event.target instanceof Node && (tableToolbarShell.value?.contains(event.target) || tableMenuShell.value?.contains(event.target))) return;
2696
+ cancelHeldPress();
2697
+ const wasMobile = mobile.value;
2670
2698
  updateMobileViewport();
2699
+ if (wasMobile !== mobile.value) heldFormattingSelection = null;
2671
2700
  updateTableUi();
2672
2701
  updateCodeUi();
2702
+ if (floatingFormattingMode() && editor.value && !formattingSelectionActive(editor.value)) formattingPopup?.hide();
2673
2703
  tableUi.menuOpen = false;
2674
2704
  };
2675
2705
  const onDocumentPointerDown = (event) => {
@@ -2682,6 +2712,40 @@ var LoomaEditor = defineComponent({
2682
2712
  }, 0);
2683
2713
  if ((tableUi.toolbarOpen || tableUi.overlayOpen || tableUi.menuOpen) && !inElement(root.value) && !inTableUi) closeTableUi();
2684
2714
  };
2715
+ const onFormattingPointerDown = (event) => {
2716
+ cancelHeldPress();
2717
+ const instance = editor.value;
2718
+ if (!floatingFormattingMode() || tableResizeActive || !props.editable || !instance || event.button !== 0 || !event.isPrimary || !(event.target instanceof Element) || !instance.view.dom.contains(event.target) || event.target.closest("button, input, select, textarea, [data-looma-image-node]")) return;
2719
+ heldPress = {
2720
+ id: event.pointerId,
2721
+ x: event.clientX,
2722
+ y: event.clientY
2723
+ };
2724
+ heldPressTimer = setTimeout(() => {
2725
+ heldPressTimer = void 0;
2726
+ const selection = instance.state.selection;
2727
+ if (!heldPress || !floatingFormattingMode() || tableResizeActive || !props.editable || !instance.isFocused || slash.active || mention.active || !(selection instanceof TextSelection)) return;
2728
+ dismissedFormattingSelection = null;
2729
+ heldFormattingSelection = selection;
2730
+ formattingPopup?.setProps({ getReferenceClientRect: () => posToDOMRect(instance.view, selection.from, selection.to) });
2731
+ formattingPopup?.show();
2732
+ }, 500);
2733
+ };
2734
+ const onFormattingPointerMove = (event) => {
2735
+ if (heldPress?.id === event.pointerId && Math.hypot(event.clientX - heldPress.x, event.clientY - heldPress.y) > 8) cancelHeldPress();
2736
+ };
2737
+ const onFormattingPointerEnd = (event) => {
2738
+ if (heldPress?.id === event.pointerId) cancelHeldPress();
2739
+ };
2740
+ const onFormattingKeyDown = (event) => {
2741
+ cancelHeldPress();
2742
+ if (event.key !== "Escape" || !floatingFormattingMode() || !(event.target instanceof Node) || !(root.value?.contains(event.target) || formattingPopup?.popper.contains(event.target))) return;
2743
+ const selection = editor.value?.state.selection;
2744
+ dismissedFormattingSelection = selection instanceof TextSelection ? selection : null;
2745
+ heldFormattingSelection = null;
2746
+ formattingPopup?.hide();
2747
+ hideTool();
2748
+ };
2685
2749
  const onResizePointerDown = (event) => {
2686
2750
  tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
2687
2751
  };
@@ -2770,6 +2834,11 @@ var LoomaEditor = defineComponent({
2770
2834
  window.visualViewport?.addEventListener("resize", onViewportChange);
2771
2835
  window.visualViewport?.addEventListener("scroll", onViewportChange);
2772
2836
  document.addEventListener("pointerdown", onDocumentPointerDown, true);
2837
+ document.addEventListener("pointerdown", onFormattingPointerDown);
2838
+ document.addEventListener("pointermove", onFormattingPointerMove);
2839
+ document.addEventListener("pointerup", onFormattingPointerEnd);
2840
+ document.addEventListener("pointercancel", onFormattingPointerEnd);
2841
+ document.addEventListener("keydown", onFormattingKeyDown);
2773
2842
  document.addEventListener("pointerdown", onResizePointerDown, true);
2774
2843
  document.addEventListener("pointerup", onResizePointerUp, true);
2775
2844
  root.value?.addEventListener("error", onImageError, true);
@@ -2779,7 +2848,14 @@ var LoomaEditor = defineComponent({
2779
2848
  window.removeEventListener("scroll", onViewportChange, true);
2780
2849
  window.visualViewport?.removeEventListener("resize", onViewportChange);
2781
2850
  window.visualViewport?.removeEventListener("scroll", onViewportChange);
2851
+ cancelHeldPress();
2852
+ formattingPopup = null;
2782
2853
  document.removeEventListener("pointerdown", onDocumentPointerDown, true);
2854
+ document.removeEventListener("pointerdown", onFormattingPointerDown);
2855
+ document.removeEventListener("pointermove", onFormattingPointerMove);
2856
+ document.removeEventListener("pointerup", onFormattingPointerEnd);
2857
+ document.removeEventListener("pointercancel", onFormattingPointerEnd);
2858
+ document.removeEventListener("keydown", onFormattingKeyDown);
2783
2859
  document.removeEventListener("pointerdown", onResizePointerDown, true);
2784
2860
  document.removeEventListener("pointerup", onResizePointerUp, true);
2785
2861
  root.value?.removeEventListener("error", onImageError, true);
@@ -3131,8 +3207,15 @@ var LoomaEditor = defineComponent({
3131
3207
  instance && props.editable && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3132
3208
  editor: instance,
3133
3209
  pluginKey: "looma-text-formatting-menu",
3134
- shouldShow: ({ editor: menuEditor, from, to }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && (props.toolbarMode === "contextual" ? !slash.active && !mention.active && (menuEditor.isFocused || linkContextEditing.value) : shouldShowTextFormattingToolbar(menuEditor, from, to)),
3210
+ updateDelay: 0,
3211
+ shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (menuEditor.isFocused || linkContextEditing.value) && formattingSelectionActive(menuEditor),
3135
3212
  tippyOptions: {
3213
+ onCreate: (popup) => {
3214
+ formattingPopup = popup;
3215
+ },
3216
+ onDestroy: () => {
3217
+ formattingPopup = null;
3218
+ },
3136
3219
  appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
3137
3220
  onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
3138
3221
  duration: 100,