vira 31.5.7 → 31.6.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.
@@ -19,4 +19,5 @@ export * from './vira-modal.element.js';
19
19
  export * from './vira-overflow-switch.element.js';
20
20
  export * from './vira-progress.element.js';
21
21
  export * from './vira-select.element.js';
22
+ export * from './vira-tabs.element.js';
22
23
  export * from './vira-tag.element.js';
@@ -19,4 +19,5 @@ export * from './vira-modal.element.js';
19
19
  export * from './vira-overflow-switch.element.js';
20
20
  export * from './vira-progress.element.js';
21
21
  export * from './vira-select.element.js';
22
+ export * from './vira-tabs.element.js';
22
23
  export * from './vira-tag.element.js';
@@ -0,0 +1,79 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type FullSpaRoute, type GenericTreePaths, type SpaRouter } from 'spa-router-vir';
3
+ import { type ViraIconSvg } from '../icons/icon-svg.js';
4
+ import { ViraColorVariant } from '../styles/form-variants.js';
5
+ import { type HorizontalAnchor, type PopUpOffset } from './pop-up/vira-pop-up-trigger.element.js';
6
+ /**
7
+ * Controls which edge of the tab the selection indicator bar appears on.
8
+ *
9
+ * @category Internal
10
+ */
11
+ export declare enum ViraTabsBarDirection {
12
+ Top = "top",
13
+ Bottom = "bottom",
14
+ Left = "left",
15
+ Right = "right"
16
+ }
17
+ /**
18
+ * Controls whether tab icons render above/below or beside the label text.
19
+ *
20
+ * @category Internal
21
+ */
22
+ export declare enum ViraTabsIconLayout {
23
+ /** Icon renders above (or below) the label. */
24
+ Vertical = "vertical",
25
+ /** Icon renders beside the label. */
26
+ Horizontal = "horizontal"
27
+ }
28
+ /**
29
+ * A single tab entry for {@link ViraTabs}.
30
+ *
31
+ * @category Internal
32
+ */
33
+ export type ViraTab = {
34
+ label: string;
35
+ paths: GenericTreePaths;
36
+ } & PartialWithUndefined<{
37
+ icon: Readonly<ViraIconSvg>;
38
+ isHidden: boolean;
39
+ isDisabled: boolean;
40
+ }>;
41
+ /**
42
+ * A tab bar element that renders an array of tabs with an animated selection indicator.
43
+ *
44
+ * @category Elements
45
+ */
46
+ export declare const ViraTabs: import("element-vir").DeclarativeElementDefinition<"vira-tabs", {
47
+ tabs: ReadonlyArray<Readonly<ViraTab>>;
48
+ router: Pick<SpaRouter<any, any, any>, "createRouteUrl" | "setRouteOnDirectNavigation">;
49
+ currentRoute: Readonly<FullSpaRoute>;
50
+ } & PartialWithUndefined<{
51
+ /**
52
+ * Which edge of the tab the selection bar appears on.
53
+ *
54
+ * @default ViraTabsBarDirection.Bottom
55
+ */
56
+ barDirection: ViraTabsBarDirection;
57
+ /**
58
+ * Color variant for the tab selection indicator and active tab text.
59
+ *
60
+ * @default ViraColorVariant.Accent
61
+ */
62
+ colorVariant: ViraColorVariant.Accent | ViraColorVariant.Plain;
63
+ /**
64
+ * Layout direction for icons relative to their label text.
65
+ *
66
+ * @default ViraTabsIconLayout.Vertical
67
+ */
68
+ iconLayout: ViraTabsIconLayout;
69
+ /**
70
+ * Horizontal anchor for the dropdown menu. Only used when tabs overflow into a dropdown.
71
+ *
72
+ * @default HorizontalAnchor.Left
73
+ */
74
+ menuHorizontalAnchor: HorizontalAnchor;
75
+ /** Whether the dropdown trigger is disabled. Only used when tabs overflow into a dropdown. */
76
+ menuIsDisabled: boolean;
77
+ /** Offset for the dropdown pop-up. Only used when tabs overflow into a dropdown. */
78
+ menuPopUpOffset: Readonly<PopUpOffset>;
79
+ }>, {}, {}, "vira-tabs-bar-top" | "vira-tabs-bar-bottom" | "vira-tabs-bar-left" | "vira-tabs-bar-right" | "vira-tabs-color-accent" | "vira-tabs-color-plain" | "vira-tabs-icon-layout-vertical" | "vira-tabs-icon-layout-horizontal", "vira-tabs-active-color" | "vira-tabs-active-hover-color" | "vira-tabs-inactive-color" | "vira-tabs-inactive-hover-color" | "vira-tabs-bar-thickness", readonly [], readonly []>;
@@ -0,0 +1,319 @@
1
+ import { check } from '@augment-vir/assert';
2
+ import { filterMap } from '@augment-vir/common';
3
+ import { classMap, css, html, nothing } from 'element-vir';
4
+ import { routeHasPaths, } from 'spa-router-vir';
5
+ import { createFocusStyles } from '../styles/focus.js';
6
+ import { viraFormCssVars } from '../styles/form-styles.js';
7
+ import { ViraColorVariant } from '../styles/form-variants.js';
8
+ import { noNativeFormStyles, noUserSelect, viraDisabledStyles, viraTheme } from '../styles/index.js';
9
+ import { defineViraElement } from '../util/define-vira-element.js';
10
+ import { renderMenuItemEntries } from '../util/pop-up-helpers.js';
11
+ import { ViraMenuTrigger } from './pop-up/vira-menu-trigger.element.js';
12
+ import { ViraMenuCornerStyle } from './pop-up/vira-menu.element.js';
13
+ import { ViraButton } from './vira-button.element.js';
14
+ import { ViraIcon } from './vira-icon.element.js';
15
+ import { ViraLink } from './vira-link.element.js';
16
+ import { ViraOverflowSwitch } from './vira-overflow-switch.element.js';
17
+ /**
18
+ * Controls which edge of the tab the selection indicator bar appears on.
19
+ *
20
+ * @category Internal
21
+ */
22
+ export var ViraTabsBarDirection;
23
+ (function (ViraTabsBarDirection) {
24
+ ViraTabsBarDirection["Top"] = "top";
25
+ ViraTabsBarDirection["Bottom"] = "bottom";
26
+ ViraTabsBarDirection["Left"] = "left";
27
+ ViraTabsBarDirection["Right"] = "right";
28
+ })(ViraTabsBarDirection || (ViraTabsBarDirection = {}));
29
+ /**
30
+ * Controls whether tab icons render above/below or beside the label text.
31
+ *
32
+ * @category Internal
33
+ */
34
+ export var ViraTabsIconLayout;
35
+ (function (ViraTabsIconLayout) {
36
+ /** Icon renders above (or below) the label. */
37
+ ViraTabsIconLayout["Vertical"] = "vertical";
38
+ /** Icon renders beside the label. */
39
+ ViraTabsIconLayout["Horizontal"] = "horizontal";
40
+ })(ViraTabsIconLayout || (ViraTabsIconLayout = {}));
41
+ /**
42
+ * A tab bar element that renders an array of tabs with an animated selection indicator.
43
+ *
44
+ * @category Elements
45
+ */
46
+ export const ViraTabs = defineViraElement()({
47
+ tagName: 'vira-tabs',
48
+ hostClasses: {
49
+ 'vira-tabs-bar-top': ({ inputs }) => inputs.barDirection === ViraTabsBarDirection.Top,
50
+ 'vira-tabs-bar-bottom': ({ inputs }) => !inputs.barDirection || inputs.barDirection === ViraTabsBarDirection.Bottom,
51
+ 'vira-tabs-bar-left': ({ inputs }) => inputs.barDirection === ViraTabsBarDirection.Left,
52
+ 'vira-tabs-bar-right': ({ inputs }) => inputs.barDirection === ViraTabsBarDirection.Right,
53
+ 'vira-tabs-color-accent': ({ inputs }) => !inputs.colorVariant || inputs.colorVariant === ViraColorVariant.Accent,
54
+ 'vira-tabs-color-plain': ({ inputs }) => inputs.colorVariant === ViraColorVariant.Plain,
55
+ 'vira-tabs-icon-layout-vertical': ({ inputs }) => !inputs.iconLayout || inputs.iconLayout === ViraTabsIconLayout.Vertical,
56
+ 'vira-tabs-icon-layout-horizontal': ({ inputs }) => inputs.iconLayout === ViraTabsIconLayout.Horizontal,
57
+ },
58
+ cssVars: {
59
+ 'vira-tabs-active-color': viraFormCssVars['vira-form-accent-primary-color'].value,
60
+ 'vira-tabs-active-hover-color': viraFormCssVars['vira-form-accent-primary-hover-color'].value,
61
+ 'vira-tabs-inactive-color': viraTheme.colors['vira-grey-foreground-header'].foreground.value,
62
+ 'vira-tabs-inactive-hover-color': viraTheme.colors['vira-grey-foreground-non-body'].foreground.value,
63
+ 'vira-tabs-bar-thickness': '3px',
64
+ },
65
+ styles: ({ hostClasses, cssVars }) => {
66
+ return css `
67
+ :host {
68
+ display: inline-flex;
69
+ box-sizing: border-box;
70
+ ${noUserSelect};
71
+ max-width: 100%;
72
+ max-height: 100%;
73
+ }
74
+
75
+ .tabs-container {
76
+ display: flex;
77
+ position: relative;
78
+ list-style: none;
79
+ margin: 0;
80
+ padding: 0;
81
+ }
82
+
83
+ ${hostClasses['vira-tabs-bar-top'].selector},
84
+ ${hostClasses['vira-tabs-bar-bottom'].selector} {
85
+ & .tabs-container {
86
+ flex-direction: row;
87
+ }
88
+ }
89
+
90
+ ${hostClasses['vira-tabs-bar-left'].selector},
91
+ ${hostClasses['vira-tabs-bar-right'].selector} {
92
+ & .tabs-container {
93
+ flex-direction: column;
94
+ }
95
+ }
96
+
97
+ li {
98
+ ${noNativeFormStyles};
99
+ cursor: pointer;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ padding: 8px 16px;
104
+ position: relative;
105
+ color: ${cssVars['vira-tabs-inactive-color'].value};
106
+ font-size: ${viraFormCssVars['vira-form-medium-text-size'].value};
107
+ text-decoration: none;
108
+ ${createFocusStyles({
109
+ renderInside: true,
110
+ elementBorderSize: '0',
111
+ })}
112
+
113
+ &::after {
114
+ content: '';
115
+ position: absolute;
116
+ background-color: transparent;
117
+ }
118
+
119
+ &:hover {
120
+ color: ${cssVars['vira-tabs-inactive-hover-color'].value};
121
+ }
122
+
123
+ &.selected {
124
+ pointer-events: none;
125
+ color: ${cssVars['vira-tabs-active-color'].value};
126
+
127
+ &::after {
128
+ background-color: ${cssVars['vira-tabs-active-color'].value};
129
+ }
130
+ }
131
+
132
+ &.disabled {
133
+ ${viraDisabledStyles};
134
+ }
135
+ }
136
+
137
+ ${hostClasses['vira-tabs-bar-bottom'].selector} {
138
+ & li::after {
139
+ bottom: 0;
140
+ left: 0;
141
+ right: 0;
142
+ height: ${cssVars['vira-tabs-bar-thickness'].value};
143
+ border-radius: ${cssVars['vira-tabs-bar-thickness'].value}
144
+ ${cssVars['vira-tabs-bar-thickness'].value} 0 0;
145
+ }
146
+ }
147
+
148
+ ${hostClasses['vira-tabs-bar-top'].selector} {
149
+ & li::after {
150
+ top: 0;
151
+ left: 0;
152
+ right: 0;
153
+ height: ${cssVars['vira-tabs-bar-thickness'].value};
154
+ border-radius: 0 0 ${cssVars['vira-tabs-bar-thickness'].value}
155
+ ${cssVars['vira-tabs-bar-thickness'].value};
156
+ }
157
+ }
158
+
159
+ ${hostClasses['vira-tabs-bar-left'].selector} {
160
+ & li::after {
161
+ top: 0;
162
+ bottom: 0;
163
+ left: 0;
164
+ width: ${cssVars['vira-tabs-bar-thickness'].value};
165
+ border-radius: 0 ${cssVars['vira-tabs-bar-thickness'].value}
166
+ ${cssVars['vira-tabs-bar-thickness'].value} 0;
167
+ }
168
+ }
169
+
170
+ ${hostClasses['vira-tabs-bar-right'].selector} {
171
+ & li::after {
172
+ top: 0;
173
+ bottom: 0;
174
+ right: 0;
175
+ width: ${cssVars['vira-tabs-bar-thickness'].value};
176
+ border-radius: ${cssVars['vira-tabs-bar-thickness'].value} 0 0
177
+ ${cssVars['vira-tabs-bar-thickness'].value};
178
+ }
179
+ }
180
+
181
+ ${hostClasses['vira-tabs-color-plain'].selector} {
182
+ ${cssVars['vira-tabs-active-color'].name}: ${viraTheme.colors['vira-grey-foreground-small-body'].foreground.value};
183
+ ${cssVars['vira-tabs-active-hover-color'].name}: ${viraTheme.colors['vira-grey-foreground-body'].foreground.value};
184
+ }
185
+
186
+ .tab-content {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ }
191
+
192
+ ${hostClasses['vira-tabs-icon-layout-vertical'].selector} {
193
+ & .tab-content {
194
+ flex-direction: column;
195
+ gap: 4px;
196
+ }
197
+ }
198
+
199
+ ${hostClasses['vira-tabs-icon-layout-horizontal'].selector} {
200
+ & .tab-content {
201
+ flex-direction: row;
202
+ gap: 8px;
203
+ }
204
+ }
205
+
206
+ ${ViraOverflowSwitch} {
207
+ max-width: 100%;
208
+ }
209
+
210
+ ${ViraLink} {
211
+ text-decoration: none;
212
+ }
213
+ `;
214
+ },
215
+ render({ inputs }) {
216
+ const tabs = filterMap(inputs.tabs, (tab) => {
217
+ if (tab.isHidden) {
218
+ return undefined;
219
+ }
220
+ const isSelected = routeHasPaths(inputs.currentRoute, tab.paths);
221
+ const iconTemplate = tab.icon
222
+ ? html `
223
+ <${ViraIcon.assign({
224
+ icon: tab.icon,
225
+ })}></${ViraIcon}>
226
+ `
227
+ : nothing;
228
+ const isInert = isSelected || !!tab.isDisabled;
229
+ return html `
230
+ <li
231
+ class=${classMap({
232
+ selected: isSelected,
233
+ disabled: !!tab.isDisabled,
234
+ })}
235
+ role="presentation"
236
+ >
237
+ <${ViraLink.assign({
238
+ route: {
239
+ router: inputs.router,
240
+ route: {
241
+ paths: tab.paths.fullPaths,
242
+ },
243
+ scrollToTop: true,
244
+ },
245
+ disableLinkStyles: true,
246
+ attributePassthrough: {
247
+ a: {
248
+ role: 'tab',
249
+ 'aria-selected': String(isSelected),
250
+ 'aria-disabled': String(!!tab.isDisabled),
251
+ tabindex: isInert ? '-1' : undefined,
252
+ },
253
+ },
254
+ })}>
255
+ <span class="tab-content">
256
+ ${iconTemplate}
257
+ <span class="tab-label">${tab.label}</span>
258
+ </span>
259
+ </${ViraLink}>
260
+ </li>
261
+ `;
262
+ }, check.isTruthy);
263
+ const selectedTab = inputs.tabs.find((tab) => routeHasPaths(inputs.currentRoute, tab.paths));
264
+ const menuItems = renderMenuItemEntries(filterMap(inputs.tabs, (tab) => {
265
+ if (tab.isHidden) {
266
+ return undefined;
267
+ }
268
+ const isSelected = routeHasPaths(inputs.currentRoute, tab.paths);
269
+ return {
270
+ content: html `
271
+ <${ViraLink.assign({
272
+ route: {
273
+ router: inputs.router,
274
+ route: {
275
+ paths: tab.paths.fullPaths,
276
+ },
277
+ scrollToTop: true,
278
+ },
279
+ disableLinkStyles: true,
280
+ })}>
281
+ ${tab.label}
282
+ </${ViraLink}>
283
+ `,
284
+ selected: isSelected,
285
+ disabled: tab.isDisabled,
286
+ };
287
+ }, check.isTruthy));
288
+ return html `
289
+ <${ViraOverflowSwitch.assign({
290
+ automaticallySwitch: true,
291
+ })}>
292
+ <ul
293
+ class="tabs-container"
294
+ role="tablist"
295
+ slot=${ViraOverflowSwitch.slotNames.large}
296
+ >
297
+ ${tabs}
298
+ </ul>
299
+ <${ViraMenuTrigger.assign({
300
+ horizontalAnchor: inputs.menuHorizontalAnchor,
301
+ isDisabled: inputs.menuIsDisabled,
302
+ popUpOffset: inputs.menuPopUpOffset,
303
+ menuCornerStyle: ViraMenuCornerStyle.AllRounded,
304
+ })}
305
+ slot=${ViraOverflowSwitch.slotNames.small}
306
+ >
307
+ <${ViraButton.assign({
308
+ text: selectedTab?.label || '',
309
+ showMenuCaret: true,
310
+ colorVariant: ViraColorVariant.Neutral,
311
+ })}
312
+ slot=${ViraMenuTrigger.slotNames.trigger}
313
+ ></${ViraButton}>
314
+ ${menuItems}
315
+ </${ViraMenuTrigger}>
316
+ </${ViraOverflowSwitch}>
317
+ `;
318
+ },
319
+ });
@@ -48,6 +48,11 @@ export function renderMenuItemEntries(items) {
48
48
  ...item,
49
49
  })}
50
50
  ${listen('click', async (event) => {
51
+ if (item.disabled) {
52
+ event.stopImmediatePropagation();
53
+ event.preventDefault();
54
+ return;
55
+ }
51
56
  await item.onClick?.({
52
57
  event,
53
58
  index,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vira",
3
- "version": "31.5.7",
3
+ "version": "31.6.0",
4
4
  "description": "A simple and highly versatile design system using element-vir.",
5
5
  "keywords": [
6
6
  "design",