@nexxtmove/ui 1.47.0 → 1.49.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.
- package/dist/components/molecules/Tabs/Tabs.vue.d.ts +12 -0
- package/dist/components/organisms/IconPicker/IconPicker.vue.d.ts +142 -0
- package/dist/components/organisms/IconPicker/_IconPickerGrid.vue.d.ts +40 -0
- package/dist/components/organisms/IconPicker/_IconPickerRail.vue.d.ts +22 -0
- package/dist/components/organisms/IconPicker/iconPickerHelpers.d.ts +39 -0
- package/dist/components/organisms/IconPicker/useIconPickerCatalogue.d.ts +72 -0
- package/dist/components/organisms/IconPicker/useIconPickerNavigation.d.ts +60 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2152 -1400
- package/dist/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/molecules/Tabs/Tabs.vue +76 -13
- package/src/components/organisms/IconPicker/IconPicker.vue +621 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.vue +282 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.vue +112 -0
- package/src/components/organisms/IconPicker/iconPickerHelpers.ts +111 -0
- package/src/components/organisms/IconPicker/useIconPickerCatalogue.ts +238 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.ts +206 -0
- package/src/components.json +1 -0
- package/src/index.ts +18 -0
package/dist/nuxt.js
CHANGED
|
@@ -64,6 +64,7 @@ var s = {
|
|
|
64
64
|
NexxtDropdownButton: "components/organisms/DropdownButton/DropdownButton.vue",
|
|
65
65
|
NexxtFontPicker: "components/organisms/FontPicker/FontPicker.vue",
|
|
66
66
|
NexxtHeader: "components/organisms/Header/Header.vue",
|
|
67
|
+
NexxtIconPicker: "components/organisms/IconPicker/IconPicker.vue",
|
|
67
68
|
NexxtModal: "components/organisms/Modal/Modal.vue",
|
|
68
69
|
NexxtPickerPanel: "components/organisms/PickerPanel/PickerPanel.vue",
|
|
69
70
|
NexxtProgressBar: "components/organisms/ProgressBar/ProgressBar.vue",
|
package/package.json
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed, ref, useTemplateRef, watch, onMounted, nextTick } from 'vue'
|
|
2
|
+
import { computed, ref, useTemplateRef, watch, watchEffect, onMounted, nextTick } from 'vue'
|
|
3
3
|
import Icon, { type NexxtIconName } from '../../atoms/Icon/Icon.vue'
|
|
4
4
|
|
|
5
5
|
export interface NexxtTabItem {
|
|
6
|
+
/** Visible text, the `v-model` value and the key; in icon-only mode the tab's accessible name. */
|
|
6
7
|
label: string
|
|
8
|
+
/** Icon shown before the label, or on its own in icon-only mode. */
|
|
7
9
|
icon?: NexxtIconName
|
|
10
|
+
/** Show only the icon for this tab. Overrides the tabs-wide `iconOnly` either way. */
|
|
11
|
+
iconOnly?: boolean
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
interface NexxtTabs {
|
|
15
|
+
/** The tabs, as plain labels or `{ label, icon, iconOnly }` objects. */
|
|
11
16
|
tabs: (string | NexxtTabItem)[]
|
|
17
|
+
/** Accessible name of the tablist. */
|
|
12
18
|
tablistLabel?: string
|
|
19
|
+
/**
|
|
20
|
+
* Show only the icon of every tab, as a near-square button. The label stays the `v-model`
|
|
21
|
+
* value and the accessible name, in a visually hidden span. A tab without an `icon` keeps its
|
|
22
|
+
* visible label.
|
|
23
|
+
*/
|
|
24
|
+
iconOnly?: boolean
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
defineOptions({
|
|
@@ -31,15 +43,47 @@ const items = computed<NexxtTabItem[]>(() =>
|
|
|
31
43
|
props.tabs.map((tab) => (typeof tab === 'string' ? { label: tab } : tab)),
|
|
32
44
|
)
|
|
33
45
|
|
|
46
|
+
const wantsIconOnly = (item: NexxtTabItem) => item.iconOnly ?? props.iconOnly ?? false
|
|
47
|
+
|
|
48
|
+
interface TabView extends NexxtTabItem {
|
|
49
|
+
compact: boolean
|
|
50
|
+
/** The visible label; empty in icon-only mode, where it moves to a visually hidden span. */
|
|
51
|
+
text: string
|
|
52
|
+
spacing: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// icon-only needs an icon to show; without one the tab falls back to its visible label. The
|
|
56
|
+
// labelled tab keeps `text` as the same bare text node it always was, so its markup is unchanged
|
|
57
|
+
const views = computed<TabView[]>(() =>
|
|
58
|
+
items.value.map((item) => {
|
|
59
|
+
const compact = Boolean(item.icon) && wantsIconOnly(item)
|
|
60
|
+
return {
|
|
61
|
+
...item,
|
|
62
|
+
compact,
|
|
63
|
+
text: compact ? '' : item.label,
|
|
64
|
+
spacing: compact ? 'px-3' : 'gap-2 px-4',
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
|
|
34
69
|
const updateSlider = async () => {
|
|
35
70
|
await nextTick()
|
|
36
71
|
const index = items.value.findIndex((item) => item.label === selected.value)
|
|
37
72
|
const button = buttonRefs.value[index]
|
|
38
73
|
if (!button || !containerRef.value) return
|
|
39
74
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
75
|
+
// offsetLeft/offsetWidth round to whole pixels while label and icon widths are fractional, which
|
|
76
|
+
// left the pill up to half a pixel off its tab. The rects are exact but include any ancestor
|
|
77
|
+
// transform (a modal scaling in), so they are divided back to layout pixels
|
|
78
|
+
const container = containerRef.value
|
|
79
|
+
const box = container.getBoundingClientRect()
|
|
80
|
+
const target = button.getBoundingClientRect()
|
|
81
|
+
const layoutWidth = parseFloat(getComputedStyle(container).width)
|
|
82
|
+
const scale = layoutWidth > 0 && box.width > 0 ? box.width / layoutWidth : 1
|
|
83
|
+
|
|
84
|
+
const left = (target.left - box.left) / scale
|
|
85
|
+
const width = target.width / scale
|
|
86
|
+
const right = box.width / scale - left - width
|
|
43
87
|
|
|
44
88
|
sliderStyle.value = {
|
|
45
89
|
width: `${width}px`,
|
|
@@ -64,8 +108,21 @@ const onKeydown = (event: KeyboardEvent, index: number) => {
|
|
|
64
108
|
|
|
65
109
|
watch(selected, async () => await updateSlider())
|
|
66
110
|
|
|
67
|
-
// the overlay copy must stay the same width as the button, so re-measure
|
|
68
|
-
|
|
111
|
+
// the overlay copy must stay the same width as the button, so re-measure whenever a tab's width
|
|
112
|
+
// can change: new tabs, or a switch between icon-only and labelled
|
|
113
|
+
watch(views, async () => await updateSlider())
|
|
114
|
+
|
|
115
|
+
// `process.env.NODE_ENV` stays in the library build, so the consumer's bundler decides whether
|
|
116
|
+
// this runs (as in NexxtIconPicker)
|
|
117
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
118
|
+
watchEffect(() => {
|
|
119
|
+
const missing = items.value.filter((item) => !item.icon && wantsIconOnly(item))
|
|
120
|
+
if (missing.length) {
|
|
121
|
+
const labels = missing.map((item) => `"${item.label}"`).join(', ')
|
|
122
|
+
console.warn(`[NexxtTabs] icon-only tab without an \`icon\` shows its label: ${labels}.`)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
69
126
|
|
|
70
127
|
onMounted(async () => {
|
|
71
128
|
await document.fonts.ready
|
|
@@ -94,14 +151,15 @@ onMounted(async () => {
|
|
|
94
151
|
|
|
95
152
|
<!-- dark text buttons -->
|
|
96
153
|
<button
|
|
97
|
-
v-for="(item, index) in
|
|
154
|
+
v-for="(item, index) in views"
|
|
98
155
|
:key="item.label"
|
|
99
156
|
:ref="(el) => (buttonRefs[index] = el as HTMLButtonElement)"
|
|
100
157
|
role="tab"
|
|
101
158
|
:aria-selected="selected === item.label"
|
|
102
159
|
:tabindex="selected === item.label ? 0 : -1"
|
|
103
|
-
class="relative z-10 inline-flex h-9 cursor-pointer items-center
|
|
160
|
+
class="relative z-10 inline-flex h-9 cursor-pointer items-center rounded-full small-normal"
|
|
104
161
|
:class="[
|
|
162
|
+
item.spacing,
|
|
105
163
|
// the dark label is what the pill wipe reveals, so it only drops out once the pill
|
|
106
164
|
// has arrived — otherwise its anti-aliased edges halo around the white copy
|
|
107
165
|
selected === item.label ? 'text-transparent' : 'text-gray-950',
|
|
@@ -111,8 +169,12 @@ onMounted(async () => {
|
|
|
111
169
|
@click="selected = item.label"
|
|
112
170
|
@keydown="onKeydown($event, index)"
|
|
113
171
|
>
|
|
114
|
-
<
|
|
115
|
-
|
|
172
|
+
<template v-if="item.compact && item.icon">
|
|
173
|
+
<Icon :name="item.icon" aria-hidden="true" class="body-normal" />
|
|
174
|
+
<span class="sr-only">{{ item.label }}</span>
|
|
175
|
+
</template>
|
|
176
|
+
<Icon v-else-if="item.icon" :name="item.icon" aria-hidden="true" class="body-normal" />
|
|
177
|
+
{{ item.text }}
|
|
116
178
|
</button>
|
|
117
179
|
|
|
118
180
|
<!-- white text overlay, clipped to slider bounds; must mirror the buttons exactly -->
|
|
@@ -125,12 +187,13 @@ onMounted(async () => {
|
|
|
125
187
|
:style="{ clipPath }"
|
|
126
188
|
>
|
|
127
189
|
<span
|
|
128
|
-
v-for="item in
|
|
190
|
+
v-for="item in views"
|
|
129
191
|
:key="item.label"
|
|
130
|
-
class="flex h-9 items-center
|
|
192
|
+
class="flex h-9 items-center rounded-full small-normal text-white"
|
|
193
|
+
:class="item.spacing"
|
|
131
194
|
>
|
|
132
195
|
<Icon v-if="item.icon" :name="item.icon" class="body-normal" />
|
|
133
|
-
{{ item.
|
|
196
|
+
{{ item.text }}
|
|
134
197
|
</span>
|
|
135
198
|
</div>
|
|
136
199
|
</div>
|