@threadlabs/looma 0.6.1 → 0.6.3

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 (38) hide show
  1. package/components/ui-avatar/ui-avatar.html +13 -0
  2. package/components/ui-avatar-group/ui-avatar-group.html +7 -2
  3. package/components/ui-avatar-group/ui-avatar-group.js +3 -3
  4. package/components/ui-badge/ui-badge.html +14 -0
  5. package/components/ui-combobox/ui-combobox.html +112 -46
  6. package/components/ui-combobox/ui-combobox.js +14 -2
  7. package/components/ui-menu-item/ui-menu-item.html +13 -0
  8. package/components/ui-tabs/ui-tabs.html +13 -0
  9. package/components/ui-tooltip/ui-tooltip.html +3 -0
  10. package/components/ui-tooltip/ui-tooltip.js +24 -3
  11. package/dist/index.js +1 -1
  12. package/package.json +1 -1
  13. package/styles/ui-avatar-group.css +7 -2
  14. package/styles/ui-avatar.css +19 -0
  15. package/styles/ui-badge.css +21 -1
  16. package/styles/ui-combobox.css +52 -1
  17. package/styles/ui-menu-item.css +20 -1
  18. package/styles/ui-tabs.css +19 -0
  19. package/vanilla/UiCombobox.js +141 -119
  20. package/vanilla/UiTooltip.d.ts +1 -0
  21. package/vanilla/UiTooltip.js +1 -1
  22. package/vue/UiAvatar.vue +13 -0
  23. package/vue/UiAvatarGroup.vue +7 -2
  24. package/vue/UiBadge.vue +14 -0
  25. package/vue/UiCombobox.vue +135 -53
  26. package/vue/UiMenuItem.vue +13 -0
  27. package/vue/UiTabs.vue +13 -0
  28. package/vue/UiTooltip.d.ts +4 -0
  29. package/vue/UiTooltip.vue +6 -0
  30. package/vue/chunks/UiAvatar.js +1 -1
  31. package/vue/chunks/UiAvatarGroup.js +1 -1
  32. package/vue/chunks/UiBadge.js +1 -1
  33. package/vue/chunks/UiCombobox.js +66 -41
  34. package/vue/chunks/UiMenuItem.js +1 -1
  35. package/vue/chunks/UiTabs.js +1 -1
  36. package/vue/chunks/UiTooltip.js +5 -2
  37. package/vue/components.css +191 -89
  38. package/vue/editor/index.js +44 -4
package/vue/UiAvatar.vue CHANGED
@@ -181,6 +181,19 @@ onBeforeUnmount(() => {
181
181
  block-size: 100%;
182
182
  }
183
183
 
184
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high. */
185
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high.
186
+ * Trimming applies to block containers, so the box is a block with centred content. */
187
+ @supports (text-box-trim: trim-both) {
188
+ .fallback {
189
+ display: block;
190
+ text-align: center;
191
+ align-content: center;
192
+ text-box-trim: trim-both;
193
+ text-box-edge: cap alphabetic;
194
+ }
195
+ }
196
+
184
197
  [hidden] {
185
198
  display: none;
186
199
  }
@@ -134,6 +134,11 @@ onBeforeUnmount(() => {
134
134
 
135
135
  <style scoped>
136
136
  [data-component~="ui-avatar-group"] {
137
+ /* Avatars separate with a tight, crisp edge, matching Looma's 1px borders rather than the
138
+ * diffuse elevation overlays use. */
139
+ --_overlap-shadow: -2px 0 2px -1px rgba(0, 0, 0, 0.22);
140
+ --_edge-ring: 0 0 0 1px rgba(0, 0, 0, 0.08);
141
+
137
142
  display: flex;
138
143
  align-items: center;
139
144
  flex-direction: row;
@@ -164,8 +169,8 @@ onBeforeUnmount(() => {
164
169
  background: var(--ui-surface-elevated);
165
170
  color: var(--ui-text-primary);
166
171
  box-shadow:
167
- var(--ui-avatar-group-overlap-shadow, -2px 0 5px -1px rgb(0 0 0 / 0.28)),
168
- 0 0 0 1px rgb(0 0 0 / 0.06);
172
+ var(--ui-avatar-group-overlap-shadow, var(--_overlap-shadow)),
173
+ var(--ui-avatar-group-edge-ring, var(--_edge-ring));
169
174
  font: inherit;
170
175
  font-size: 0.75rem;
171
176
  font-weight: 600;
package/vue/UiBadge.vue CHANGED
@@ -55,6 +55,20 @@ const hostState = computed(() =>
55
55
  white-space: nowrap;
56
56
  }
57
57
 
58
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high. */
59
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high.
60
+ * Trimming applies to block containers, so the box is a block with centred content. */
61
+ @supports (text-box-trim: trim-both) {
62
+ [data-component~="ui-badge"] {
63
+ display: block;
64
+ min-block-size: var(--ui-badge-min-block-size, 1.5rem);
65
+ text-align: center;
66
+ align-content: center;
67
+ text-box-trim: trim-both;
68
+ text-box-edge: cap alphabetic;
69
+ }
70
+ }
71
+
58
72
  [data-component~="ui-badge"][data-ui-badge-state~="tone=accent"] {
59
73
  --_badge-surface: var(--ui-accent-strong-surface, var(--ui-accent-solid));
60
74
  --_badge-border: var(--ui-accent-strong-surface, var(--ui-accent-solid));
@@ -184,6 +184,9 @@ const inputElement = useTemplateRef<HTMLElement>('input')
184
184
  const popupElement = useTemplateRef<HTMLElement>('popup')
185
185
  const listboxElement = useTemplateRef<HTMLElement>('listbox')
186
186
  const optionsElement = useTemplateRef<HTMLElement>('options')
187
+ const internalItems = ref<
188
+ { id: string; value: string; label: string; group?: string; disabled?: boolean }[]
189
+ >([])
187
190
  const raw = ref('')
188
191
  const display = ref('')
189
192
  const selected = ref<string | null>(null)
@@ -525,6 +528,7 @@ function dispatch(name: string, detail?: unknown): boolean {
525
528
  }
526
529
 
527
530
  function read(name: string): unknown {
531
+ if (name === 'internalItems') return internalItems.value
528
532
  if (name === 'raw') return raw.value
529
533
  if (name === 'display') return display.value
530
534
  if (name === 'selected') return selected.value
@@ -546,6 +550,10 @@ function read(name: string): unknown {
546
550
  }
547
551
 
548
552
  function write(name: string, value: unknown): boolean {
553
+ if (name === 'internalItems') {
554
+ internalItems.value = value as never
555
+ return true
556
+ }
549
557
  if (name === 'raw') {
550
558
  raw.value = value as never
551
559
  return true
@@ -727,76 +735,100 @@ defineExpose({
727
735
  <label :for="`${uid}-input`" :class="{ 'sr-only': labelVisibility === 'sr-only' }">{{ label }}
728
736
  <template v-if="required">
729
737
  *</template></label>
730
- <div class="field" ref="field">
731
- <slot name="start" />
732
- <template v-if="multiple"><button
733
- v-for="(item, index) in items"
734
- :key="item.id"
735
- class="item"
738
+ <div class="control">
739
+ <div class="field" ref="field">
740
+ <slot name="start" />
741
+ <template v-if="multiple"><button
742
+ v-for="(item, index) in internalItems"
743
+ :key="item.id"
744
+ class="item"
745
+ type="button"
746
+ tabindex="-1"
747
+ :data-index="index"
748
+ :disabled="disabled || readonly || item.disabled"
749
+ :aria-label="`${item.label}, press Delete or Backspace to remove`"
750
+ >
751
+ <slot :name="`item-${item.id}`"><UiBadge>{{ item.label }}</UiBadge></slot>
752
+ </button></template>
753
+ <input
754
+ role="combobox"
755
+ aria-autocomplete="list"
756
+ autocomplete="off"
757
+ :id="`${uid}-input`"
758
+ :aria-controls="`${uid}-listbox`"
759
+ :aria-expanded="expanded"
760
+ :aria-invalid="validationStatus === 'error'"
761
+ :aria-busy="validationStatus === 'pending'"
762
+ :value.prop="display"
763
+ :placeholder="placeholder"
764
+ :name="name"
765
+ :disabled="disabled"
766
+ :readonly="readonly"
767
+ :required="required"
768
+ ref="input"
769
+ >
770
+ <button v-if="clearable" type="button" data-combobox-action="clear" aria-label="Clear">
771
+ <svg
772
+ class="affordance-icon"
773
+ aria-hidden="true"
774
+ viewBox="0 0 24 24"
775
+ fill="none"
776
+ stroke="currentColor"
777
+ stroke-width="2"
778
+ stroke-linecap="round"
779
+ >
780
+ <path d="M7 7l10 10M17 7 7 17"></path>
781
+ </svg>
782
+ </button>
783
+ <button
784
+ v-if="disclosure"
736
785
  type="button"
786
+ data-combobox-action="disclosure"
737
787
  tabindex="-1"
738
- :data-index="index"
739
- :disabled="disabled || readonly || item.disabled"
740
- :aria-label="`${item.label}, press Delete or Backspace to remove`"
788
+ aria-label="Show suggestions"
789
+ :aria-controls="`${uid}-listbox`"
741
790
  >
742
- <slot :name="`item-${item.id}`"><UiBadge>{{ item.label }}</UiBadge></slot>
743
- </button></template>
744
- <input
745
- role="combobox"
746
- aria-autocomplete="list"
747
- autocomplete="off"
748
- :id="`${uid}-input`"
749
- :aria-controls="`${uid}-listbox`"
750
- :aria-expanded="expanded"
751
- :aria-invalid="validationStatus === 'error'"
752
- :aria-busy="validationStatus === 'pending'"
753
- :value.prop="display"
754
- :placeholder="placeholder"
755
- :name="name"
756
- :disabled="disabled"
757
- :readonly="readonly"
758
- :required="required"
759
- ref="input"
760
- >
761
- <button v-if="clearable" type="button" data-combobox-action="clear" aria-label="Clear">
762
- <svg
763
- class="affordance-icon"
764
- aria-hidden="true"
765
- viewBox="0 0 24 24"
766
- fill="none"
767
- stroke="currentColor"
768
- stroke-width="2"
769
- stroke-linecap="round"
770
- >
771
- <path d="M7 7l10 10M17 7 7 17"></path>
772
- </svg>
773
- </button>
774
- <button v-if="help" type="button" aria-label="Help" :id="`${uid}-help`" :disabled="disabled">
775
- <span aria-hidden="true">?</span>
776
- </button>
791
+ <svg
792
+ class="affordance-icon"
793
+ aria-hidden="true"
794
+ viewBox="0 0 24 24"
795
+ fill="none"
796
+ stroke="currentColor"
797
+ stroke-width="2.25"
798
+ stroke-linecap="round"
799
+ stroke-linejoin="round"
800
+ >
801
+ <path d="m6 9 6 6 6-6"></path>
802
+ </svg>
803
+ </button>
804
+ </div>
777
805
  <button
778
- v-if="disclosure"
806
+ v-if="help"
807
+ class="help"
779
808
  type="button"
780
- data-combobox-action="disclosure"
781
- tabindex="-1"
782
- aria-label="Show suggestions"
783
- :aria-controls="`${uid}-listbox`"
809
+ aria-label="Help"
810
+ :id="`${uid}-help`"
811
+ :disabled="disabled"
784
812
  >
785
813
  <svg
786
- class="affordance-icon"
814
+ class="help-icon"
787
815
  aria-hidden="true"
788
816
  viewBox="0 0 24 24"
789
817
  fill="none"
790
818
  stroke="currentColor"
791
- stroke-width="2.25"
819
+ stroke-width="2"
792
820
  stroke-linecap="round"
793
821
  stroke-linejoin="round"
794
822
  >
795
- <path d="m6 9 6 6 6-6"></path>
823
+ <circle cx="12" cy="12" r="10"></circle>
824
+ <path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"></path>
825
+ <path d="M12 17h.01"></path>
796
826
  </svg>
797
827
  </button>
798
828
  </div>
799
- <UiTooltip v-if="help" :id="`${uid}-help-text`" :for="`${uid}-help`">{{ help }}</UiTooltip>
829
+ <UiTooltip v-if="help" trigger="click" :id="`${uid}-help-text`" :for="`${uid}-help`">{{
830
+ help
831
+ }}</UiTooltip>
800
832
  <div class="popup" :hidden="!expanded" ref="popup">
801
833
  <div
802
834
  role="listbox"
@@ -862,7 +894,17 @@ label {
862
894
  font-size: var(--ui-combobox-label-font-size, 0.875rem);
863
895
  font-weight: 500;
864
896
  }
897
+ /* The field and anything that annotates it sit on one row; the help button is beside the box,
898
+ not inside it, because a control's box holds its value. */
899
+ .control {
900
+ display: flex;
901
+ align-items: center;
902
+ gap: var(--ui-space-2);
903
+ min-inline-size: 0;
904
+ }
905
+
865
906
  .field {
907
+ flex: 1 1 auto;
866
908
  display: flex;
867
909
  min-inline-size: 0;
868
910
  border: 1px solid var(--ui-control-border, var(--ui-border-default));
@@ -879,6 +921,46 @@ label {
879
921
  var(--ui-control-inset),
880
922
  var(--ui-control-focus-halo-shadow);
881
923
  }
924
+ /* A ghost icon button: the affordance is the mark, not a box around it. */
925
+ .help {
926
+ flex: none;
927
+ display: inline-flex;
928
+ align-items: center;
929
+ justify-content: center;
930
+ inline-size: var(--ui-control-size-sm);
931
+ block-size: var(--ui-control-size-sm);
932
+ padding: 0;
933
+ border: 0;
934
+ border-radius: var(--ui-radius-round);
935
+ background: transparent;
936
+ color: var(--ui-text-secondary);
937
+ cursor: pointer;
938
+ transition:
939
+ background-color var(--ui-motion-fast) var(--ui-motion-ease),
940
+ color var(--ui-motion-fast) var(--ui-motion-ease);
941
+ }
942
+
943
+ .help:hover:enabled,
944
+ .help[aria-expanded="true"] {
945
+ background: color-mix(in srgb, currentColor 8%, transparent);
946
+ color: var(--ui-text-primary);
947
+ }
948
+
949
+ .help:focus-visible {
950
+ outline: none;
951
+ box-shadow: var(--ui-focus-halo);
952
+ }
953
+
954
+ .help:disabled {
955
+ color: var(--ui-disabled-text);
956
+ cursor: not-allowed;
957
+ }
958
+
959
+ .help-icon {
960
+ inline-size: var(--ui-icon-size-sm);
961
+ block-size: var(--ui-icon-size-sm);
962
+ }
963
+
882
964
  input::selection {
883
965
  background: color-mix(in srgb, var(--ui-accent-solid) 20%, transparent);
884
966
  color: inherit;
@@ -46,6 +46,19 @@ withDefaults(
46
46
  color var(--ui-motion-fast) var(--ui-motion-ease);
47
47
  }
48
48
 
49
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high. */
50
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high.
51
+ * Trimming applies to block containers, so the box is a block with centred content. */
52
+ @supports (text-box-trim: trim-both) {
53
+ [data-component~="ui-menu-item"] {
54
+ display: block;
55
+ min-block-size: var(--ui-menu-item-min-block-size, var(--ui-control-size-md, 2.5rem));
56
+ align-content: center;
57
+ text-box-trim: trim-both;
58
+ text-box-edge: cap alphabetic;
59
+ }
60
+ }
61
+
49
62
  [data-component~="ui-menu-item"]:is(:hover, :focus-visible):not([aria-disabled="true"]) {
50
63
  background: var(--ui-menu-item-hover-surface, var(--ui-surface-subtle));
51
64
  }
package/vue/UiTabs.vue CHANGED
@@ -259,6 +259,19 @@ onBeforeUnmount(() => {
259
259
  border-color var(--ui-motion-fast) var(--ui-motion-ease);
260
260
  }
261
261
 
262
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high. */
263
+ /* Centre the glyphs, not the line box: a label without descenders otherwise reads high.
264
+ * Trimming applies to block containers, so the box is a block with centred content. */
265
+ @supports (text-box-trim: trim-both) {
266
+ .list button {
267
+ display: block;
268
+ text-align: center;
269
+ align-content: center;
270
+ text-box-trim: trim-both;
271
+ text-box-edge: cap alphabetic;
272
+ }
273
+ }
274
+
262
275
  [data-component~="ui-tabs"][data-ui-tabs-state~="stretch"] .list {
263
276
  grid-auto-columns: minmax(max-content, 1fr);
264
277
  }
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  open?: boolean;
6
6
  placement?: string;
7
7
  showDelay?: number;
8
+ trigger?: 'hover' | 'click' | 'focus';
8
9
  };
9
10
  declare var __VLS_1: {};
10
11
  type __VLS_Slots = {} & {
@@ -22,6 +23,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
22
23
  readonly trigger: "keyboard" | "pointer" | "programmatic";
23
24
  }) => any;
24
25
  "update:open": (value: boolean) => any;
26
+ "update:trigger": (value: "click" | "focus" | "hover") => any;
25
27
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
28
  onClose?: ((detail: {
27
29
  readonly open: boolean;
@@ -34,7 +36,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
34
36
  readonly trigger: "keyboard" | "pointer" | "programmatic";
35
37
  }) => any) | undefined;
36
38
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
39
+ "onUpdate:trigger"?: ((value: "click" | "focus" | "hover") => any) | undefined;
37
40
  }>, {
41
+ trigger: "hover" | "click" | "focus";
38
42
  for: string;
39
43
  hideDelay: number;
40
44
  inverse: boolean;
package/vue/UiTooltip.vue CHANGED
@@ -13,6 +13,7 @@ const props = withDefaults(
13
13
  open?: boolean
14
14
  placement?: string
15
15
  showDelay?: number
16
+ trigger?: 'hover' | 'click' | 'focus'
16
17
  }>(),
17
18
  {
18
19
  for: '',
@@ -21,10 +22,12 @@ const props = withDefaults(
21
22
  open: false,
22
23
  placement: 'top-start',
23
24
  showDelay: 500,
25
+ trigger: 'hover',
24
26
  },
25
27
  )
26
28
  const emit = defineEmits<{
27
29
  'update:open': [value: boolean]
30
+ 'update:trigger': [value: 'hover' | 'click' | 'focus']
28
31
  open: [
29
32
  detail: {
30
33
  readonly open: boolean
@@ -91,6 +94,9 @@ function dispatch(name: string, detail?: unknown): boolean {
91
94
  if (detail !== null && typeof detail === 'object' && 'open' in detail) {
92
95
  emitEvent('update:open', (detail as Record<string, unknown>)['open'])
93
96
  }
97
+ if (detail !== null && typeof detail === 'object' && 'trigger' in detail) {
98
+ emitEvent('update:trigger', (detail as Record<string, unknown>)['trigger'])
99
+ }
94
100
  return root.value?.dispatchEvent(
95
101
  new CustomEvent(name, { bubbles: true, composed: true, ...declared[name], detail }),
96
102
  ) ?? true
@@ -133,6 +133,6 @@ var UiAvatar_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PUR
133
133
  ], 16);
134
134
  };
135
135
  }
136
- }), [["__scopeId", "data-v-038b6eb1"]]);
136
+ }), [["__scopeId", "data-v-3a27ff1d"]]);
137
137
  //#endregion
138
138
  export { UiAvatar_default as t };
@@ -114,6 +114,6 @@ var UiAvatarGroup_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @
114
114
  }, toDisplayString(`+${overflowCount.value}`), 9, _hoisted_2)], 16, _hoisted_1);
115
115
  };
116
116
  }
117
- }), [["__scopeId", "data-v-6029679e"]]);
117
+ }), [["__scopeId", "data-v-2ad5d86f"]]);
118
118
  //#endregion
119
119
  export { UiAvatarGroup_default as t };
@@ -19,6 +19,6 @@ var UiBadge_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE
19
19
  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);
20
20
  };
21
21
  }
22
- }), [["__scopeId", "data-v-43aa138f"]]);
22
+ }), [["__scopeId", "data-v-7e9b5784"]]);
23
23
  //#endregion
24
24
  export { UiBadge_default as t };