@flux-ui/components 3.1.2 → 3.1.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 (61) hide show
  1. package/dist/component/FluxAvatarGroup.vue.d.ts +17 -0
  2. package/dist/component/FluxContextMenu.vue.d.ts +26 -0
  3. package/dist/component/FluxDataTable.vue.d.ts +20 -10
  4. package/dist/component/FluxDescriptionItem.vue.d.ts +19 -0
  5. package/dist/component/FluxDescriptionList.vue.d.ts +17 -0
  6. package/dist/component/FluxFormCombobox.vue.d.ts +20 -0
  7. package/dist/component/FluxFormRating.vue.d.ts +21 -0
  8. package/dist/component/FluxFormTagsInput.vue.d.ts +27 -0
  9. package/dist/component/FluxFormTextArea.vue.d.ts +6 -1
  10. package/dist/component/FluxInlineEdit.vue.d.ts +41 -0
  11. package/dist/component/FluxMenu.vue.d.ts +1 -0
  12. package/dist/component/FluxMenuFlyout.vue.d.ts +22 -0
  13. package/dist/component/FluxTableCell.vue.d.ts +1 -0
  14. package/dist/component/FluxTour.vue.d.ts +35 -0
  15. package/dist/component/FluxTourItem.vue.d.ts +18 -0
  16. package/dist/component/FluxVirtualScroller.vue.d.ts +27 -0
  17. package/dist/component/index.d.ts +12 -0
  18. package/dist/component/primitive/AnchorPopup.vue.d.ts +7 -1
  19. package/dist/component/primitive/SelectBase.vue.d.ts +3 -0
  20. package/dist/composable/private/index.d.ts +1 -0
  21. package/dist/composable/private/useMenuFlyout.d.ts +42 -0
  22. package/dist/data/di.d.ts +35 -0
  23. package/dist/data/i18n.d.ts +7 -0
  24. package/dist/index.css +441 -1
  25. package/dist/index.js +2018 -407
  26. package/dist/index.js.map +1 -1
  27. package/package.json +7 -7
  28. package/src/component/FluxAvatarGroup.vue +52 -0
  29. package/src/component/FluxContextMenu.vue +134 -0
  30. package/src/component/FluxDataTable.vue +113 -32
  31. package/src/component/FluxDescriptionItem.vue +43 -0
  32. package/src/component/FluxDescriptionList.vue +37 -0
  33. package/src/component/FluxFormCombobox.vue +98 -0
  34. package/src/component/FluxFormRating.vue +172 -0
  35. package/src/component/FluxFormTagsInput.vue +249 -0
  36. package/src/component/FluxFormTextArea.vue +16 -1
  37. package/src/component/FluxInlineEdit.vue +176 -0
  38. package/src/component/FluxMenu.vue +13 -3
  39. package/src/component/FluxMenuFlyout.vue +118 -0
  40. package/src/component/FluxTableCell.vue +2 -0
  41. package/src/component/FluxTour.vue +332 -0
  42. package/src/component/FluxTourItem.vue +27 -0
  43. package/src/component/FluxVirtualScroller.vue +96 -0
  44. package/src/component/index.ts +12 -0
  45. package/src/component/primitive/AnchorPopup.vue +27 -0
  46. package/src/component/primitive/SelectBase.vue +37 -2
  47. package/src/composable/private/index.ts +1 -0
  48. package/src/composable/private/useMenuFlyout.ts +417 -0
  49. package/src/css/component/AvatarGroup.module.scss +22 -0
  50. package/src/css/component/ContextMenu.module.scss +17 -0
  51. package/src/css/component/DescriptionList.module.scss +98 -0
  52. package/src/css/component/Form.module.scss +51 -0
  53. package/src/css/component/FormRating.module.scss +47 -0
  54. package/src/css/component/InlineEdit.module.scss +45 -0
  55. package/src/css/component/Menu.module.scss +4 -1
  56. package/src/css/component/MenuFlyout.module.scss +38 -0
  57. package/src/css/component/Table.module.scss +16 -0
  58. package/src/css/component/Tour.module.scss +108 -0
  59. package/src/css/component/VirtualScroller.module.scss +17 -0
  60. package/src/data/di.ts +40 -0
  61. package/src/data/i18n.ts +7 -0
@@ -20,12 +20,14 @@
20
20
 
21
21
  const {
22
22
  anchor,
23
+ clampToViewport,
23
24
  direction = 'vertical',
24
25
  margin = 12,
25
26
  position,
26
27
  useAnchorWidth
27
28
  } = defineProps<{
28
29
  readonly anchor?: ComponentPublicInstance | HTMLElement | null;
30
+ readonly clampToViewport?: boolean;
29
31
  readonly direction?: FluxDirection;
30
32
  readonly margin?: number;
31
33
  readonly position?:
@@ -52,6 +54,9 @@
52
54
  onMounted(() => {
53
55
  window.addEventListener('resize', onResize, {passive: true});
54
56
  window.addEventListener('scroll', onScroll, {capture: true, passive: true});
57
+
58
+ resize();
59
+ reposition();
55
60
  });
56
61
 
57
62
  onUnmounted(() => {
@@ -161,6 +166,23 @@
161
166
  break;
162
167
  }
163
168
 
169
+ if (clampToViewport) {
170
+ if (position?.startsWith('right') && px + popupWidth > innerWidth - margin) {
171
+ px = x - popupWidth - margin;
172
+ } else if (position?.startsWith('left') && px < margin) {
173
+ px = x + width + margin;
174
+ }
175
+
176
+ if (position?.startsWith('bottom') && py + popupHeight > innerHeight - margin) {
177
+ py = y - popupHeight - margin;
178
+ } else if (position?.startsWith('top') && py < margin) {
179
+ py = y + height + margin;
180
+ }
181
+
182
+ px = Math.min(Math.max(px, margin), Math.max(margin, innerWidth - popupWidth - margin));
183
+ py = Math.min(Math.max(py, margin), Math.max(margin, innerHeight - popupHeight - margin));
184
+ }
185
+
164
186
  state.x = px;
165
187
  state.y = py;
166
188
  }
@@ -185,6 +207,11 @@
185
207
  reposition();
186
208
  }
187
209
 
210
+ defineExpose({
211
+ reposition,
212
+ resize
213
+ });
214
+
188
215
  watchEffect(() => {
189
216
  if (!anchor || (!isHtmlElement(anchor) && !anchor.$el)) {
190
217
  return;
@@ -80,11 +80,19 @@
80
80
  :placeholder="translate('flux.search')"
81
81
  @keydown="onKeyDown"/>
82
82
 
83
- <FluxMenu v-if="!isLoading && options.length === 0">
83
+ <FluxMenu v-if="canCreate">
84
+ <FluxMenuItem
85
+ icon-leading="plus"
86
+ :label="translate('flux.createOption', {value: trimmedSearch})"
87
+ type="button"
88
+ @click="create()"/>
89
+ </FluxMenu>
90
+
91
+ <FluxMenu v-if="!isLoading && options.length === 0 && !canCreate">
84
92
  <FluxMenuSubHeader :label="translate('flux.noItems')"/>
85
93
  </FluxMenu>
86
94
 
87
- <FluxMenu v-else>
95
+ <FluxMenu v-else-if="options.length > 0">
88
96
  <template
89
97
  v-for="([item, subItems], index) of options"
90
98
  :key="`group-${index}`">
@@ -162,6 +170,7 @@
162
170
  keyDown: [KeyboardEvent];
163
171
  deselect: [string | number | null];
164
172
  select: [string | number | null];
173
+ create: [string];
165
174
  search: [string];
166
175
  close: [];
167
176
  open: [];
@@ -177,11 +186,13 @@
177
186
 
178
187
  const {
179
188
  disabled: componentDisabled,
189
+ isCreatable,
180
190
  isMultiple,
181
191
  options,
182
192
  selected
183
193
  } = defineProps<{
184
194
  readonly disabled?: boolean;
195
+ readonly isCreatable?: boolean;
185
196
  readonly isLoading?: boolean;
186
197
  readonly isMultiple?: boolean;
187
198
  readonly isSearchable?: boolean;
@@ -205,6 +216,15 @@
205
216
  const focusElement = computed(() => unrefTemplateElement(searchInputElementRef) ?? unrefTemplateElement(anchorRef));
206
217
  const highlightedId = computed(() => unref(rawOptions)[unref(highlightedIndex)]?.value);
207
218
  const rawOptions = computed(() => options.map(group => group[1]).flat());
219
+ const trimmedSearch = computed(() => unref(modelSearch).trim());
220
+ const canCreate = computed(() => {
221
+ if (!isCreatable || trimmedSearch.value === '') {
222
+ return false;
223
+ }
224
+
225
+ const query = trimmedSearch.value.toLowerCase();
226
+ return !unref(rawOptions).some(o => o.label.toLowerCase() === query);
227
+ });
208
228
 
209
229
  const {
210
230
  isOpen: isPopupOpen,
@@ -236,6 +256,19 @@
236
256
  nextTick(() => unref(focusElement)?.focus());
237
257
  }
238
258
 
259
+ function create(): void {
260
+ emit('create', trimmedSearch.value);
261
+
262
+ if (!isMultiple) {
263
+ isPopupOpen.value = false;
264
+ }
265
+
266
+ highlightedIndex.value = INITIAL_HIGHLIGHTED_INDEX;
267
+ modelSearch.value = '';
268
+
269
+ nextTick(() => unref(focusElement)?.focus());
270
+ }
271
+
239
272
  function onKeyDown(evt: KeyboardEvent): void {
240
273
  emit('keyDown', evt);
241
274
 
@@ -284,6 +317,8 @@
284
317
 
285
318
  if (id !== undefined) {
286
319
  select(id);
320
+ } else if (unref(canCreate)) {
321
+ create();
287
322
  }
288
323
  break;
289
324
 
@@ -4,6 +4,7 @@ export { default as useDateFlyout } from './useDateFlyout';
4
4
  export { default as useDropdownPopup, type UseDropdownPopupOptions, type UseDropdownPopupReturn } from './useDropdownPopup';
5
5
  export { default as useFormSelect } from './useFormSelect';
6
6
  export { useKanban } from './useKanban';
7
+ export { default as useMenuFlyout, useMenuFlyoutContext, useMenuFlyoutProvider, type UseMenuFlyoutOptions, type UseMenuFlyoutProviderOptions, type UseMenuFlyoutReturn } from './useMenuFlyout';
7
8
  export { useSplitView, type SplitViewPane, type UseSplitViewOptions, type UseSplitViewReturn } from './useSplitView';
8
9
  export { default as useTranslate } from './useTranslate';
9
10
  export { useCommandPalette, type CommandPaletteGroup, type CommandPaletteResultItem } from './useCommandPalette';
@@ -0,0 +1,417 @@
1
+ import { animationFrameDebounce, isSSR } from '@flux-ui/internals';
2
+ import { inject, nextTick, onMounted, onUnmounted, provide, ref, type ComponentPublicInstance, type Ref, watch } from 'vue';
3
+ import { FluxMenuFlyoutInjectionKey, type FluxMenuFlyoutCone, type FluxMenuFlyoutEntry, type FluxMenuFlyoutInjection, type FluxMenuFlyoutPointer } from '~flux/components/data';
4
+
5
+ let flyoutId = 0;
6
+
7
+ export type UseMenuFlyoutProviderOptions = {
8
+ readonly debugCone: Ref<boolean>;
9
+ readonly onCloseAll?: () => void;
10
+ };
11
+
12
+ export type UseMenuFlyoutOptions = {
13
+ readonly triggerRef: Ref<ComponentPublicInstance | HTMLElement | null>;
14
+ readonly popupRef: Ref<ComponentPublicInstance | HTMLElement | null>;
15
+ readonly disabled?: Ref<boolean>;
16
+ };
17
+
18
+ export type UseMenuFlyoutReturn = {
19
+ readonly context: FluxMenuFlyoutInjection | null;
20
+ readonly cone: Ref<FluxMenuFlyoutCone | null>;
21
+ readonly isOpen: Ref<boolean>;
22
+ closeAll(): void;
23
+ focusTrigger(): void;
24
+ onPopupKeydown(evt: KeyboardEvent): void;
25
+ onTriggerClick(evt: MouseEvent): void;
26
+ onTriggerKeydown(evt: KeyboardEvent): void;
27
+ };
28
+
29
+ /**
30
+ * Creates and provides the shared menu-flyout context for an entire open menu tree. Used by the
31
+ * outermost menu surface (FluxContextMenu, or a standalone root FluxMenu). Tracks the pointer, the
32
+ * active prediction cone, the keyboard-trap stack and the set of teleported flyout popups so that
33
+ * click-outside and close-all can reason about the whole tree.
34
+ */
35
+ export function useMenuFlyoutProvider(options: UseMenuFlyoutProviderOptions): FluxMenuFlyoutInjection {
36
+ const {debugCone, onCloseAll} = options;
37
+ const entries = new Set<FluxMenuFlyoutEntry>();
38
+
39
+ const pointer = ref<FluxMenuFlyoutPointer>({x: 0, y: 0, px: 0, py: 0});
40
+ const activeCone = ref<FluxMenuFlyoutCone | null>(null);
41
+ const keyboardStack = ref<number[]>([]);
42
+
43
+ let pointerX = 0;
44
+ let pointerY = 0;
45
+
46
+ // pointermove can fire several times per frame (high-poll-rate mice), and every flyout entry
47
+ // reacts to the pointer ref with synchronous getBoundingClientRect reads. Coalesce updates to
48
+ // one per animation frame so those layout reads happen at most once per frame instead of per event.
49
+ const flushPointer = animationFrameDebounce(() => {
50
+ const previous = pointer.value;
51
+ pointer.value = {x: pointerX, y: pointerY, px: previous.x, py: previous.y};
52
+ });
53
+
54
+ function onPointerMove(evt: PointerEvent): void {
55
+ pointerX = evt.clientX;
56
+ pointerY = evt.clientY;
57
+ flushPointer();
58
+ }
59
+
60
+ function startTracking(): void {
61
+ if (isSSR) {
62
+ return;
63
+ }
64
+
65
+ window.addEventListener('pointermove', onPointerMove, {capture: true, passive: true});
66
+ }
67
+
68
+ function stopTracking(): void {
69
+ if (isSSR) {
70
+ return;
71
+ }
72
+
73
+ window.removeEventListener('pointermove', onPointerMove, {capture: true});
74
+ }
75
+
76
+ const context: FluxMenuFlyoutInjection = {
77
+ debugCone,
78
+ pointer,
79
+ activeCone,
80
+ keyboardStack,
81
+
82
+ register(entry: FluxMenuFlyoutEntry): void {
83
+ if (entries.size === 0) {
84
+ startTracking();
85
+ }
86
+
87
+ entries.add(entry);
88
+ },
89
+
90
+ unregister(entry: FluxMenuFlyoutEntry): void {
91
+ entries.delete(entry);
92
+
93
+ if (entries.size === 0) {
94
+ stopTracking();
95
+ activeCone.value = null;
96
+ }
97
+ },
98
+
99
+ closeOthers(self: FluxMenuFlyoutEntry): void {
100
+ const trigger = self.getTrigger();
101
+
102
+ for (const entry of entries) {
103
+ if (entry === self || !entry.isOpen.value) {
104
+ continue;
105
+ }
106
+
107
+ const popup = entry.getPopup();
108
+
109
+ if (popup && trigger && popup.contains(trigger)) {
110
+ continue;
111
+ }
112
+
113
+ entry.close();
114
+ }
115
+ },
116
+
117
+ hasOpenDescendant(self: FluxMenuFlyoutEntry): boolean {
118
+ const popup = self.getPopup();
119
+
120
+ if (!popup) {
121
+ return false;
122
+ }
123
+
124
+ for (const entry of entries) {
125
+ if (entry === self || !entry.isOpen.value) {
126
+ continue;
127
+ }
128
+
129
+ const trigger = entry.getTrigger();
130
+
131
+ if (trigger && popup.contains(trigger)) {
132
+ return true;
133
+ }
134
+ }
135
+
136
+ return false;
137
+ },
138
+
139
+ isAimingAtOpenSubmenu(): boolean {
140
+ const {x, y, px, py} = pointer.value;
141
+
142
+ for (const entry of entries) {
143
+ if (!entry.isOpen.value) {
144
+ continue;
145
+ }
146
+
147
+ const trigger = entry.getTrigger();
148
+ const popup = entry.getPopup();
149
+
150
+ if (!trigger || !popup) {
151
+ continue;
152
+ }
153
+
154
+ const t = trigger.getBoundingClientRect();
155
+ const r = popup.getBoundingClientRect();
156
+ const edgeX = r.left >= t.right ? r.left : (r.right <= t.left ? r.right : r.left);
157
+
158
+ if (pointInTriangle(x, y, px, py, edgeX, r.top, edgeX, r.bottom)) {
159
+ return true;
160
+ }
161
+ }
162
+
163
+ return false;
164
+ },
165
+
166
+ isInsidePopups(target: Node | null): boolean {
167
+ if (!target) {
168
+ return false;
169
+ }
170
+
171
+ for (const entry of entries) {
172
+ const popup = entry.getPopup();
173
+
174
+ if (popup && popup.contains(target)) {
175
+ return true;
176
+ }
177
+ }
178
+
179
+ return false;
180
+ },
181
+
182
+ closeAll(): void {
183
+ if (onCloseAll) {
184
+ onCloseAll();
185
+ return;
186
+ }
187
+
188
+ for (const entry of entries) {
189
+ entry.close();
190
+ }
191
+ }
192
+ };
193
+
194
+ onUnmounted(stopTracking);
195
+
196
+ provide(FluxMenuFlyoutInjectionKey, context);
197
+
198
+ return context;
199
+ }
200
+
201
+ /**
202
+ * Returns the shared menu-flyout context, creating (and providing) a new root context when none
203
+ * exists higher up the tree. Used by FluxMenu so that a standalone menu becomes the root while a
204
+ * menu nested inside a FluxContextMenu or another flyout inherits the existing context.
205
+ */
206
+ export function useMenuFlyoutContext(options: UseMenuFlyoutProviderOptions): FluxMenuFlyoutInjection {
207
+ const parent = inject(FluxMenuFlyoutInjectionKey, null);
208
+
209
+ if (parent) {
210
+ return parent;
211
+ }
212
+
213
+ return useMenuFlyoutProvider(options);
214
+ }
215
+
216
+ /**
217
+ * Per-flyout open/close and prediction-cone behaviour for FluxMenuFlyout. Submenus open instantly
218
+ * on hover and close instantly once the pointer is neither over the trigger/popup (or an open
219
+ * descendant) nor aiming at the submenu through the prediction cone (the safe triangle). There are
220
+ * no open/close delays — the cone is the only thing that keeps a submenu open during a diagonal
221
+ * move, which is also why the debug cone only shows while it actually applies.
222
+ */
223
+ export default function useMenuFlyout(options: UseMenuFlyoutOptions): UseMenuFlyoutReturn {
224
+ const {triggerRef, popupRef, disabled} = options;
225
+ const context = inject(FluxMenuFlyoutInjectionKey, null);
226
+
227
+ const id = ++flyoutId;
228
+ const isOpen = ref(false);
229
+ const openSource = ref<'pointer' | 'keyboard'>('pointer');
230
+ const cone = ref<FluxMenuFlyoutCone | null>(null);
231
+
232
+ const entry: FluxMenuFlyoutEntry = {
233
+ getTrigger: () => elementOf(triggerRef.value),
234
+ getPopup: () => elementOf(popupRef.value),
235
+ isOpen,
236
+ close: () => doClose()
237
+ };
238
+
239
+ function open(source: 'pointer' | 'keyboard'): void {
240
+ if (disabled?.value) {
241
+ return;
242
+ }
243
+
244
+ openSource.value = source;
245
+
246
+ if (context && source === 'keyboard' && !context.keyboardStack.value.includes(id)) {
247
+ context.keyboardStack.value = [...context.keyboardStack.value, id];
248
+ }
249
+
250
+ if (!isOpen.value) {
251
+ isOpen.value = true;
252
+ context?.closeOthers(entry);
253
+ }
254
+
255
+ if (source === 'keyboard') {
256
+ nextTick(focusFirstItem);
257
+ }
258
+ }
259
+
260
+ function doClose(): void {
261
+ if (!isOpen.value) {
262
+ return;
263
+ }
264
+
265
+ isOpen.value = false;
266
+ cone.value = null;
267
+
268
+ if (context) {
269
+ if (context.activeCone.value?.id === id) {
270
+ context.activeCone.value = null;
271
+ }
272
+
273
+ context.keyboardStack.value = context.keyboardStack.value.filter(value => value !== id);
274
+ }
275
+ }
276
+
277
+ function focusTrigger(): void {
278
+ elementOf(triggerRef.value)?.focus();
279
+ }
280
+
281
+ function focusFirstItem(): void {
282
+ const popup = elementOf(popupRef.value);
283
+
284
+ if (!popup) {
285
+ return;
286
+ }
287
+
288
+ const focusable = popup.querySelector<HTMLElement>('[tabindex="0"]') ?? popup.querySelector<HTMLElement>('a[href], button:not([disabled])');
289
+ focusable?.focus();
290
+ }
291
+
292
+ function onTriggerClick(evt: MouseEvent): void {
293
+ evt.stopPropagation();
294
+
295
+ // evt.detail === 0 means the click came from the keyboard (Enter/Space), in which case we
296
+ // move focus into the submenu. A real pointer click keeps focus where it is.
297
+ open(evt.detail === 0 ? 'keyboard' : 'pointer');
298
+ }
299
+
300
+ function onTriggerKeydown(evt: KeyboardEvent): void {
301
+ if (evt.key === 'ArrowRight') {
302
+ evt.preventDefault();
303
+ evt.stopPropagation();
304
+ open('keyboard');
305
+ }
306
+ }
307
+
308
+ function onPopupKeydown(evt: KeyboardEvent): void {
309
+ if (evt.key === 'ArrowLeft' || evt.key === 'Escape') {
310
+ evt.preventDefault();
311
+ evt.stopPropagation();
312
+ doClose();
313
+ focusTrigger();
314
+ }
315
+ }
316
+
317
+ if (context && !isSSR) {
318
+ watch(context.pointer, () => {
319
+ const trigger = elementOf(triggerRef.value);
320
+
321
+ if (!trigger) {
322
+ return;
323
+ }
324
+
325
+ const {x, y, px, py} = context.pointer.value;
326
+ const t = trigger.getBoundingClientRect();
327
+ const overTrigger = x >= t.left && x <= t.right && y >= t.top && y <= t.bottom;
328
+
329
+ if (!isOpen.value) {
330
+ if (overTrigger && !context.isAimingAtOpenSubmenu()) {
331
+ open('pointer');
332
+ }
333
+
334
+ return;
335
+ }
336
+
337
+ const popup = elementOf(popupRef.value);
338
+
339
+ if (!popup) {
340
+ return;
341
+ }
342
+
343
+ const r = popup.getBoundingClientRect();
344
+ const overPopup = x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
345
+
346
+ if (overTrigger || overPopup || context.hasOpenDescendant(entry)) {
347
+ cone.value = null;
348
+
349
+ if (context.activeCone.value?.id === id) {
350
+ context.activeCone.value = null;
351
+ }
352
+
353
+ return;
354
+ }
355
+
356
+ const edgeX = r.left >= t.right ? r.left : (r.right <= t.left ? r.right : r.left);
357
+ const candidate: FluxMenuFlyoutCone = {id, ax: x, ay: y, bx: edgeX, by: r.top, cx: edgeX, cy: r.bottom};
358
+
359
+ if (pointInTriangle(x, y, px, py, edgeX, r.top, edgeX, r.bottom)) {
360
+ cone.value = candidate;
361
+ context.activeCone.value = candidate;
362
+ } else {
363
+ cone.value = null;
364
+
365
+ if (context.activeCone.value?.id === id) {
366
+ context.activeCone.value = null;
367
+ }
368
+
369
+ doClose();
370
+ }
371
+ });
372
+ }
373
+
374
+ onMounted(() => context?.register(entry));
375
+
376
+ onUnmounted(() => {
377
+ doClose();
378
+ context?.unregister(entry);
379
+ });
380
+
381
+ return {
382
+ context,
383
+ cone,
384
+ isOpen,
385
+ closeAll: () => context?.closeAll(),
386
+ focusTrigger,
387
+ onPopupKeydown,
388
+ onTriggerClick,
389
+ onTriggerKeydown
390
+ };
391
+ }
392
+
393
+ function elementOf(value: ComponentPublicInstance | HTMLElement | null | undefined): HTMLElement | null {
394
+ if (!value) {
395
+ return null;
396
+ }
397
+
398
+ if (value instanceof HTMLElement) {
399
+ return value;
400
+ }
401
+
402
+ return (value.$el as HTMLElement | null) ?? null;
403
+ }
404
+
405
+ function pointInTriangle(px: number, py: number, ax: number, ay: number, bx: number, by: number, cx: number, cy: number): boolean {
406
+ const d1 = sign(px, py, ax, ay, bx, by);
407
+ const d2 = sign(px, py, bx, by, cx, cy);
408
+ const d3 = sign(px, py, cx, cy, ax, ay);
409
+ const hasNegative = d1 < 0 || d2 < 0 || d3 < 0;
410
+ const hasPositive = d1 > 0 || d2 > 0 || d3 > 0;
411
+
412
+ return !(hasNegative && hasPositive);
413
+ }
414
+
415
+ function sign(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {
416
+ return (px - bx) * (ay - by) - (ax - bx) * (py - by);
417
+ }
@@ -0,0 +1,22 @@
1
+ .avatarGroup {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ flex-flow: row nowrap;
5
+ }
6
+
7
+ .avatarGroupItem {
8
+ position: relative;
9
+ display: inline-flex;
10
+ margin-left: calc(var(--overlap, .3) * -1em);
11
+ border-radius: .3em;
12
+ box-shadow: 0 0 0 .125em var(--gray-25);
13
+ transition: translate .2s var(--swift-out);
14
+
15
+ &:first-child {
16
+ margin-left: 0;
17
+ }
18
+
19
+ &:hover {
20
+ z-index: 1;
21
+ }
22
+ }
@@ -0,0 +1,17 @@
1
+ .contextMenu {
2
+ display: contents;
3
+ }
4
+
5
+ .contextMenuPopup {
6
+ composes: basePane from './base/Pane.module.scss';
7
+
8
+ position: fixed;
9
+ top: 0;
10
+ left: 0;
11
+ min-width: 270px;
12
+ max-height: max(330px, 50dvh);
13
+ overflow: auto;
14
+ box-shadow: var(--shadow-md);
15
+ translate: var(--x) var(--y);
16
+ z-index: 10000;
17
+ }
@@ -0,0 +1,98 @@
1
+ .descriptionList {
2
+ display: flex;
3
+ flex-flow: column;
4
+ gap: 12px;
5
+ }
6
+
7
+ .descriptionListHeader {
8
+ color: var(--foreground-secondary);
9
+ font-size: 14px;
10
+ font-weight: 500;
11
+ }
12
+
13
+ .descriptionListItems {
14
+ display: flex;
15
+ margin: 0;
16
+ flex-flow: column;
17
+ gap: 12px;
18
+ }
19
+
20
+ .descriptionItem {
21
+ display: flex;
22
+ gap: 12px;
23
+ align-items: center;
24
+ }
25
+
26
+ .descriptionItem.isStacked {
27
+ gap: 2px;
28
+ align-items: stretch;
29
+ flex-flow: column;
30
+ }
31
+
32
+ .descriptionItem.isStacked .descriptionItemValue {
33
+ margin-left: 0;
34
+ justify-content: flex-start;
35
+ text-align: left;
36
+ }
37
+
38
+ .descriptionItemTerm {
39
+ display: flex;
40
+ gap: 9px;
41
+ align-items: center;
42
+ color: var(--foreground);
43
+ font-size: 15px;
44
+ }
45
+
46
+ .descriptionItemIcon {
47
+ flex-shrink: 0;
48
+ color: var(--foreground);
49
+ font-size: 16px;
50
+ }
51
+
52
+ .descriptionItemLabel {
53
+ overflow-wrap: anywhere;
54
+ }
55
+
56
+ .descriptionItemValue {
57
+ display: flex;
58
+ margin: 0;
59
+ margin-left: auto;
60
+ gap: 6px;
61
+ align-items: center;
62
+ color: var(--foreground-prominent);
63
+ font-size: 15px;
64
+ font-weight: 600;
65
+ text-align: right;
66
+ overflow-wrap: anywhere;
67
+ }
68
+
69
+ .descriptionListItems.isHorizontal {
70
+ gap: 0;
71
+ flex-flow: row;
72
+
73
+ .descriptionItem {
74
+ gap: 6px;
75
+ align-items: stretch;
76
+ flex: 1 1 0;
77
+ flex-flow: column;
78
+ padding-right: 24px;
79
+ padding-left: 24px;
80
+ border-left: 1px solid var(--gray-100);
81
+ }
82
+
83
+ .descriptionItem:first-child {
84
+ padding-left: 0;
85
+ border-left: 0;
86
+ }
87
+
88
+ .descriptionItemTerm {
89
+ font-size: 14px;
90
+ }
91
+
92
+ .descriptionItemValue {
93
+ margin-left: 0;
94
+ justify-content: flex-start;
95
+ font-weight: 500;
96
+ text-align: left;
97
+ }
98
+ }