@functionalcms/svelte-components 4.2.17 → 4.2.19

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.
@@ -0,0 +1,356 @@
1
+ <script lang="ts">
2
+ import { cn } from '../../utils.js';
3
+ import type { NavigationDirection, Tab, TabSizes } from './tabs.js';
4
+
5
+ interface Props {
6
+ size: TabSizes;
7
+ tabs: Tab[];
8
+ isBorderless: boolean;
9
+ isVerticalOrientation: boolean;
10
+ isDisabled: boolean;
11
+ disabledOptions: string[];
12
+ isSkinned: boolean;
13
+ }
14
+
15
+ let {
16
+ size = '',
17
+ tabs = [],
18
+ isBorderless = false,
19
+ isVerticalOrientation = false,
20
+ isDisabled = false,
21
+ disabledOptions = [],
22
+ isSkinned = true
23
+ }: Partial<Props> = $props();
24
+
25
+ let dynamicComponentRefs = $state([]);
26
+ let tabButtonRefs = $state([]);
27
+ let baseStyles = $derived(`tabs ${isVerticalOrientation ? 'tabs-vertical' : ''}`);
28
+
29
+ const selectTab = (index: number) => {
30
+ tabs = tabs.map((tab, i) => {
31
+ tab.isActive = index === i ? true : false;
32
+ return tab;
33
+ });
34
+ };
35
+ let activeTabs = tabs.filter((tab: Tab) => tab.isActive);
36
+ if (activeTabs.length === 0) {
37
+ selectTab(0);
38
+ }
39
+
40
+ let tablistClasses = $derived(
41
+ cn(isSkinned ? 'tab-list' : 'tab-list-base', isBorderless ? `tab-borderless` : '')
42
+ );
43
+
44
+ let tabButtonClasses = (tab: Tab) => {
45
+ return cn(
46
+ `tab-item`,
47
+ `tab-button`,
48
+ tab.isActive ? 'active' : '',
49
+ size === 'large' ? 'tab-button-large' : '',
50
+ size === 'xlarge' ? 'tab-button-xlarge' : ''
51
+ );
52
+ };
53
+
54
+ const focusTab = (index: number, direction?: NavigationDirection) => {
55
+ let i = index;
56
+ if (direction === 'asc') {
57
+ i += 1;
58
+ } else if (direction === 'desc') {
59
+ i -= 1;
60
+ }
61
+
62
+ // Circular navigation
63
+ //
64
+ // If we've went beyond "start" circle around to last
65
+ if (i < 0) {
66
+ i = tabs.length - 1;
67
+ } else if (i >= tabs.length) {
68
+ // We've went beyond "last" so circle around to first
69
+ i = 0;
70
+ }
71
+
72
+ /**
73
+ * Figure out at run-time whether this was build with dynamicComponentRefs (consumer
74
+ * used their own tabButtonComponent), or tabButtonRefs (we generated the buttons here)
75
+ */
76
+
77
+ let nextTab;
78
+ if (tabButtonRefs.length) {
79
+ nextTab = tabButtonRefs[i];
80
+ } else if (dynamicComponentRefs.length) {
81
+ // Same logic as above, but we're using the binding to component instance
82
+ nextTab = dynamicComponentRefs[i];
83
+ }
84
+ // Edge case: We hit a tab button that's been disabled. If so, we recurse, but
85
+ // only if we've been supplied a `direction`. Otherwise, nothing left to do.
86
+ if ((nextTab.isDisabled && nextTab.isDisabled()) || (nextTab.disabled && direction)) {
87
+ // Retry with new `i` index going in same direction
88
+ focusTab(i, direction);
89
+ } else {
90
+ // Nominal case is to just focs next tab :)
91
+ nextTab.focus();
92
+ }
93
+ };
94
+
95
+ const handleKeyDown = (ev: KeyboardEvent, index: number) => {
96
+ switch (ev.key) {
97
+ case 'Up': // These first cases are IEEdge :(
98
+ case 'ArrowUp':
99
+ if (isVerticalOrientation) {
100
+ focusTab(index, 'desc');
101
+ }
102
+ break;
103
+ case 'Down':
104
+ case 'ArrowDown':
105
+ if (isVerticalOrientation) {
106
+ focusTab(index, 'asc');
107
+ }
108
+ break;
109
+ case 'Left':
110
+ case 'ArrowLeft':
111
+ if (!isVerticalOrientation) {
112
+ focusTab(index, 'desc');
113
+ }
114
+ break;
115
+ case 'Right':
116
+ case 'ArrowRight':
117
+ if (!isVerticalOrientation) {
118
+ focusTab(index, 'asc');
119
+ }
120
+ break;
121
+ case 'Home':
122
+ case 'ArrowHome':
123
+ focusTab(0);
124
+ break;
125
+ case 'End':
126
+ case 'ArrowEnd':
127
+ focusTab(tabs.length - 1);
128
+ break;
129
+ case 'Enter':
130
+ case 'Space':
131
+ focusTab(index);
132
+ selectTab(index);
133
+ break;
134
+ default:
135
+ return;
136
+ }
137
+ ev.preventDefault();
138
+ };
139
+
140
+ let activePanel = $derived(tabs.find((x) => x.isActive));
141
+ </script>
142
+
143
+ <div class={baseStyles}>
144
+ <div
145
+ class={tablistClasses}
146
+ role="tablist"
147
+ aria-orientation={isVerticalOrientation ? 'vertical' : 'horizontal'}
148
+ >
149
+ {#each tabs as tab, i}
150
+ {#if tab.tabButtonComponent}
151
+ {@render tab.tabButtonComponent()}
152
+ <!-- <svelte:component
153
+ this={tab.tabButtonComponent}
154
+ bind:this={dynamicComponentRefs[i]}
155
+ onclick={() => selectTab(i)}
156
+ onkeydown={(e) => handleKeyDown(e, i)}
157
+ disabled={isDisabled || disabledOptions.includes(tab.title) || undefined}
158
+ classes={tabButtonClasses(tab)}
159
+ role="tab"
160
+ ariaControls={tab.ariaControls}
161
+ isActive={tab.isActive}
162
+ >
163
+ {tab.title}
164
+ </svelte:component> -->
165
+ {:else}
166
+ <button
167
+ onclick={() => selectTab(i)}
168
+ onkeydown={(e) => handleKeyDown(e, i)}
169
+ disabled={isDisabled || disabledOptions.includes(tab.title) || undefined}
170
+ class={tabButtonClasses(tab)}
171
+ role="tab"
172
+ aria-controls={tab.ariaControls}
173
+ tabindex={tab.isActive ? 0 : -1}
174
+ aria-selected={tab.isActive}
175
+ >
176
+ {tab.title}
177
+ </button>
178
+ {/if}
179
+ {/each}
180
+ </div>
181
+ {@render activePanel?.tabPanelComponent()}
182
+ <!-- {#each tabs as panel}
183
+ {#if panel.isActive}
184
+ <svelte:component this={panel.tabPanelComponent} tabindex="0" />
185
+ {/if}
186
+ {/each} -->
187
+ </div>
188
+
189
+ <style>
190
+ /* TODO -- should we use these for .nav? */
191
+ .tabs {
192
+ display: flex;
193
+ flex-direction: column;
194
+ }
195
+
196
+ .tabs-vertical {
197
+ flex-direction: row;
198
+ }
199
+
200
+ .tab-list,
201
+ .tab-list-base {
202
+ display: flex;
203
+ flex-flow: row wrap;
204
+ flex: 0 0 auto;
205
+ }
206
+
207
+ .tab-list,
208
+ .tab-skinned {
209
+ padding-inline-start: 0;
210
+ margin-block-end: 0;
211
+ border-bottom: var(--agnostic-tabs-border-size, 1px) solid
212
+ var(--agnostic-tabs-bgcolor, var(--agnostic-gray-light));
213
+ transition-property: all;
214
+ transition-duration: var(--agnostic-timing-medium);
215
+ }
216
+
217
+ /* In vertical orientation we want our tab buttons to stack */
218
+ .tabs-vertical .tab-list,
219
+ .tabs-vertical .tab-base {
220
+ flex-direction: column;
221
+ border: none;
222
+ }
223
+
224
+ /* We can ask for .tab-button which is base and skin combined, or, we can utilize .tab-button-base
225
+ if we'd like to only blank out buttons but otherwise skin ourselves. */
226
+ .tab-button,
227
+ .tab-button-base {
228
+ /* Blank out the button */
229
+ background-color: transparent;
230
+ border: 0;
231
+ border-radius: 0;
232
+ box-shadow: none;
233
+
234
+ /* This fixes issue where upon focus, the a11y focus ring's box shadow would get tucked beneat
235
+ adjacent tab buttons; relative creates new stacking context https://stackoverflow.com/a/31276836 */
236
+ position: relative;
237
+
238
+ /* Reset margins/padding; this will get added back if it's a "skinned" tab button. However, we have
239
+ a use case where a tab-button is wrapping a faux button. For that, we don't want margins/padding because
240
+ the faux button provides that. */
241
+ margin-inline-start: 0;
242
+ margin-inline-end: 0;
243
+ padding-block-start: 0;
244
+ padding-block-end: 0;
245
+ padding-inline-start: 0;
246
+ padding-inline-end: 0;
247
+ }
248
+
249
+ .tab-button,
250
+ .tab-button-skin {
251
+ display: block;
252
+
253
+ /* Since this is a "skinned tab button" we add our padding here to previously "reset" .tab-button-base */
254
+ padding-block-start: var(--agnostic-vertical-pad, 0.5rem);
255
+ padding-block-end: var(--agnostic-vertical-pad, 0.5rem);
256
+ padding-inline-start: var(--agnostic-side-padding, 0.75rem);
257
+ padding-inline-end: var(--agnostic-side-padding, 0.75rem);
258
+ font-family: var(--agnostic-btn-font-family, var(--agnostic-font-family-body));
259
+ font-weight: var(--agnostic-btn-font-weight, 400);
260
+ font-size: var(--agnostic-btn-font-size, 1rem);
261
+
262
+ /* this can be overriden, but it might mess with the balance of the button heights across variants */
263
+ line-height: var(--agnostic-line-height, var(--fluid-20, 1.25rem));
264
+ color: var(--agnostic-tabs-primary, var(--agnostic-primary));
265
+ text-decoration: none;
266
+ transition:
267
+ color var(--agnostic-timing-fast) ease-in-out,
268
+ background-color var(--agnostic-timing-fast) ease-in-out,
269
+ border-color var(--agnostic-timing-fast) ease-in-out;
270
+ }
271
+
272
+ /* We pull back the 2nd subsequent tabs to remove the double border */
273
+ .tab-button:not(:first-of-type),
274
+ .tab-button-base:not(:first-of-type) {
275
+ margin-inline-start: -1px;
276
+ }
277
+
278
+ .tab-borderless {
279
+ border: none !important;
280
+ }
281
+
282
+ .tab-button-large {
283
+ padding-block-start: calc(var(--agnostic-input-side-padding) * 1.25);
284
+ padding-block-end: calc(var(--agnostic-input-side-padding) * 1.25);
285
+ padding-inline-start: calc(var(--agnostic-input-side-padding) * 1.75);
286
+ padding-inline-end: calc(var(--agnostic-input-side-padding) * 1.75);
287
+ }
288
+
289
+ .tab-button-xlarge {
290
+ padding-block-start: calc(var(--agnostic-input-side-padding) * 2);
291
+ padding-block-end: calc(var(--agnostic-input-side-padding) * 2);
292
+ padding-inline-start: calc(var(--agnostic-input-side-padding) * 3);
293
+ padding-inline-end: calc(var(--agnostic-input-side-padding) * 3);
294
+ }
295
+
296
+ .tab-item.tab-button {
297
+ margin-block-end: -1px;
298
+ background: 0 0;
299
+ border: 1px solid transparent;
300
+ border-top-left-radius: var(--agnostic-tabs-radius, 0.25rem);
301
+ border-top-right-radius: var(--agnostic-tabs-radius, 0.25rem);
302
+ }
303
+
304
+ .tab-item.tab-button.active {
305
+ color: var(--agnostic-dark);
306
+ background-color: var(--agnostic-light);
307
+ border-color: var(--agnostic-gray-light) var(--agnostic-gray-light) var(--agnostic-light);
308
+ }
309
+
310
+ .tab-item:hover,
311
+ .tab-button:focus {
312
+ border-color: var(--agnostic-focus-ring-outline-width) var(--agnostic-focus-ring-outline-width)
313
+ var(--agnostic-gray-light);
314
+ isolation: isolate;
315
+ z-index: 1;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .tabs-vertical .tab-button {
320
+ border: none;
321
+ }
322
+
323
+ .tab-button:disabled {
324
+ color: var(--agnostic-tabs-disabled-bg, var(--agnostic-gray-mid-dark));
325
+ background-color: transparent;
326
+ border-color: transparent;
327
+ opacity: 80%;
328
+ }
329
+
330
+ /**
331
+ * Elects to additively use the AgnosticUI custom focus ring alongside the border
332
+ * we already add above. It just makes things look more consistent across components.
333
+ * For example, when we tab into the panels and links within.
334
+ */
335
+ .tab-button-base:focus,
336
+ .tab-panel:focus,
337
+ .tab-button:focus {
338
+ box-shadow: 0 0 0 var(--agnostic-focus-ring-outline-width) var(--agnostic-focus-ring-color);
339
+
340
+ /* Needed for High Contrast mode */
341
+ outline: var(--agnostic-focus-ring-outline-width) var(--agnostic-focus-ring-outline-style)
342
+ var(--agnostic-focus-ring-outline-color);
343
+ transition: box-shadow var(--agnostic-timing-fast) ease-out;
344
+ }
345
+
346
+ @media (prefers-reduced-motion), (update: slow) {
347
+ .tab-button,
348
+ .tab-button-base:focus,
349
+ .tab-button:focus,
350
+ .tab-panel:focus,
351
+ .tab-list,
352
+ .tab-skinned {
353
+ transition-duration: 0.001ms !important;
354
+ }
355
+ }
356
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Tab, TabSizes } from './tabs.js';
2
+ declare const Tabs: import("svelte").Component<Partial<{
3
+ size: TabSizes;
4
+ tabs: Tab[];
5
+ isBorderless: boolean;
6
+ isVerticalOrientation: boolean;
7
+ isDisabled: boolean;
8
+ disabledOptions: string[];
9
+ isSkinned: boolean;
10
+ }>, {}, "">;
11
+ type Tabs = ReturnType<typeof Tabs>;
12
+ export default Tabs;
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from "svelte";
2
+ export interface Tab {
3
+ title: string;
4
+ tabPanelComponent: Snippet;
5
+ tabButtonComponent?: Snippet;
6
+ isActive?: boolean;
7
+ ariaControls?: string;
8
+ }
9
+ export type TabSizes = 'small' | 'large' | 'xlarge' | '';
10
+ export type NavigationDirection = 'asc' | 'desc';
@@ -0,0 +1,2 @@
1
+ ;
2
+ export {};
@@ -12,7 +12,7 @@ export declare class HeaderNavigationItem {
12
12
  visiblity: Visiblity;
13
13
  subItems?: HeaderNavigationItem[];
14
14
  }
15
- export declare function selectVisible(pages: Array<any>, visiblity: Visiblity): any[];
15
+ export declare function selectVisible(pages: Array<HeaderNavigationItem>, visiblity: Visiblity): HeaderNavigationItem[];
16
16
  export declare function isSelected(includeSubpagesForSelect: boolean, page: any, item: HeaderNavigationItem): any;
17
17
  export declare const defaultCss: {
18
18
  buttonCss: never[];
package/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@ export { default as TwoColumnsLayout } from './components/layouts/TwoColumnsLayo
5
5
  export { default as Well } from './components/layouts/Well.svelte';
6
6
  export { default as Banner } from './components/layouts/Banner.svelte';
7
7
  export { default as SimpleFooter } from './components/layouts/SimpleFooter.svelte';
8
- export { HeaderNavigationItem, Visiblity } from './components/menu/types.js';
8
+ export { default as Tabs } from './components/layouts/Tabs.svelte';
9
+ export type { Tab, TabSizes, NavigationDirection } from './components/layouts/tabs.js';
9
10
  export { Justify, AlignItmes, Placement, Orientation, Position, Sizes, ComponentSize } from './components/Styling.js';
10
11
  export { default as Link } from './components/presentation/Link.svelte';
11
12
  export { default as Logo } from './components/presentation/Logo.svelte';
@@ -19,6 +20,7 @@ export { default as ListMenu } from './components/menu/ListMenu.svelte';
19
20
  export { default as DynamicMenu } from './components/menu/DynamicMenu.svelte';
20
21
  export { default as HamburgerMenu } from './components/menu/HamburgerMenu.svelte';
21
22
  export { isAuthenticated, selectVisible } from './components/menu/types.js';
23
+ export { HeaderNavigationItem, Visiblity } from './components/menu/types.js';
22
24
  export { default as Button } from './components/form/Button.svelte';
23
25
  export { default as Input } from './components/form/Input.svelte';
24
26
  export { default as Switch } from './components/form/Switch.svelte';
package/dist/index.js CHANGED
@@ -8,10 +8,7 @@ export { default as TwoColumnsLayout } from './components/layouts/TwoColumnsLayo
8
8
  export { default as Well } from './components/layouts/Well.svelte';
9
9
  export { default as Banner } from './components/layouts/Banner.svelte';
10
10
  export { default as SimpleFooter } from './components/layouts/SimpleFooter.svelte';
11
- /*
12
- * Navigation
13
- */
14
- export { HeaderNavigationItem, Visiblity } from './components/menu/types.js';
11
+ export { default as Tabs } from './components/layouts/Tabs.svelte';
15
12
  /*
16
13
  * Styling
17
14
  */
@@ -33,6 +30,7 @@ export { default as ListMenu } from './components/menu/ListMenu.svelte';
33
30
  export { default as DynamicMenu } from './components/menu/DynamicMenu.svelte';
34
31
  export { default as HamburgerMenu } from './components/menu/HamburgerMenu.svelte';
35
32
  export { isAuthenticated, selectVisible } from './components/menu/types.js';
33
+ export { HeaderNavigationItem, Visiblity } from './components/menu/types.js';
36
34
  /*
37
35
  * Form
38
36
  */
package/dist/utils.js CHANGED
@@ -1,54 +1,3 @@
1
- // import { cubicOut } from "svelte/easing";
2
- // import type { TransitionConfig } from "svelte/transition";
3
- // export function cn(...inputs: ClassValue[]) {
4
- // return twMerge(clsx(inputs));
5
- // }
6
1
  export function cn(...params) {
7
2
  return Array.from(params).flat(2).filter((c) => c).join(' ');
8
3
  }
9
- // type FlyAndScaleParams = {
10
- // y?: number;
11
- // x?: number;
12
- // start?: number;
13
- // duration?: number;
14
- // };
15
- // export const flyAndScale = (
16
- // node: Element,
17
- // params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
18
- // ): TransitionConfig => {
19
- // const style = getComputedStyle(node);
20
- // const transform = style.transform === "none" ? "" : style.transform;
21
- // const scaleConversion = (
22
- // valueA: number,
23
- // scaleA: [number, number],
24
- // scaleB: [number, number]
25
- // ) => {
26
- // const [minA, maxA] = scaleA;
27
- // const [minB, maxB] = scaleB;
28
- // const percentage = (valueA - minA) / (maxA - minA);
29
- // const valueB = percentage * (maxB - minB) + minB;
30
- // return valueB;
31
- // };
32
- // const styleToString = (
33
- // style: Record<string, number | string | undefined>
34
- // ): string => {
35
- // return Object.keys(style).reduce((str, key) => {
36
- // if (style[key] === undefined) return str;
37
- // return str + `${key}:${style[key]};`;
38
- // }, "");
39
- // };
40
- // return {
41
- // duration: params.duration ?? 200,
42
- // delay: 0,
43
- // css: (t) => {
44
- // const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
45
- // const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
46
- // const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
47
- // return styleToString({
48
- // transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
49
- // opacity: t
50
- // });
51
- // },
52
- // easing: cubicOut
53
- // };
54
- // };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@functionalcms/svelte-components",
3
- "version": "4.2.17",
3
+ "version": "4.2.19",
4
4
  "watch": {
5
5
  "build": {
6
6
  "patterns": [