@flux-ui/components 3.1.2 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component/FluxAvatarGroup.vue.d.ts +17 -0
- package/dist/component/FluxButton.vue.d.ts +2 -0
- package/dist/component/FluxContextMenu.vue.d.ts +26 -0
- package/dist/component/FluxDataTable.vue.d.ts +20 -10
- package/dist/component/FluxDescriptionItem.vue.d.ts +19 -0
- package/dist/component/FluxDescriptionList.vue.d.ts +17 -0
- package/dist/component/FluxFlyout.vue.d.ts +9 -2
- package/dist/component/FluxFormCombobox.vue.d.ts +20 -0
- package/dist/component/FluxFormRating.vue.d.ts +21 -0
- package/dist/component/FluxFormTagsInput.vue.d.ts +27 -0
- package/dist/component/FluxFormTextArea.vue.d.ts +6 -1
- package/dist/component/FluxInlineEdit.vue.d.ts +41 -0
- package/dist/component/FluxMenu.vue.d.ts +1 -0
- package/dist/component/FluxMenuFlyout.vue.d.ts +22 -0
- package/dist/component/FluxTableCell.vue.d.ts +1 -0
- package/dist/component/FluxTour.vue.d.ts +35 -0
- package/dist/component/FluxTourItem.vue.d.ts +18 -0
- package/dist/component/FluxVirtualScroller.vue.d.ts +27 -0
- package/dist/component/index.d.ts +12 -0
- package/dist/component/primitive/AnchorPopup.vue.d.ts +7 -1
- package/dist/component/primitive/SelectBase.vue.d.ts +3 -0
- package/dist/composable/private/index.d.ts +1 -0
- package/dist/composable/private/useMenuFlyout.d.ts +42 -0
- package/dist/data/di.d.ts +35 -0
- package/dist/data/i18n.d.ts +7 -0
- package/dist/index.css +449 -5
- package/dist/index.js +2156 -408
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/component/FluxAvatarGroup.vue +52 -0
- package/src/component/FluxButton.vue +3 -0
- package/src/component/FluxContextMenu.vue +134 -0
- package/src/component/FluxDataTable.vue +113 -32
- package/src/component/FluxDescriptionItem.vue +43 -0
- package/src/component/FluxDescriptionList.vue +37 -0
- package/src/component/FluxDestructiveButton.vue +2 -1
- package/src/component/FluxFlyout.vue +16 -3
- package/src/component/FluxFormCombobox.vue +98 -0
- package/src/component/FluxFormRating.vue +172 -0
- package/src/component/FluxFormTagsInput.vue +249 -0
- package/src/component/FluxFormTextArea.vue +16 -1
- package/src/component/FluxInlineEdit.vue +176 -0
- package/src/component/FluxMenu.vue +13 -3
- package/src/component/FluxMenuFlyout.vue +118 -0
- package/src/component/FluxPrimaryButton.vue +2 -1
- package/src/component/FluxPrimaryLinkButton.vue +2 -1
- package/src/component/FluxPublishButton.vue +2 -1
- package/src/component/FluxSecondaryButton.vue +2 -1
- package/src/component/FluxSecondaryLinkButton.vue +2 -1
- package/src/component/FluxTableCell.vue +2 -0
- package/src/component/FluxTour.vue +332 -0
- package/src/component/FluxTourItem.vue +27 -0
- package/src/component/FluxVirtualScroller.vue +96 -0
- package/src/component/index.ts +12 -0
- package/src/component/primitive/AnchorPopup.vue +27 -0
- package/src/component/primitive/SelectBase.vue +37 -2
- package/src/composable/private/index.ts +1 -0
- package/src/composable/private/useMenuFlyout.ts +417 -0
- package/src/css/component/AvatarGroup.module.scss +22 -0
- package/src/css/component/ContextMenu.module.scss +17 -0
- package/src/css/component/DescriptionList.module.scss +98 -0
- package/src/css/component/Form.module.scss +51 -0
- package/src/css/component/FormRating.module.scss +47 -0
- package/src/css/component/InlineEdit.module.scss +45 -0
- package/src/css/component/Menu.module.scss +4 -1
- package/src/css/component/MenuFlyout.module.scss +38 -0
- package/src/css/component/Table.module.scss +16 -0
- package/src/css/component/Tour.module.scss +108 -0
- package/src/css/component/VirtualScroller.module.scss +17 -0
- package/src/css/mixin/button-active.scss +3 -1
- package/src/data/di.ts +40 -0
- package/src/data/i18n.ts +7 -0
|
@@ -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
|
+
}
|
|
@@ -1134,3 +1134,54 @@
|
|
|
1134
1134
|
transition: 210ms var(--swift-out);
|
|
1135
1135
|
transition-property: background, border-color, color, opacity, scale, translate, mixin.focus-ring-transition-properties();
|
|
1136
1136
|
}
|
|
1137
|
+
|
|
1138
|
+
.formTagsInput {
|
|
1139
|
+
composes: formInput;
|
|
1140
|
+
|
|
1141
|
+
display: flex;
|
|
1142
|
+
height: unset;
|
|
1143
|
+
min-height: 42px;
|
|
1144
|
+
padding: 4px 6px;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
flex-wrap: wrap;
|
|
1147
|
+
gap: 6px;
|
|
1148
|
+
cursor: text;
|
|
1149
|
+
|
|
1150
|
+
&:not(.formTagsInputDisabled) {
|
|
1151
|
+
@include mixin.focus-ring(-1px, true);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.formTagsInputEnabled {
|
|
1156
|
+
composes: formTagsInput;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.formTagsInputDisabled {
|
|
1160
|
+
composes: formTagsInput;
|
|
1161
|
+
|
|
1162
|
+
background: var(--gray-100);
|
|
1163
|
+
cursor: not-allowed;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.formTagsInputField {
|
|
1167
|
+
height: 30px;
|
|
1168
|
+
min-width: 60px;
|
|
1169
|
+
flex: 1 1 60px;
|
|
1170
|
+
background: none;
|
|
1171
|
+
border: 0;
|
|
1172
|
+
color: inherit;
|
|
1173
|
+
font: inherit;
|
|
1174
|
+
outline: 0;
|
|
1175
|
+
|
|
1176
|
+
&::placeholder {
|
|
1177
|
+
color: var(--foreground-secondary);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
&:disabled {
|
|
1181
|
+
cursor: not-allowed;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.formTagsInputPopup {
|
|
1186
|
+
composes: formSelectPopup;
|
|
1187
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
@use '~flux/components/css/mixin';
|
|
2
|
+
|
|
3
|
+
.formRating {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
gap: .15em;
|
|
6
|
+
font-size: 24px;
|
|
7
|
+
line-height: 1;
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
|
|
10
|
+
@include mixin.focus-ring(4px);
|
|
11
|
+
|
|
12
|
+
&.isDisabled {
|
|
13
|
+
opacity: .6;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.formRatingStar {
|
|
18
|
+
position: relative;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
padding: 0;
|
|
21
|
+
border: 0;
|
|
22
|
+
background: none;
|
|
23
|
+
line-height: 0;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
|
|
26
|
+
&:disabled {
|
|
27
|
+
cursor: default;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.formRatingStarEmpty {
|
|
32
|
+
color: var(--gray-300);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.formRatingStarFull {
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 0;
|
|
38
|
+
bottom: 0;
|
|
39
|
+
left: 0;
|
|
40
|
+
color: var(--warning-400);
|
|
41
|
+
clip-path: inset(0 calc((1 - var(--fill, 0)) * 100%) 0 0);
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.isInvalid .formRatingStarEmpty {
|
|
46
|
+
color: var(--danger-200);
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@use '~flux/components/css/mixin';
|
|
2
|
+
|
|
3
|
+
.inlineEdit {
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: 6px;
|
|
6
|
+
align-items: flex-start;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.inlineEditField {
|
|
10
|
+
min-width: 0;
|
|
11
|
+
flex: 1 1 auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.inlineEditActions {
|
|
15
|
+
display: flex;
|
|
16
|
+
gap: 6px;
|
|
17
|
+
align-items: center;
|
|
18
|
+
flex-shrink: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.inlineEditDisplay {
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
min-height: 30px;
|
|
24
|
+
padding: 3px 6px;
|
|
25
|
+
align-items: center;
|
|
26
|
+
border-radius: 6px;
|
|
27
|
+
color: var(--foreground);
|
|
28
|
+
line-height: 21px;
|
|
29
|
+
white-space: pre-wrap;
|
|
30
|
+
overflow-wrap: anywhere;
|
|
31
|
+
|
|
32
|
+
&.isInteractive {
|
|
33
|
+
cursor: text;
|
|
34
|
+
|
|
35
|
+
@include mixin.focus-ring(2px);
|
|
36
|
+
|
|
37
|
+
@include mixin.hover {
|
|
38
|
+
background: var(--surface-hover);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.isPlaceholder {
|
|
43
|
+
color: var(--foreground-secondary);
|
|
44
|
+
}
|
|
45
|
+
}
|