@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.
Files changed (72) hide show
  1. package/dist/component/FluxAvatarGroup.vue.d.ts +17 -0
  2. package/dist/component/FluxButton.vue.d.ts +2 -0
  3. package/dist/component/FluxContextMenu.vue.d.ts +26 -0
  4. package/dist/component/FluxDataTable.vue.d.ts +20 -10
  5. package/dist/component/FluxDescriptionItem.vue.d.ts +19 -0
  6. package/dist/component/FluxDescriptionList.vue.d.ts +17 -0
  7. package/dist/component/FluxFlyout.vue.d.ts +9 -2
  8. package/dist/component/FluxFormCombobox.vue.d.ts +20 -0
  9. package/dist/component/FluxFormRating.vue.d.ts +21 -0
  10. package/dist/component/FluxFormTagsInput.vue.d.ts +27 -0
  11. package/dist/component/FluxFormTextArea.vue.d.ts +6 -1
  12. package/dist/component/FluxInlineEdit.vue.d.ts +41 -0
  13. package/dist/component/FluxMenu.vue.d.ts +1 -0
  14. package/dist/component/FluxMenuFlyout.vue.d.ts +22 -0
  15. package/dist/component/FluxTableCell.vue.d.ts +1 -0
  16. package/dist/component/FluxTour.vue.d.ts +35 -0
  17. package/dist/component/FluxTourItem.vue.d.ts +18 -0
  18. package/dist/component/FluxVirtualScroller.vue.d.ts +27 -0
  19. package/dist/component/index.d.ts +12 -0
  20. package/dist/component/primitive/AnchorPopup.vue.d.ts +7 -1
  21. package/dist/component/primitive/SelectBase.vue.d.ts +3 -0
  22. package/dist/composable/private/index.d.ts +1 -0
  23. package/dist/composable/private/useMenuFlyout.d.ts +42 -0
  24. package/dist/data/di.d.ts +35 -0
  25. package/dist/data/i18n.d.ts +7 -0
  26. package/dist/index.css +449 -5
  27. package/dist/index.js +2156 -408
  28. package/dist/index.js.map +1 -1
  29. package/package.json +7 -7
  30. package/src/component/FluxAvatarGroup.vue +52 -0
  31. package/src/component/FluxButton.vue +3 -0
  32. package/src/component/FluxContextMenu.vue +134 -0
  33. package/src/component/FluxDataTable.vue +113 -32
  34. package/src/component/FluxDescriptionItem.vue +43 -0
  35. package/src/component/FluxDescriptionList.vue +37 -0
  36. package/src/component/FluxDestructiveButton.vue +2 -1
  37. package/src/component/FluxFlyout.vue +16 -3
  38. package/src/component/FluxFormCombobox.vue +98 -0
  39. package/src/component/FluxFormRating.vue +172 -0
  40. package/src/component/FluxFormTagsInput.vue +249 -0
  41. package/src/component/FluxFormTextArea.vue +16 -1
  42. package/src/component/FluxInlineEdit.vue +176 -0
  43. package/src/component/FluxMenu.vue +13 -3
  44. package/src/component/FluxMenuFlyout.vue +118 -0
  45. package/src/component/FluxPrimaryButton.vue +2 -1
  46. package/src/component/FluxPrimaryLinkButton.vue +2 -1
  47. package/src/component/FluxPublishButton.vue +2 -1
  48. package/src/component/FluxSecondaryButton.vue +2 -1
  49. package/src/component/FluxSecondaryLinkButton.vue +2 -1
  50. package/src/component/FluxTableCell.vue +2 -0
  51. package/src/component/FluxTour.vue +332 -0
  52. package/src/component/FluxTourItem.vue +27 -0
  53. package/src/component/FluxVirtualScroller.vue +96 -0
  54. package/src/component/index.ts +12 -0
  55. package/src/component/primitive/AnchorPopup.vue +27 -0
  56. package/src/component/primitive/SelectBase.vue +37 -2
  57. package/src/composable/private/index.ts +1 -0
  58. package/src/composable/private/useMenuFlyout.ts +417 -0
  59. package/src/css/component/AvatarGroup.module.scss +22 -0
  60. package/src/css/component/ContextMenu.module.scss +17 -0
  61. package/src/css/component/DescriptionList.module.scss +98 -0
  62. package/src/css/component/Form.module.scss +51 -0
  63. package/src/css/component/FormRating.module.scss +47 -0
  64. package/src/css/component/InlineEdit.module.scss +45 -0
  65. package/src/css/component/Menu.module.scss +4 -1
  66. package/src/css/component/MenuFlyout.module.scss +38 -0
  67. package/src/css/component/Table.module.scss +16 -0
  68. package/src/css/component/Tour.module.scss +108 -0
  69. package/src/css/component/VirtualScroller.module.scss +17 -0
  70. package/src/css/mixin/button-active.scss +3 -1
  71. package/src/data/di.ts +40 -0
  72. package/src/data/i18n.ts +7 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flux-ui/components",
3
3
  "description": "A set of opiniated UI components.",
4
- "version": "3.1.2",
4
+ "version": "3.1.4",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/basmilius",
@@ -55,10 +55,10 @@
55
55
  "**/dist/index.css"
56
56
  ],
57
57
  "dependencies": {
58
- "@basmilius/common": "^3.41.0",
59
- "@basmilius/utils": "^3.41.0",
60
- "@flux-ui/internals": "3.1.2",
61
- "@flux-ui/types": "3.1.2",
58
+ "@basmilius/common": "^3.43.0",
59
+ "@basmilius/utils": "^3.43.0",
60
+ "@flux-ui/internals": "3.1.4",
61
+ "@flux-ui/types": "3.1.4",
62
62
  "@fortawesome/fontawesome-common-types": "^7.2.0",
63
63
  "clsx": "^2.1.1",
64
64
  "imask": "^7.6.1",
@@ -66,10 +66,10 @@
66
66
  },
67
67
  "peerDependencies": {
68
68
  "luxon": "^3.7.2",
69
- "vue": "^3.6.0-beta.15"
69
+ "vue": "^3.6.0-beta.16"
70
70
  },
71
71
  "devDependencies": {
72
- "@basmilius/vite-preset": "^3.41.0",
72
+ "@basmilius/vite-preset": "^3.43.0",
73
73
  "@types/lodash-es": "^4.17.12",
74
74
  "@types/luxon": "^3.7.1",
75
75
  "@types/node": "^25.9.3",
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <div
3
+ :class="$style.avatarGroup"
4
+ :style="{
5
+ fontSize: `${size}px`,
6
+ '--overlap': overlap
7
+ }">
8
+ <component :is="renderVisible"/>
9
+
10
+ <span
11
+ v-if="overflowCount > 0"
12
+ :class="$style.avatarGroupItem">
13
+ <FluxAvatar
14
+ :alt="overflowLabel"
15
+ fallback="neutral"
16
+ :fallback-initials="`+${overflowCount}`"/>
17
+ </span>
18
+ </div>
19
+ </template>
20
+
21
+ <script
22
+ lang="ts"
23
+ setup>
24
+ import { flattenVNodeTree } from '@flux-ui/internals';
25
+ import { computed, h, type VNode } from 'vue';
26
+ import { useTranslate } from '~flux/components/composable/private';
27
+ import FluxAvatar from './FluxAvatar.vue';
28
+ import $style from '~flux/components/css/component/AvatarGroup.module.scss';
29
+
30
+ const {
31
+ max,
32
+ overlap = 0.3,
33
+ size = 32
34
+ } = defineProps<{
35
+ readonly max?: number;
36
+ readonly overlap?: number;
37
+ readonly size?: number;
38
+ }>();
39
+
40
+ const slots = defineSlots<{
41
+ default(): VNode[];
42
+ }>();
43
+
44
+ const translate = useTranslate();
45
+
46
+ const children = computed(() => flattenVNodeTree(slots.default?.() ?? []).filter(vnode => typeof vnode.type !== 'symbol'));
47
+ const visibleNodes = computed(() => max !== undefined && children.value.length > max ? children.value.slice(0, max) : children.value);
48
+ const overflowCount = computed(() => max !== undefined ? Math.max(0, children.value.length - max) : 0);
49
+ const overflowLabel = computed(() => translate('flux.andNMore', {n: overflowCount.value}));
50
+
51
+ const renderVisible = () => visibleNodes.value.map(vnode => h('span', {class: $style.avatarGroupItem}, [vnode]));
52
+ </script>
@@ -3,6 +3,7 @@
3
3
  :component-type="type"
4
4
  :class="clsx(
5
5
  cssClass,
6
+ isActive && cssClassActive,
6
7
  isFilled && $style.isFilled,
7
8
  size === 'small' && $style.isSmall,
8
9
  size === 'medium' && $style.isMedium,
@@ -11,6 +12,7 @@
11
12
  )"
12
13
  :type="isSubmit ? 'submit' : 'button'"
13
14
  :aria-disabled="disabled ? true : undefined"
15
+ :aria-pressed="isActive && type === 'button' ? true : undefined"
14
16
  :disabled="disabled ? true : undefined"
15
17
  :tabindex="disabled ? -1 : tabindex"
16
18
  :href="href"
@@ -82,6 +84,7 @@
82
84
  type = 'button'
83
85
  } = defineProps<FluxButtonProps & {
84
86
  readonly cssClass: string;
87
+ readonly cssClassActive?: string;
85
88
  readonly cssClassIcon: string;
86
89
  readonly cssClassLabel: string;
87
90
  }>();
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <div
3
+ :class="$style.contextMenu"
4
+ @contextmenu="onContextMenu">
5
+ <slot/>
6
+
7
+ <Teleport to="body">
8
+ <FluxFadeTransition>
9
+ <AnchorPopup
10
+ v-if="isOpen"
11
+ ref="popup"
12
+ :anchor="virtualAnchor"
13
+ :class="$style.contextMenuPopup"
14
+ clamp-to-viewport
15
+ :margin="2"
16
+ :position="position"
17
+ role="menu"
18
+ @click="close()">
19
+ <slot
20
+ name="menu"
21
+ v-bind="{close}"/>
22
+ </AnchorPopup>
23
+ </FluxFadeTransition>
24
+ </Teleport>
25
+ </div>
26
+ </template>
27
+
28
+ <script
29
+ lang="ts"
30
+ setup>
31
+ import { isSSR, useEventListener, useFocusTrap } from '@flux-ui/internals';
32
+ import { computed, type ComponentPublicInstance, reactive, ref, toRef, useTemplateRef, type VNode } from 'vue';
33
+ import { useDisabled } from '~flux/components/composable';
34
+ import { useMenuFlyoutProvider } from '~flux/components/composable/private';
35
+ import { FluxFadeTransition } from '~flux/components/transition';
36
+ import { AnchorPopup } from '~flux/components/component/primitive';
37
+ import $style from '~flux/components/css/component/ContextMenu.module.scss';
38
+
39
+ const {
40
+ debugCone = false,
41
+ disabled: componentDisabled,
42
+ position = 'bottom-left'
43
+ } = defineProps<{
44
+ readonly debugCone?: boolean;
45
+ readonly disabled?: boolean;
46
+ readonly position?:
47
+ | 'top' | 'top-left' | 'top-right'
48
+ | 'left' | 'left-top' | 'left-bottom'
49
+ | 'right' | 'right-top' | 'right-bottom'
50
+ | 'bottom' | 'bottom-left' | 'bottom-right';
51
+ }>();
52
+
53
+ const emit = defineEmits<{
54
+ open: [MouseEvent];
55
+ close: [];
56
+ }>();
57
+
58
+ defineSlots<{
59
+ default(): VNode[];
60
+ menu(props: {close(): void}): VNode[];
61
+ }>();
62
+
63
+ const disabled = useDisabled(toRef(() => componentDisabled));
64
+ const popupRef = useTemplateRef<ComponentPublicInstance>('popup');
65
+
66
+ const isOpen = ref(false);
67
+ const cursor = reactive({x: 0, y: 0});
68
+ const virtualAnchor = {
69
+ $el: {
70
+ getBoundingClientRect: () => new DOMRect(cursor.x, cursor.y, 0, 0)
71
+ }
72
+ } as unknown as ComponentPublicInstance;
73
+
74
+ const menuFlyout = useMenuFlyoutProvider({
75
+ debugCone: toRef(() => debugCone),
76
+ onCloseAll: () => close()
77
+ });
78
+
79
+ useFocusTrap(popupRef, {
80
+ disable: computed(() => menuFlyout.keyboardStack.value.length > 0)
81
+ });
82
+
83
+ function onContextMenu(evt: MouseEvent): void {
84
+ if (disabled.value) {
85
+ return;
86
+ }
87
+
88
+ evt.preventDefault();
89
+
90
+ cursor.x = evt.clientX;
91
+ cursor.y = evt.clientY;
92
+ isOpen.value = true;
93
+
94
+ emit('open', evt);
95
+ }
96
+
97
+ function close(): void {
98
+ if (!isOpen.value) {
99
+ return;
100
+ }
101
+
102
+ isOpen.value = false;
103
+ emit('close');
104
+ }
105
+
106
+ if (!isSSR) {
107
+ useEventListener(ref(window), 'pointerdown', (evt: PointerEvent) => {
108
+ if (!isOpen.value) {
109
+ return;
110
+ }
111
+
112
+ const target = evt.target as Node | null;
113
+ const root = (popupRef.value?.$el ?? null) as HTMLElement | null;
114
+
115
+ if ((root && target && root.contains(target)) || menuFlyout.isInsidePopups(target)) {
116
+ return;
117
+ }
118
+
119
+ close();
120
+ }, {capture: true});
121
+
122
+ useEventListener(ref(window), 'keydown', (evt: KeyboardEvent) => {
123
+ if (isOpen.value && evt.key === 'Escape') {
124
+ close();
125
+ }
126
+ });
127
+
128
+ useEventListener(ref(window), 'scroll', () => {
129
+ if (isOpen.value) {
130
+ close();
131
+ }
132
+ }, {capture: true});
133
+ }
134
+ </script>
@@ -14,7 +14,7 @@
14
14
  </template>
15
15
 
16
16
  <template
17
- v-if="'header' in slots || selectionMode"
17
+ v-if="'header' in slots || selectionMode || hasExpandable"
18
18
  #header>
19
19
  <slot
20
20
  name="filter"
@@ -31,6 +31,11 @@
31
31
  @update:model-value="onSelectAll"/>
32
32
  </FluxTableHeader>
33
33
 
34
+ <FluxTableHeader
35
+ v-if="hasExpandable"
36
+ is-shrinking
37
+ :class="$style.tableCellExpand"/>
38
+
34
39
  <slot
35
40
  name="header"
36
41
  v-bind="{page, perPage, items: limitedItems, total}"/>
@@ -63,27 +68,54 @@
63
68
  </slot>
64
69
  </template>
65
70
 
66
- <FluxTableRow
71
+ <template
67
72
  v-for="(item, index) of limitedItems"
68
- :key="uniqueKey ? item[uniqueKey] : index"
69
- :class="selectionMode && !treeDisabled && $style.isSelectableRow"
70
- :is-selected="selectionMode ? isItemSelected(item) : false"
71
- @click="onRowClick(item, $event)">
72
- <FluxTableCell
73
- v-if="selectionMode"
74
- :class="$style.tableCellSelection">
75
- <FluxFormCheckbox
76
- :model-value="isItemSelected(item)"
77
- @update:model-value="onSelectRow(item)"/>
78
- </FluxTableCell>
79
-
80
- <template v-for="(_, name) of slots">
81
- <slot
82
- v-if="!IGNORED_SLOTS.includes(name as string)"
83
- v-bind="{index, item, items: limitedItems, page, perPage, total, isSelected: isItemSelected(item)}"
84
- :name="name"/>
85
- </template>
86
- </FluxTableRow>
73
+ :key="uniqueKey ? item[uniqueKey] : index">
74
+ <FluxTableRow
75
+ :class="selectionMode && !treeDisabled && $style.isSelectableRow"
76
+ :is-selected="selectionMode ? isItemSelected(item) : false"
77
+ @click="onRowClick(item, $event)">
78
+ <FluxTableCell
79
+ v-if="selectionMode"
80
+ :class="$style.tableCellSelection">
81
+ <FluxFormCheckbox
82
+ :model-value="isItemSelected(item)"
83
+ @update:model-value="onSelectRow(item)"/>
84
+ </FluxTableCell>
85
+
86
+ <FluxTableCell
87
+ v-if="hasExpandable"
88
+ :class="$style.tableCellExpand">
89
+ <FluxAction
90
+ :class="clsx($style.tableExpandToggle, isItemExpanded(item) && $style.isExpanded)"
91
+ icon="chevron-right"
92
+ :aria-expanded="isItemExpanded(item)"
93
+ :aria-label="isItemExpanded(item) ? translate('flux.collapseRow') : translate('flux.expandRow')"
94
+ @click="toggleExpand(item)"/>
95
+ </FluxTableCell>
96
+
97
+ <template v-for="(_, name) of slots">
98
+ <slot
99
+ v-if="!IGNORED_SLOTS.includes(name as string)"
100
+ v-bind="{index, item, items: limitedItems, page, perPage, total, isSelected: isItemSelected(item)}"
101
+ :name="name"/>
102
+ </template>
103
+ </FluxTableRow>
104
+
105
+ <FluxTableRow
106
+ v-if="hasExpandable && isItemExpanded(item)"
107
+ :class="$style.tableExpandRow">
108
+ <FluxTableCell :colspan="columnCount">
109
+ <template #content>
110
+ <div :class="$style.tableExpandContent">
111
+ <slot
112
+ name="expandable"
113
+ v-bind="{index, item, isExpanded: true, toggle: () => toggleExpand(item)}"/>
114
+ </div>
115
+ </template>
116
+ </FluxTableCell>
117
+ </FluxTableRow>
118
+ </template>
87
119
  </FluxTable>
88
120
  </template>
89
121
 
@@ -91,8 +123,11 @@
91
123
  lang="ts"
92
124
  setup
93
125
  generic="T extends Record<string, any>">
126
+ import { clsx } from 'clsx';
94
127
  import { computed, unref, useTemplateRef, type VNode, watch } from 'vue';
95
128
  import { useDisabledInjection } from '~flux/components/composable';
129
+ import { useTranslate } from '~flux/components/composable/private';
130
+ import FluxAction from './FluxAction.vue';
96
131
  import FluxFormCheckbox from './FluxFormCheckbox.vue';
97
132
  import FluxPaginationBar from './FluxPaginationBar.vue';
98
133
  import FluxTable from './FluxTable.vue';
@@ -104,7 +139,7 @@
104
139
  type SelectionId = string | number;
105
140
  type SelectionValue = SelectionId | null | SelectionId[];
106
141
 
107
- const IGNORED_SLOTS: string[] = ['filter', 'header', 'footer', 'colgroups', 'pagination'];
142
+ const IGNORED_SLOTS: string[] = ['filter', 'header', 'footer', 'colgroups', 'pagination', 'expandable'];
108
143
 
109
144
  const emit = defineEmits<{
110
145
  limit: [number];
@@ -112,8 +147,12 @@
112
147
  }>();
113
148
 
114
149
  const selected = defineModel<SelectionValue>('selected');
150
+ const expanded = defineModel<SelectionId[]>('expanded', {
151
+ default: () => []
152
+ });
115
153
 
116
154
  const {
155
+ expandMode = 'multiple',
117
156
  isBordered = true,
118
157
  isHoverable = false,
119
158
  isLoading = false,
@@ -124,6 +163,7 @@
124
163
  selectionMode,
125
164
  uniqueKey
126
165
  } = defineProps<{
166
+ readonly expandMode?: 'single' | 'multiple';
127
167
  readonly fillColumns?: number;
128
168
  readonly isBordered?: boolean;
129
169
  readonly isHoverable?: boolean;
@@ -140,16 +180,6 @@
140
180
  }>();
141
181
 
142
182
  const slots = defineSlots<{
143
- [key: string]: (props: {
144
- readonly index: number;
145
- readonly page: number;
146
- readonly perPage: number;
147
- readonly item: T;
148
- readonly items: T[];
149
- readonly total: number;
150
- readonly isSelected: boolean;
151
- }) => VNode;
152
-
153
183
  filter(props: {
154
184
  readonly page: number;
155
185
  readonly perPage: number;
@@ -179,13 +209,38 @@
179
209
  }): VNode;
180
210
 
181
211
  colgroups(): VNode;
212
+
213
+ expandable(props: {
214
+ readonly index: number;
215
+ readonly item: T;
216
+ readonly isExpanded: boolean;
217
+
218
+ toggle(): void;
219
+ }): VNode;
220
+ } & {
221
+ [key: string]: (props: {
222
+ readonly index: number;
223
+ readonly page: number;
224
+ readonly perPage: number;
225
+ readonly item: T;
226
+ readonly items: T[];
227
+ readonly total: number;
228
+ readonly isSelected: boolean;
229
+ }) => VNode;
182
230
  }>();
183
231
 
184
232
  const table = useTemplateRef('table');
185
233
  const treeDisabled = useDisabledInjection();
234
+ const translate = useTranslate();
186
235
 
187
236
  const limitedItems = computed(() => items.slice(0, perPage));
188
237
 
238
+ const hasExpandable = computed(() => 'expandable' in slots);
239
+ const columnCount = computed(() => {
240
+ const userColumns = Object.keys(slots).filter(name => !IGNORED_SLOTS.includes(name)).length;
241
+ return userColumns + (selectionMode ? 1 : 0) + (unref(hasExpandable) ? 1 : 0);
242
+ });
243
+
189
244
  const currentPageIds = computed<SelectionId[]>(() => {
190
245
  if (!uniqueKey) {
191
246
  return [];
@@ -294,10 +349,36 @@
294
349
  selected.value = current.filter(id => !ids.includes(id));
295
350
  }
296
351
 
352
+ function isItemExpanded(item: T): boolean {
353
+ const id = getItemId(item);
354
+ return id !== undefined && unref(expanded).includes(id);
355
+ }
356
+
357
+ function toggleExpand(item: T): void {
358
+ const id = getItemId(item);
359
+
360
+ if (id === undefined) {
361
+ return;
362
+ }
363
+
364
+ const current = unref(expanded);
365
+
366
+ if (current.includes(id)) {
367
+ expanded.value = current.filter(v => v !== id);
368
+ return;
369
+ }
370
+
371
+ expanded.value = expandMode === 'single' ? [id] : [...current, id];
372
+ }
373
+
297
374
  if (import.meta.env.DEV && selectionMode && !uniqueKey) {
298
375
  console.warn('[FluxDataTable] `uniqueKey` is required when `selectionMode` is set, otherwise rows cannot be tracked across renders.');
299
376
  }
300
377
 
378
+ if (import.meta.env.DEV && unref(hasExpandable) && !uniqueKey) {
379
+ console.warn('[FluxDataTable] `uniqueKey` is required when the `expandable` slot is used, otherwise rows cannot be tracked across renders.');
380
+ }
381
+
301
382
  watch(() => items, () => {
302
383
  unref(table)?.$el.scrollTo(0, 0);
303
384
  });
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div
3
+ :class="clsx(
4
+ $style.descriptionItem,
5
+ isStacked && $style.isStacked
6
+ )">
7
+ <dt :class="$style.descriptionItemTerm">
8
+ <FluxIcon
9
+ v-if="icon"
10
+ :class="$style.descriptionItemIcon"
11
+ :name="icon"/>
12
+
13
+ <span :class="$style.descriptionItemLabel">
14
+ <slot name="label">{{ label }}</slot>
15
+ </span>
16
+ </dt>
17
+
18
+ <dd :class="$style.descriptionItemValue">
19
+ <slot/>
20
+ </dd>
21
+ </div>
22
+ </template>
23
+
24
+ <script
25
+ lang="ts"
26
+ setup>
27
+ import type { FluxIconName } from '@flux-ui/types';
28
+ import { clsx } from 'clsx';
29
+ import type { VNode } from 'vue';
30
+ import FluxIcon from './FluxIcon.vue';
31
+ import $style from '~flux/components/css/component/DescriptionList.module.scss';
32
+
33
+ defineProps<{
34
+ readonly icon?: FluxIconName;
35
+ readonly isStacked?: boolean;
36
+ readonly label?: string;
37
+ }>();
38
+
39
+ defineSlots<{
40
+ default(): VNode[];
41
+ label(): VNode[];
42
+ }>();
43
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div :class="$style.descriptionList">
3
+ <div
4
+ v-if="title || slots.header"
5
+ :class="$style.descriptionListHeader">
6
+ <slot name="header">{{ title }}</slot>
7
+ </div>
8
+
9
+ <dl
10
+ :class="clsx(
11
+ $style.descriptionListItems,
12
+ direction === 'horizontal' && $style.isHorizontal
13
+ )">
14
+ <slot/>
15
+ </dl>
16
+ </div>
17
+ </template>
18
+
19
+ <script
20
+ lang="ts"
21
+ setup>
22
+ import { clsx } from 'clsx';
23
+ import type { VNode } from 'vue';
24
+ import $style from '~flux/components/css/component/DescriptionList.module.scss';
25
+
26
+ const {
27
+ direction = 'vertical'
28
+ } = defineProps<{
29
+ readonly direction?: 'horizontal' | 'vertical';
30
+ readonly title?: string;
31
+ }>();
32
+
33
+ const slots = defineSlots<{
34
+ default(): VNode[];
35
+ header(): VNode[];
36
+ }>();
37
+ </script>
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
  <FluxButton
3
- :="{type, disabled, iconLeading, iconTrailing, isFilled, isLoading, isSubmit, label, size, tabindex, href, rel, target, to}"
3
+ :="{type, disabled, iconLeading, iconTrailing, isActive, isFilled, isLoading, isSubmit, label, size, tabindex, href, rel, target, to}"
4
4
  :css-class="$style.destructiveButton"
5
+ :css-class-active="$style.isActive"
5
6
  :css-class-icon="$style.destructiveButtonIcon"
6
7
  :css-class-label="$style.destructiveButtonLabel"
7
8
  @click="$emit('click', $event)"
@@ -16,8 +16,7 @@
16
16
  <dialog
17
17
  ref="dialog"
18
18
  :class="$style.flyoutDialog"
19
- @click="onDialogBackdropClick"
20
- @keydown.prevent.esc="close">
19
+ @click="onDialogBackdropClick">
21
20
  <FluxPane
22
21
  v-if="isOpen"
23
22
  ref="pane"
@@ -46,6 +45,12 @@
46
45
  import { FluxFlyoutInjectionKey } from '~flux/components/data';
47
46
  import FluxPane from './FluxPane.vue';
48
47
  import $style from '~flux/components/css/component/Flyout.module.scss';
48
+ import { useHotKey } from '@basmilius/common';
49
+
50
+ const emit = defineEmits<{
51
+ close: [];
52
+ open: [];
53
+ }>();
49
54
 
50
55
  const {
51
56
  direction = 'vertical',
@@ -93,6 +98,11 @@
93
98
  !isSSR && useEventListener(ref(window), 'resize', () => unref(isOpen) && reposition());
94
99
  useFocusTrap(paneRef);
95
100
 
101
+ useHotKey('esc', () => close(), {
102
+ enabled: isOpen,
103
+ target: dialogRef
104
+ });
105
+
96
106
  let closeAnimationEndListener: ((evt: AnimationEvent) => void) | null = null;
97
107
  let openAnimationEndListener: ((evt: AnimationEvent) => void) | null = null;
98
108
 
@@ -233,11 +243,13 @@
233
243
 
234
244
  if (isOpen && !dialog.open) {
235
245
  dialog.showModal();
246
+ emit('open');
236
247
 
237
248
  window.addEventListener('scroll', reposition, {passive: true});
238
249
  onCleanup(() => window.removeEventListener('scroll', reposition));
239
250
  } else if (!isOpen && dialog.open) {
240
251
  dialog.close();
252
+ emit('close');
241
253
  }
242
254
  });
243
255
 
@@ -250,6 +262,7 @@
250
262
  defineExpose({
251
263
  close,
252
264
  open,
253
- toggle
265
+ toggle,
266
+ isOpen
254
267
  });
255
268
  </script>