@luna-park/design 1.0.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.
Files changed (130) hide show
  1. package/eslint.config.js +9 -0
  2. package/histoire.config.ts +60 -0
  3. package/package.json +71 -0
  4. package/public/favicon_rc.png +0 -0
  5. package/src/app.ts +9 -0
  6. package/src/assets/controls/mouse.svg +54 -0
  7. package/src/assets/logo_neon.svg +18 -0
  8. package/src/assets/logo_rc_color.svg +54 -0
  9. package/src/assets/logo_rc_square_blue.svg +16 -0
  10. package/src/assets/logo_square_blue.svg +17 -0
  11. package/src/assets/logo_square_white.svg +17 -0
  12. package/src/assets/logo_text_white.svg +32 -0
  13. package/src/assets/stars.svg +66 -0
  14. package/src/components/breadcrumb/LBreadLink.vue +40 -0
  15. package/src/components/breadcrumb/LBreadcrumb.story.vue +29 -0
  16. package/src/components/breadcrumb/LBreadcrumb.vue +54 -0
  17. package/src/components/breadcrumb/type.ts +6 -0
  18. package/src/components/context/LContextMenu.story.vue +73 -0
  19. package/src/components/context/LContextMenu.vue +24 -0
  20. package/src/components/context/LContextMenuElement.vue +54 -0
  21. package/src/components/context/LContextMenuWrapper.vue +55 -0
  22. package/src/components/context/LContextOption.story.vue +18 -0
  23. package/src/components/context/LContextOption.vue +160 -0
  24. package/src/components/context/LContextWrapper.story.vue +11 -0
  25. package/src/components/context/LContextWrapper.vue +60 -0
  26. package/src/components/context/store.ts +62 -0
  27. package/src/components/context/type.ts +27 -0
  28. package/src/components/dialog/LDialogAlert.vue +38 -0
  29. package/src/components/dialog/LDialogConfirm.vue +45 -0
  30. package/src/components/dialog/LDialogInjector.story.vue +41 -0
  31. package/src/components/dialog/LDialogInjector.vue +40 -0
  32. package/src/components/dialog/LDialogPrompt.vue +67 -0
  33. package/src/components/dialog/LDialogWrapper.vue +66 -0
  34. package/src/components/dialog/lib.ts +50 -0
  35. package/src/components/dialog/store.ts +32 -0
  36. package/src/components/floating/LFloating.story.vue +35 -0
  37. package/src/components/floating/LFloating.vue +362 -0
  38. package/src/components/form/LAutoComplete.vue +13 -0
  39. package/src/components/form/LAutoInput.story.vue +43 -0
  40. package/src/components/form/LAutoInput.vue +101 -0
  41. package/src/components/form/LButton.story.vue +147 -0
  42. package/src/components/form/LButton.vue +227 -0
  43. package/src/components/form/LCheckbox.story.vue +13 -0
  44. package/src/components/form/LCheckbox.vue +70 -0
  45. package/src/components/form/LColorInput.story.vue +28 -0
  46. package/src/components/form/LColorInput.vue +101 -0
  47. package/src/components/form/LImageInput.story.vue +28 -0
  48. package/src/components/form/LImageInput.vue +75 -0
  49. package/src/components/form/LInfo.story.vue +22 -0
  50. package/src/components/form/LInfo.vue +44 -0
  51. package/src/components/form/LInput.story.vue +150 -0
  52. package/src/components/form/LInput.vue +493 -0
  53. package/src/components/form/LInputDateFloating.vue +61 -0
  54. package/src/components/form/LInputNumber.story.vue +58 -0
  55. package/src/components/form/LProgress.story.vue +49 -0
  56. package/src/components/form/LProgress.vue +77 -0
  57. package/src/components/form/LSelect.story.vue +67 -0
  58. package/src/components/form/LSelect.vue +142 -0
  59. package/src/components/form/LSwitch.story.vue +15 -0
  60. package/src/components/form/LSwitch.vue +79 -0
  61. package/src/components/form/LTextarea.story.vue +29 -0
  62. package/src/components/form/LTextarea.vue +151 -0
  63. package/src/components/form/color-picker/LColorAlpha.vue +129 -0
  64. package/src/components/form/color-picker/LColorHue.vue +109 -0
  65. package/src/components/form/color-picker/LColorModels.vue +223 -0
  66. package/src/components/form/color-picker/LColorPicker.story.vue +44 -0
  67. package/src/components/form/color-picker/LColorPicker.vue +105 -0
  68. package/src/components/form/color-picker/LColorShade.vue +114 -0
  69. package/src/components/form/color-picker/LImagePicker.vue +477 -0
  70. package/src/components/form/dropdown/LDropdown.story.vue +123 -0
  71. package/src/components/form/dropdown/LDropdown.vue +483 -0
  72. package/src/components/form/dropdown/LDropdownOption.vue +224 -0
  73. package/src/components/form/dropdown/LDropdownSelection.vue +76 -0
  74. package/src/components/form/dropdown/types.ts +15 -0
  75. package/src/components/form/emoji-picker/LEmojiList.vue +54 -0
  76. package/src/components/form/emoji-picker/LEmojiListCategory.vue +92 -0
  77. package/src/components/form/emoji-picker/LEmojiPicker.story.vue +32 -0
  78. package/src/components/form/emoji-picker/LEmojiPicker.vue +55 -0
  79. package/src/components/form/emoji-picker/LEmojiSelect.story.vue +22 -0
  80. package/src/components/form/emoji-picker/LEmojiSelect.vue +51 -0
  81. package/src/components/form/icon-picker/LIconList.vue +100 -0
  82. package/src/components/form/icon-picker/LIconMaterial.vue +43 -0
  83. package/src/components/form/icon-picker/LIconPicker.story.vue +39 -0
  84. package/src/components/form/icon-picker/LIconPicker.vue +92 -0
  85. package/src/components/form/icon-picker/LIconSelect.story.vue +25 -0
  86. package/src/components/form/icon-picker/LIconSelect.vue +91 -0
  87. package/src/components/icons/LControls.story.vue +92 -0
  88. package/src/components/icons/LKeyIcon.vue +66 -0
  89. package/src/components/icons/LMouseIcon.vue +85 -0
  90. package/src/components/icons/LShortcut.story.vue +12 -0
  91. package/src/components/icons/LShortcut.vue +45 -0
  92. package/src/components/layout/LResizer.story.vue +89 -0
  93. package/src/components/layout/LResizer.vue +138 -0
  94. package/src/components/misc/LIcon.vue +34 -0
  95. package/src/components/misc/LLineLoader.story.vue +18 -0
  96. package/src/components/misc/LLineLoader.vue +52 -0
  97. package/src/components/misc/LLoading.story.vue +14 -0
  98. package/src/components/misc/LLoading.vue +28 -0
  99. package/src/components/misc/LStarsBackground.story.vue +16 -0
  100. package/src/components/misc/LStarsBackground.vue +121 -0
  101. package/src/components/navigation/LElementsPagination.vue +75 -0
  102. package/src/components/navigation/LPagination.story.vue +57 -0
  103. package/src/components/navigation/LPagination.vue +125 -0
  104. package/src/components/table/LCell.vue +37 -0
  105. package/src/components/table/LLine.vue +24 -0
  106. package/src/components/table/LTable.story.vue +35 -0
  107. package/src/components/table/LTable.vue +21 -0
  108. package/src/components/toasts/LContainer.story.vue +47 -0
  109. package/src/components/toasts/LContainer.vue +140 -0
  110. package/src/components/toasts/LToast.story.vue +47 -0
  111. package/src/components/toasts/LToast.vue +30 -0
  112. package/src/components/toasts/LToastInjector.vue +54 -0
  113. package/src/components/toasts/requests.ts +46 -0
  114. package/src/components/toasts/store.ts +45 -0
  115. package/src/components/utils/LVirtualElement.vue +36 -0
  116. package/src/components/utils/LVirtualScroller.story.vue +62 -0
  117. package/src/components/utils/LVirtualScroller.vue +105 -0
  118. package/src/components/utils/virtual.ts +6 -0
  119. package/src/env.d.ts +9 -0
  120. package/src/histoire.setup.ts +8 -0
  121. package/src/icons.ts +8 -0
  122. package/src/index.ts +58 -0
  123. package/src/style/colors.scss +152 -0
  124. package/src/style/fonts.scss +45 -0
  125. package/src/style/index.scss +64 -0
  126. package/src/style/layout.scss +3 -0
  127. package/src/style/lengths.scss +21 -0
  128. package/src/style/scrollbar.scss +27 -0
  129. package/tsconfig.json +34 -0
  130. package/vite.config.ts +68 -0
@@ -0,0 +1,483 @@
1
+ <template>
2
+ <LFloating
3
+ ref="floating"
4
+ class="dropdown"
5
+ :class="{ 'with-label':!!label, 'with-value': hasValue, big, small, disabled, updated, borderless }"
6
+ :disabled="disabled"
7
+ tabindex="0"
8
+ @show="processShow"
9
+ >
10
+ <slot>
11
+ <span
12
+ v-if="label"
13
+ class="label"
14
+ >
15
+ {{ label }}
16
+ </span>
17
+ <div
18
+ class="selector"
19
+ :class="{cell, transparent}"
20
+ :title="currentOption?.value"
21
+ @click.alt.stop="reset"
22
+ >
23
+ <div v-if="slots.prefix">
24
+ <slot name="prefix" />
25
+ </div>
26
+ <LIcon
27
+ v-if="current || currentOption?.icon"
28
+ class="icon"
29
+ :icon="current?.icon ?? currentOption?.icon?.icon"
30
+ :style="{color: current?.color ?? currentOption?.icon?.color ?? 'var(--color-content-liter)'}"
31
+ />
32
+ <input
33
+ v-if="custom"
34
+ v-model="value"
35
+ class="text"
36
+ :placeholder="placeholder ?? (label ? '' : 'Select...')"
37
+ >
38
+ <input
39
+ v-else-if="search"
40
+ v-model="filter"
41
+ class="text"
42
+ :class="{defined: currentOption}"
43
+ :placeholder="currentOption?.value ?? placeholder ?? (label ? '' : 'Select...')"
44
+ @keydown="processKeyDown"
45
+ >
46
+ <input
47
+ v-else
48
+ class="text"
49
+ :placeholder="placeholder ?? (label ? '' : 'Select...')"
50
+ :readonly="true"
51
+ :value="currentOption?.value"
52
+ >
53
+ <LIcon
54
+ v-if="clearable && currentOption?.value"
55
+ class="clear"
56
+ :icon="faCircleXmark"
57
+ @click.stop="reset"
58
+ />
59
+ <LIcon
60
+ v-if="!noCaret"
61
+ class="caret"
62
+ :icon="faCaretDown"
63
+ />
64
+ </div>
65
+ </slot>
66
+ <template #popper="{hide}">
67
+ <input
68
+ v-if="searchInside"
69
+ ref="search"
70
+ v-model="filter"
71
+ autofocus
72
+ class="search-inside"
73
+ placeholder="Search..."
74
+ @keydown="processKeyDown"
75
+ >
76
+ <LDropdownSelection
77
+ :big="big"
78
+ :hover="hoverOption"
79
+ :max-height="maxHeight"
80
+ :options="internalOptions"
81
+ :selected="value"
82
+ @select="select($event, hide)"
83
+ />
84
+ </template>
85
+ </LFloating>
86
+ </template>
87
+
88
+ <script setup lang="ts">
89
+ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
90
+ import { faCaretDown, faCircleXmark } from "@fortawesome/pro-solid-svg-icons";
91
+ import { cloneWithFunctions } from "@luna-park/utils";
92
+ import { computed, nextTick, ref, useSlots, useTemplateRef, watch } from "vue";
93
+
94
+ import LFloating from "@/components/floating/LFloating.vue";
95
+ import LDropdownSelection from "@/components/form/dropdown/LDropdownSelection.vue";
96
+ import { TOption, TOptions } from "@/components/form/dropdown/types";
97
+ import LIcon from "@/components/misc/LIcon.vue";
98
+
99
+ const props = withDefaults(defineProps<{
100
+ big?: boolean;
101
+ borderless?: boolean;
102
+ cell?: boolean;
103
+ clearable?: boolean;
104
+ current?: {
105
+ color?: string;
106
+ icon?: string | IconDefinition;
107
+ };
108
+ custom?: boolean;
109
+ disabled?: boolean;
110
+ label?: string;
111
+ maxHeight?: boolean;
112
+ noCaret?: boolean;
113
+ options?: TOptions;
114
+ placeholder?: string;
115
+ search?: boolean;
116
+ searchInside?: boolean;
117
+ small?: boolean;
118
+ transparent?: boolean;
119
+ updated?: boolean;
120
+ }>(), {
121
+ big: false,
122
+ options: () => []
123
+ });
124
+
125
+ const value = defineModel<string | number>({
126
+ default: ""
127
+ });
128
+
129
+ const filter = ref("");
130
+ const searchElement = useTemplateRef("search");
131
+ const floatingElement = useTemplateRef("floating");
132
+ const hoverOption = ref<Array<string>>([]);
133
+
134
+ const slots = useSlots();
135
+
136
+ const rawOptions = computed<Array<TOption>>(() => {
137
+ if (!Array.isArray(props.options)) {
138
+ return Object.entries(props.options).map(([id, value]) => ({ id, value }));
139
+ }
140
+ else {
141
+ return props.options.map((value) => {
142
+ if (["string", "number"].includes(typeof value)) {
143
+ return { id: value, value };
144
+ }
145
+
146
+ return value;
147
+ });
148
+ }
149
+ });
150
+
151
+ const internalOptions = computed<Array<TOption>>(() => {
152
+ let options = rawOptions.value;
153
+
154
+ if (props.search || props.searchInside) {
155
+ const filterLower = filter.value.toLowerCase();
156
+ options = cloneWithFunctions(options);
157
+ options = filterOptions(options, filterLower);
158
+ }
159
+
160
+ return options;
161
+
162
+ function filterOptions(options: Array<TOption>, filterText: string): Array<TOption> {
163
+ return options.filter((option) => {
164
+ if (option.children) {
165
+ option.children = filterOptions(option.children, filterText);
166
+ }
167
+
168
+ return option.value.toLowerCase().includes(filterText) ||
169
+ option.id.toLowerCase().includes(filterText) ||
170
+ option.children?.length;
171
+ });
172
+ }
173
+ });
174
+
175
+ const flatRawOptions = computed(() => flattenOptions(rawOptions.value));
176
+ const flatInternalOptions = computed(() => flattenOptions(internalOptions.value));
177
+
178
+ const currentOption = computed(() => {
179
+ return flatRawOptions.value.find((option) => option.id === value.value);
180
+ });
181
+
182
+ const hasValue = computed(() => !!value.value.toString().trim() || props.placeholder);
183
+
184
+ async function select(option: TOption, hide: () => void) {
185
+ if (props.search || props.searchInside) {
186
+ filter.value = "";
187
+ }
188
+
189
+ if (option.disabled) {
190
+ return;
191
+ }
192
+
193
+ if (option.action) {
194
+ await option.action();
195
+ }
196
+ else {
197
+ value.value = option.id;
198
+ }
199
+
200
+ hide();
201
+ }
202
+
203
+ function processKeyDown(event: KeyboardEvent) {
204
+ if (event.key === "ArrowDown") {
205
+ event.preventDefault();
206
+ moveOption("down");
207
+ }
208
+
209
+ if (event.key === "ArrowUp") {
210
+ event.preventDefault();
211
+ moveOption("up");
212
+ }
213
+
214
+ if (event.key === "ArrowRight" && hoverOption.value.length) {
215
+ event.preventDefault();
216
+ moveOption("right");
217
+ }
218
+
219
+ if (event.key === "ArrowLeft" && hoverOption.value.length) {
220
+ event.preventDefault();
221
+ moveOption("left");
222
+ }
223
+
224
+ if (event.key === "Enter") {
225
+ event.preventDefault();
226
+ const option = flatRawOptions.value.find((option) => option.id === hoverOption.value.at(-1));
227
+ if (option) {
228
+ select(option, floatingElement.value?.hide);
229
+ }
230
+ }
231
+
232
+ if (event.key === "Escape") {
233
+ floatingElement.value?.hide();
234
+ }
235
+ }
236
+
237
+ function moveOption(direction: "down" | "up" | "right" | "left") {
238
+ if (!hoverOption.value.length && internalOptions.value[0]) {
239
+ hoverOption.value.push(internalOptions.value[0].id);
240
+ return;
241
+ }
242
+
243
+ if (direction === "left") {
244
+ if (hoverOption.value.length) {
245
+ hoverOption.value.pop();
246
+ }
247
+ return;
248
+ }
249
+
250
+ let parentOption = internalOptions.value;
251
+
252
+ if (hoverOption.value.length > 1) {
253
+ parentOption = flatInternalOptions.value.find((option) => option.id === hoverOption.value.at(-2))?.children;
254
+ }
255
+
256
+ const lastIndex = parentOption.length - 1;
257
+ const currentIndex = parentOption?.findIndex((o) => o.id === hoverOption.value.at(-1));
258
+
259
+ if (direction === "down") {
260
+ hoverOption.value.pop();
261
+ hoverOption.value.push(parentOption?.[Math.min(currentIndex + 1, lastIndex)]?.id);
262
+ return;
263
+ }
264
+
265
+ if (direction === "up") {
266
+ hoverOption.value.pop();
267
+ hoverOption.value.push(parentOption?.[Math.max(currentIndex - 1, 0)]?.id);
268
+ return;
269
+ }
270
+
271
+ if (direction === "right") {
272
+ const currentOption = parentOption[currentIndex];
273
+ if (currentOption.children?.length) {
274
+ hoverOption.value.push(currentOption.children[0].id);
275
+ }
276
+ return;
277
+ }
278
+ }
279
+
280
+ async function processShow() {
281
+ hoverOption.value = [];
282
+ if (props.searchInside) {
283
+ await nextTick();
284
+ filter.value = "";
285
+ searchElement.value?.focus();
286
+ }
287
+ }
288
+
289
+ function reset() {
290
+ value.value = "";
291
+ }
292
+
293
+ function flattenOptions(options: Array<TOption>): Array<TOption> {
294
+ return options.reduce(
295
+ (acc, option) => [...acc, option, ...(option.children ? flattenOptions(option.children) : [])],
296
+ [] as Array<TOption>
297
+ );
298
+ }
299
+
300
+ watch(filter, () => {
301
+ hoverOption.value = [];
302
+
303
+ if (internalOptions.value[0]) {
304
+ hoverOption.value.push(internalOptions.value[0].id);
305
+
306
+ while (flatInternalOptions.value.find((option) => option.id === hoverOption.value.at(-1))?.children?.length) {
307
+ hoverOption.value.push(flatInternalOptions.value.find((option) => option.id === hoverOption.value.at(-1))?.children[0].id);
308
+ }
309
+ }
310
+ });
311
+ </script>
312
+
313
+ <style scoped>
314
+ .dropdown {
315
+ font-size: 0.8rem;
316
+ position: relative;
317
+ transition: var(--transition-fast);
318
+
319
+ .label {
320
+ position: absolute;
321
+ top: 2px;
322
+ left: 0;
323
+ transform: translate(6px, 0px);
324
+ transition: var(--transition-fast), transform var(--duration-fast);
325
+ color: var(--color-content-liter);
326
+ opacity: 0.5;
327
+ }
328
+
329
+ &.big {
330
+ font-size: .9rem;
331
+
332
+ .label {
333
+ transform: translate(10px, 0px);
334
+ top: 7px;
335
+ }
336
+
337
+ .selector {
338
+ border-width: 2px;
339
+ --dropdown-radius: var(--length-radius-m);
340
+ height: 36px;
341
+
342
+ .icon, .caret, .clear {
343
+ width: 12px;
344
+ height: 12px;
345
+ }
346
+ }
347
+ }
348
+
349
+ &.updated {
350
+ .selector {
351
+ background: var(--color-primary-litest);
352
+ }
353
+ }
354
+
355
+ &.small {
356
+ font-size: .7rem;
357
+
358
+ .selector {
359
+ --dropdown-radius: var(--length-radius-xs);
360
+ padding: 0 var(--length-xxxs);
361
+ height: 20px;
362
+ }
363
+ }
364
+
365
+ &.with-label.with-value {
366
+ margin-top: 18px;
367
+
368
+ .label {
369
+ transform: translate(0px, -24px);
370
+ color: var(--color-content-liter);
371
+ opacity: 1;
372
+ }
373
+
374
+ &.big {
375
+ margin-top: 24px;
376
+
377
+ .label {
378
+ transform: translate(0px, -36px);
379
+ }
380
+ }
381
+ }
382
+
383
+ .selector {
384
+ --dropdown-radius: var(--length-radius-s);
385
+
386
+ background: var(--color-background-1);
387
+ cursor: pointer;
388
+ border: 1px solid var(--color-soft);
389
+ padding: 0 var(--length-xxs);
390
+ display: flex;
391
+ align-items: center;
392
+ gap: var(--length-xxs);
393
+ transition: background var(--duration-fast);
394
+ box-sizing: border-box;
395
+ height: 24px;
396
+
397
+ &:not(.cell) {
398
+ border-radius: var(--dropdown-radius);
399
+ }
400
+
401
+ &.transparent {
402
+ background: none;
403
+ }
404
+
405
+ .icon, .caret, .clear {
406
+ flex: 0 0 auto;
407
+ width: 10px;
408
+ height: 10px;
409
+ }
410
+
411
+ .caret {
412
+ flex: none;
413
+ color: var(--color-content-litest);
414
+ padding: 0 var(--length-xxs);
415
+ transition: var(--transition-fast);
416
+ }
417
+
418
+ .clear {
419
+ color: var(--color-content-litest);
420
+ cursor: pointer;
421
+ transition: var(--transition-fast);
422
+
423
+ &:hover {
424
+ color: var(--color-content-liter);
425
+ }
426
+ }
427
+
428
+ .text {
429
+ cursor: inherit;
430
+ width: 100%;
431
+ overflow: hidden;
432
+ text-overflow: ellipsis;
433
+
434
+ &::placeholder {
435
+ color: var(--color-content-liter);
436
+ }
437
+
438
+ &.defined::placeholder {
439
+ color: inherit;
440
+ opacity: 1;
441
+ }
442
+ }
443
+
444
+ &:hover {
445
+ &:not(.transparent) {
446
+ background: var(--color-background-2);
447
+ }
448
+
449
+ .caret {
450
+ color: var(--color-content-liter);
451
+ }
452
+ }
453
+ }
454
+
455
+ &.disabled {
456
+ .selector {
457
+ cursor: default;
458
+ background: var(--color-background-0);
459
+ border-color: var(--color-soft);
460
+ color: var(--color-content-liter);
461
+
462
+ &:hover {
463
+ background: var(--color-background-0);
464
+
465
+ .caret {
466
+ color: var(--color-content-litest);
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ &.borderless {
473
+ .selector {
474
+ border-color: transparent;
475
+ }
476
+ }
477
+ }
478
+
479
+ .search-inside {
480
+ width: 100%;
481
+ border-bottom: 1px solid var(--color-background-3);
482
+ }
483
+ </style>
@@ -0,0 +1,224 @@
1
+ <template>
2
+ <LFloating
3
+ :container="container"
4
+ deep
5
+ :depth="depth + 1"
6
+ :disabled="!option.children"
7
+ :display="hover[0] === option.id && hover.length > 1"
8
+ mode="hover"
9
+ placement="right-start"
10
+ >
11
+ <li
12
+ ref="option"
13
+ class="option"
14
+ :class="{disabled: option.disabled, parent: option.children, selected: option.id === selected, hover: hover[0] === option.id}"
15
+ @click="select(option)"
16
+ @mousemove="onOptionMouseMove"
17
+ >
18
+ <LIcon
19
+ v-if="option.icon"
20
+ class="icon"
21
+ :icon="option.icon.icon"
22
+ :style="getStyle(option)"
23
+ />
24
+ <span class="value">
25
+ {{ option.value }}
26
+ </span>
27
+ <LIcon
28
+ v-if="option.children"
29
+ class="icon"
30
+ :icon="faCaretRight"
31
+ />
32
+ </li>
33
+ <template #popper>
34
+ <LDropdownSelection
35
+ v-if="option.children"
36
+ :big="big"
37
+ :depth="depth + 1"
38
+ :hover="hover[0] === option.id ? hover.slice(1) : []"
39
+ :max-height="maxHeight"
40
+ :options="option.children"
41
+ :selected="selected"
42
+ @mouseenter="resetShadow"
43
+ @select="select"
44
+ />
45
+ </template>
46
+ <template #content>
47
+ <div
48
+ v-if="option.children"
49
+ ref="shadow"
50
+ class="shadow-polygon"
51
+ :style="shadowStyle"
52
+ @mousemove="onShadowMouseMove"
53
+ />
54
+ </template>
55
+ </LFloating>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ import { faCaretRight } from "@fortawesome/pro-solid-svg-icons";
60
+ import LDropdownSelection from "@/components/form/dropdown/LDropdownSelection.vue";
61
+ import LFloating from "@/components/floating/LFloating.vue";
62
+ import LIcon from "@/components/misc/LIcon.vue";
63
+ import { TOption } from "@/components/form/dropdown/types";
64
+ import { ref, useTemplateRef } from "vue";
65
+
66
+ const SHADOW_THRESHOLD = 4;
67
+
68
+ const props = withDefaults(defineProps<{
69
+ big?: boolean;
70
+ container?: string;
71
+ depth?: number;
72
+ hover: Array<string>;
73
+ maxHeight?: boolean;
74
+ option: TOption;
75
+ selected?: string | number
76
+ }>(), {
77
+ depth: 0
78
+ });
79
+
80
+ const emits = defineEmits<(e: "select", value: TOption) => void>();
81
+
82
+ const optionTemplate = useTemplateRef("option");
83
+ const shadowTemplate = useTemplateRef("shadow");
84
+
85
+ const shadowStyle = ref<Partial<CSSStyleDeclaration>>({});
86
+ const shadowOffset = ref(0);
87
+
88
+ async function select(option: TOption) {
89
+ emits("select", option);
90
+ }
91
+
92
+ function getStyle(option: TOption) {
93
+ return {
94
+ color: option.icon?.color ?? "var(--color-content-liter)"
95
+ };
96
+ }
97
+
98
+ function onOptionMouseMove(event: MouseEvent) {
99
+ requestAnimationFrame(() => {
100
+ const optionBounding = optionTemplate.value?.getBoundingClientRect();
101
+ const contentBounding = shadowTemplate.value?.parentElement?.querySelector(".content")?.getBoundingClientRect();
102
+
103
+ if (!optionBounding || !contentBounding) {
104
+ return;
105
+ }
106
+
107
+ shadowStyle.value = {
108
+ height: `${ contentBounding.height - optionBounding.height }px`
109
+ };
110
+ shadowOffset.value = 0;
111
+
112
+ if (contentBounding.left - optionBounding.left > 0) {
113
+ shadowStyle.value.left = `${ event.clientX - optionBounding.left - SHADOW_THRESHOLD - optionBounding.width }px`;
114
+ shadowStyle.value.right = "100%";
115
+
116
+ if (contentBounding.top - optionBounding.top > -8) {
117
+ shadowStyle.value.top = `${ optionBounding.height }px`;
118
+ shadowStyle.value.clipPath = `polygon(0 0, 100% 0, 100% 100%, calc(100% - ${ SHADOW_THRESHOLD }px) 100%, 0 2px)`;
119
+ }
120
+ else {
121
+ shadowStyle.value.bottom = `${ optionBounding.height + (contentBounding.bottom - optionBounding.bottom) - 2 }px`;
122
+ shadowStyle.value.clipPath = `polygon(0 100%, 100% 100%, 100% 0, calc(100% - ${ SHADOW_THRESHOLD }px) 0, 0 calc(100% - 2px))`;
123
+ }
124
+ }
125
+ else {
126
+ shadowStyle.value.left = "100%";
127
+ shadowStyle.value.right = `${ - event.clientX + optionBounding.left - SHADOW_THRESHOLD }px`;
128
+
129
+ if (contentBounding.top - optionBounding.top > -8) {
130
+ shadowStyle.value.clipPath = `polygon(100% 0, 0 0, 0 100%, ${ SHADOW_THRESHOLD }px 100%, 100% 2px)`;
131
+ }
132
+ else {
133
+ shadowStyle.value.clipPath = `polygon(100% 100%, 0 100%, 0 0, ${ SHADOW_THRESHOLD }px 0, 100% calc(100% - 2px))`;
134
+ shadowStyle.value.bottom = `${ optionBounding.height + (contentBounding.bottom - optionBounding.bottom) - 2 }px`;
135
+ }
136
+ }
137
+ });
138
+ }
139
+
140
+ function onShadowMouseMove(event: MouseEvent) {
141
+ requestAnimationFrame(() => {
142
+ const optionBounding = optionTemplate.value?.getBoundingClientRect();
143
+ const shadowBounding = shadowTemplate.value?.getBoundingClientRect();
144
+ const contentBounding = shadowTemplate.value?.parentElement?.querySelector(".content")?.getBoundingClientRect();
145
+
146
+ if (contentBounding.left - optionBounding.left > 0) {
147
+ shadowOffset.value = Math.max(event.clientX - shadowBounding.left - SHADOW_THRESHOLD, shadowOffset.value);
148
+
149
+ if (contentBounding.top - optionBounding.top > -8) {
150
+ shadowStyle.value.clipPath = `polygon(${ shadowOffset.value }px 0, 100% 0, 100% 100%, calc(100% - ${ SHADOW_THRESHOLD }px) 100% ,${ shadowOffset.value }px calc(2px + ${ shadowOffset.value / shadowBounding.width * 100 }%))`;
151
+ }
152
+ else {
153
+ shadowStyle.value.clipPath = `polygon(${ shadowOffset.value }px 100%, 100% 100%, 100% 0, calc(100% - ${ SHADOW_THRESHOLD }px) 0, ${ shadowOffset.value }px calc(100% - 2px - ${ shadowOffset.value / shadowBounding.width * 100 }%))`;
154
+ }
155
+ }
156
+ else {
157
+ shadowOffset.value = Math.max(shadowBounding.right - event.clientX - SHADOW_THRESHOLD, shadowOffset.value);
158
+
159
+ if (contentBounding.top - optionBounding.top > -8) {
160
+ shadowStyle.value.clipPath = `polygon(calc(100% - ${ shadowOffset.value }px) 0, 0 0, 0 100%, 8px 100%, calc(100% - ${ shadowOffset.value }px) calc(2px + ${ shadowOffset.value / shadowBounding.width * 100 }%))`;
161
+ }
162
+ else {
163
+ shadowStyle.value.clipPath = `polygon(calc(100% - ${ shadowOffset.value }px) 100%, 0 100%, 0 0, 8px 0, calc(100% - ${ shadowOffset.value }px) calc(100% - 2px - ${ shadowOffset.value / shadowBounding.width * 100 }%))`;
164
+ }
165
+ }
166
+ });
167
+ }
168
+
169
+ function resetShadow() {
170
+ shadowStyle.value = {};
171
+ }
172
+ </script>
173
+
174
+ <style scoped>
175
+
176
+
177
+ .option {
178
+ padding: 0 var(--length-xxs);
179
+ opacity: 0.75;
180
+ display: flex;
181
+ align-items: center;
182
+ gap: var(--length-xxs);
183
+ cursor: pointer;
184
+
185
+ .icon {
186
+ width: 10px;
187
+ height: 10px;
188
+ }
189
+
190
+ .value {
191
+ flex: 1 1 0;
192
+ }
193
+
194
+ &.disabled {
195
+ cursor: default;
196
+ opacity: 0.5;
197
+ }
198
+
199
+ &.selected {
200
+ background: var(--color-background-2);
201
+ opacity: 1;
202
+ }
203
+
204
+ &:is(:not(.disabled), .parent):hover {
205
+ background: var(--color-primary-liter);
206
+ opacity: 1;
207
+ }
208
+
209
+ &.hover {
210
+ background: var(--color-primary-liter);
211
+ opacity: 1;
212
+
213
+ &.disabled {
214
+ opacity: 0.5;
215
+ }
216
+ }
217
+ }
218
+
219
+ .shadow-polygon {
220
+ position: absolute;
221
+ z-index: 1;
222
+ bottom: 0;
223
+ }
224
+ </style>