@obosbbl/grunnmuren-react 3.3.4 → 3.4.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/__stories__/form-validation.stories.cjs +70 -56
- package/dist/__stories__/form-validation.stories.js +71 -57
- package/dist/__stories__/layout.stories.cjs +32 -12
- package/dist/__stories__/layout.stories.js +32 -12
- package/dist/index.d.mts +116 -32
- package/dist/index.mjs +595 -482
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useContextProps, DisclosureContext, DisclosureGroupStateContext, Provider, ButtonContext as ButtonContext$1, DEFAULT_SLOT, Button as Button$1, useLocale, Link as Link$1, Breadcrumb as Breadcrumb$1, Breadcrumbs as Breadcrumbs$1, Text, CheckboxContext, Checkbox as Checkbox$1, FieldError, Label as Label$1, CheckboxGroup as CheckboxGroup$1, Header, ListBoxItem as ListBoxItem$1, ListBoxSection as ListBoxSection$1, ListBox as ListBox$1, ComboBox, Group, Input, Popover, useSlottedContext, FormContext, FieldErrorContext, LabelContext, InputContext, I18nProvider, RouterProvider,
|
|
2
|
+
import { useContextProps, DisclosureContext, DisclosureGroupStateContext, Provider, ButtonContext as ButtonContext$1, DEFAULT_SLOT, Button as Button$1, useLocale, Link as Link$1, Breadcrumb as Breadcrumb$1, Breadcrumbs as Breadcrumbs$1, GroupContext, Text, CheckboxContext, Checkbox as Checkbox$1, FieldError, Label as Label$1, CheckboxGroup as CheckboxGroup$1, Header, ListBoxItem as ListBoxItem$1, ListBoxSection as ListBoxSection$1, ListBox as ListBox$1, ComboBox, Group, Input, Popover, useSlottedContext, FormContext, FieldErrorContext, LabelContext, InputContext, I18nProvider, RouterProvider, Dialog as Dialog$1, DialogTrigger as DialogTrigger$1, Modal as Modal$1, ModalOverlay, NumberField as NumberField$1, ProgressBar as ProgressBar$1, Radio as Radio$1, RadioGroup as RadioGroup$1, Select as Select$1, SelectValue, ColumnResizer, Table as Table$1, TableBody as TableBody$1, Cell, Column, ResizableTableContainer, TableHeader as TableHeader$1, Row, Tab as Tab$1, TabListStateContext, TabList as TabList$1, TabPanel as TabPanel$1, Tabs as Tabs$1, TagGroup as TagGroup$1, TagList as TagList$1, Tag as Tag$1, TextField as TextField$1, TextArea as TextArea$1 } from 'react-aria-components';
|
|
3
3
|
export { DisclosureGroup, Form, Group } from 'react-aria-components';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
import { cva, cx, compose } from 'cva';
|
|
6
|
-
import { createContext, useContext, useId, useRef, Children, useState, useEffect,
|
|
6
|
+
import { createContext, useContext, useId, useRef, Children, useState, useEffect, useMemo, useCallback } from 'react';
|
|
7
7
|
import { ChevronDown, Error, Warning, CheckCircle, InfoCircle, Close, User, ChevronLeft, ChevronRight, LoadingSpinner, Check, Trash, ArrowRight, Download, LinkExternal, PlayerPause, PlayerPlay } from '@obosbbl/grunnmuren-icons-react';
|
|
8
|
-
import { filterDOMProps, mergeProps, mergeRefs,
|
|
8
|
+
import { filterDOMProps, mergeProps, mergeRefs, useObjectRef, useFormReset, useUpdateEffect } from '@react-aria/utils';
|
|
9
9
|
import { useFocusRing, useDisclosure, useProgressBar, useDateFormatter, useField } from 'react-aria';
|
|
10
10
|
import { useDisclosureState } from 'react-stately';
|
|
11
|
-
import
|
|
11
|
+
import Autoplay from 'embla-carousel-autoplay';
|
|
12
|
+
import useEmblaCarousel from 'embla-carousel-react';
|
|
13
|
+
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
|
12
14
|
import { useFormValidation } from '@react-aria/form';
|
|
13
15
|
import { useFormValidationState } from '@react-stately/form';
|
|
14
16
|
import { useControlledState } from '@react-stately/utils';
|
|
15
17
|
import { PressResponder } from '@react-aria/interactions';
|
|
18
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
16
19
|
|
|
17
20
|
const HeadingContext = /*#__PURE__*/ createContext({});
|
|
18
21
|
const headingVariants = cva({
|
|
@@ -487,44 +490,73 @@ const Avatar = ({ src, alt = '', className, onError, loading = 'lazy', ...rest }
|
|
|
487
490
|
});
|
|
488
491
|
};
|
|
489
492
|
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
493
|
+
const formField = cx('group flex flex-col gap-2');
|
|
494
|
+
const formFieldError = cx('w-fit bg-red-light px-2 py-1 text-red text-sm leading-6', 'group-data-[slot=file-upload]:rounded-lg');
|
|
495
|
+
const input = cva({
|
|
496
|
+
base: [
|
|
497
|
+
// All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
|
|
498
|
+
'bg-white',
|
|
499
|
+
// Use box-content to enable auto width based on number of characters (size)
|
|
500
|
+
// Setting min-height to prevent the input from collapsing in Safari
|
|
501
|
+
// Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
|
|
502
|
+
'box-content min-h-6 py-2.5',
|
|
503
|
+
'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
|
|
504
|
+
// invalid styles
|
|
505
|
+
'group-data-invalid:ring-focus group-data-invalid:ring-red',
|
|
506
|
+
// Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
|
|
507
|
+
'appearance-none'
|
|
508
|
+
],
|
|
509
|
+
variants: {
|
|
510
|
+
// Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
|
|
511
|
+
focusModifier: {
|
|
512
|
+
focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
|
|
513
|
+
visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
|
|
514
|
+
},
|
|
515
|
+
isGrouped: {
|
|
516
|
+
false: 'px-3',
|
|
517
|
+
true: '!ring-0 flex-1'
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
defaultVariants: {
|
|
521
|
+
focusModifier: 'focus',
|
|
522
|
+
isGrouped: false
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
const inputGroup = cx([
|
|
526
|
+
'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
|
|
527
|
+
'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
|
|
528
|
+
]);
|
|
529
|
+
const dropdown = {
|
|
530
|
+
popover: cx('data-entering:fade-in data-exiting:fade-out min-w-(--trigger-width) overflow-y-auto rounded-md border border-black bg-white shadow-sm data-entering:animate-in data-exiting:animate-out'),
|
|
531
|
+
// overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
|
|
532
|
+
listbox: cx('max-h-100 overflow-x-hidden text-sm outline-hidden'),
|
|
533
|
+
chevronIcon: cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
|
|
516
534
|
};
|
|
535
|
+
const animateIconVariants = cva({
|
|
536
|
+
base: '*:[svg]:shrink-0 *:[svg]:transition-transform',
|
|
537
|
+
variants: {
|
|
538
|
+
animateIcon: {
|
|
539
|
+
right: 'hover:*:[svg]:motion-safe:translate-x-1',
|
|
540
|
+
left: 'hover:*:[svg]:motion-safe:-translate-x-1',
|
|
541
|
+
down: 'hover:*:[svg]:motion-safe:translate-y-1',
|
|
542
|
+
up: 'hover:*:[svg]:motion-safe:-translate-y-1',
|
|
543
|
+
'up-right': 'hover:*:[svg]:motion-safe:-translate-y-0.5 hover:*:[svg]:motion-safe:translate-x-0.5'
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
});
|
|
517
547
|
|
|
518
548
|
function isLinkProps$1(props) {
|
|
519
549
|
return !!props.href;
|
|
520
550
|
}
|
|
521
551
|
function Backlink(props) {
|
|
522
|
-
const { className,
|
|
523
|
-
const _className = cx(className,
|
|
552
|
+
const { className, children, withUnderline, ref, ...restProps } = props;
|
|
553
|
+
const _className = cx(className, animateIconVariants({
|
|
554
|
+
animateIcon: 'left'
|
|
555
|
+
}), 'group flex max-w-fit cursor-pointer items-center gap-3 rounded-md p-2.5 font-normal no-underline focus-visible:outline-focus');
|
|
524
556
|
const content = /*#__PURE__*/ jsxs(Fragment, {
|
|
525
557
|
children: [
|
|
526
558
|
/*#__PURE__*/ jsx(ChevronLeft, {
|
|
527
|
-
className: cx('-ml-[0.5em]
|
|
559
|
+
className: cx('-ml-[0.5em] duration-300')
|
|
528
560
|
}),
|
|
529
561
|
/*#__PURE__*/ jsx("span", {
|
|
530
562
|
children: /*#__PURE__*/ jsx("span", {
|
|
@@ -535,10 +567,9 @@ function Backlink(props) {
|
|
|
535
567
|
]
|
|
536
568
|
});
|
|
537
569
|
if (isLinkProps$1(props)) {
|
|
538
|
-
return /*#__PURE__*/ jsx(Link, {
|
|
570
|
+
return /*#__PURE__*/ jsx(Link$1, {
|
|
539
571
|
...restProps,
|
|
540
572
|
className: _className,
|
|
541
|
-
style: style,
|
|
542
573
|
ref: ref,
|
|
543
574
|
children: content
|
|
544
575
|
});
|
|
@@ -546,7 +577,6 @@ function Backlink(props) {
|
|
|
546
577
|
return /*#__PURE__*/ jsx(Button$1, {
|
|
547
578
|
...restProps,
|
|
548
579
|
className: _className,
|
|
549
|
-
style: style,
|
|
550
580
|
ref: ref,
|
|
551
581
|
children: content
|
|
552
582
|
});
|
|
@@ -589,6 +619,40 @@ function Badge(props) {
|
|
|
589
619
|
});
|
|
590
620
|
}
|
|
591
621
|
|
|
622
|
+
const _LinkContext = /*#__PURE__*/ createContext({});
|
|
623
|
+
const linkVariants = compose(animateIconVariants, cva({
|
|
624
|
+
base: 'inline-flex cursor-pointer items-center gap-1 font-medium hover:no-underline focus-visible:outline-current focus-visible:outline-focus-offset data-disabled:cursor-default'
|
|
625
|
+
}));
|
|
626
|
+
const Link = ({ ref: _ref, animateIcon, ..._props })=>{
|
|
627
|
+
const [props, ref] = useContextProps(_props, _ref, _LinkContext);
|
|
628
|
+
const { className, _innerWrapper, children: _children, ...restProps } = props;
|
|
629
|
+
const locale = _useLocale();
|
|
630
|
+
const externalLinkSR = props.rel?.includes('external') ? /*#__PURE__*/ jsx("span", {
|
|
631
|
+
className: "sr-only",
|
|
632
|
+
children: translations$1.externalLink[locale]
|
|
633
|
+
}) : null;
|
|
634
|
+
const reactNodeChildren = /*#__PURE__*/ jsxs(Fragment, {
|
|
635
|
+
children: [
|
|
636
|
+
_children,
|
|
637
|
+
externalLinkSR
|
|
638
|
+
]
|
|
639
|
+
});
|
|
640
|
+
const children = _innerWrapper ? _innerWrapper({
|
|
641
|
+
...restProps,
|
|
642
|
+
children: typeof _children === 'function' ? (values)=>_children(values) : reactNodeChildren
|
|
643
|
+
}) : reactNodeChildren;
|
|
644
|
+
return /*#__PURE__*/ jsx(Link$1, {
|
|
645
|
+
...restProps,
|
|
646
|
+
ref: ref,
|
|
647
|
+
"data-slot": "link",
|
|
648
|
+
className: linkVariants({
|
|
649
|
+
className,
|
|
650
|
+
animateIcon
|
|
651
|
+
}),
|
|
652
|
+
children: children
|
|
653
|
+
});
|
|
654
|
+
};
|
|
655
|
+
|
|
592
656
|
function Breadcrumb(props) {
|
|
593
657
|
const { className, children, href, ...restProps } = props;
|
|
594
658
|
return /*#__PURE__*/ jsxs(Breadcrumb$1, {
|
|
@@ -619,38 +683,38 @@ function Breadcrumbs(props) {
|
|
|
619
683
|
|
|
620
684
|
/**
|
|
621
685
|
* Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
|
|
622
|
-
*/ const buttonVariants = cva({
|
|
686
|
+
*/ const buttonVariants = compose(animateIconVariants, cva({
|
|
623
687
|
base: [
|
|
624
688
|
'inline-flex min-h-[44px] cursor-pointer items-center justify-center whitespace-nowrap rounded-lg font-medium transition-colors duration-200 focus-visible:outline-focus-offset'
|
|
625
689
|
],
|
|
626
690
|
variants: {
|
|
627
691
|
/**
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
692
|
+
* The variant of the button
|
|
693
|
+
* @default primary
|
|
694
|
+
*/ variant: {
|
|
631
695
|
primary: 'no-underline',
|
|
632
696
|
// by using an inset box-shadow to emulate a border instead of an actual border, the button size will be equal regardless of the variant
|
|
633
697
|
secondary: 'border-2 border-current no-underline hover:border-transparent',
|
|
634
698
|
tertiary: 'underline hover:no-underline'
|
|
635
699
|
},
|
|
636
700
|
/**
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
701
|
+
* Adjusts the color of the button for usage on different backgrounds.
|
|
702
|
+
* @default blue
|
|
703
|
+
*/ color: {
|
|
640
704
|
blue: 'focus-visible:outline-focus',
|
|
641
705
|
mint: 'focus-visible:outline-focus focus-visible:outline-mint',
|
|
642
706
|
white: 'focus-visible:outline-focus focus-visible:outline-white'
|
|
643
707
|
},
|
|
644
708
|
/**
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
709
|
+
* When the button is without text, but with a single icon.
|
|
710
|
+
* @default false
|
|
711
|
+
*/ isIconOnly: {
|
|
648
712
|
true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
|
|
649
713
|
false: 'gap-2.5 px-4 py-2'
|
|
650
714
|
},
|
|
651
715
|
// Make the content of the button transparent to hide it's content, but keep the button width
|
|
652
716
|
isPending: {
|
|
653
|
-
true: '
|
|
717
|
+
true: 'relative text-transparent!',
|
|
654
718
|
false: null
|
|
655
719
|
}
|
|
656
720
|
},
|
|
@@ -709,15 +773,17 @@ function Breadcrumbs(props) {
|
|
|
709
773
|
isIconOnly: false,
|
|
710
774
|
isPending: false
|
|
711
775
|
}
|
|
712
|
-
});
|
|
776
|
+
}));
|
|
713
777
|
const ButtonContext = /*#__PURE__*/ createContext({});
|
|
714
778
|
function isLinkProps(props) {
|
|
715
779
|
return !!props.href;
|
|
716
780
|
}
|
|
717
781
|
function Button({ ref = null, ...props }) {
|
|
718
782
|
[props, ref] = useContextProps(props, ref, ButtonContext);
|
|
719
|
-
const { children: _children, color, isIconOnly, variant, isPending, ...restProps } = props;
|
|
783
|
+
const { animateIcon, children: _children, color, isIconOnly, variant, isPending, ...restProps } = props;
|
|
720
784
|
const className = buttonVariants({
|
|
785
|
+
// Don't animate the icon when we're pending, as it affects the loading spinner
|
|
786
|
+
animateIcon: isPending ? undefined : animateIcon,
|
|
721
787
|
className: props.className,
|
|
722
788
|
color,
|
|
723
789
|
isIconOnly,
|
|
@@ -977,12 +1043,145 @@ const cardLinkVariants = cva({
|
|
|
977
1043
|
});
|
|
978
1044
|
};
|
|
979
1045
|
|
|
1046
|
+
const HeroContext = /*#__PURE__*/ createContext(null);
|
|
1047
|
+
// Common variant for "standard" and "full-bleed" Hero variants
|
|
1048
|
+
const oneColumnLayout = [
|
|
1049
|
+
// Vertical spacing in the <Content>
|
|
1050
|
+
'lg:*:data-[slot="content"]:gap-y-4',
|
|
1051
|
+
// Main text content takes up 9 columns on medium screens and above
|
|
1052
|
+
'lg:*:data-[slot="content"]:col-span-9',
|
|
1053
|
+
// Make sure other elements than <Content> and <Media> (i.e CTA) does not span the full width on small screens
|
|
1054
|
+
'*:not-data-[slot="content"]:not-data-[slot="media"]:w-fit',
|
|
1055
|
+
// Other elements than <Content> and <Media> (e.g. CTA, SVG logo or Badge) take up 3 columns on medium screens and above, and are right aligned
|
|
1056
|
+
'lg:*:not-data-[slot="content"]:not-data-[slot="media"]:not-data-[slot="carousel"]:col-span-3 lg:*:not-data-[slot="content"]:not-data-[slot="media"]:justify-self-end',
|
|
1057
|
+
// <Media> and <Carousel> content takes up the full width on medium screens and above
|
|
1058
|
+
'lg:*:data-[slot="media"]:col-span-full *:data-[slot="media"]:*:w-full',
|
|
1059
|
+
'lg:*:data-[slot="carousel"]:col-span-full',
|
|
1060
|
+
// Aligns <Content> and any element beside it (e.g. <Media>, <Badge>, <CTA> etc.) to the bottom of the <Content> container
|
|
1061
|
+
'lg:items-end'
|
|
1062
|
+
];
|
|
1063
|
+
const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot="media"]:*:aspect-[1/1] sm:*:data-[slot="media"]:*:aspect-4/3 md:*:data-[slot="media"]:*:aspect-3/2';
|
|
1064
|
+
const variants = cva({
|
|
1065
|
+
base: [
|
|
1066
|
+
'container px-0',
|
|
1067
|
+
// Grid variant to position the Hero's content
|
|
1068
|
+
'grid lg:grid-cols-12 lg:gap-x-12 xl:gap-x-16',
|
|
1069
|
+
'gap-y-10 lg:gap-y-12',
|
|
1070
|
+
// Enable vertical gap within <Content>
|
|
1071
|
+
'*:data-[slot="content"]:grid',
|
|
1072
|
+
// Vertical spacing in the <Content>
|
|
1073
|
+
'*:data-[slot="content"]:gap-y-3',
|
|
1074
|
+
// Make sure <Media> content fills any available vertical and horizontal space
|
|
1075
|
+
'*:data-[slot="media"]:*:object-cover',
|
|
1076
|
+
'*:data-[slot="carousel"]:overflow-hidden *:data-[slot="carousel"]:rounded-3xl',
|
|
1077
|
+
// Make the carousel items full width, so we scroll one at a time
|
|
1078
|
+
'**:data-[slot="carousel-item"]:basis-full'
|
|
1079
|
+
],
|
|
1080
|
+
variants: {
|
|
1081
|
+
/**
|
|
1082
|
+
* Defines the variant of the Hero
|
|
1083
|
+
* @default standard
|
|
1084
|
+
* */ variant: {
|
|
1085
|
+
standard: [
|
|
1086
|
+
oneColumnLayout,
|
|
1087
|
+
nonFullBleedAspectRatiosForSmallScreens,
|
|
1088
|
+
'lg:*:data-[slot="media"]:*:aspect-2/1'
|
|
1089
|
+
],
|
|
1090
|
+
'full-bleed': [
|
|
1091
|
+
oneColumnLayout,
|
|
1092
|
+
// Position the media and carousel content to fill the entire viewport width
|
|
1093
|
+
'*:data-[slot="media"]:*:absolute *:data-[slot="media"]:*:left-0',
|
|
1094
|
+
// Special case for Carousel, where the Media is nested inside a CarouselItem
|
|
1095
|
+
'*:data-[slot="carousel"]:**:data-[slot="media"]:w-full',
|
|
1096
|
+
// Match the heights of the <Media> or <Carousel> wrapper for the Media content (e.g. image, VideoLoop, video etc.)
|
|
1097
|
+
// This is necessary due to the absolute positioning of the media and carousel containers in this variant
|
|
1098
|
+
// biome-ignore lint/nursery/useSortedClasses: biome is unable to sort the custom classes for 3xl and 4xl breakpoints
|
|
1099
|
+
'**:data-[slot="media"]:h-80 sm:**:data-[slot="media"]:h-[25rem] md:**:data-[slot="media"]:h-[30rem] lg:**:data-[slot="media"]:h-[35rem] xl:**:data-[slot="media"]:h-[40rem] 2xl:**:data-[slot="media"]:h-[42rem] 3xl:**:data-[slot="media"]:h-[48rem] 4xl:**:data-[slot="media"]:h-[53rem]',
|
|
1100
|
+
'**:data-[slot="media"]:*:h-[inherit]',
|
|
1101
|
+
// biome-ignore lint/nursery/useSortedClasses: biome is unable to sort the custom classes for 3xl and 4xl breakpoints
|
|
1102
|
+
'*:data-[slot="carousel"]:h-80 sm:*:data-[slot="carousel"]:h-[25rem] md:*:data-[slot="carousel"]:h-[30rem] lg:*:data-[slot="carousel"]:h-[35rem] xl:*:data-[slot="carousel"]:h-[40rem] 2xl:*:data-[slot="carousel"]:h-[42rem] 3xl:*:data-[slot="carousel"]:h-[48rem] 4xl:*:data-[slot="carousel"]:h-[53rem]',
|
|
1103
|
+
'*:data-[slot="carousel"]:w-full!',
|
|
1104
|
+
// Override aspect ratio of the media and carousel-item slots (since we can not use aspect for full-bleed layout)
|
|
1105
|
+
'**:data-[slot="carousel-item"]:data-[slot="media"]:*:aspect-none',
|
|
1106
|
+
// break out the carousel out of the container
|
|
1107
|
+
'**:data-[slot="carousel-items-container"]:absolute **:data-[slot="carousel-items-container"]:right-0 **:data-[slot="carousel-items-container"]:left-0 **:data-[slot="carousel-items-container"]:h-[inherit]',
|
|
1108
|
+
// Positions the carousel controls inside the carousel
|
|
1109
|
+
'**:data-[slot="carousel-controls"]:z-10 **:data-[slot="carousel-controls"]:mb-4 *:data-[slot="carousel"]:flex *:data-[slot="carousel"]:items-end *:data-[slot="carousel"]:justify-end'
|
|
1110
|
+
],
|
|
1111
|
+
'two-column': [
|
|
1112
|
+
'lg:items-center lg:*:col-span-6',
|
|
1113
|
+
// Vertical spacing in the <Content>
|
|
1114
|
+
'lg:*:data-[slot="content"]:gap-y-7',
|
|
1115
|
+
nonFullBleedAspectRatiosForSmallScreens,
|
|
1116
|
+
// Set media aspect ratio to 1:1 (square)
|
|
1117
|
+
'lg:*:data-[slot="media"]:*:aspect-square'
|
|
1118
|
+
]
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
compoundVariants: [
|
|
1122
|
+
{
|
|
1123
|
+
variant: [
|
|
1124
|
+
'standard',
|
|
1125
|
+
'two-column'
|
|
1126
|
+
],
|
|
1127
|
+
className: [
|
|
1128
|
+
'*:data-[slot="media"]:*:rounded-3xl',
|
|
1129
|
+
'**:data-[slot="carousel-controls"]:absolute *:data-[slot="carousel"]:relative **:data-[slot="carousel-controls"]:right-4 **:data-[slot="carousel-controls"]:bottom-4 **:data-[slot="carousel-container"]:rounded-3xl'
|
|
1130
|
+
]
|
|
1131
|
+
}
|
|
1132
|
+
],
|
|
1133
|
+
defaultVariants: {
|
|
1134
|
+
variant: 'standard'
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
const Hero = ({ variant, className, children, ...rest })=>{
|
|
1138
|
+
const variantsClassName = variants({
|
|
1139
|
+
variant,
|
|
1140
|
+
className
|
|
1141
|
+
});
|
|
1142
|
+
return /*#__PURE__*/ jsx(Provider, {
|
|
1143
|
+
values: [
|
|
1144
|
+
[
|
|
1145
|
+
HeroContext,
|
|
1146
|
+
{
|
|
1147
|
+
variant
|
|
1148
|
+
}
|
|
1149
|
+
],
|
|
1150
|
+
[
|
|
1151
|
+
HeadingContext,
|
|
1152
|
+
{
|
|
1153
|
+
// Sets the default heading size for the Hero based on the variant
|
|
1154
|
+
size: variant === 'two-column' ? 'xl' : 'l',
|
|
1155
|
+
className: // word-break:break-word to allow long words to break (this is necessary to make hyphens work in grid containers in Safari)
|
|
1156
|
+
'hyphens-auto text-pretty [word-break:break-word]'
|
|
1157
|
+
}
|
|
1158
|
+
],
|
|
1159
|
+
[
|
|
1160
|
+
GroupContext,
|
|
1161
|
+
{
|
|
1162
|
+
// Prevents the group from being announced as a group by screen readers
|
|
1163
|
+
// The Group component is used to group the Hero's CTA buttons together visually, and has no semantic meaning
|
|
1164
|
+
role: 'presentation',
|
|
1165
|
+
className: 'flex flex-wrap gap-3 *:w-fit'
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
],
|
|
1169
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
1170
|
+
className: cx(variantsClassName, className),
|
|
1171
|
+
...rest,
|
|
1172
|
+
children: children
|
|
1173
|
+
})
|
|
1174
|
+
});
|
|
1175
|
+
};
|
|
1176
|
+
|
|
980
1177
|
/**
|
|
981
1178
|
* Hook that detects the user's preference for reduced motion.
|
|
982
1179
|
*
|
|
983
1180
|
* Keep in mind that this hook relies on a browser API's and doesn't run on the server.
|
|
984
1181
|
* You can supply an initial value that will be used for server side rendering.
|
|
985
|
-
|
|
1182
|
+
*
|
|
1183
|
+
* The default initial value is `false` as this aligns with most users. This is to prevent unecessary rerenders on mount for the common user.
|
|
1184
|
+
*/ function usePrefersReducedMotion(initialValue = false) {
|
|
986
1185
|
const [prefersReducedMotion, setPrefersReducedMotion] = useState(initialValue);
|
|
987
1186
|
useEffect(()=>{
|
|
988
1187
|
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
@@ -998,188 +1197,193 @@ const cardLinkVariants = cva({
|
|
|
998
1197
|
return prefersReducedMotion;
|
|
999
1198
|
}
|
|
1000
1199
|
|
|
1001
|
-
|
|
1200
|
+
/**
|
|
1201
|
+
* A helper to get the prev/next button from the carousel DOM
|
|
1202
|
+
* @param ref The carousel ref
|
|
1203
|
+
* @param slot The slot of the button to get ('prev' or 'next')
|
|
1204
|
+
* @returns The button element, or undefined if not found
|
|
1205
|
+
*/ const getCarouselButton = (ref, slot)=>ref.current?.querySelector(`button[slot="${slot}"]`);
|
|
1206
|
+
/**
|
|
1207
|
+
* Focus the first focusable element in the currently snapped slide
|
|
1208
|
+
* @param emblaApi The embla carousel API instance
|
|
1209
|
+
*/ const focusElementInSnappedSlide = (emblaApi)=>{
|
|
1210
|
+
if (!emblaApi) {
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const index = emblaApi.selectedScrollSnap();
|
|
1214
|
+
const targetSlide = emblaApi.slideNodes()[index];
|
|
1215
|
+
if (!targetSlide) {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
// Find first focusable element in the slide
|
|
1219
|
+
const focusableElement = targetSlide.querySelector('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
|
|
1220
|
+
// Use preventScroll to avoid the browser's default scroll-into-view behavior
|
|
1221
|
+
// which would conflict with embla's scroll animation
|
|
1222
|
+
focusableElement?.focus({
|
|
1223
|
+
preventScroll: true
|
|
1224
|
+
});
|
|
1225
|
+
};
|
|
1226
|
+
const Carousel = ({ autoPlayDelay, align = 'center', children, initialIndex = 0, orientation = 'horizontal', onSelect, onSettled, loop = false, scrollGestures = false, ref, ...rest })=>{
|
|
1002
1227
|
const carouselRef = useRef(null);
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const scrollTimeoutRef = useRef(null);
|
|
1009
|
-
const scrollQueue = useRef([]);
|
|
1010
|
-
const [hasReachedScrollStart, setHasReachedScrollStart] = useState(scrollTargetIndex === 0);
|
|
1011
|
-
const [hasReachedScrollEnd, setHasReachedScrollEnd] = useState(!carouselItemsRef.current || carouselItemsRef.current.children.length - 1 === scrollTargetIndex);
|
|
1012
|
-
const prefersReducedMotion = usePrefersReducedMotion();
|
|
1013
|
-
useEffect(()=>{
|
|
1014
|
-
setHasReachedScrollStart(scrollTargetIndex === 0);
|
|
1015
|
-
setHasReachedScrollEnd(!carouselItemsRef.current || carouselItemsRef.current.children.length - 1 === scrollTargetIndex);
|
|
1016
|
-
}, [
|
|
1017
|
-
scrollTargetIndex
|
|
1018
|
-
]);
|
|
1019
|
-
// Keep track of the previous index to determine if the user is scrolling forward or backward
|
|
1020
|
-
// This is used to determine which callback to call (onPrev or onNext)
|
|
1021
|
-
const prevIndex = useRef(0);
|
|
1022
|
-
// Processes the next scroll action in the queue, if any
|
|
1023
|
-
// All clicks on the prev/next buttons are queued while a programmatic scroll is in progress
|
|
1024
|
-
// This is to ensure that rapid clicks on the buttons do not cause janky scrolling behavior
|
|
1025
|
-
// while still a snappy response to user clicks
|
|
1026
|
-
const processQueue = ()=>{
|
|
1027
|
-
if (scrollQueue.current.length > 0 && !isScrollingProgrammatically.current) {
|
|
1028
|
-
const nextIndex = scrollQueue.current?.shift();
|
|
1029
|
-
if (nextIndex !== undefined) {
|
|
1030
|
-
setScrollTargetIndex(nextIndex);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
// Handle scrolling when user clicks the arrow icons
|
|
1035
|
-
useUpdateEffect(()=>{
|
|
1036
|
-
if (!carouselItemsRef.current) {
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
if (scrollTimeoutRef.current) {
|
|
1040
|
-
clearTimeout(scrollTimeoutRef.current);
|
|
1041
|
-
}
|
|
1042
|
-
isScrollingProgrammatically.current = true;
|
|
1043
|
-
const elementWithFocusVisible = carouselRef.current?.querySelector(':focus-visible');
|
|
1044
|
-
const targetElement = carouselItemsRef.current.children[scrollTargetIndex];
|
|
1045
|
-
if (targetElement) {
|
|
1046
|
-
// Calculate the scroll position to scroll the target element into view
|
|
1047
|
-
const containerRect = carouselItemsRef.current.getBoundingClientRect();
|
|
1048
|
-
const targetRect = targetElement.getBoundingClientRect();
|
|
1049
|
-
const scrollLeft = carouselItemsRef.current.scrollLeft + (targetRect.left - containerRect.left);
|
|
1050
|
-
carouselItemsRef.current.scrollTo({
|
|
1051
|
-
left: Math.round(scrollLeft),
|
|
1052
|
-
behavior: prefersReducedMotion ? 'instant' : 'smooth'
|
|
1053
|
-
});
|
|
1228
|
+
const prefersReducedMotion = usePrefersReducedMotion() ?? false;
|
|
1229
|
+
const emblaPlugins = useMemo(()=>{
|
|
1230
|
+
const plugins = [];
|
|
1231
|
+
if (scrollGestures) {
|
|
1232
|
+
plugins.push(WheelGesturesPlugin());
|
|
1054
1233
|
}
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
});
|
|
1234
|
+
if (autoPlayDelay) {
|
|
1235
|
+
plugins.push(Autoplay({
|
|
1236
|
+
delay: autoPlayDelay,
|
|
1237
|
+
stopOnLastSnap: !loop,
|
|
1238
|
+
jump: prefersReducedMotion
|
|
1239
|
+
}));
|
|
1062
1240
|
}
|
|
1063
|
-
|
|
1064
|
-
scrollTimeoutRef.current = setTimeout(()=>{
|
|
1065
|
-
isScrollingProgrammatically.current = false;
|
|
1066
|
-
scrollTimeoutRef.current = null;
|
|
1067
|
-
if (elementWithFocusVisible && !carouselRef.current?.contains(document.activeElement)) {
|
|
1068
|
-
// Restore focus to the appropriate element after scrolling
|
|
1069
|
-
// First check if the prev or next buttons just got hidden due to reaching the start/end of the carousel
|
|
1070
|
-
// If so, move focus to the other button. This is to avoid a scroll jank that occurs if instead focus is restored to the carousel container
|
|
1071
|
-
// This jank happens because of the delays used for scrolling with these buttons (debounce, queuing etc.).
|
|
1072
|
-
switch(elementWithFocusVisible.slot){
|
|
1073
|
-
case 'prev':
|
|
1074
|
-
{
|
|
1075
|
-
// Focus was lost when the prev button turned invisible, set it to the next button
|
|
1076
|
-
const nextButton = carouselRef.current?.querySelector('[slot="next"]');
|
|
1077
|
-
nextButton?.focus();
|
|
1078
|
-
break;
|
|
1079
|
-
}
|
|
1080
|
-
case 'next':
|
|
1081
|
-
{
|
|
1082
|
-
// Focus was lost when the next button turned invisible, set it to the prev button
|
|
1083
|
-
const prevButton = carouselRef.current?.querySelector('[slot="prev"]');
|
|
1084
|
-
prevButton?.focus();
|
|
1085
|
-
break;
|
|
1086
|
-
}
|
|
1087
|
-
default:
|
|
1088
|
-
{
|
|
1089
|
-
// Focus was lost during while scrolling with left/right arrows, restore it to the carousel container
|
|
1090
|
-
carouselItemsRef.current?.focus();
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
processQueue(); // Process any queued scrolls
|
|
1096
|
-
}, 500);
|
|
1241
|
+
return plugins;
|
|
1097
1242
|
}, [
|
|
1098
|
-
|
|
1243
|
+
autoPlayDelay,
|
|
1244
|
+
scrollGestures,
|
|
1245
|
+
loop,
|
|
1099
1246
|
prefersReducedMotion
|
|
1100
1247
|
]);
|
|
1101
|
-
|
|
1248
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
1249
|
+
align,
|
|
1250
|
+
loop,
|
|
1251
|
+
startIndex: initialIndex,
|
|
1252
|
+
axis: orientation === 'horizontal' ? 'x' : 'y',
|
|
1253
|
+
inViewThreshold: 0.2
|
|
1254
|
+
}, emblaPlugins);
|
|
1255
|
+
const [slidesInView, setSlidesInView] = useState([
|
|
1256
|
+
initialIndex
|
|
1257
|
+
]);
|
|
1258
|
+
// We need some default values here. The proper initial values will be set by the embla init handler later
|
|
1259
|
+
// for the default values, assume that we can scroll next, but for prev only if looping is enabled
|
|
1260
|
+
const [canScrollNext, setCanScrollNext] = useState(true);
|
|
1261
|
+
const [canScrollPrev, setCanScrollPrev] = useState(loop);
|
|
1262
|
+
const previousSettledScrollIndex = useRef(initialIndex);
|
|
1102
1263
|
useEffect(()=>{
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1264
|
+
if (!emblaApi) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const emblaHandler = (_, type)=>{
|
|
1268
|
+
const scrollSnapIndex = emblaApi.selectedScrollSnap();
|
|
1269
|
+
switch(type){
|
|
1270
|
+
case 'select':
|
|
1271
|
+
onSelect?.(scrollSnapIndex);
|
|
1272
|
+
setCanScrollNext(emblaApi.canScrollNext());
|
|
1273
|
+
setCanScrollPrev(emblaApi.canScrollPrev());
|
|
1274
|
+
break;
|
|
1275
|
+
case 'settle':
|
|
1276
|
+
{
|
|
1277
|
+
// We only invoke the callback if the scroll index actually changed from the previous settled index
|
|
1278
|
+
// Otherwise this gets triggered if the user does the tiniest bit of scrolling in the carousel, but doesn't transition to the next slide
|
|
1279
|
+
if (scrollSnapIndex !== previousSettledScrollIndex.current) {
|
|
1280
|
+
previousSettledScrollIndex.current = scrollSnapIndex;
|
|
1281
|
+
onSettled?.(scrollSnapIndex);
|
|
1282
|
+
}
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
case 'slidesInView':
|
|
1286
|
+
{
|
|
1287
|
+
setSlidesInView(emblaApi.slidesInView());
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
case 'init':
|
|
1291
|
+
{
|
|
1292
|
+
setCanScrollNext(emblaApi.canScrollNext());
|
|
1293
|
+
setCanScrollPrev(emblaApi.canScrollPrev());
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1106
1296
|
}
|
|
1107
1297
|
};
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1298
|
+
emblaApi.on('select', emblaHandler);
|
|
1299
|
+
emblaApi.on('slidesInView', emblaHandler);
|
|
1300
|
+
emblaApi.on('settle', emblaHandler);
|
|
1301
|
+
emblaApi.on('init', emblaHandler);
|
|
1302
|
+
return ()=>{
|
|
1303
|
+
emblaApi.off('select', emblaHandler);
|
|
1304
|
+
emblaApi.off('settle', emblaHandler);
|
|
1305
|
+
emblaApi.off('slidesInView', emblaHandler);
|
|
1306
|
+
emblaApi.off('init', emblaHandler);
|
|
1307
|
+
};
|
|
1308
|
+
}, [
|
|
1309
|
+
emblaApi,
|
|
1310
|
+
onSelect,
|
|
1311
|
+
onSettled
|
|
1312
|
+
]);
|
|
1313
|
+
const handleNextPress = useCallback(()=>{
|
|
1314
|
+
if (!emblaApi) {
|
|
1112
1315
|
return;
|
|
1113
1316
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
//
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
// Check if the item is more than 50% visible within the container
|
|
1120
|
-
const visibleWidth = Math.min(rect.right, containerRect.right) - Math.max(rect.left, containerRect.left);
|
|
1121
|
-
const itemWidth = rect.width;
|
|
1122
|
-
return visibleWidth / itemWidth > 0.5;
|
|
1123
|
-
});
|
|
1124
|
-
if (newScrollTargetIndex !== -1 && newScrollTargetIndex !== scrollTargetIndex) {
|
|
1125
|
-
if (onChange) {
|
|
1126
|
-
onChange({
|
|
1127
|
-
index: newScrollTargetIndex,
|
|
1128
|
-
id: target.children[newScrollTargetIndex]?.id,
|
|
1129
|
-
prevIndex: prevIndex.current,
|
|
1130
|
-
prevId: target.children[prevIndex.current]?.id
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
// Update the index and prevIndex
|
|
1134
|
-
setScrollTargetIndex(newScrollTargetIndex);
|
|
1135
|
-
prevIndex.current = newScrollTargetIndex;
|
|
1317
|
+
emblaApi.plugins().autoplay?.stop();
|
|
1318
|
+
emblaApi.scrollNext(prefersReducedMotion);
|
|
1319
|
+
// we need to move focus if we are about to disable this button due to start/end of carousel
|
|
1320
|
+
if (!loop && !emblaApi.canScrollNext() && getCarouselButton(carouselRef, 'next')?.matches(':focus-visible')) {
|
|
1321
|
+
getCarouselButton(carouselRef, 'prev')?.focus();
|
|
1136
1322
|
}
|
|
1137
|
-
},
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1323
|
+
}, [
|
|
1324
|
+
emblaApi,
|
|
1325
|
+
prefersReducedMotion,
|
|
1326
|
+
loop
|
|
1327
|
+
]);
|
|
1328
|
+
const handlePrevPress = useCallback(()=>{
|
|
1329
|
+
if (!emblaApi) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
emblaApi.plugins().autoplay?.stop();
|
|
1333
|
+
emblaApi.scrollPrev(prefersReducedMotion);
|
|
1334
|
+
// we need to move focus if we are about to disable this button due to start/end of carousel
|
|
1335
|
+
if (!loop && !emblaApi.canScrollPrev() && getCarouselButton(carouselRef, 'prev')?.matches(':focus-visible')) {
|
|
1336
|
+
getCarouselButton(carouselRef, 'next')?.focus();
|
|
1337
|
+
}
|
|
1338
|
+
}, [
|
|
1339
|
+
emblaApi,
|
|
1340
|
+
prefersReducedMotion,
|
|
1341
|
+
loop
|
|
1342
|
+
]);
|
|
1343
|
+
const locale = _useLocale();
|
|
1344
|
+
const handleKeyDown = useCallback((e)=>{
|
|
1345
|
+
// Check if either prev or next button has focus - if so, don't override their focus management
|
|
1346
|
+
const carouselButtonHasFocus = getCarouselButton(carouselRef, 'prev')?.matches(':focus-visible') || getCarouselButton(carouselRef, 'next')?.matches(':focus-visible');
|
|
1347
|
+
if (e.key === 'ArrowRight' && !e.repeat) {
|
|
1348
|
+
handleNextPress();
|
|
1349
|
+
// Focus first focusable element in the next slide, unless a carousel button has focus
|
|
1350
|
+
if (!carouselButtonHasFocus) {
|
|
1351
|
+
focusElementInSnappedSlide(emblaApi);
|
|
1159
1352
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
return currentTargetIndex;
|
|
1353
|
+
} else if (e.key === 'ArrowLeft' && !e.repeat) {
|
|
1354
|
+
handlePrevPress();
|
|
1355
|
+
// Focus first focusable element in the previous slide, unless a carousel button has focus
|
|
1356
|
+
if (!carouselButtonHasFocus) {
|
|
1357
|
+
focusElementInSnappedSlide(emblaApi);
|
|
1166
1358
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1359
|
+
}
|
|
1360
|
+
}, [
|
|
1361
|
+
handleNextPress,
|
|
1362
|
+
handlePrevPress,
|
|
1363
|
+
emblaApi
|
|
1364
|
+
]);
|
|
1365
|
+
const hasHeroContext = !!useContext(HeroContext);
|
|
1366
|
+
const nextPrevStyles = hasHeroContext ? {
|
|
1367
|
+
color: 'white',
|
|
1368
|
+
variant: 'primary'
|
|
1369
|
+
} : {
|
|
1370
|
+
variant: 'tertiary'
|
|
1169
1371
|
};
|
|
1170
|
-
return
|
|
1372
|
+
return(// biome-ignore lint/a11y/noStaticElementInteractions: This is just to enhance keyboard navigation, this is not a replacement for proper focusable elements inside the carousel
|
|
1373
|
+
/*#__PURE__*/ jsx("div", {
|
|
1374
|
+
...rest,
|
|
1375
|
+
"data-orientation": orientation,
|
|
1171
1376
|
"data-slot": "carousel",
|
|
1172
|
-
ref: carouselRef,
|
|
1377
|
+
ref: mergeRefs(ref, carouselRef),
|
|
1378
|
+
onKeyDown: handleKeyDown,
|
|
1173
1379
|
children: /*#__PURE__*/ jsx(Provider, {
|
|
1174
1380
|
values: [
|
|
1175
1381
|
[
|
|
1176
|
-
|
|
1382
|
+
CarouselContext,
|
|
1177
1383
|
{
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
handlePrevious,
|
|
1182
|
-
handleNext
|
|
1384
|
+
slidesInView,
|
|
1385
|
+
'~emblaRef': emblaRef,
|
|
1386
|
+
orientation
|
|
1183
1387
|
}
|
|
1184
1388
|
],
|
|
1185
1389
|
[
|
|
@@ -1188,115 +1392,119 @@ const Carousel = ({ className, children, onChange, ...rest })=>{
|
|
|
1188
1392
|
slots: {
|
|
1189
1393
|
[DEFAULT_SLOT]: {},
|
|
1190
1394
|
prev: {
|
|
1191
|
-
'aria-label': previous[locale],
|
|
1192
|
-
|
|
1395
|
+
'aria-label': translations$1.previous[locale],
|
|
1396
|
+
isDisabled: !canScrollPrev,
|
|
1397
|
+
onPress: handlePrevPress,
|
|
1398
|
+
...nextPrevStyles
|
|
1193
1399
|
},
|
|
1194
1400
|
next: {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
onPress:
|
|
1401
|
+
'aria-label': translations$1.next[locale],
|
|
1402
|
+
isDisabled: !canScrollNext,
|
|
1403
|
+
onPress: handleNextPress,
|
|
1404
|
+
...nextPrevStyles
|
|
1198
1405
|
}
|
|
1199
1406
|
}
|
|
1200
1407
|
}
|
|
1201
1408
|
]
|
|
1202
1409
|
],
|
|
1203
|
-
children:
|
|
1204
|
-
...rest,
|
|
1205
|
-
className: cx(className, 'relative rounded-3xl', // If any <CarouselItems/> (the scroll-snap container) or <VideoLoop/> component is focused, apply custom focus styles around the carousel, this makes ensures that the focus outline is visible around the carousel in all cases
|
|
1206
|
-
'[&:has([data-slot="carousel-items"]:focus-visible,[data-slot="video-loop-button"]:focus-visible)]:outline-focus', '[&:has([data-slot="carousel-items"]:focus-visible,[data-slot="video-loop-button"]:focus-visible)]:outline-focus-offset', // Unset the default focus outline for potential video loop buttons, as it interferes with the custom focus styles for the carousel
|
|
1207
|
-
'**:data-[slot="video-loop-button"]:focus-visible:outline-none'),
|
|
1208
|
-
children: [
|
|
1209
|
-
children,
|
|
1210
|
-
/*#__PURE__*/ jsxs(_CarouselControls, {
|
|
1211
|
-
children: [
|
|
1212
|
-
/*#__PURE__*/ jsx(Button, {
|
|
1213
|
-
isIconOnly: true,
|
|
1214
|
-
slot: "prev",
|
|
1215
|
-
variant: "primary",
|
|
1216
|
-
color: "white",
|
|
1217
|
-
className: cx('group/carousel-previous', hasReachedScrollStart && 'invisible'),
|
|
1218
|
-
children: /*#__PURE__*/ jsx(ChevronLeft, {
|
|
1219
|
-
className: "group-hover/carousel-previous:motion-safe:-translate-x-1 transition-transform"
|
|
1220
|
-
})
|
|
1221
|
-
}),
|
|
1222
|
-
/*#__PURE__*/ jsx(Button, {
|
|
1223
|
-
isIconOnly: true,
|
|
1224
|
-
slot: "next",
|
|
1225
|
-
variant: "primary",
|
|
1226
|
-
color: "white",
|
|
1227
|
-
className: cx('group/carousel-next', hasReachedScrollEnd && 'invisible'),
|
|
1228
|
-
children: /*#__PURE__*/ jsx(ChevronRight, {
|
|
1229
|
-
className: "transition-transform group-hover/carousel-next:motion-safe:translate-x-1"
|
|
1230
|
-
})
|
|
1231
|
-
})
|
|
1232
|
-
]
|
|
1233
|
-
})
|
|
1234
|
-
]
|
|
1235
|
-
})
|
|
1410
|
+
children: children
|
|
1236
1411
|
})
|
|
1412
|
+
}));
|
|
1413
|
+
};
|
|
1414
|
+
const CarouselContext = /*#__PURE__*/ createContext({
|
|
1415
|
+
'~emblaRef': null,
|
|
1416
|
+
orientation: 'horizontal',
|
|
1417
|
+
slidesInView: []
|
|
1418
|
+
});
|
|
1419
|
+
const CarouselItemsContainer = ({ children, className, ...rest })=>{
|
|
1420
|
+
const { '~emblaRef': emblaRef } = useContext(CarouselContext);
|
|
1421
|
+
return /*#__PURE__*/ jsx("div", {
|
|
1422
|
+
className: cx(className, 'overflow-hidden'),
|
|
1423
|
+
ref: emblaRef,
|
|
1424
|
+
"data-slot": "carousel-items-container",
|
|
1425
|
+
...rest,
|
|
1426
|
+
children: children
|
|
1427
|
+
});
|
|
1428
|
+
};
|
|
1429
|
+
const CarouselItems = ({ className, children })=>{
|
|
1430
|
+
const { orientation } = useContext(CarouselContext);
|
|
1431
|
+
return /*#__PURE__*/ jsx("div", {
|
|
1432
|
+
className: cx(className, 'flex', orientation === 'vertical' && 'flex-col'),
|
|
1433
|
+
"data-slot": "carousel-items",
|
|
1434
|
+
children: children
|
|
1237
1435
|
});
|
|
1238
1436
|
};
|
|
1239
1437
|
/**
|
|
1240
1438
|
* This is internal for now, but we will expose it in the future when we support more flexible positioning of prev/next and other actions.
|
|
1241
1439
|
* It is used to render the prev/next buttons in the carousel for now.
|
|
1242
|
-
*/ const
|
|
1243
|
-
className: cx(className, '
|
|
1244
|
-
'items-end *:h-fit'),
|
|
1440
|
+
*/ const CarouselControls = ({ children, className, ...rest })=>/*#__PURE__*/ jsx("div", {
|
|
1441
|
+
className: cx(className, 'flex justify-end gap-x-2'),
|
|
1245
1442
|
"data-slot": "carousel-controls",
|
|
1443
|
+
...rest,
|
|
1444
|
+
// All items of the carousel are accessible to the screen reader at all times, so these controls will only confuse screen reader users
|
|
1445
|
+
"aria-hidden": "true",
|
|
1246
1446
|
children: children
|
|
1247
1447
|
});
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
activeIndex: 0
|
|
1448
|
+
const carouselButtonVariants = cva({
|
|
1449
|
+
base: 'group data-disabled:invisible'
|
|
1251
1450
|
});
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1451
|
+
const carouselButtonIconSlotVariants = cva({
|
|
1452
|
+
base: 'transition-transform',
|
|
1453
|
+
variants: {
|
|
1454
|
+
slot: {
|
|
1455
|
+
next: null,
|
|
1456
|
+
prev: null
|
|
1457
|
+
},
|
|
1458
|
+
orientation: {
|
|
1459
|
+
horizontal: null,
|
|
1460
|
+
vertical: null
|
|
1261
1461
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1462
|
+
},
|
|
1463
|
+
compoundVariants: [
|
|
1464
|
+
// horizontal controls
|
|
1465
|
+
{
|
|
1466
|
+
slot: 'next',
|
|
1467
|
+
orientation: 'horizontal',
|
|
1468
|
+
className: 'group-hover:motion-safe:translate-x-1'
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
slot: 'prev',
|
|
1472
|
+
orientation: 'horizontal',
|
|
1473
|
+
className: 'group-hover:motion-safe:-translate-x-1 rotate-180'
|
|
1474
|
+
},
|
|
1475
|
+
// vertical controls
|
|
1476
|
+
{
|
|
1477
|
+
slot: 'next',
|
|
1478
|
+
orientation: 'vertical',
|
|
1479
|
+
className: 'rotate-90 group-hover:motion-safe:translate-y-1'
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
slot: 'prev',
|
|
1483
|
+
orientation: 'vertical',
|
|
1484
|
+
className: 'group-hover:motion-safe:-translate-y-1 -rotate-90'
|
|
1271
1485
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
children: Children.map(children, (child, index)=>{
|
|
1289
|
-
if (/*#__PURE__*/ isValidElement(child)) {
|
|
1290
|
-
return /*#__PURE__*/ cloneElement(child, {
|
|
1291
|
-
inert: activeIndex === index ? undefined : true
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1486
|
+
]
|
|
1487
|
+
});
|
|
1488
|
+
const CarouselButton = ({ className, isIconOnly = true, slot, ...rest })=>{
|
|
1489
|
+
const { orientation } = useContext(CarouselContext);
|
|
1490
|
+
return /*#__PURE__*/ jsx(Button, {
|
|
1491
|
+
className: carouselButtonVariants({
|
|
1492
|
+
className
|
|
1493
|
+
}),
|
|
1494
|
+
isIconOnly: isIconOnly,
|
|
1495
|
+
slot: slot,
|
|
1496
|
+
...rest,
|
|
1497
|
+
children: /*#__PURE__*/ jsx(ChevronRight, {
|
|
1498
|
+
className: carouselButtonIconSlotVariants({
|
|
1499
|
+
orientation,
|
|
1500
|
+
slot
|
|
1501
|
+
})
|
|
1294
1502
|
})
|
|
1295
|
-
})
|
|
1503
|
+
});
|
|
1296
1504
|
};
|
|
1297
1505
|
const CarouselItem = ({ className, children, ...rest })=>{
|
|
1298
1506
|
return /*#__PURE__*/ jsx("div", {
|
|
1299
|
-
className: cx(className, '
|
|
1507
|
+
className: cx(className, 'min-w-0 shrink-0 grow-0'),
|
|
1300
1508
|
"data-slot": "carousel-item",
|
|
1301
1509
|
...rest,
|
|
1302
1510
|
children: /*#__PURE__*/ jsx(Provider, {
|
|
@@ -1314,49 +1522,6 @@ const CarouselItem = ({ className, children, ...rest })=>{
|
|
|
1314
1522
|
});
|
|
1315
1523
|
};
|
|
1316
1524
|
|
|
1317
|
-
const formField = cx('group flex flex-col gap-2');
|
|
1318
|
-
const formFieldError = cx('w-fit bg-red-light px-2 py-1 text-red text-sm leading-6', 'group-data-[slot=file-upload]:rounded-lg');
|
|
1319
|
-
const input = cva({
|
|
1320
|
-
base: [
|
|
1321
|
-
// All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
|
|
1322
|
-
'bg-white',
|
|
1323
|
-
// Use box-content to enable auto width based on number of characters (size)
|
|
1324
|
-
// Setting min-height to prevent the input from collapsing in Safari
|
|
1325
|
-
// Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
|
|
1326
|
-
'box-content min-h-6 py-2.5',
|
|
1327
|
-
'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
|
|
1328
|
-
// invalid styles
|
|
1329
|
-
'group-data-invalid:ring-focus group-data-invalid:ring-red',
|
|
1330
|
-
// Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
|
|
1331
|
-
'appearance-none'
|
|
1332
|
-
],
|
|
1333
|
-
variants: {
|
|
1334
|
-
// Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
|
|
1335
|
-
focusModifier: {
|
|
1336
|
-
focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
|
|
1337
|
-
visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
|
|
1338
|
-
},
|
|
1339
|
-
isGrouped: {
|
|
1340
|
-
false: 'px-3',
|
|
1341
|
-
true: '!ring-0 flex-1'
|
|
1342
|
-
}
|
|
1343
|
-
},
|
|
1344
|
-
defaultVariants: {
|
|
1345
|
-
focusModifier: 'focus',
|
|
1346
|
-
isGrouped: false
|
|
1347
|
-
}
|
|
1348
|
-
});
|
|
1349
|
-
const inputGroup = cx([
|
|
1350
|
-
'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
|
|
1351
|
-
'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
|
|
1352
|
-
]);
|
|
1353
|
-
const dropdown = {
|
|
1354
|
-
popover: cx('data-entering:fade-in data-exiting:fade-out min-w-(--trigger-width) overflow-y-auto rounded-md border border-black bg-white shadow-sm data-entering:animate-in data-exiting:animate-out'),
|
|
1355
|
-
// overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
|
|
1356
|
-
listbox: cx('max-h-100 overflow-x-hidden text-sm outline-hidden'),
|
|
1357
|
-
chevronIcon: cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
1525
|
function ErrorMessage(props) {
|
|
1361
1526
|
const { children, className, ...restProps } = props;
|
|
1362
1527
|
return /*#__PURE__*/ jsx(Text, {
|
|
@@ -1612,7 +1777,7 @@ function Combobox(props) {
|
|
|
1612
1777
|
/**
|
|
1613
1778
|
* A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
|
|
1614
1779
|
*/ const FileTrigger = (props)=>{
|
|
1615
|
-
const { onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, children, acceptDirectory, ref, isInvalid, isRequired, name,
|
|
1780
|
+
const { onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, children, acceptDirectory, ref, isInvalid, isRequired, name, ...rest } = props;
|
|
1616
1781
|
const inputRef = useObjectRef(ref);
|
|
1617
1782
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
1618
1783
|
children: [
|
|
@@ -1661,6 +1826,46 @@ const translations = {
|
|
|
1661
1826
|
en: 'Remove'
|
|
1662
1827
|
}
|
|
1663
1828
|
};
|
|
1829
|
+
/**
|
|
1830
|
+
* Extracts a simple file extension from a file name or converts MIME type to extension
|
|
1831
|
+
* @param file The file object
|
|
1832
|
+
* @returns A simple file extension (e.g., "pdf", "jpg", "svg")
|
|
1833
|
+
*/ function getFileExtension(file) {
|
|
1834
|
+
const match = file.name.match(/\.([^.]+)$/);
|
|
1835
|
+
if (match) {
|
|
1836
|
+
return match[1].toUpperCase();
|
|
1837
|
+
}
|
|
1838
|
+
const mimeType = file.type;
|
|
1839
|
+
if (!mimeType) {
|
|
1840
|
+
return '';
|
|
1841
|
+
}
|
|
1842
|
+
const parts = mimeType.split('/');
|
|
1843
|
+
if (parts.length === 2) {
|
|
1844
|
+
const subtype = parts[1].split('+')[0]; // Handle cases like "svg+xml"
|
|
1845
|
+
return subtype.toUpperCase();
|
|
1846
|
+
}
|
|
1847
|
+
return '';
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Formats a file size in bytes to a human-readable string (B, KB, MB, GB, etc.)
|
|
1851
|
+
* @param bytes The file size in bytes
|
|
1852
|
+
* @returns A formatted string with the appropriate unit
|
|
1853
|
+
*/ function formatFileSize(bytes) {
|
|
1854
|
+
if (bytes === 0) {
|
|
1855
|
+
return '0 B';
|
|
1856
|
+
}
|
|
1857
|
+
const units = [
|
|
1858
|
+
'B',
|
|
1859
|
+
'KB',
|
|
1860
|
+
'MB',
|
|
1861
|
+
'GB',
|
|
1862
|
+
'TB'
|
|
1863
|
+
];
|
|
1864
|
+
const base = 1024;
|
|
1865
|
+
const unitIndex = Math.floor(Math.log(bytes) / Math.log(base));
|
|
1866
|
+
const size = bytes / base ** unitIndex;
|
|
1867
|
+
return `${size.toFixed(2).replace(/\.?0+$/, '')} ${units[unitIndex]}`;
|
|
1868
|
+
}
|
|
1664
1869
|
/**
|
|
1665
1870
|
* Converts an array of files to a DataTransfer object which can be used as a FileList.
|
|
1666
1871
|
* This is necessary for setting the files on a native file input.
|
|
@@ -1688,7 +1893,7 @@ const translations = {
|
|
|
1688
1893
|
const extension = fileName.match(/(\.[^.]+)$/)?.[0] || '';
|
|
1689
1894
|
if (!fileNameCounts[baseName]) {
|
|
1690
1895
|
// Extract any number from the file name (if any, otherwise default to 0)
|
|
1691
|
-
const baseNameCount = Number.parseInt(fileName.match(/\((\d+)\)/)?.[1] ?? '0');
|
|
1896
|
+
const baseNameCount = Number.parseInt(fileName.match(/\((\d+)\)/)?.[1] ?? '0', 10);
|
|
1692
1897
|
fileNameCounts[baseName] = baseNameCount;
|
|
1693
1898
|
}
|
|
1694
1899
|
fileNameCounts[baseName]++;
|
|
@@ -1837,11 +2042,34 @@ const FileUpload = ({ children, files: _files, onChange, validate, isInvalid: _i
|
|
|
1837
2042
|
return /*#__PURE__*/ jsxs("li", {
|
|
1838
2043
|
children: [
|
|
1839
2044
|
/*#__PURE__*/ jsxs("div", {
|
|
1840
|
-
className: cx('flex items-center justify-between gap-
|
|
2045
|
+
className: cx('flex items-center justify-between gap-3 rounded-lg border p-1.5', hasError ? 'border-red bg-red-light' : 'border-gray'),
|
|
1841
2046
|
children: [
|
|
1842
|
-
|
|
2047
|
+
/*#__PURE__*/ jsxs("div", {
|
|
2048
|
+
className: "flex items-center gap-3",
|
|
2049
|
+
children: [
|
|
2050
|
+
/*#__PURE__*/ jsx("div", {
|
|
2051
|
+
className: "footnote rounded-md border border-gray-light bg-gray-lightest px-2.5 py-2",
|
|
2052
|
+
children: getFileExtension(file)
|
|
2053
|
+
}),
|
|
2054
|
+
/*#__PURE__*/ jsxs("div", {
|
|
2055
|
+
className: "flex flex-col",
|
|
2056
|
+
children: [
|
|
2057
|
+
/*#__PURE__*/ jsx("span", {
|
|
2058
|
+
className: "description truncate font-medium",
|
|
2059
|
+
children: fileName
|
|
2060
|
+
}),
|
|
2061
|
+
/*#__PURE__*/ jsx("span", {
|
|
2062
|
+
className: "footnote text-gray-dark",
|
|
2063
|
+
children: formatFileSize(file.size)
|
|
2064
|
+
})
|
|
2065
|
+
]
|
|
2066
|
+
})
|
|
2067
|
+
]
|
|
2068
|
+
}),
|
|
1843
2069
|
/*#__PURE__*/ jsx("button", {
|
|
1844
|
-
|
|
2070
|
+
type: "button",
|
|
2071
|
+
"aria-label": translations.remove[locale],
|
|
2072
|
+
className: cx('-m-2 grid h-11 w-11 shrink-0 cursor-pointer place-items-center rounded-xl', // Focus styles
|
|
1845
2073
|
'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
|
|
1846
2074
|
onClick: ()=>{
|
|
1847
2075
|
// For controlled component
|
|
@@ -1852,8 +2080,6 @@ const FileUpload = ({ children, files: _files, onChange, validate, isInvalid: _i
|
|
|
1852
2080
|
// (without this, the focus will be set to the top of the page for screen readers)
|
|
1853
2081
|
buttonRef.current?.focus();
|
|
1854
2082
|
},
|
|
1855
|
-
"aria-label": translations.remove[locale],
|
|
1856
|
-
type: "button",
|
|
1857
2083
|
children: /*#__PURE__*/ jsx(Trash, {})
|
|
1858
2084
|
})
|
|
1859
2085
|
]
|
|
@@ -1885,117 +2111,6 @@ function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
|
|
|
1885
2111
|
});
|
|
1886
2112
|
}
|
|
1887
2113
|
|
|
1888
|
-
const roundedMediaCorners = '*:data-[slot="media"]:*:rounded-3xl';
|
|
1889
|
-
// Common variant for "standard" and "full-bleed" Hero variants
|
|
1890
|
-
const oneColumnLayout = [
|
|
1891
|
-
// Vertical spacing in the <Content>
|
|
1892
|
-
'lg:*:data-[slot="content"]:gap-y-4',
|
|
1893
|
-
// Main text content takes up 9 columns on medium screens and above
|
|
1894
|
-
'lg:*:data-[slot="content"]:col-span-9',
|
|
1895
|
-
// Make sure other elements than <Content> and <Media> (i.e CTA) does not span the full width on small screens
|
|
1896
|
-
'*:not-data-[slot="content"]:not-data-[slot="media"]:w-fit',
|
|
1897
|
-
// Other elements than <Content> and <Media> (e.g. CTA, SVG logo or Badge) take up 3 columns on medium screens and above, and are right aligned
|
|
1898
|
-
'lg:*:not-data-[slot="content"]:not-data-[slot="media"]:not-data-[slot="carousel"]:col-span-3 lg:*:not-data-[slot="content"]:not-data-[slot="media"]:justify-self-end',
|
|
1899
|
-
// <Media> and <Carousel> content takes up the full width on medium screens and above
|
|
1900
|
-
'lg:*:data-[slot="media"]:col-span-full *:data-[slot="media"]:*:w-full',
|
|
1901
|
-
'lg:*:data-[slot="carousel"]:col-span-full *:data-[slot="carousel"]:*:w-full',
|
|
1902
|
-
// Aligns <Content> and any element beside it (e.g. <Media>, <Badge>, <CTA> etc.) to the bottom of the <Content> container
|
|
1903
|
-
'lg:items-end'
|
|
1904
|
-
];
|
|
1905
|
-
const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot="media"]:*:aspect-[1/1] sm:*:data-[slot="media"]:*:aspect-4/3 md:*:data-[slot="media"]:*:aspect-3/2';
|
|
1906
|
-
const variants = cva({
|
|
1907
|
-
base: [
|
|
1908
|
-
'container px-0',
|
|
1909
|
-
// Grid variant to position the Hero's content
|
|
1910
|
-
'grid lg:grid-cols-12 lg:gap-x-12 xl:gap-x-16',
|
|
1911
|
-
'gap-y-10 lg:gap-y-12',
|
|
1912
|
-
// Enable vertical gap within <Content>
|
|
1913
|
-
'*:data-[slot="content"]:grid',
|
|
1914
|
-
// Vertical spacing in the <Content>
|
|
1915
|
-
'*:data-[slot="content"]:gap-y-3',
|
|
1916
|
-
// Make sure <Media> content fills any available vertical and horizontal space
|
|
1917
|
-
'*:data-[slot="media"]:*:object-cover'
|
|
1918
|
-
],
|
|
1919
|
-
variants: {
|
|
1920
|
-
/**
|
|
1921
|
-
* Defines the variant of the Hero
|
|
1922
|
-
* @default standard
|
|
1923
|
-
* */ variant: {
|
|
1924
|
-
standard: [
|
|
1925
|
-
roundedMediaCorners,
|
|
1926
|
-
oneColumnLayout,
|
|
1927
|
-
nonFullBleedAspectRatiosForSmallScreens,
|
|
1928
|
-
'lg:*:data-[slot="media"]:*:aspect-2/1'
|
|
1929
|
-
],
|
|
1930
|
-
'full-bleed': [
|
|
1931
|
-
oneColumnLayout,
|
|
1932
|
-
// Position the media and carousel content to fill the entire viewport width
|
|
1933
|
-
'*:data-[slot="media"]:*:absolute *:data-[slot="media"]:*:left-0',
|
|
1934
|
-
// Special case for Carousel, where the Media is nested inside a CarouselItem
|
|
1935
|
-
'*:data-[slot="carousel"]:**:data-[slot="media"]:w-full *:data-[slot="carousel"]:*:absolute *:data-[slot="carousel"]:*:left-0',
|
|
1936
|
-
// Match the heights of the <Media> or <Carousel> wrapper for the Media content (e.g. image, VideoLoop, video etc.)
|
|
1937
|
-
// This is necessary due to the absolute positioning of the media and carousel containers in this variant
|
|
1938
|
-
// biome-ignore lint/nursery/useSortedClasses: biome is unable to sort the custom classes for 3xl and 4xl breakpoints
|
|
1939
|
-
'**:data-[slot="media"]:h-80 sm:**:data-[slot="media"]:h-[25rem] md:**:data-[slot="media"]:h-[30rem] lg:**:data-[slot="media"]:h-[35rem] xl:**:data-[slot="media"]:h-[40rem] 2xl:**:data-[slot="media"]:h-[42rem] 3xl:**:data-[slot="media"]:h-[48rem] 4xl:**:data-[slot="media"]:h-[53rem]',
|
|
1940
|
-
// biome-ignore lint/nursery/useSortedClasses: biome is unable to sort the custom classes for 3xl and 4xl breakpoints
|
|
1941
|
-
'**:data-[slot="media"]:*:h-80 sm:**:data-[slot="media"]:*:h-[25rem] md:**:data-[slot="media"]:*:h-[30rem] lg:**:data-[slot="media"]:*:h-[35rem] xl:**:data-[slot="media"]:*:h-[40rem] 2xl:**:data-[slot="media"]:*:h-[42rem] 3xl:**:data-[slot="media"]:*:h-[48rem] 4xl:**:data-[slot="media"]:*:h-[53rem]',
|
|
1942
|
-
// biome-ignore lint/nursery/useSortedClasses: biome is unable to sort the custom classes for 3xl and 4xl breakpoints
|
|
1943
|
-
'*:data-[slot="carousel"]:h-80 sm:*:data-[slot="carousel"]:h-[25rem] md:*:data-[slot="carousel"]:h-[30rem] lg:*:data-[slot="carousel"]:h-[35rem] xl:*:data-[slot="carousel"]:h-[40rem] 2xl:*:data-[slot="carousel"]:h-[42rem] 3xl:*:data-[slot="carousel"]:h-[48rem] 4xl:*:data-[slot="carousel"]:h-[53rem]',
|
|
1944
|
-
// Override aspect ratio of the media and carousel-item slots (since we can not use aspect for full-bleed layout)
|
|
1945
|
-
'**:data-[slot="carousel-item"]:data-[slot="media"]:*:aspect-none',
|
|
1946
|
-
'**:data-[slot="carousel-controls"]:container **:data-[slot="carousel-controls"]:right-0 **:data-[slot="carousel-controls"]:bottom-4 **:data-[slot="carousel-controls"]:left-0 **:data-[slot="carousel-controls"]:justify-end',
|
|
1947
|
-
// Override rounded corners of Carousel slots
|
|
1948
|
-
'*:data-[slot="carousel"]:*:rounded-none'
|
|
1949
|
-
],
|
|
1950
|
-
'two-column': [
|
|
1951
|
-
'lg:items-center lg:*:col-span-6',
|
|
1952
|
-
// Vertical spacing in the <Content>
|
|
1953
|
-
'lg:*:data-[slot="content"]:gap-y-7',
|
|
1954
|
-
roundedMediaCorners,
|
|
1955
|
-
nonFullBleedAspectRatiosForSmallScreens,
|
|
1956
|
-
// Set media aspect ratio to 1:1 (square)
|
|
1957
|
-
'lg:*:data-[slot="media"]:*:aspect-[1/1]'
|
|
1958
|
-
]
|
|
1959
|
-
}
|
|
1960
|
-
},
|
|
1961
|
-
defaultVariants: {
|
|
1962
|
-
variant: 'standard'
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
1965
|
-
const Hero = ({ variant, className, children, ...rest })=>{
|
|
1966
|
-
const variantsClassName = variants({
|
|
1967
|
-
variant,
|
|
1968
|
-
className
|
|
1969
|
-
});
|
|
1970
|
-
return /*#__PURE__*/ jsx(Provider, {
|
|
1971
|
-
values: [
|
|
1972
|
-
[
|
|
1973
|
-
HeadingContext,
|
|
1974
|
-
{
|
|
1975
|
-
// Sets the default heading size for the Hero based on the variant
|
|
1976
|
-
size: variant === 'two-column' ? 'xl' : 'l',
|
|
1977
|
-
className: // word-break:break-word to allow long words to break (this is necessary to make hyphens work in grid containers in Safari)
|
|
1978
|
-
'hyphens-auto text-pretty [word-break:break-word]'
|
|
1979
|
-
}
|
|
1980
|
-
],
|
|
1981
|
-
[
|
|
1982
|
-
GroupContext,
|
|
1983
|
-
{
|
|
1984
|
-
// Prevents the group from being announced as a group by screen readers
|
|
1985
|
-
// The Group component is used to group the Hero's CTA buttons together visually, and has no semantic meaning
|
|
1986
|
-
role: 'presentation',
|
|
1987
|
-
className: 'flex flex-wrap gap-3 *:w-fit'
|
|
1988
|
-
}
|
|
1989
|
-
]
|
|
1990
|
-
],
|
|
1991
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
1992
|
-
className: cx(variantsClassName, className),
|
|
1993
|
-
...rest,
|
|
1994
|
-
children: children
|
|
1995
|
-
})
|
|
1996
|
-
});
|
|
1997
|
-
};
|
|
1998
|
-
|
|
1999
2114
|
// Sets the correct icons for each link in the link list
|
|
2000
2115
|
const _LinkProvider = ({ children })=>/*#__PURE__*/ jsx(Provider, {
|
|
2001
2116
|
values: [
|
|
@@ -2110,30 +2225,28 @@ const Modal = ({ isDismissable = true, isOpen, onOpenChange, defaultOpen, classN
|
|
|
2110
2225
|
};
|
|
2111
2226
|
const Dialog = ({ className, children, ...restProps })=>/*#__PURE__*/ jsx(Dialog$1, {
|
|
2112
2227
|
...restProps,
|
|
2113
|
-
className: cx('relative grid gap-y-5 outline-none', // Footer
|
|
2228
|
+
className: cx(className, 'relative grid gap-y-5 outline-none', // Footer
|
|
2114
2229
|
'[&_[data-slot="footer"]]:flex [&_[data-slot="footer"]]:gap-x-2'),
|
|
2115
|
-
children: ({ close })=>/*#__PURE__*/ jsx(
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
className: 'w-fit'
|
|
2130
|
-
}
|
|
2230
|
+
children: ({ close })=>/*#__PURE__*/ jsx(Provider, {
|
|
2231
|
+
values: [
|
|
2232
|
+
[
|
|
2233
|
+
ButtonContext$1,
|
|
2234
|
+
{
|
|
2235
|
+
// This is necessary to support multiple close buttons
|
|
2236
|
+
slots: {
|
|
2237
|
+
// We need to define default slot in order to also support non-slotted buttons (i.e. buttons without slot prop)
|
|
2238
|
+
[DEFAULT_SLOT]: {
|
|
2239
|
+
className: 'w-fit'
|
|
2240
|
+
},
|
|
2241
|
+
close: {
|
|
2242
|
+
onPress: close,
|
|
2243
|
+
className: 'w-fit'
|
|
2131
2244
|
}
|
|
2132
2245
|
}
|
|
2133
|
-
|
|
2134
|
-
]
|
|
2135
|
-
|
|
2136
|
-
|
|
2246
|
+
}
|
|
2247
|
+
]
|
|
2248
|
+
],
|
|
2249
|
+
children: children
|
|
2137
2250
|
})
|
|
2138
2251
|
});
|
|
2139
2252
|
|
|
@@ -2701,7 +2814,7 @@ const tabsVariants = cva({
|
|
|
2701
2814
|
const { className, children, ...restProps } = props;
|
|
2702
2815
|
return /*#__PURE__*/ jsx(Tab$1, {
|
|
2703
2816
|
...restProps,
|
|
2704
|
-
className: cx(className, 'data-focus-visible:-outline-offset-10 data-focus-visible:outline-2 data-focus-visible:outline-black', 'cursor-pointer border-transparent px-4 py-2 font-light
|
|
2817
|
+
className: cx(className, 'data-focus-visible:-outline-offset-10 data-focus-visible:outline-2 data-focus-visible:outline-black', 'description cursor-pointer border-transparent px-4 py-2 font-light', // Transition
|
|
2705
2818
|
'transition-colors duration-150 ease-out', // TODO: Should disabled tabs just be hidden?
|
|
2706
2819
|
'data-disabled:cursor-not-allowed data-disabled:opacity-50', // Selection
|
|
2707
2820
|
'data-selected:font-medium data-selected:text-blue-dark', // Hover with layout shift prevention using pseudo-element
|
|
@@ -2962,4 +3075,4 @@ const VideoLoop = ({ src, format, alt, className })=>{
|
|
|
2962
3075
|
});
|
|
2963
3076
|
};
|
|
2964
3077
|
|
|
2965
|
-
export { Accordion, AccordionItem, Alertbox, Avatar, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, ButtonContext, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, Disclosure, DisclosureButton, DisclosurePanel, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, LinkList, LinkListContainer, LinkListItem, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, Tag, TagGroup, TagList, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, FileUpload as UNSAFE_FileUpload, Hero as UNSAFE_Hero, Link as UNSAFE_Link, Modal as UNSAFE_Modal, ProgressBar as UNSAFE_ProgressBar, ProgressBarValueText as UNSAFE_ProgressBarValueText, Tab as UNSAFE_Tab, TabList as UNSAFE_TabList, TabPanel as UNSAFE_TabPanel, Table as UNSAFE_Table, TableBody as UNSAFE_TableBody, TableCell as UNSAFE_TableCell, TableColumn as UNSAFE_TableColumn, TableColumnResizer as UNSAFE_TableColumnResizer, TableContainer as UNSAFE_TableContainer, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, Tabs as UNSAFE_Tabs, VideoLoop, _LinkContext, _useLocale as useLocale };
|
|
3078
|
+
export { Accordion, AccordionItem, Alertbox, Avatar, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, ButtonContext, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, Disclosure, DisclosureButton, DisclosurePanel, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, LinkList, LinkListContainer, LinkListItem, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, Tag, TagGroup, TagList, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselButton as UNSAFE_CarouselButton, CarouselContext as UNSAFE_CarouselContext, CarouselControls as UNSAFE_CarouselControls, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, CarouselItemsContainer as UNSAFE_CarouselItemsContainer, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, FileUpload as UNSAFE_FileUpload, Hero as UNSAFE_Hero, HeroContext as UNSAFE_HeroContext, Link as UNSAFE_Link, Modal as UNSAFE_Modal, ProgressBar as UNSAFE_ProgressBar, ProgressBarValueText as UNSAFE_ProgressBarValueText, Tab as UNSAFE_Tab, TabList as UNSAFE_TabList, TabPanel as UNSAFE_TabPanel, Table as UNSAFE_Table, TableBody as UNSAFE_TableBody, TableCell as UNSAFE_TableCell, TableColumn as UNSAFE_TableColumn, TableColumnResizer as UNSAFE_TableColumnResizer, TableContainer as UNSAFE_TableContainer, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, Tabs as UNSAFE_Tabs, VideoLoop, _LinkContext, _useLocale as useLocale };
|