@obosbbl/grunnmuren-react 3.0.15 → 3.1.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/index.mjs CHANGED
@@ -1,37 +1,18 @@
1
1
  'use client';
2
- import { I18nProvider, RouterProvider, useLocale, useContextProps, Provider, Link, Button as Button$1, Text, CheckboxContext, Checkbox as Checkbox$1, FieldError, Label as Label$1, CheckboxGroup as CheckboxGroup$1, ListBoxItem as ListBoxItem$1, ListBoxSection as ListBoxSection$1, Header, ListBox as ListBox$1, ComboBox, Group, Input, Popover, RadioGroup as RadioGroup$1, Radio as Radio$1, Select as Select$1, SelectValue, TextField as TextField$1, TextArea as TextArea$1, NumberField as NumberField$1, Breadcrumbs as Breadcrumbs$1, Breadcrumb as Breadcrumb$1, ButtonContext as ButtonContext$1, DisclosureContext, DisclosureGroupStateContext, DEFAULT_SLOT, useSlottedContext, FormContext, FieldErrorContext, LabelContext, InputContext, DialogTrigger as DialogTrigger$1, Modal as Modal$1, Dialog as Dialog$1, ModalOverlay as ModalOverlay$1, TagGroup as TagGroup$1, TagList as TagList$1, Tag as Tag$1, GroupContext, Tabs as Tabs$1, TabListStateContext, TabList as TabList$1, Tab as Tab$1, TabPanel as TabPanel$1, Table as Table$1, TableHeader as TableHeader$1, Column, TableBody as TableBody$1, Row, Cell } from 'react-aria-components';
2
+ import { useContextProps, DisclosureContext, DisclosureGroupStateContext, Provider, ButtonContext as ButtonContext$1, DEFAULT_SLOT, Button as Button$1, DisclosureGroup, useLocale, Link, 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, GroupContext, Dialog as Dialog$1, DialogTrigger as DialogTrigger$1, Modal as Modal$1, ModalOverlay as ModalOverlay$1, NumberField as NumberField$1, Radio as Radio$1, RadioGroup as RadioGroup$1, Select as Select$1, SelectValue, Table as Table$1, TableBody as TableBody$1, Cell, Column, 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 { Form, Group, DisclosureGroup as UNSAFE_DisclosureGroup } from 'react-aria-components';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { ChevronDown, LoadingSpinner, Check, Close, InfoCircle, CheckCircle, Warning, Error, ChevronRight, ChevronLeft, PlayerPause, PlayerPlay, Trash, User, ArrowRight, Download, LinkExternal } from '@obosbbl/grunnmuren-icons-react';
6
- import { useLayoutEffect, filterDOMProps, mergeRefs, mergeProps, useObjectRef, useFormReset, useUpdateEffect } from '@react-aria/utils';
7
5
  import { cva, cx, compose } from 'cva';
8
- import { createContext, Children, useId, useState, useRef, useEffect, useContext, useCallback } from 'react';
9
- import { useProgressBar, useDateFormatter, useFocusRing, useDisclosure, useField } from 'react-aria';
6
+ import { createContext, useContext, useId, useRef, Children, useState, useEffect, useCallback } from 'react';
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, useUpdateEffect, useObjectRef, useFormReset } from '@react-aria/utils';
9
+ import { useFocusRing, useDisclosure, useProgressBar, useDateFormatter, useField } from 'react-aria';
10
10
  import { useDisclosureState } from 'react-stately';
11
+ import { useDebouncedCallback } from 'use-debounce';
11
12
  import { useFormValidation } from '@react-aria/form';
12
13
  import { useFormValidationState } from '@react-stately/form';
13
14
  import { useControlledState } from '@react-stately/utils';
14
15
  import { PressResponder } from '@react-aria/interactions';
15
- import { useDebouncedCallback } from 'use-debounce';
16
-
17
- function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
18
- return /*#__PURE__*/ jsx(I18nProvider, {
19
- locale: locale,
20
- children: navigate ? /*#__PURE__*/ jsx(RouterProvider, {
21
- navigate: navigate,
22
- useHref: useHref,
23
- children: children
24
- }) : children
25
- });
26
- }
27
-
28
- /**
29
- * Returns the locale set in `<GrunnmurenProvider />`
30
- */ function _useLocale() {
31
- // a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
32
- const locale = useLocale();
33
- return locale.locale;
34
- }
35
16
 
36
17
  const HeadingContext = /*#__PURE__*/ createContext({});
37
18
  const headingVariants = cva({
@@ -109,11 +90,177 @@ const Footer = (props)=>/*#__PURE__*/ jsx("div", {
109
90
  "data-slot": "footer"
110
91
  });
111
92
 
93
+ const disclosureButtonVariants = cva({
94
+ base: [
95
+ 'inline-flex cursor-pointer items-center justify-between rounded-lg focus-visible:outline-current focus-visible:outline-focus',
96
+ // Ensure a minimum click area of 44x44px, while making it look like it only has the size of the content
97
+ 'p-2.5 focus-visible:outline-offset-[-0.625rem]',
98
+ 'data-accordion:-m-2.5'
99
+ ],
100
+ variants: {
101
+ withChevron: {
102
+ true: '[&[aria-expanded="true"]_svg]:rotate-180',
103
+ false: null
104
+ },
105
+ /**
106
+ * When the button is without text, but with a single icon.
107
+ * @default false
108
+ */ isIconOnly: {
109
+ true: '[&>svg]:h-7 [&>svg]:w-7',
110
+ false: 'gap-2.5'
111
+ }
112
+ },
113
+ defaultVariants: {
114
+ withChevron: false,
115
+ isIconOnly: false
116
+ }
117
+ });
118
+ const DisclosureButton = ({ className, withChevron, isIconOnly, children, ref: _ref, ...restProps })=>{
119
+ const [props, ref] = useContextProps(restProps, _ref, ButtonContext$1);
120
+ return /*#__PURE__*/ jsxs(Button$1, {
121
+ ...props,
122
+ ref: ref,
123
+ className: disclosureButtonVariants({
124
+ className,
125
+ withChevron,
126
+ isIconOnly
127
+ }),
128
+ slot: "trigger",
129
+ children: [
130
+ children,
131
+ withChevron && /*#__PURE__*/ jsx(ChevronDown, {
132
+ className: "flex-none transition-transform duration-300 motion-reduce:transition-none"
133
+ })
134
+ ]
135
+ });
136
+ };
137
+ const DisclosureStateContext = /*#__PURE__*/ createContext(null);
138
+ const Disclosure = ({ ref: _ref, children, ..._props })=>{
139
+ const [props, ref] = useContextProps(_props, _ref, DisclosureContext);
140
+ const groupState = useContext(DisclosureGroupStateContext);
141
+ let { id, ...otherProps } = props;
142
+ const defaultId = useId();
143
+ id ||= defaultId;
144
+ const isExpanded = groupState ? groupState.expandedKeys.has(id) : props.isExpanded;
145
+ const state = useDisclosureState({
146
+ ...props,
147
+ isExpanded,
148
+ onExpandedChange (isExpanded) {
149
+ if (groupState) {
150
+ groupState.toggleKey(id);
151
+ }
152
+ props.onExpandedChange?.(isExpanded);
153
+ }
154
+ });
155
+ const isDisabled = props.isDisabled || groupState?.isDisabled || false;
156
+ const domProps = filterDOMProps(otherProps);
157
+ const { isFocusVisible: isFocusVisibleWithin } = useFocusRing({
158
+ within: true
159
+ });
160
+ const panelRef = useRef(null);
161
+ const { buttonProps, panelProps } = useDisclosure({
162
+ ...props,
163
+ isExpanded,
164
+ isDisabled
165
+ }, state, panelRef);
166
+ const { role: _, ...propsWithoutRole } = panelProps;
167
+ return /*#__PURE__*/ jsx(Provider, {
168
+ values: [
169
+ [
170
+ DisclosureContext,
171
+ state
172
+ ],
173
+ [
174
+ ButtonContext$1,
175
+ {
176
+ slots: {
177
+ [DEFAULT_SLOT]: {},
178
+ trigger: buttonProps
179
+ }
180
+ }
181
+ ],
182
+ [
183
+ DisclosurePanelContext,
184
+ {
185
+ ...propsWithoutRole,
186
+ panelRef
187
+ }
188
+ ],
189
+ [
190
+ DisclosureStateContext,
191
+ state
192
+ ]
193
+ ],
194
+ children: /*#__PURE__*/ jsx("div", {
195
+ ...domProps,
196
+ className: otherProps.className,
197
+ ref: ref,
198
+ "data-focus-visible-within": isFocusVisibleWithin || undefined,
199
+ "data-expanded": state.isExpanded || undefined,
200
+ "data-disabled": isDisabled || undefined,
201
+ children: typeof children === 'function' ? children({
202
+ isExpanded: state.isExpanded,
203
+ isFocusVisibleWithin,
204
+ isDisabled,
205
+ state,
206
+ defaultChildren: null
207
+ }) : children
208
+ })
209
+ });
210
+ };
211
+ const DisclosurePanelContext = /*#__PURE__*/ createContext({});
212
+ const DisclosurePanel = ({ ref, children, ...props })=>{
213
+ const disclosureContext = useContext(DisclosureContext);
214
+ const { panelProps, panelRef } = useContext(DisclosurePanelContext);
215
+ const { role: _role = 'group', className, ...restProps } = props;
216
+ const ariaLabelledby = props['aria-labelledby'] ?? restProps['aria-labelledby'];
217
+ const isWithoutRole = _role === 'none';
218
+ const role = isWithoutRole ? undefined : _role;
219
+ const { isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps } = useFocusRing({
220
+ within: true
221
+ });
222
+ const domProps = filterDOMProps(props);
223
+ return /*#__PURE__*/ jsx("div", {
224
+ className: cx('grid transition-all duration-300 motion-reduce:transition-none', disclosureContext?.isExpanded ? 'grid-rows-[1fr] after:h-3.5' : 'grid-rows-[0fr]'),
225
+ "data-expanded": disclosureContext?.isExpanded || undefined,
226
+ children: /*#__PURE__*/ jsx("div", {
227
+ className: "overflow-hidden",
228
+ children: /*#__PURE__*/ jsx("div", {
229
+ ref: mergeRefs(ref, panelRef),
230
+ ...mergeProps(panelProps, focusWithinProps),
231
+ ...restProps,
232
+ ...domProps,
233
+ "data-focus-visible-within": isFocusVisibleWithin || undefined,
234
+ className: cx(className, '[content-visibility:visible]'),
235
+ role: role,
236
+ "aria-labelledby": isWithoutRole ? undefined : ariaLabelledby,
237
+ inert: disclosureContext?.isExpanded ? undefined : true,
238
+ children: /*#__PURE__*/ jsx(Provider, {
239
+ values: [
240
+ // Reset the context to avoid passing the same context to children, in case of nested Disclosures
241
+ [
242
+ DisclosureContext,
243
+ null
244
+ ],
245
+ [
246
+ ButtonContext$1,
247
+ null
248
+ ]
249
+ ],
250
+ children: children
251
+ })
252
+ })
253
+ })
254
+ });
255
+ };
256
+
112
257
  function Accordion(props) {
113
- const { children, className, ...restProps } = props;
258
+ const { children, className, allowsMultipleExpanded = true, ...restProps } = props;
114
259
  const childCount = Children.count(children);
115
- return /*#__PURE__*/ jsx("div", {
260
+ return /*#__PURE__*/ jsx(DisclosureGroup, {
116
261
  ...restProps,
262
+ "data-accordion": true,
263
+ allowsMultipleExpanded: allowsMultipleExpanded,
117
264
  className: cx('rounded-lg bg-white', className),
118
265
  children: Children.map(children, (child, index)=>/*#__PURE__*/ jsxs(Fragment, {
119
266
  children: [
@@ -128,40 +275,16 @@ function Accordion(props) {
128
275
  });
129
276
  }
130
277
  function AccordionItem(props) {
131
- const { className, children, defaultOpen = false, isOpen: controlledIsOpen, onOpenChange, ...restProps } = props;
132
- const contentId = useId();
133
- const buttonId = useId();
134
- const isControlled = controlledIsOpen != null;
135
- // This component has internal state that controls whether it is open or not,
136
- // regardless if we are controlled or uncontrolled.
137
- // If we are controlled, we use a layout effect to sync the controlled state
138
- // with the internal state.
139
- //
140
- const [isOpen, setIsOpen] = useState(// If we are controlled, use that open state, otherwise use the uncontrolled
141
- isControlled ? controlledIsOpen : defaultOpen);
142
- useLayoutEffect(()=>{
143
- if (isControlled) {
144
- setIsOpen(controlledIsOpen);
145
- }
146
- }, [
147
- controlledIsOpen,
148
- isControlled
149
- ]);
150
- const handleOpenChange = ()=>{
151
- const newOpenState = !isOpen;
152
- if (!isControlled) {
153
- setIsOpen(newOpenState);
154
- }
155
- // Always call the change handler, even if we're uncontrolled.
156
- // Easier to add stuff such as tracking etc.
157
- if (onOpenChange) {
158
- onOpenChange(newOpenState);
159
- }
160
- };
161
- return /*#__PURE__*/ jsx("div", {
278
+ const { className, children, defaultOpen = false, isOpen: controlledIsOpen, onOpenChange, defaultExpanded, isExpanded, onExpandedChange, ...restProps } = props;
279
+ const _defaultExpanded = defaultOpen ?? defaultExpanded;
280
+ const _isExpanded = controlledIsOpen ?? isExpanded;
281
+ const _onExpandedChange = onOpenChange ?? onExpandedChange;
282
+ return /*#__PURE__*/ jsx(Disclosure, {
162
283
  ...restProps,
163
284
  className: cx('relative px-2', className),
164
- "data-open": isOpen,
285
+ defaultExpanded: _defaultExpanded,
286
+ onExpandedChange: _onExpandedChange,
287
+ isExpanded: _isExpanded,
165
288
  children: /*#__PURE__*/ jsx(Provider, {
166
289
  values: [
167
290
  [
@@ -171,33 +294,22 @@ function AccordionItem(props) {
171
294
  className: 'font-medium leading-7 -mx-2 text-base',
172
295
  // Supply a default level here to make this typecheck ok. Will be overwritten with the consumers set heading level anyways
173
296
  level: 3,
174
- _innerWrapper: (children)=>/*#__PURE__*/ jsxs("button", {
175
- "aria-controls": contentId,
176
- "aria-expanded": isOpen,
297
+ _innerWrapper: (children)=>/*#__PURE__*/ jsx(DisclosureButton, {
177
298
  // Use outline with offset as focus indicator, this does not cover the left sky border on the expanded content and works with or without a background color on the accordion container
178
- className: "flex min-h-[44px] w-full cursor-pointer items-center justify-between gap-1.5 rounded-lg px-2 py-3.5 text-left focus-visible:outline-focus focus-visible:outline-focus-inset",
179
- id: buttonId,
180
- onClick: handleOpenChange,
299
+ className: "flex min-h-11 w-full gap-1.5 rounded-lg px-2 py-3.5 text-left focus-visible:outline-focus-inset!",
181
300
  type: "button",
182
- children: [
183
- children,
184
- /*#__PURE__*/ jsx(ChevronDown, {
185
- className: cx('flex-none transition-transform duration-300 motion-reduce:transition-none', isOpen && 'rotate-180')
186
- })
187
- ]
301
+ withChevron: true,
302
+ children: children
188
303
  })
189
304
  }
190
305
  ],
191
306
  [
192
307
  ContentContext,
193
308
  {
194
- className: // Uses pseudo element for vertical padding, since that doesn't affect the height when the accordion is closed
195
- 'text-sm font-light leading-6 px-3.5 relative overflow-hidden border-sky border-l-[3px] before:relative before:block before:h-1.5 after:relative after:block after:h-1.5',
309
+ className: // Uses pseudo elements for vertical padding, since that doesn't affect the height when the accordion is closed
310
+ 'text-sm font-light leading-6 px-3.5 data-[expanded]:after:h-3.5 relative overflow-hidden border-sky border-l-[3px] before:relative before:block before:h-1.5 after:relative after:block after:h-1.5',
196
311
  role: 'region',
197
- inert: !isOpen,
198
- 'aria-labelledby': buttonId,
199
- _outerWrapper: (children)=>/*#__PURE__*/ jsx("div", {
200
- className: cx('grid transition-all duration-300 after:relative after:block after:h-0 after:transition-all after:duration-300 motion-reduce:transition-none', isOpen ? 'grid-rows-[1fr] after:h-3.5' : 'grid-rows-[0fr]'),
312
+ _outerWrapper: (children)=>/*#__PURE__*/ jsx(DisclosurePanel, {
201
313
  children: children
202
314
  })
203
315
  }
@@ -208,43 +320,6 @@ function AccordionItem(props) {
208
320
  });
209
321
  }
210
322
 
211
- const badgeVariants = cva({
212
- base: [
213
- 'inline-flex w-fit items-center justify-center gap-1.5 rounded-lg [&_svg]:shrink-0'
214
- ],
215
- variants: {
216
- color: {
217
- 'gray-dark': 'bg-gray-dark text-white',
218
- mint: 'bg-mint',
219
- sky: 'bg-sky',
220
- white: 'bg-white',
221
- 'blue-dark': 'bg-blue-dark text-white',
222
- 'green-dark': 'bg-green-dark text-white'
223
- },
224
- size: {
225
- small: 'description px-2 py-0.5 [&_svg]:h-4 [&_svg]:w-4',
226
- medium: 'description px-2.5 py-1.5 [&_svg]:h-4 [&_svg]:w-4',
227
- large: 'paragraph px-3 py-2 [&_svg]:h-5 [&_svg]:w-5'
228
- }
229
- },
230
- defaultVariants: {
231
- size: 'medium'
232
- }
233
- });
234
- function Badge(props) {
235
- const { className: _className, color, size, ...restProps } = props;
236
- const className = badgeVariants({
237
- className: _className,
238
- color,
239
- size
240
- });
241
- return /*#__PURE__*/ jsx("span", {
242
- className: className,
243
- ...restProps,
244
- "data-slot": "badge"
245
- });
246
- }
247
-
248
323
  const translations$1 = {
249
324
  close: {
250
325
  nb: 'Lukk',
@@ -279,17 +354,243 @@ const translations$1 = {
279
354
  };
280
355
 
281
356
  /**
282
- * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
283
- */ const buttonVariants = cva({
357
+ * Returns the locale set in `<GrunnmurenProvider />`
358
+ */ function _useLocale() {
359
+ // a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
360
+ const locale = useLocale();
361
+ return locale.locale;
362
+ }
363
+
364
+ const iconMap = {
365
+ info: InfoCircle,
366
+ success: CheckCircle,
367
+ warning: Warning,
368
+ danger: Error
369
+ };
370
+ const alertVariants = cva({
284
371
  base: [
285
- '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'
372
+ 'grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-md border-2 px-3 py-2',
373
+ // Heading styles:
374
+ '[&_[data-slot="heading"]]:font-medium [&_[data-slot="heading"]]:text-base [&_[data-slot="heading"]]:leading-7',
375
+ // Content styles:
376
+ '[&:has([data-slot="heading"])_[data-slot="content"]]:col-span-full [&_[data-slot="content"]]:text-sm [&_[data-slot="content"]]:leading-6',
377
+ // Footer styles:
378
+ '[&_[data-slot="footer"]]:col-span-full [&_[data-slot="footer"]]:font-light [&_[data-slot="footer"]]:text-xs [&_[data-slot="footer"]]:leading-6'
286
379
  ],
287
380
  variants: {
288
381
  /**
289
- * The variant of the button
290
- * @default primary
382
+ * The variant of the alert
383
+ * @default info
291
384
  */ variant: {
292
- primary: 'no-underline',
385
+ info: 'border-[#1A7FA7] bg-sky-light',
386
+ success: 'border-[#0F9B6E] bg-mint-light',
387
+ warning: 'border-[#C57C13] bg-[#FFF2DE]',
388
+ danger: 'border-[#C0385D] bg-red-light'
389
+ }
390
+ },
391
+ defaultVariants: {
392
+ variant: 'info'
393
+ }
394
+ });
395
+ const Alertbox = ({ children, role, className, icon, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
396
+ const Icon = icon ?? iconMap[variant];
397
+ const locale = _useLocale();
398
+ const id = useId();
399
+ const [isExpanded, setIsExpanded] = useState(false);
400
+ const isCollapsed = isExpandable && !isExpanded;
401
+ const [isUncontrolledVisible, setIsUncontrolledVisible] = useState(true);
402
+ const isVisible = isDismissed !== undefined ? !isDismissed : isUncontrolledVisible;
403
+ if (!isVisible) return;
404
+ const close = ()=>{
405
+ setIsUncontrolledVisible(false);
406
+ if (onDismiss) onDismiss();
407
+ };
408
+ const isInDevMode = process.env.NODE_ENV !== 'production';
409
+ if (isInDevMode && onDismiss && !isDismissable) {
410
+ console.warn('Passing an `onDismiss` callback without setting the `isDismissable` prop to `true` will not have any effect.');
411
+ }
412
+ if (isInDevMode && !children) {
413
+ console.error('`No children was passed to the <AlertBox/>` component.');
414
+ return;
415
+ }
416
+ const [firstChild, ...restChildren] = Children.toArray(children);
417
+ return /*#__PURE__*/ jsxs("div", {
418
+ className: alertVariants({
419
+ className,
420
+ variant
421
+ }),
422
+ // The role prop is required to force consumers to consider and choose the appropriate alertbox role.
423
+ // role="none" will not have any effect on a div, so it can be omitted.
424
+ role: role === 'none' ? undefined : role,
425
+ children: [
426
+ /*#__PURE__*/ jsx(Icon, {}),
427
+ firstChild,
428
+ isDismissable && /*#__PURE__*/ jsx("button", {
429
+ className: cx('-m-2 grid h-11 w-11 place-items-center rounded-xl', 'focus-visible:-outline-offset-8 cursor-pointer focus-visible:outline-focus'),
430
+ onClick: close,
431
+ "aria-label": translations$1.close[locale],
432
+ type: "button",
433
+ children: /*#__PURE__*/ jsx(Close, {})
434
+ }),
435
+ isExpandable && /*#__PURE__*/ jsxs("button", {
436
+ className: cx('-my-3 relative col-span-full row-start-2 inline-flex max-w-fit cursor-pointer items-center gap-1 py-3 text-sm leading-6', // Focus styles:
437
+ 'outline-none after:absolute after:right-0 after:bottom-3 after:left-0 after:h-0', 'focus-visible:after:h-[2px] focus-visible:after:bg-black'),
438
+ onClick: ()=>setIsExpanded((prevState)=>!prevState),
439
+ "aria-expanded": isExpanded,
440
+ "aria-controls": id,
441
+ type: "button",
442
+ children: [
443
+ isExpanded ? translations$1.showLess[locale] : translations$1.showMore[locale],
444
+ /*#__PURE__*/ jsx(ChevronDown, {
445
+ className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
446
+ })
447
+ ]
448
+ }),
449
+ restChildren?.length > 0 && /*#__PURE__*/ jsx("div", {
450
+ className: cx('col-span-full grid gap-y-4', isCollapsed && '[&>*:not([data-slot="footer"])]:hidden'),
451
+ id: id,
452
+ children: restChildren
453
+ })
454
+ ]
455
+ });
456
+ };
457
+
458
+ const baseClassName = 'h-20 w-20 shrink-0 rounded-full';
459
+ const Avatar = ({ src, alt = '', className, onError, loading = 'lazy', ...rest })=>{
460
+ const [hasError, setHasError] = useState(false);
461
+ const hasValidImage = !hasError && src;
462
+ return hasValidImage ? /*#__PURE__*/ jsx("img", {
463
+ ...rest,
464
+ src: src,
465
+ alt: alt,
466
+ loading: loading,
467
+ className: cx(className, baseClassName, 'object-cover'),
468
+ onError: (event)=>{
469
+ onError?.(event);
470
+ setHasError(true);
471
+ }
472
+ }) : /*#__PURE__*/ jsx("div", {
473
+ className: cx(className, baseClassName, 'grid place-items-center bg-gray-light text-gray-dark'),
474
+ children: /*#__PURE__*/ jsx(User, {
475
+ className: "scale-[2.25]"
476
+ })
477
+ });
478
+ };
479
+
480
+ function isLinkProps$1(props) {
481
+ return !!props.href;
482
+ }
483
+ function Backlink(props) {
484
+ const { className, style, children, withUnderline, ref, ...restProps } = props;
485
+ const _className = cx(className, 'group flex max-w-fit cursor-pointer items-center gap-3 rounded-md p-2.5 no-underline focus-visible:outline-focus');
486
+ const content = /*#__PURE__*/ jsxs(Fragment, {
487
+ children: [
488
+ /*#__PURE__*/ jsx(ChevronLeft, {
489
+ className: cx('-ml-[0.5em] group-hover:-translate-x-1 shrink-0 transition-transform duration-300')
490
+ }),
491
+ /*#__PURE__*/ jsx("span", {
492
+ children: /*#__PURE__*/ jsx("span", {
493
+ className: cx('border-transparent border-t-[1px] border-b-[1px] transition-colors duration-300', withUnderline ? 'border-b-black' : 'group-hover:border-b-black'),
494
+ children: children
495
+ })
496
+ })
497
+ ]
498
+ });
499
+ if (isLinkProps$1(props)) {
500
+ return /*#__PURE__*/ jsx(Link, {
501
+ ...restProps,
502
+ className: _className,
503
+ style: style,
504
+ ref: ref,
505
+ children: content
506
+ });
507
+ }
508
+ return /*#__PURE__*/ jsx(Button$1, {
509
+ ...restProps,
510
+ className: _className,
511
+ style: style,
512
+ ref: ref,
513
+ children: content
514
+ });
515
+ }
516
+
517
+ const badgeVariants = cva({
518
+ base: [
519
+ 'inline-flex w-fit items-center justify-center gap-1.5 rounded-lg [&_svg]:shrink-0'
520
+ ],
521
+ variants: {
522
+ color: {
523
+ 'gray-dark': 'bg-gray-dark text-white',
524
+ mint: 'bg-mint',
525
+ sky: 'bg-sky',
526
+ white: 'bg-white',
527
+ 'blue-dark': 'bg-blue-dark text-white',
528
+ 'green-dark': 'bg-green-dark text-white'
529
+ },
530
+ size: {
531
+ small: 'description px-2 py-0.5 [&_svg]:h-4 [&_svg]:w-4',
532
+ medium: 'description px-2.5 py-1.5 [&_svg]:h-4 [&_svg]:w-4',
533
+ large: 'paragraph px-3 py-2 [&_svg]:h-5 [&_svg]:w-5'
534
+ }
535
+ },
536
+ defaultVariants: {
537
+ size: 'medium'
538
+ }
539
+ });
540
+ function Badge(props) {
541
+ const { className: _className, color, size, ...restProps } = props;
542
+ const className = badgeVariants({
543
+ className: _className,
544
+ color,
545
+ size
546
+ });
547
+ return /*#__PURE__*/ jsx("span", {
548
+ className: className,
549
+ ...restProps,
550
+ "data-slot": "badge"
551
+ });
552
+ }
553
+
554
+ function Breadcrumb(props) {
555
+ const { className, children, href, ...restProps } = props;
556
+ return /*#__PURE__*/ jsxs(Breadcrumb$1, {
557
+ className: cx(className, 'group flex items-center'),
558
+ ...restProps,
559
+ children: [
560
+ href ? /*#__PURE__*/ jsx(Link, {
561
+ href: href,
562
+ // use outline instead of ring-3 for focus marker that can be offset without creating a white background between the focus marker and the element content
563
+ className: "rounded-xs focus-visible:outline-focus group-last:no-underline",
564
+ children: children
565
+ }) : children,
566
+ /*#__PURE__*/ jsx(ChevronRight, {
567
+ className: "px-1 group-last:hidden"
568
+ })
569
+ ]
570
+ });
571
+ }
572
+
573
+ function Breadcrumbs(props) {
574
+ const { className, children, ...restProps } = props;
575
+ return /*#__PURE__*/ jsx(Breadcrumbs$1, {
576
+ ...restProps,
577
+ className: cx(className, 'flex flex-wrap text-sm leading-6'),
578
+ children: children
579
+ });
580
+ }
581
+
582
+ /**
583
+ * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
584
+ */ const buttonVariants = cva({
585
+ base: [
586
+ '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'
587
+ ],
588
+ variants: {
589
+ /**
590
+ * The variant of the button
591
+ * @default primary
592
+ */ variant: {
593
+ primary: 'no-underline',
293
594
  // 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
294
595
  secondary: 'border-2 border-current no-underline hover:border-transparent',
295
596
  tertiary: 'underline hover:no-underline'
@@ -372,7 +673,7 @@ const translations$1 = {
372
673
  }
373
674
  });
374
675
  const ButtonContext = /*#__PURE__*/ createContext({});
375
- function isLinkProps$1(props) {
676
+ function isLinkProps(props) {
376
677
  return !!props.href;
377
678
  }
378
679
  function Button({ ref = null, ...props }) {
@@ -399,7 +700,7 @@ function Button({ ref = null, ...props }) {
399
700
  })
400
701
  ]
401
702
  }) : _children;
402
- return isLinkProps$1(restProps) ? /*#__PURE__*/ jsx(Link, {
703
+ return isLinkProps(restProps) ? /*#__PURE__*/ jsx(Link, {
403
704
  ...restProps,
404
705
  className: className,
405
706
  ref: ref,
@@ -413,708 +714,7 @@ function Button({ ref = null, ...props }) {
413
714
  });
414
715
  }
415
716
 
416
- const formField = cx('group flex flex-col gap-2');
417
- 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');
418
- const input = cva({
419
- base: [
420
- // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
421
- 'bg-white',
422
- // Use box-content to enable auto width based on number of characters (size)
423
- // Setting min-height to prevent the input from collapsing in Safari
424
- // Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
425
- 'box-content min-h-6 py-2.5',
426
- 'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
427
- // invalid styles
428
- 'group-data-invalid:ring-focus group-data-invalid:ring-red',
429
- // Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
430
- 'appearance-none'
431
- ],
432
- variants: {
433
- // Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
434
- focusModifier: {
435
- focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
436
- visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
437
- },
438
- isGrouped: {
439
- false: 'px-3',
440
- true: '!ring-0 flex-1'
441
- }
442
- },
443
- defaultVariants: {
444
- focusModifier: 'focus',
445
- isGrouped: false
446
- }
447
- });
448
- const inputGroup = cx([
449
- 'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
450
- 'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
451
- ]);
452
- const dropdown = {
453
- 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'),
454
- // overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
455
- listbox: cx('max-h-[25rem] overflow-x-hidden text-sm outline-hidden'),
456
- chevronIcon: cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
457
- };
458
-
459
- function ErrorMessage(props) {
460
- const { children, className, ...restProps } = props;
461
- return /*#__PURE__*/ jsx(Text, {
462
- ...restProps,
463
- className: cx(className, formFieldError),
464
- slot: "errorMessage",
465
- children: children
466
- });
467
- }
468
-
469
- const defaultClasses$1 = cx([
470
- 'group -mx-2.5 relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4 p-2.5 leading-7'
471
- ]);
472
- // Pulling this out into it's own component. Will probably export it in the future
473
- // so it can be used in other views, outside of an input of type checkbox, like in table rows.
474
- function CheckmarkBox() {
475
- return /*#__PURE__*/ jsx("span", {
476
- className: cx([
477
- 'relative left-0 grid flex-none place-content-center rounded-sm border-2 border-black text-white',
478
- // to vertically align the radio we need to calculate the label's height, which is equal to it's font size multiplied by the line height.
479
- // For the ::before psuedo element the line height of the label is always 1em.
480
- // When we know the height of the label we use the height of the radio to push it down to align with the label's first line
481
- // TODO: 1.75 here is the unit less lineheight, altough we use 1.75rem as the line height, so there is a mismatch here. Revisit this when we've worked on typography in v2. Should this be a CSS custom property instead?
482
- 'mt-[calc((1em_*_1.75_-_24px)_/_2)] h-[24px] w-[24px]',
483
- // selected
484
- 'group-data-selected:group-not-data-hovered:group-not-data-invalid:border-blue group-data-selected:group-not-data-hovered:group-not-data-invalid:bg-blue',
485
- 'group-data-selected:group-not-data-hovered:group-data-invalid:border-red group-data-selected:group-not-data-hovered:group-data-invalid:bg-red',
486
- // focus
487
- 'group-data-focus-visible:outline-focus-offset',
488
- // hovered
489
- 'group-data-hovered:group-data-invalid:bg-red-light',
490
- 'group-data-hovered:border-blue',
491
- 'group-data-hovered:bg-sky',
492
- 'group-data-hovered:group-data-selected:group-not-data-invalid:border-blue-dark',
493
- 'group-data-hovered:group-data-selected:group-not-data-invalid:bg-blue-dark',
494
- // invalid - The border is 1 px thicker when invalid. We don't actually want to change the border width, as that causes the element's size to change
495
- // so we use an inner shadow of 1 px instead to pad the actual border
496
- 'group-data-invalid:border-red group-data-invalid:shadow-[inset_0_0_0_1px] group-data-invalid:shadow-red'
497
- ]),
498
- children: /*#__PURE__*/ jsx(Check, {
499
- className: "h-full w-full opacity-0 group-data-invalid:group-data-hovered:group-data-selected:text-red group-data-selected:opacity-100"
500
- })
501
- });
502
- }
503
- function Checkbox(props) {
504
- const { children, className, description, errorMessage, isInvalid: _isInvalid, ...restProps } = props;
505
- const id = useId();
506
- const descriptionId = `desc${id}`;
507
- const errorMessageId = `error${id}`;
508
- const isInvalid = !!errorMessage || _isInvalid;
509
- return /*#__PURE__*/ jsx("div", {
510
- children: /*#__PURE__*/ jsxs(CheckboxContext.Provider, {
511
- value: {
512
- 'aria-describedby': description ? descriptionId : undefined,
513
- 'aria-errormessage': errorMessage ? errorMessageId : undefined
514
- },
515
- children: [
516
- /*#__PURE__*/ jsxs(Checkbox$1, {
517
- ...restProps,
518
- className: cx(className, defaultClasses$1),
519
- isInvalid: isInvalid,
520
- children: [
521
- /*#__PURE__*/ jsx(CheckmarkBox, {}),
522
- children
523
- ]
524
- }),
525
- description && // {/* Use a div instead of the Description component to avoid infinite re-render loops in React until this bug in RAC is fixed: https://github.com/adobe/react-spectrum/issues/6229 */}
526
- /*#__PURE__*/ jsx("div", {
527
- id: descriptionId,
528
- slot: "description",
529
- className: "description block",
530
- children: description
531
- }),
532
- errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
533
- className: "mt-2 block",
534
- id: errorMessageId,
535
- children: errorMessage
536
- })
537
- ]
538
- })
539
- });
540
- }
541
-
542
- function Description(props) {
543
- const { className, ...restProps } = props;
544
- return /*#__PURE__*/ jsx(Text, {
545
- ...restProps,
546
- className: cx(className, 'description'),
547
- slot: "description"
548
- });
549
- }
550
-
551
- /**
552
- * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
553
- * In other words, this handles controlled and uncontrolled form errors.
554
- */ function ErrorMessageOrFieldError({ errorMessage }) {
555
- return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
556
- children: errorMessage
557
- }) : /*#__PURE__*/ jsx(FieldError, {
558
- className: formFieldError
559
- });
560
- }
561
-
562
- function Label(props) {
563
- const { children, className, ...restProps } = props;
564
- return /*#__PURE__*/ jsx(Label$1, {
565
- className: cx(className, 'font-semibold leading-7'),
566
- ...restProps,
567
- children: children
568
- });
569
- }
570
-
571
- function CheckboxGroup(props) {
572
- const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
573
- // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
574
- // which will override any built in validation
575
- const isInvalid = !!errorMessage || _isInvalid;
576
- return /*#__PURE__*/ jsxs(CheckboxGroup$1, {
577
- ...restProps,
578
- className: cx(className, 'flex flex-col gap-2'),
579
- isInvalid: isInvalid,
580
- isRequired: isRequired,
581
- children: [
582
- label && /*#__PURE__*/ jsx(Label, {
583
- children: label
584
- }),
585
- description && /*#__PURE__*/ jsx(Description, {
586
- children: description
587
- }),
588
- children,
589
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
590
- errorMessage: errorMessage
591
- })
592
- ]
593
- });
594
- }
595
-
596
- const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
597
- ...restProps,
598
- className: cx(dropdown.listbox, className)
599
- });
600
- const ListBoxItem = (props)=>{
601
- let textValue = props.textValue;
602
- // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
603
- // Since we use a render function (to handle the selected state) the child is never a string.
604
- // This condition adds back that behaviour
605
- if (textValue == null && typeof props.children === 'string') {
606
- textValue = props.children;
607
- }
608
- return /*#__PURE__*/ jsx(ListBoxItem$1, {
609
- ...props,
610
- className: cx(props.className, 'flex cursor-pointer px-6 py-3 leading-6 outline-none data-focused:bg-sky-lightest'),
611
- textValue: textValue,
612
- children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
613
- children: [
614
- isSelected && /*#__PURE__*/ jsx(Check, {
615
- className: "-ml-6 text-base"
616
- }),
617
- props.children
618
- ]
619
- })
620
- });
621
- };
622
- /**
623
- * This component can be used to group items in a listbox
624
- */ const ListBoxSection = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBoxSection$1, {
625
- ...restProps,
626
- // The :not(:first-child) selector adds extra spacing to all the options, but not the section (group) headings
627
- // This way we get the desired extra indent on all options within a group
628
- className: cx(className, 'pb-1 [&>:not(:first-child)]:pl-10')
629
- });
630
- /**
631
- * This component can be used to label grouped items in a `ListBoxSection` with a heading
632
- */ const ListBoxHeader = (props)=>/*#__PURE__*/ jsx(Header, {
633
- ...props,
634
- className: cx(props.className, 'mx-6 cursor-default py-2 font-medium text-blue-dark leading-6')
635
- });
636
-
637
- function InputAddonDivider() {
638
- return /*#__PURE__*/ jsx("span", {
639
- className: "block h-6 w-px flex-none bg-black"
640
- });
641
- }
642
-
643
- function Combobox(props) {
644
- const { className, children, description, errorMessage, isPending, label, isInvalid: _isInvalid, ref, ...restProps } = props;
645
- // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
646
- // which will override any built in validation
647
- const isInvalid = !!errorMessage || _isInvalid;
648
- return /*#__PURE__*/ jsxs(ComboBox, {
649
- ...restProps,
650
- className: cx(className, formField),
651
- isInvalid: isInvalid,
652
- children: [
653
- label && /*#__PURE__*/ jsx(Label, {
654
- children: label
655
- }),
656
- description && /*#__PURE__*/ jsx(Description, {
657
- children: description
658
- }),
659
- /*#__PURE__*/ jsxs(Group, {
660
- className: inputGroup,
661
- children: [
662
- /*#__PURE__*/ jsx(Input, {
663
- className: input({
664
- isGrouped: true
665
- }),
666
- ref: ref
667
- }),
668
- /*#__PURE__*/ jsx(Button$1, {
669
- children: isPending ? /*#__PURE__*/ jsx(LoadingSpinner, {
670
- className: "animate-spin"
671
- }) : /*#__PURE__*/ jsx(ChevronDown, {
672
- className: dropdown.chevronIcon
673
- })
674
- })
675
- ]
676
- }),
677
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
678
- errorMessage: errorMessage
679
- }),
680
- /*#__PURE__*/ jsx(Popover, {
681
- // FIXME: The trigger width doesn't include the padding of the group, so for now we have to apply this workaround.
682
- // Also... the combobox border gets a pixel wider when focused, so we account for that as well when calculating the width
683
- // and the offset.
684
- // The input gutter should probably be moved to a theme variable instead of using the hardcoded value as here.
685
- className: cx(dropdown.popover, 'min-w-[calc(var(--trigger-width)+26px)]'),
686
- crossOffset: -13,
687
- children: /*#__PURE__*/ jsx(ListBox, {
688
- className: dropdown.listbox,
689
- children: children
690
- })
691
- })
692
- ]
693
- });
694
- }
695
-
696
- function RadioGroup(props) {
697
- const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, value, ...restProps } = props;
698
- // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
699
- // which will override any built in validation
700
- const isInvalid = !!errorMessage || _isInvalid;
701
- return /*#__PURE__*/ jsxs(RadioGroup$1, {
702
- ...restProps,
703
- // Tabindex is set to -1 when the value is an empty string, which makes the radio input not focusable
704
- value: value === '' ? undefined : value,
705
- className: cx(className, 'flex flex-col gap-2'),
706
- isInvalid: isInvalid,
707
- isRequired: isRequired,
708
- children: [
709
- label && /*#__PURE__*/ jsx(Label, {
710
- children: label
711
- }),
712
- description && /*#__PURE__*/ jsx(Description, {
713
- children: description
714
- }),
715
- children,
716
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
717
- errorMessage: errorMessage
718
- })
719
- ]
720
- });
721
- }
722
-
723
- const defaultClasses = cx([
724
- '-ml-2.5 relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
725
- // the radio button itself
726
- 'before:flex-none before:rounded-full before:border-2 before:border-black',
727
- // to vertically align the radio we need to calculate the label's height, which is equal to it's font size multiplied by the line height.
728
- // For the ::before psuedo element the line height of the label is always 1em.
729
- // When we know the height of the label we use the height of the radio to push it down to align with the label's first line
730
- // TODO: 1.75 here is the unit less lineheight, altough we use 1.75rem as the line height, so there is a mismatch here. Revisit this when we've worked on typography in v2. Should this be a CSS custom property instead?
731
- 'before:mt-[calc((1em_*_1.75_-_24px)_/_2)] before:h-[24px] before:w-[24px]',
732
- // selected
733
- 'data-selected:before:border-black data-selected:before:bg-blue data-selected:before:shadow-[inset_0_0_0_4px_rgb(255,255,255)]',
734
- // hover
735
- 'data-hovered:data-selected:before:border-blue-dark data-hovered:data-invalid:before:bg-red-light data-hovered:data-selected:before:bg-blue-dark data-hovered:before:border-blue data-hovered:before:bg-sky',
736
- // focus
737
- 'data-focus-visible:before:ring-focus-offset',
738
- // invalid - The border is 1 px thicker when invalid. We don't actually want to change the border width, as that causes the element's size to change
739
- // so we use an inner outline to artifically pad the border
740
- 'data-invalid:data-selected:before:!bg-red data-invalid:before:border-red data-invalid:before:outline data-invalid:before:outline-[3px] data-invalid:before:outline-red data-invalid:before:outline-solid data-invalid:before:outline-offset-[-3px]'
741
- ]);
742
- function Radio(props) {
743
- const { children, className, description, ...restProps } = props;
744
- return /*#__PURE__*/ jsx(Radio$1, {
745
- ...restProps,
746
- className: cx(className, defaultClasses),
747
- children: /*#__PURE__*/ jsxs("div", {
748
- children: [
749
- children,
750
- description && /*#__PURE__*/ jsx(Description, {
751
- className: "mt-2 block",
752
- children: description
753
- })
754
- ]
755
- })
756
- });
757
- }
758
-
759
- function Select(props) {
760
- const { className, children, description, errorMessage, label, isInvalid: _isInvalid, ref, ...restProps } = props;
761
- // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
762
- // which will override any built in validation
763
- const isInvalid = !!errorMessage || _isInvalid;
764
- return /*#__PURE__*/ jsxs(Select$1, {
765
- ...restProps,
766
- className: cx(className, formField),
767
- isInvalid: isInvalid,
768
- children: [
769
- label && /*#__PURE__*/ jsx(Label, {
770
- children: label
771
- }),
772
- description && /*#__PURE__*/ jsx(Description, {
773
- children: description
774
- }),
775
- /*#__PURE__*/ jsxs(Button$1, {
776
- className: cx(input({
777
- focusModifier: 'visible'
778
- }), // How to reuse placeholder text?
779
- 'inline-flex cursor-default items-center gap-2'),
780
- // See https://github.com/adobe/react-spectrum/discussions/4792#discussioncomment-6492305
781
- ref: ref,
782
- children: [
783
- /*#__PURE__*/ jsx(SelectValue, {
784
- className: "flex-1 truncate text-left data-[placeholder]:text-[#727070]"
785
- }),
786
- /*#__PURE__*/ jsx(ChevronDown, {
787
- className: dropdown.chevronIcon
788
- })
789
- ]
790
- }),
791
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
792
- errorMessage: errorMessage
793
- }),
794
- /*#__PURE__*/ jsx(Popover, {
795
- className: dropdown.popover,
796
- children: /*#__PURE__*/ jsx(ListBox, {
797
- className: dropdown.listbox,
798
- children: children
799
- })
800
- })
801
- ]
802
- });
803
- }
804
-
805
- function TextArea(props) {
806
- const { className, description, errorMessage, label, isInvalid: _isInvalid, rows, ref, ...restProps } = props;
807
- const isInvalid = !!errorMessage || _isInvalid;
808
- return /*#__PURE__*/ jsxs(TextField$1, {
809
- ...restProps,
810
- className: cx(className, formField),
811
- isInvalid: isInvalid,
812
- children: [
813
- label && /*#__PURE__*/ jsx(Label, {
814
- children: label
815
- }),
816
- description && /*#__PURE__*/ jsx(Description, {
817
- children: description
818
- }),
819
- /*#__PURE__*/ jsx(TextArea$1, {
820
- className: input(),
821
- rows: rows,
822
- ref: ref
823
- }),
824
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
825
- errorMessage: errorMessage
826
- })
827
- ]
828
- });
829
- }
830
-
831
- const inputVariants$1 = compose(input, cva({
832
- base: '',
833
- variants: {
834
- textAlign: {
835
- right: 'text-right',
836
- left: ''
837
- },
838
- autoWidth: {
839
- true: 'max-w-fit',
840
- false: ''
841
- }
842
- }
843
- }));
844
- function TextField(props) {
845
- const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
846
- // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
847
- // which will override any built in validation
848
- const isInvalid = !!errorMessage || _isInvalid;
849
- return /*#__PURE__*/ jsxs(TextField$1, {
850
- ...restProps,
851
- className: cx(className, formField),
852
- isInvalid: isInvalid,
853
- children: [
854
- label && /*#__PURE__*/ jsx(Label, {
855
- children: label
856
- }),
857
- description && /*#__PURE__*/ jsx(Description, {
858
- children: description
859
- }),
860
- leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
861
- className: cx(inputGroup, {
862
- 'w-fit': !!size
863
- }),
864
- children: [
865
- leftAddon,
866
- withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
867
- /*#__PURE__*/ jsx(Input, {
868
- className: inputVariants$1({
869
- textAlign,
870
- isGrouped: true,
871
- autoWidth: !!size
872
- }),
873
- ref: ref,
874
- size: size
875
- }),
876
- withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
877
- rightAddon
878
- ]
879
- }) : /*#__PURE__*/ jsx(Input, {
880
- className: inputVariants$1({
881
- textAlign,
882
- autoWidth: !!size
883
- }),
884
- ref: ref,
885
- size: size
886
- }),
887
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
888
- errorMessage: errorMessage
889
- })
890
- ]
891
- });
892
- }
893
-
894
- // This component is based on a copy of ../textfield/TextField, refactoring is TBD: https://github.com/code-obos/grunnmuren/pull/722#issuecomment-1931478786
895
- const inputVariants = compose(input, cva({
896
- base: '',
897
- variants: {
898
- textAlign: {
899
- right: 'text-right',
900
- left: ''
901
- },
902
- autoWidth: {
903
- true: 'max-w-fit',
904
- false: ''
905
- }
906
- }
907
- }));
908
- function NumberField(props) {
909
- const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
910
- // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
911
- // which will override any built in validation
912
- const isInvalid = !!errorMessage || _isInvalid;
913
- return /*#__PURE__*/ jsxs(NumberField$1, {
914
- ...restProps,
915
- className: cx(className, formField),
916
- isInvalid: isInvalid,
917
- children: [
918
- label && /*#__PURE__*/ jsx(Label, {
919
- children: label
920
- }),
921
- description && /*#__PURE__*/ jsx(Description, {
922
- children: description
923
- }),
924
- leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
925
- className: cx(inputGroup, {
926
- 'w-fit': !!size
927
- }),
928
- children: [
929
- leftAddon,
930
- withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
931
- /*#__PURE__*/ jsx(Input, {
932
- className: inputVariants({
933
- textAlign,
934
- isGrouped: true,
935
- autoWidth: !!size
936
- }),
937
- ref: ref,
938
- size: size
939
- }),
940
- withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
941
- rightAddon
942
- ]
943
- }) : /*#__PURE__*/ jsx(Input, {
944
- className: inputVariants({
945
- textAlign,
946
- autoWidth: !!size
947
- }),
948
- ref: ref,
949
- size: size
950
- }),
951
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
952
- errorMessage: errorMessage
953
- })
954
- ]
955
- });
956
- }
957
-
958
- const iconMap = {
959
- info: InfoCircle,
960
- success: CheckCircle,
961
- warning: Warning,
962
- danger: Error
963
- };
964
- const alertVariants = cva({
965
- base: [
966
- 'grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-md border-2 px-3 py-2',
967
- // Heading styles:
968
- '[&_[data-slot="heading"]]:font-medium [&_[data-slot="heading"]]:text-base [&_[data-slot="heading"]]:leading-7',
969
- // Content styles:
970
- '[&:has([data-slot="heading"])_[data-slot="content"]]:col-span-full [&_[data-slot="content"]]:text-sm [&_[data-slot="content"]]:leading-6',
971
- // Footer styles:
972
- '[&_[data-slot="footer"]]:col-span-full [&_[data-slot="footer"]]:font-light [&_[data-slot="footer"]]:text-xs [&_[data-slot="footer"]]:leading-6'
973
- ],
974
- variants: {
975
- /**
976
- * The variant of the alert
977
- * @default info
978
- */ variant: {
979
- info: 'border-[#1A7FA7] bg-sky-light',
980
- success: 'border-[#0F9B6E] bg-mint-light',
981
- warning: 'border-[#C57C13] bg-[#FFF2DE]',
982
- danger: 'border-[#C0385D] bg-red-light'
983
- }
984
- },
985
- defaultVariants: {
986
- variant: 'info'
987
- }
988
- });
989
- const Alertbox = ({ children, role, className, icon, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
990
- const Icon = icon ?? iconMap[variant];
991
- const locale = _useLocale();
992
- const id = useId();
993
- const [isExpanded, setIsExpanded] = useState(false);
994
- const isCollapsed = isExpandable && !isExpanded;
995
- const [isUncontrolledVisible, setIsUncontrolledVisible] = useState(true);
996
- const isVisible = isDismissed !== undefined ? !isDismissed : isUncontrolledVisible;
997
- if (!isVisible) return;
998
- const close = ()=>{
999
- setIsUncontrolledVisible(false);
1000
- if (onDismiss) onDismiss();
1001
- };
1002
- const isInDevMode = process.env.NODE_ENV !== 'production';
1003
- if (isInDevMode && onDismiss && !isDismissable) {
1004
- console.warn('Passing an `onDismiss` callback without setting the `isDismissable` prop to `true` will not have any effect.');
1005
- }
1006
- if (isInDevMode && !children) {
1007
- console.error('`No children was passed to the <AlertBox/>` component.');
1008
- return;
1009
- }
1010
- const [firstChild, ...restChildren] = Children.toArray(children);
1011
- return /*#__PURE__*/ jsxs("div", {
1012
- className: alertVariants({
1013
- className,
1014
- variant
1015
- }),
1016
- // The role prop is required to force consumers to consider and choose the appropriate alertbox role.
1017
- // role="none" will not have any effect on a div, so it can be omitted.
1018
- role: role === 'none' ? undefined : role,
1019
- children: [
1020
- /*#__PURE__*/ jsx(Icon, {}),
1021
- firstChild,
1022
- isDismissable && /*#__PURE__*/ jsx("button", {
1023
- className: cx('-m-2 grid h-11 w-11 place-items-center rounded-xl', 'focus-visible:-outline-offset-8 cursor-pointer focus-visible:outline-focus'),
1024
- onClick: close,
1025
- "aria-label": translations$1.close[locale],
1026
- type: "button",
1027
- children: /*#__PURE__*/ jsx(Close, {})
1028
- }),
1029
- isExpandable && /*#__PURE__*/ jsxs("button", {
1030
- className: cx('-my-3 relative col-span-full row-start-2 inline-flex max-w-fit cursor-pointer items-center gap-1 py-3 text-sm leading-6', // Focus styles:
1031
- 'outline-none after:absolute after:right-0 after:bottom-3 after:left-0 after:h-0', 'focus-visible:after:h-[2px] focus-visible:after:bg-black'),
1032
- onClick: ()=>setIsExpanded((prevState)=>!prevState),
1033
- "aria-expanded": isExpanded,
1034
- "aria-controls": id,
1035
- type: "button",
1036
- children: [
1037
- isExpanded ? translations$1.showLess[locale] : translations$1.showMore[locale],
1038
- /*#__PURE__*/ jsx(ChevronDown, {
1039
- className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
1040
- })
1041
- ]
1042
- }),
1043
- restChildren?.length > 0 && /*#__PURE__*/ jsx("div", {
1044
- className: cx('col-span-full grid gap-y-4', isCollapsed && '[&>*:not([data-slot="footer"])]:hidden'),
1045
- id: id,
1046
- children: restChildren
1047
- })
1048
- ]
1049
- });
1050
- };
1051
-
1052
- function Breadcrumbs(props) {
1053
- const { className, children, ...restProps } = props;
1054
- return /*#__PURE__*/ jsx(Breadcrumbs$1, {
1055
- ...restProps,
1056
- className: cx(className, 'flex flex-wrap text-sm leading-6'),
1057
- children: children
1058
- });
1059
- }
1060
-
1061
- function Breadcrumb(props) {
1062
- const { className, children, href, ...restProps } = props;
1063
- return /*#__PURE__*/ jsxs(Breadcrumb$1, {
1064
- className: cx(className, 'group flex items-center'),
1065
- ...restProps,
1066
- children: [
1067
- href ? /*#__PURE__*/ jsx(Link, {
1068
- href: href,
1069
- // use outline instead of ring-3 for focus marker that can be offset without creating a white background between the focus marker and the element content
1070
- className: "rounded-xs focus-visible:outline-focus group-last:no-underline",
1071
- children: children
1072
- }) : children,
1073
- /*#__PURE__*/ jsx(ChevronRight, {
1074
- className: "px-1 group-last:hidden"
1075
- })
1076
- ]
1077
- });
1078
- }
1079
-
1080
- function isLinkProps(props) {
1081
- return !!props.href;
1082
- }
1083
- function Backlink(props) {
1084
- const { className, style, children, withUnderline, ref, ...restProps } = props;
1085
- const _className = cx(className, 'group flex max-w-fit cursor-pointer items-center gap-3 rounded-md p-2.5 no-underline focus-visible:outline-focus');
1086
- const content = /*#__PURE__*/ jsxs(Fragment, {
1087
- children: [
1088
- /*#__PURE__*/ jsx(ChevronLeft, {
1089
- className: cx('-ml-[0.5em] group-hover:-translate-x-1 shrink-0 transition-transform duration-300')
1090
- }),
1091
- /*#__PURE__*/ jsx("span", {
1092
- children: /*#__PURE__*/ jsx("span", {
1093
- className: cx('border-transparent border-t-[1px] border-b-[1px] transition-colors duration-300', withUnderline ? 'border-b-black' : 'group-hover:border-b-black'),
1094
- children: children
1095
- })
1096
- })
1097
- ]
1098
- });
1099
- if (isLinkProps(props)) {
1100
- return /*#__PURE__*/ jsx(Link, {
1101
- ...restProps,
1102
- className: _className,
1103
- style: style,
1104
- ref: ref,
1105
- children: content
1106
- });
1107
- }
1108
- return /*#__PURE__*/ jsx(Button$1, {
1109
- ...restProps,
1110
- className: _className,
1111
- style: style,
1112
- ref: ref,
1113
- children: content
1114
- });
1115
- }
1116
-
1117
- const cardVariants = cva({
717
+ const cardVariants = cva({
1118
718
  base: [
1119
719
  'group/card',
1120
720
  'rounded-[inherit]',
@@ -1339,259 +939,548 @@ const cardLinkVariants = cva({
1339
939
  });
1340
940
  };
1341
941
 
1342
- /**
1343
- * A React component that wraps https://react-spectrum.adobe.com/react-aria/useDateFormatter.html
1344
- * By default it sets the timeZone to `Europe/Berlin` to prevent the server's timezone from affecting
1345
- * the localized format
1346
- */ const DateFormatter = ({ options: _options, value, children: render })=>{
1347
- const options = {
1348
- timeZone: 'Europe/Berlin',
1349
- ..._options
942
+ const Carousel = ({ className, children, onChange, ...rest })=>{
943
+ const ref = useRef(null);
944
+ const locale = _useLocale();
945
+ const { previous, next } = translations$1;
946
+ const [scrollTargetIndex, setScrollTargetIndex] = useState(0);
947
+ const isScrollingProgrammatically = useRef(false);
948
+ const scrollTimeoutRef = useRef(null);
949
+ const scrollQueue = useRef([]);
950
+ const [hasReachedScrollStart, setHasReachedScrollStart] = useState(scrollTargetIndex === 0);
951
+ const [hasReachedScrollEnd, setHasReachedScrollEnd] = useState(!ref.current || ref.current.children.length - 1 === scrollTargetIndex);
952
+ useEffect(()=>{
953
+ setHasReachedScrollStart(scrollTargetIndex === 0);
954
+ setHasReachedScrollEnd(!ref.current || ref.current.children.length - 1 === scrollTargetIndex);
955
+ }, [
956
+ scrollTargetIndex
957
+ ]);
958
+ // Keep track of the previous index to determine if the user is scrolling forward or backward
959
+ // This is used to determine which callback to call (onPrev or onNext)
960
+ const prevIndex = useRef(0);
961
+ // Processes the next scroll action in the queue, if any
962
+ // All clicks on the prev/next buttons are queued while a programmatic scroll is in progress
963
+ // This is to ensure that rapid clicks on the buttons do not cause janky scrolling behavior
964
+ // while still a snappy response to user clicks
965
+ const processQueue = ()=>{
966
+ if (scrollQueue.current.length > 0 && !isScrollingProgrammatically.current) {
967
+ const nextIndex = scrollQueue.current?.shift();
968
+ if (nextIndex !== undefined) {
969
+ setScrollTargetIndex(nextIndex);
970
+ }
971
+ }
1350
972
  };
1351
- const formatter = useDateFormatter(options);
1352
- const date = typeof value === 'string' ? new Date(value) : value;
1353
- const formatted = formatter.format(date);
1354
- return render ? render(formatted) : formatted;
1355
- };
1356
-
1357
- const VideoLoop = ({ src, format, alt, className })=>{
1358
- // Control the video playback state, so that the user can pause and play the video at will, also control the video autoplay
1359
- const [shouldPlay, setShouldPlay] = useState(false);
1360
- // Needed to show the pause button when the video is actually playing (refer to google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes)
1361
- const [isPlaying, setIsPlaying] = useState(false);
1362
- // We need to check if the user prefers reduced motion, so that we can prevent the video from autoplaying if so
1363
- const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(null);
1364
- const videoRef = useRef(null);
973
+ // Handle scrolling when user clicks the arrow icons
974
+ useUpdateEffect(()=>{
975
+ if (!ref.current) {
976
+ return;
977
+ }
978
+ if (scrollTimeoutRef.current) {
979
+ clearTimeout(scrollTimeoutRef.current);
980
+ }
981
+ isScrollingProgrammatically.current = true;
982
+ ref.current.children[scrollTargetIndex]?.scrollIntoView({
983
+ behavior: 'smooth',
984
+ inline: 'start',
985
+ block: 'nearest'
986
+ });
987
+ if (prevIndex.current !== scrollTargetIndex && onChange) {
988
+ onChange({
989
+ index: scrollTargetIndex,
990
+ id: ref.current.children[scrollTargetIndex]?.id,
991
+ prevIndex: prevIndex.current,
992
+ prevId: ref.current.children[prevIndex.current]?.id
993
+ });
994
+ }
995
+ prevIndex.current = scrollTargetIndex;
996
+ scrollTimeoutRef.current = setTimeout(()=>{
997
+ isScrollingProgrammatically.current = false;
998
+ scrollTimeoutRef.current = null;
999
+ processQueue(); // Process any queued scrolls
1000
+ }, 500);
1001
+ }, [
1002
+ scrollTargetIndex
1003
+ ]);
1004
+ // Clean up timeout on unmount
1365
1005
  useEffect(()=>{
1366
- const { matches: userPrefersReducedMotion } = matchMedia('(prefers-reduced-motion: reduce)');
1367
- setUserPrefersReducedMotion(userPrefersReducedMotion);
1368
- // Autoplay the video if the user does not prefer reduced motion
1369
- setShouldPlay(!userPrefersReducedMotion);
1006
+ return ()=>{
1007
+ if (scrollTimeoutRef.current) {
1008
+ clearTimeout(scrollTimeoutRef.current);
1009
+ }
1010
+ };
1370
1011
  }, []);
1371
- // Follow google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
1372
- // "Don't assume a video will play, and don't show a pause button when the video is not actually playing."
1373
- // "You should always look at the Promise returned by the play function to see if it was rejected:"
1374
- // This is why we use the promise returned by the play function, and an extra state variable to determine if the video is actually playing or not
1375
- useEffect(()=>{
1376
- if (!videoRef.current) return;
1377
- if (shouldPlay) {
1378
- videoRef.current.play().then(()=>setIsPlaying(true)).catch(()=>setIsPlaying(false));
1012
+ const onScroll = useDebouncedCallback((event)=>{
1013
+ // Ignore scroll events when we're programmatically scrolling
1014
+ if (isScrollingProgrammatically.current) {
1015
+ return;
1016
+ }
1017
+ const target = event.target;
1018
+ const containerRect = target.getBoundingClientRect();
1019
+ // Calculate the index of the item that is currently in view
1020
+ const newScrollTargetIndex = Array.from(target.children).findIndex((child)=>{
1021
+ const rect = child.getBoundingClientRect();
1022
+ // Check if the item is more than 50% visible within the container
1023
+ const visibleWidth = Math.min(rect.right, containerRect.right) - Math.max(rect.left, containerRect.left);
1024
+ const itemWidth = rect.width;
1025
+ return visibleWidth / itemWidth > 0.5;
1026
+ });
1027
+ if (newScrollTargetIndex !== -1 && newScrollTargetIndex !== scrollTargetIndex) {
1028
+ if (onChange) {
1029
+ onChange({
1030
+ index: newScrollTargetIndex,
1031
+ id: target.children[newScrollTargetIndex]?.id,
1032
+ prevIndex: prevIndex.current,
1033
+ prevId: target.children[prevIndex.current]?.id
1034
+ });
1035
+ }
1036
+ // Update the index and prevIndex
1037
+ setScrollTargetIndex(newScrollTargetIndex);
1038
+ prevIndex.current = newScrollTargetIndex;
1039
+ }
1040
+ }, 150);
1041
+ const handlePrevious = ()=>{
1042
+ const targetIndex = scrollTargetIndex - 1;
1043
+ if (targetIndex < 0) return;
1044
+ if (isScrollingProgrammatically.current) {
1045
+ // If we're already scrolling, queue this action
1046
+ scrollQueue.current = [
1047
+ targetIndex
1048
+ ];
1379
1049
  } else {
1380
- videoRef.current.pause();
1381
- setIsPlaying(false);
1050
+ setScrollTargetIndex(targetIndex);
1382
1051
  }
1383
- }, [
1384
- shouldPlay
1385
- ]);
1386
- return /*#__PURE__*/ jsxs("div", {
1387
- className: cx(className, 'relative', userPrefersReducedMotion === null && 'opacity-0'),
1388
- children: [
1389
- /*#__PURE__*/ jsx("video", {
1390
- "aria-hidden": true,
1391
- ref: videoRef,
1392
- // cursor-pointer is not working on the button below, so we add it here for the same effect
1393
- className: "h-full max-h-[inherit] w-full cursor-pointer rounded-[inherit] object-cover",
1394
- playsInline: true,
1395
- loop: userPrefersReducedMotion === false,
1396
- autoPlay: userPrefersReducedMotion === false,
1397
- muted: true,
1398
- onEnded: (event)=>{
1399
- if (userPrefersReducedMotion) {
1400
- // Reset the video to the beginning if the user prefers reduced motion, since the video will not loop
1401
- event.currentTarget.currentTime = 0;
1402
- setShouldPlay(false);
1403
- setIsPlaying(false);
1052
+ };
1053
+ const handleNext = ()=>{
1054
+ if (!ref.current) return;
1055
+ const targetIndex = scrollTargetIndex + 1;
1056
+ if (targetIndex >= ref.current.children.length) return;
1057
+ if (isScrollingProgrammatically.current) {
1058
+ // If we're already scrolling, queue this action
1059
+ scrollQueue.current = [
1060
+ targetIndex
1061
+ ];
1062
+ } else {
1063
+ setScrollTargetIndex(targetIndex);
1064
+ }
1065
+ };
1066
+ return /*#__PURE__*/ jsx("div", {
1067
+ "data-slot": "carousel",
1068
+ children: /*#__PURE__*/ jsx(Provider, {
1069
+ values: [
1070
+ [
1071
+ CarouselItemsContext,
1072
+ {
1073
+ ref,
1074
+ onScroll
1404
1075
  }
1405
- },
1406
- children: /*#__PURE__*/ jsx("source", {
1407
- src: src,
1408
- type: `video/${format}`
1409
- })
1410
- }),
1411
- userPrefersReducedMotion !== null && /*#__PURE__*/ jsx("button", {
1412
- "data-slot": "video-loop-button",
1413
- "aria-hidden": true,
1414
- type: "button",
1415
- onClick: ()=>setShouldPlay((prevState)=>!prevState),
1416
- className: cx('absolute top-0 right-0 bottom-0 left-0 m-auto grid place-items-center', 'focus-visible:outline-focus focus-visible:outline-focus-offset', 'rounded-[inherit]', // Setting the opacity to 0 before applying the transition below will ensure the button only fades in after the video has started playing
1417
- shouldPlay && 'opacity-0', isPlaying && [
1418
- 'transition-opacity duration-200',
1419
- // Only show the pause button when the video is hovered or focused
1420
- 'focus-visible:opacity-100',
1421
- 'hover:opacity-100'
1076
+ ],
1077
+ [
1078
+ ButtonContext,
1079
+ {
1080
+ slots: {
1081
+ [DEFAULT_SLOT]: {},
1082
+ prev: {
1083
+ 'aria-label': previous[locale],
1084
+ onPress: handlePrevious
1085
+ },
1086
+ next: {
1087
+ isIconOnly: true,
1088
+ 'aria-label': next[locale],
1089
+ onPress: handleNext
1090
+ }
1091
+ }
1092
+ }
1093
+ ]
1094
+ ],
1095
+ children: /*#__PURE__*/ jsxs("div", {
1096
+ ...rest,
1097
+ 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
1098
+ '[&: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
1099
+ '**:data-[slot="video-loop-button"]:focus-visible:outline-none'),
1100
+ children: [
1101
+ children,
1102
+ /*#__PURE__*/ jsxs(_CarouselControls, {
1103
+ children: [
1104
+ /*#__PURE__*/ jsx(Button, {
1105
+ isIconOnly: true,
1106
+ slot: "prev",
1107
+ variant: "primary",
1108
+ color: "white",
1109
+ className: cx('group/carousel-previous', hasReachedScrollStart && 'invisible'),
1110
+ children: /*#__PURE__*/ jsx(ChevronLeft, {
1111
+ className: "group-hover/carousel-previous:motion-safe:-translate-x-1 transition-transform"
1112
+ })
1113
+ }),
1114
+ /*#__PURE__*/ jsx(Button, {
1115
+ isIconOnly: true,
1116
+ slot: "next",
1117
+ variant: "primary",
1118
+ color: "white",
1119
+ className: cx('group/carousel-next', hasReachedScrollEnd && 'invisible'),
1120
+ children: /*#__PURE__*/ jsx(ChevronRight, {
1121
+ className: "transition-transform group-hover/carousel-next:motion-safe:translate-x-1"
1122
+ })
1123
+ })
1124
+ ]
1125
+ })
1126
+ ]
1127
+ })
1128
+ })
1129
+ });
1130
+ };
1131
+ /**
1132
+ * 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.
1133
+ * It is used to render the prev/next buttons in the carousel for now.
1134
+ */ const _CarouselControls = ({ children, className })=>/*#__PURE__*/ jsx("div", {
1135
+ className: cx(className, 'absolute right-6 bottom-6 flex gap-x-2', // Make it easier to position in full-bleed hero variants (these style have no other side effects)
1136
+ 'items-end *:h-fit'),
1137
+ "data-slot": "carousel-controls",
1138
+ children: children
1139
+ });
1140
+ const CarouselItemsContext = /*#__PURE__*/ createContext({
1141
+ ref: null
1142
+ });
1143
+ const CarouselItems = ({ className, children })=>{
1144
+ const handleKeyDown = (event)=>{
1145
+ // Prevent default behavior when holding down arrow keys (when repeat is true)
1146
+ // The default behavior in scroll snapping causes a staggering scroll effect that feels janky
1147
+ if (event.repeat && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
1148
+ event.preventDefault();
1149
+ }
1150
+ };
1151
+ return /*#__PURE__*/ jsx(CarouselItemsContext.Consumer, {
1152
+ children: ({ ref, onScroll })=>// biome-ignore lint/a11y/noStaticElementInteractions: The keydown handler is only to prevent undesired scrolling behavior when using the arrow keys
1153
+ /*#__PURE__*/ jsx("div", {
1154
+ "data-slot": "carousel-items",
1155
+ className: cx(className, [
1156
+ 'scrollbar-hidden',
1157
+ 'flex',
1158
+ 'snap-x',
1159
+ 'snap-mandatory',
1160
+ 'overflow-x-auto',
1161
+ 'outline-none',
1162
+ 'rounded-[inherit]'
1422
1163
  ]),
1423
- children: /*#__PURE__*/ jsx("span", {
1424
- className: "grid h-12 w-12 place-items-center rounded-full bg-white outline-hidden",
1425
- children: isPlaying ? /*#__PURE__*/ jsx(PlayerPause, {}) : /*#__PURE__*/ jsx(PlayerPlay, {})
1426
- })
1427
- }),
1428
- alt && /*#__PURE__*/ jsx("p", {
1429
- className: "sr-only",
1430
- children: alt
1164
+ ref: ref,
1165
+ onScroll: onScroll,
1166
+ onKeyDown: handleKeyDown,
1167
+ children: children
1431
1168
  })
1432
- ]
1169
+ });
1170
+ };
1171
+ const CarouselItem = ({ className, children, id })=>{
1172
+ return /*#__PURE__*/ jsx("div", {
1173
+ className: cx(className, 'shrink-0 basis-full snap-start'),
1174
+ "data-slot": "carousel-item",
1175
+ id: id,
1176
+ children: /*#__PURE__*/ jsx(Provider, {
1177
+ values: [
1178
+ [
1179
+ MediaContext,
1180
+ {
1181
+ fit: 'cover',
1182
+ className: cx('data-[fit="contain"]:bg-blue-dark', '*:h-full *:w-full', 'aspect-square max-sm:data-[fit="contain"]:*:object-cover sm:aspect-4/3 md:aspect-3/2 lg:aspect-2/1')
1183
+ }
1184
+ ]
1185
+ ],
1186
+ children: children
1187
+ })
1433
1188
  });
1434
1189
  };
1435
1190
 
1436
- const disclosureButtonVariants = cva({
1191
+ const formField = cx('group flex flex-col gap-2');
1192
+ 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');
1193
+ const input = cva({
1437
1194
  base: [
1438
- 'inline-flex cursor-pointer items-center justify-between rounded-lg focus-visible:outline-current focus-visible:outline-focus',
1439
- // Ensure a minimum click area of 44x44px, while making it look like it only has the size of the content
1440
- '-m-2.5 p-2.5 focus-visible:outline-offset-[-0.625rem]'
1195
+ // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
1196
+ 'bg-white',
1197
+ // Use box-content to enable auto width based on number of characters (size)
1198
+ // Setting min-height to prevent the input from collapsing in Safari
1199
+ // Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
1200
+ 'box-content min-h-6 py-2.5',
1201
+ 'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
1202
+ // invalid styles
1203
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red',
1204
+ // Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
1205
+ 'appearance-none'
1441
1206
  ],
1442
1207
  variants: {
1443
- withChevron: {
1444
- true: '[&[aria-expanded="true"]_svg]:rotate-180',
1445
- false: null
1208
+ // Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
1209
+ focusModifier: {
1210
+ focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
1211
+ visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
1446
1212
  },
1447
- /**
1448
- * When the button is without text, but with a single icon.
1449
- * @default false
1450
- */ isIconOnly: {
1451
- true: '[&>svg]:h-7 [&>svg]:w-7',
1452
- false: 'gap-2.5'
1213
+ isGrouped: {
1214
+ false: 'px-3',
1215
+ true: '!ring-0 flex-1'
1453
1216
  }
1454
1217
  },
1455
1218
  defaultVariants: {
1456
- withChevron: false,
1457
- isIconOnly: false
1219
+ focusModifier: 'focus',
1220
+ isGrouped: false
1458
1221
  }
1459
1222
  });
1460
- const DisclosureButton = ({ className, withChevron, isIconOnly, children, ref: _ref, ...restProps })=>{
1461
- const [props, ref] = useContextProps(restProps, _ref, ButtonContext$1);
1462
- return /*#__PURE__*/ jsxs(Button$1, {
1463
- ...props,
1464
- ref: ref,
1465
- className: disclosureButtonVariants({
1466
- className,
1467
- withChevron,
1468
- isIconOnly
1469
- }),
1470
- slot: "trigger",
1223
+ const inputGroup = cx([
1224
+ 'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
1225
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
1226
+ ]);
1227
+ const dropdown = {
1228
+ 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'),
1229
+ // overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
1230
+ listbox: cx('max-h-[25rem] overflow-x-hidden text-sm outline-hidden'),
1231
+ chevronIcon: cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
1232
+ };
1233
+
1234
+ function ErrorMessage(props) {
1235
+ const { children, className, ...restProps } = props;
1236
+ return /*#__PURE__*/ jsx(Text, {
1237
+ ...restProps,
1238
+ className: cx(className, formFieldError),
1239
+ slot: "errorMessage",
1240
+ children: children
1241
+ });
1242
+ }
1243
+
1244
+ const defaultClasses$1 = cx([
1245
+ 'group -mx-2.5 relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4 p-2.5 leading-7'
1246
+ ]);
1247
+ // Pulling this out into it's own component. Will probably export it in the future
1248
+ // so it can be used in other views, outside of an input of type checkbox, like in table rows.
1249
+ function CheckmarkBox() {
1250
+ return /*#__PURE__*/ jsx("span", {
1251
+ className: cx([
1252
+ 'relative left-0 grid flex-none place-content-center rounded-sm border-2 border-black text-white',
1253
+ // to vertically align the radio we need to calculate the label's height, which is equal to it's font size multiplied by the line height.
1254
+ // For the ::before psuedo element the line height of the label is always 1em.
1255
+ // When we know the height of the label we use the height of the radio to push it down to align with the label's first line
1256
+ // TODO: 1.75 here is the unit less lineheight, altough we use 1.75rem as the line height, so there is a mismatch here. Revisit this when we've worked on typography in v2. Should this be a CSS custom property instead?
1257
+ 'mt-[calc((1em_*_1.75_-_24px)_/_2)] h-[24px] w-[24px]',
1258
+ // selected
1259
+ 'group-data-selected:group-not-data-hovered:group-not-data-invalid:border-blue group-data-selected:group-not-data-hovered:group-not-data-invalid:bg-blue',
1260
+ 'group-data-selected:group-not-data-hovered:group-data-invalid:border-red group-data-selected:group-not-data-hovered:group-data-invalid:bg-red',
1261
+ // focus
1262
+ 'group-data-focus-visible:outline-focus-offset',
1263
+ // hovered
1264
+ 'group-data-hovered:group-data-invalid:bg-red-light',
1265
+ 'group-data-hovered:border-blue',
1266
+ 'group-data-hovered:bg-sky',
1267
+ 'group-data-hovered:group-data-selected:group-not-data-invalid:border-blue-dark',
1268
+ 'group-data-hovered:group-data-selected:group-not-data-invalid:bg-blue-dark',
1269
+ // invalid - The border is 1 px thicker when invalid. We don't actually want to change the border width, as that causes the element's size to change
1270
+ // so we use an inner shadow of 1 px instead to pad the actual border
1271
+ 'group-data-invalid:border-red group-data-invalid:shadow-[inset_0_0_0_1px] group-data-invalid:shadow-red'
1272
+ ]),
1273
+ children: /*#__PURE__*/ jsx(Check, {
1274
+ className: "h-full w-full opacity-0 group-data-invalid:group-data-hovered:group-data-selected:text-red group-data-selected:opacity-100"
1275
+ })
1276
+ });
1277
+ }
1278
+ function Checkbox(props) {
1279
+ const { children, className, description, errorMessage, isInvalid: _isInvalid, ...restProps } = props;
1280
+ const id = useId();
1281
+ const descriptionId = `desc${id}`;
1282
+ const errorMessageId = `error${id}`;
1283
+ const isInvalid = !!errorMessage || _isInvalid;
1284
+ return /*#__PURE__*/ jsx("div", {
1285
+ children: /*#__PURE__*/ jsxs(CheckboxContext.Provider, {
1286
+ value: {
1287
+ 'aria-describedby': description ? descriptionId : undefined,
1288
+ 'aria-errormessage': errorMessage ? errorMessageId : undefined
1289
+ },
1290
+ children: [
1291
+ /*#__PURE__*/ jsxs(Checkbox$1, {
1292
+ ...restProps,
1293
+ className: cx(className, defaultClasses$1),
1294
+ isInvalid: isInvalid,
1295
+ children: [
1296
+ /*#__PURE__*/ jsx(CheckmarkBox, {}),
1297
+ children
1298
+ ]
1299
+ }),
1300
+ description && // {/* Use a div instead of the Description component to avoid infinite re-render loops in React until this bug in RAC is fixed: https://github.com/adobe/react-spectrum/issues/6229 */}
1301
+ /*#__PURE__*/ jsx("div", {
1302
+ id: descriptionId,
1303
+ slot: "description",
1304
+ className: "description block",
1305
+ children: description
1306
+ }),
1307
+ errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
1308
+ className: "mt-2 block",
1309
+ id: errorMessageId,
1310
+ children: errorMessage
1311
+ })
1312
+ ]
1313
+ })
1314
+ });
1315
+ }
1316
+
1317
+ function Description(props) {
1318
+ const { className, ...restProps } = props;
1319
+ return /*#__PURE__*/ jsx(Text, {
1320
+ ...restProps,
1321
+ className: cx(className, 'description'),
1322
+ slot: "description"
1323
+ });
1324
+ }
1325
+
1326
+ /**
1327
+ * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
1328
+ * In other words, this handles controlled and uncontrolled form errors.
1329
+ */ function ErrorMessageOrFieldError({ errorMessage }) {
1330
+ return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
1331
+ children: errorMessage
1332
+ }) : /*#__PURE__*/ jsx(FieldError, {
1333
+ className: formFieldError
1334
+ });
1335
+ }
1336
+
1337
+ function Label(props) {
1338
+ const { children, className, ...restProps } = props;
1339
+ return /*#__PURE__*/ jsx(Label$1, {
1340
+ className: cx(className, 'font-semibold leading-7'),
1341
+ ...restProps,
1342
+ children: children
1343
+ });
1344
+ }
1345
+
1346
+ function CheckboxGroup(props) {
1347
+ const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
1348
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
1349
+ // which will override any built in validation
1350
+ const isInvalid = !!errorMessage || _isInvalid;
1351
+ return /*#__PURE__*/ jsxs(CheckboxGroup$1, {
1352
+ ...restProps,
1353
+ className: cx(className, 'flex flex-col gap-2'),
1354
+ isInvalid: isInvalid,
1355
+ isRequired: isRequired,
1471
1356
  children: [
1357
+ label && /*#__PURE__*/ jsx(Label, {
1358
+ children: label
1359
+ }),
1360
+ description && /*#__PURE__*/ jsx(Description, {
1361
+ children: description
1362
+ }),
1472
1363
  children,
1473
- withChevron && /*#__PURE__*/ jsx(ChevronDown, {
1474
- className: "flex-none transition-transform duration-300 motion-reduce:transition-none"
1364
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1365
+ errorMessage: errorMessage
1475
1366
  })
1476
1367
  ]
1477
1368
  });
1478
- };
1479
- const DisclosureStateContext = /*#__PURE__*/ createContext(null);
1480
- const Disclosure = ({ ref: _ref, children, ..._props })=>{
1481
- const [props, ref] = useContextProps(_props, _ref, DisclosureContext);
1482
- const groupState = useContext(DisclosureGroupStateContext);
1483
- let { id, ...otherProps } = props;
1484
- const defaultId = useId();
1485
- id ||= defaultId;
1486
- const isExpanded = groupState ? groupState.expandedKeys.has(id) : props.isExpanded;
1487
- const state = useDisclosureState({
1488
- ...props,
1489
- isExpanded,
1490
- onExpandedChange (isExpanded) {
1491
- if (groupState) {
1492
- groupState.toggleKey(id);
1493
- }
1494
- props.onExpandedChange?.(isExpanded);
1495
- }
1369
+ }
1370
+
1371
+ function InputAddonDivider() {
1372
+ return /*#__PURE__*/ jsx("span", {
1373
+ className: "block h-6 w-px flex-none bg-black"
1496
1374
  });
1497
- const isDisabled = props.isDisabled || groupState?.isDisabled || false;
1498
- const domProps = filterDOMProps(otherProps);
1499
- const { isFocusVisible: isFocusVisibleWithin } = useFocusRing({
1500
- within: true
1375
+ }
1376
+
1377
+ const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
1378
+ ...restProps,
1379
+ className: cx(dropdown.listbox, className)
1501
1380
  });
1502
- const panelRef = useRef(null);
1503
- const { buttonProps, panelProps } = useDisclosure({
1381
+ const ListBoxItem = (props)=>{
1382
+ let textValue = props.textValue;
1383
+ // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
1384
+ // Since we use a render function (to handle the selected state) the child is never a string.
1385
+ // This condition adds back that behaviour
1386
+ if (textValue == null && typeof props.children === 'string') {
1387
+ textValue = props.children;
1388
+ }
1389
+ return /*#__PURE__*/ jsx(ListBoxItem$1, {
1504
1390
  ...props,
1505
- isExpanded,
1506
- isDisabled
1507
- }, state, panelRef);
1508
- const { role: _, ...propsWithoutRole } = panelProps;
1509
- return /*#__PURE__*/ jsx(Provider, {
1510
- values: [
1511
- [
1512
- DisclosureContext,
1513
- state
1514
- ],
1515
- [
1516
- ButtonContext$1,
1517
- {
1518
- slots: {
1519
- [DEFAULT_SLOT]: {},
1520
- trigger: buttonProps
1521
- }
1522
- }
1523
- ],
1524
- [
1525
- DisclosurePanelContext,
1526
- {
1527
- ...propsWithoutRole,
1528
- panelRef
1529
- }
1530
- ],
1531
- [
1532
- DisclosureStateContext,
1533
- state
1534
- ]
1535
- ],
1536
- children: /*#__PURE__*/ jsx("div", {
1537
- ...domProps,
1538
- className: otherProps.className,
1539
- ref: ref,
1540
- "data-focus-visible-within": isFocusVisibleWithin || undefined,
1541
- "data-expanded": state.isExpanded || undefined,
1542
- "data-disabled": isDisabled || undefined,
1543
- children: typeof children === 'function' ? children({
1544
- isExpanded: state.isExpanded,
1545
- isFocusVisibleWithin,
1546
- isDisabled,
1547
- state,
1548
- defaultChildren: null
1549
- }) : children
1550
- })
1391
+ className: cx(props.className, 'flex cursor-pointer px-6 py-3 leading-6 outline-none data-focused:bg-sky-lightest'),
1392
+ textValue: textValue,
1393
+ children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
1394
+ children: [
1395
+ isSelected && /*#__PURE__*/ jsx(Check, {
1396
+ className: "-ml-6 text-base"
1397
+ }),
1398
+ props.children
1399
+ ]
1400
+ })
1551
1401
  });
1552
1402
  };
1553
- const DisclosurePanelContext = /*#__PURE__*/ createContext({});
1554
- const DisclosurePanel = ({ ref, children, ...props })=>{
1555
- const disclosureContext = useContext(DisclosureContext);
1556
- const { panelProps, panelRef } = useContext(DisclosurePanelContext);
1557
- const { role: _role = 'group', className, ...restProps } = props;
1558
- const ariaLabelledby = props['aria-labelledby'] ?? restProps['aria-labelledby'];
1559
- const isWithoutRole = _role === 'none';
1560
- const role = isWithoutRole ? undefined : _role;
1561
- const { isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps } = useFocusRing({
1562
- within: true
1403
+ /**
1404
+ * This component can be used to group items in a listbox
1405
+ */ const ListBoxSection = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBoxSection$1, {
1406
+ ...restProps,
1407
+ // The :not(:first-child) selector adds extra spacing to all the options, but not the section (group) headings
1408
+ // This way we get the desired extra indent on all options within a group
1409
+ className: cx(className, 'pb-1 [&>:not(:first-child)]:pl-10')
1563
1410
  });
1564
- const domProps = filterDOMProps(props);
1565
- return /*#__PURE__*/ jsx("div", {
1566
- className: cx('grid transition-all duration-300 motion-reduce:transition-none', disclosureContext?.isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'),
1567
- children: /*#__PURE__*/ jsx("div", {
1568
- className: "overflow-hidden",
1569
- children: /*#__PURE__*/ jsx("div", {
1570
- ref: mergeRefs(ref, panelRef),
1571
- ...mergeProps(panelProps, focusWithinProps),
1572
- ...restProps,
1573
- ...domProps,
1574
- "data-focus-visible-within": isFocusVisibleWithin || undefined,
1575
- className: cx(className, '[content-visibility:visible]'),
1576
- role: role,
1577
- "aria-labelledby": isWithoutRole ? undefined : ariaLabelledby,
1578
- children: /*#__PURE__*/ jsx(Provider, {
1579
- values: [
1580
- // Reset the context to avoid passing the same context to children, in case of nested Disclosures
1581
- [
1582
- DisclosureContext,
1583
- null
1584
- ],
1585
- [
1586
- ButtonContext$1,
1587
- null
1588
- ]
1589
- ],
1411
+ /**
1412
+ * This component can be used to label grouped items in a `ListBoxSection` with a heading
1413
+ */ const ListBoxHeader = (props)=>/*#__PURE__*/ jsx(Header, {
1414
+ ...props,
1415
+ className: cx(props.className, 'mx-6 cursor-default py-2 font-medium text-blue-dark leading-6')
1416
+ });
1417
+
1418
+ function Combobox(props) {
1419
+ const { className, children, description, errorMessage, isPending, label, isInvalid: _isInvalid, ref, ...restProps } = props;
1420
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
1421
+ // which will override any built in validation
1422
+ const isInvalid = !!errorMessage || _isInvalid;
1423
+ return /*#__PURE__*/ jsxs(ComboBox, {
1424
+ ...restProps,
1425
+ className: cx(className, formField),
1426
+ isInvalid: isInvalid,
1427
+ children: [
1428
+ label && /*#__PURE__*/ jsx(Label, {
1429
+ children: label
1430
+ }),
1431
+ description && /*#__PURE__*/ jsx(Description, {
1432
+ children: description
1433
+ }),
1434
+ /*#__PURE__*/ jsxs(Group, {
1435
+ className: inputGroup,
1436
+ children: [
1437
+ /*#__PURE__*/ jsx(Input, {
1438
+ className: input({
1439
+ isGrouped: true
1440
+ }),
1441
+ ref: ref
1442
+ }),
1443
+ /*#__PURE__*/ jsx(Button$1, {
1444
+ children: isPending ? /*#__PURE__*/ jsx(LoadingSpinner, {
1445
+ className: "animate-spin"
1446
+ }) : /*#__PURE__*/ jsx(ChevronDown, {
1447
+ className: dropdown.chevronIcon
1448
+ })
1449
+ })
1450
+ ]
1451
+ }),
1452
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1453
+ errorMessage: errorMessage
1454
+ }),
1455
+ /*#__PURE__*/ jsx(Popover, {
1456
+ // FIXME: The trigger width doesn't include the padding of the group, so for now we have to apply this workaround.
1457
+ // Also... the combobox border gets a pixel wider when focused, so we account for that as well when calculating the width
1458
+ // and the offset.
1459
+ // The input gutter should probably be moved to a theme variable instead of using the hardcoded value as here.
1460
+ className: cx(dropdown.popover, 'min-w-[calc(var(--trigger-width)+26px)]'),
1461
+ crossOffset: -13,
1462
+ children: /*#__PURE__*/ jsx(ListBox, {
1463
+ className: dropdown.listbox,
1590
1464
  children: children
1591
1465
  })
1592
1466
  })
1593
- })
1467
+ ]
1594
1468
  });
1469
+ }
1470
+
1471
+ /**
1472
+ * A React component that wraps https://react-spectrum.adobe.com/react-aria/useDateFormatter.html
1473
+ * By default it sets the timeZone to `Europe/Berlin` to prevent the server's timezone from affecting
1474
+ * the localized format
1475
+ */ const DateFormatter = ({ options: _options, value, children: render })=>{
1476
+ const options = {
1477
+ timeZone: 'Europe/Berlin',
1478
+ ..._options
1479
+ };
1480
+ const formatter = useDateFormatter(options);
1481
+ const date = typeof value === 'string' ? new Date(value) : value;
1482
+ const formatted = formatter.format(date);
1483
+ return render ? render(formatted) : formatted;
1595
1484
  };
1596
1485
 
1597
1486
  /**
@@ -1807,222 +1696,66 @@ const FileUpload = ({ children, files: _files, onChange, validate, isInvalid: _i
1807
1696
  ref: inputRef,
1808
1697
  // Delegate focus to the button when the hidden file input is focused (for RAC auto-focusing behavior)
1809
1698
  onFocus: ()=>buttonRef.current?.focus(),
1810
- children: children
1811
- })
1812
- }),
1813
- controlledOrUncontrolledFiles.length > 0 && /*#__PURE__*/ jsx("ul", {
1814
- className: "mt-4 grid max-w-fit gap-y-2",
1815
- children: controlledOrUncontrolledFiles.map((file, fileIndex)=>{
1816
- let fileName = file.name;
1817
- if (fileTriggerProps.acceptDirectory && file.webkitRelativePath !== '') {
1818
- fileName = file.webkitRelativePath;
1819
- }
1820
- const validation = validate?.(file) ?? true;
1821
- const hasError = validation !== true;
1822
- return /*#__PURE__*/ jsxs("li", {
1823
- children: [
1824
- /*#__PURE__*/ jsxs("div", {
1825
- className: cx('flex items-center justify-between gap-2 rounded-lg border-2 px-4 py-2', hasError ? 'border-red bg-red-light' : 'border-gray bg-gray-lightest'),
1826
- children: [
1827
- fileName,
1828
- /*#__PURE__*/ jsx("button", {
1829
- className: cx('self-start', '-m-2 grid h-11 w-11 shrink-0 cursor-pointer place-items-center rounded-xl', // Focus styles
1830
- 'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
1831
- onClick: ()=>{
1832
- // For controlled component
1833
- onChange?.((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1834
- // For internal file state
1835
- setFiles((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1836
- // Make sure screen readers doesn't loose track of focus
1837
- // (without this, the focus will be set to the top of the page for screen readers)
1838
- buttonRef.current?.focus();
1839
- },
1840
- "aria-label": translations.remove[locale],
1841
- type: "button",
1842
- children: /*#__PURE__*/ jsx(Trash, {})
1843
- })
1844
- ]
1845
- }),
1846
- hasError && /*#__PURE__*/ jsx(ErrorMessage, {
1847
- className: "mt-1 block w-full",
1848
- children: validation
1849
- })
1850
- ]
1851
- }, fileName);
1852
- })
1853
- }),
1854
- (controlledOrUncontrolledFiles.length === 0 || !!errorMessage) && /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1855
- errorMessage: errorMessage
1856
- })
1857
- ]
1858
- })
1859
- });
1860
- };
1861
-
1862
- const baseClassName = 'h-20 w-20 shrink-0 rounded-full';
1863
- const Avatar = ({ src, alt = '', className, onError, loading = 'lazy', ...rest })=>{
1864
- const [hasError, setHasError] = useState(false);
1865
- const hasValidImage = !hasError && src;
1866
- return hasValidImage ? /*#__PURE__*/ jsx("img", {
1867
- ...rest,
1868
- src: src,
1869
- alt: alt,
1870
- loading: loading,
1871
- className: cx(className, baseClassName, 'object-cover'),
1872
- onError: (event)=>{
1873
- onError?.(event);
1874
- setHasError(true);
1875
- }
1876
- }) : /*#__PURE__*/ jsx("div", {
1877
- className: cx(className, baseClassName, 'grid place-items-center bg-gray-light text-gray-dark'),
1878
- children: /*#__PURE__*/ jsx(User, {
1879
- className: "scale-[2.25]"
1880
- })
1881
- });
1882
- };
1883
-
1884
- const DialogTrigger = (props)=>/*#__PURE__*/ jsx(DialogTrigger$1, {
1885
- ...props
1886
- });
1887
- const ModalOverlay = (props)=>/*#__PURE__*/ jsx(ModalOverlay$1, {
1888
- ...props,
1889
- isDismissable: true,
1890
- className: ({ isEntering, isExiting })=>cx('fixed inset-0 z-10 flex min-h-full items-center justify-center overflow-y-auto bg-black/25 p-4 text-center backdrop-blur-sm', isEntering && 'fade-in animate-in duration-300 ease-out', isExiting && 'fade-out animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1891
- 'motion-reduce:animate-none')
1892
- });
1893
- const Modal = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ModalOverlay, {
1894
- children: /*#__PURE__*/ jsx(Modal$1, {
1895
- ...restProps,
1896
- className: ({ isEntering, isExiting })=>cx(className, 'w-full max-w-md overflow-hidden rounded-2xl bg-white p-4 text-left align-middle shadow-xl', isEntering && 'zoom-in-95 animate-in duration-300 ease-out', isExiting && 'zoom-out-95 animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1897
- 'motion-reduce:animate-none')
1898
- })
1899
- });
1900
- const Dialog = ({ className, children, ...restProps })=>{
1901
- const locale = _useLocale();
1902
- return /*#__PURE__*/ jsx(Dialog$1, {
1903
- ...restProps,
1904
- className: cx('relative grid gap-y-5 outline-none', // Footer
1905
- '[&_[data-slot="footer"]]:flex [&_[data-slot="footer"]]:gap-x-2'),
1906
- children: ({ close })=>/*#__PURE__*/ jsx(Fragment, {
1907
- children: /*#__PURE__*/ jsx(Provider, {
1908
- values: [
1909
- [
1910
- HeadingContext,
1911
- {
1912
- slots: {
1913
- [DEFAULT_SLOT]: {},
1914
- title: {
1915
- className: 'heading-s',
1916
- _outerWrapper: (children)=>/*#__PURE__*/ jsxs("div", {
1917
- className: "flex items-center justify-between gap-x-2",
1918
- children: [
1919
- children,
1920
- /*#__PURE__*/ jsx(Button, {
1921
- slot: "close" // RAC Dialog suppors one close button out of the box, so we utilize that here. For other close buttons we use ButtonContext
1922
- ,
1923
- variant: "tertiary",
1924
- className: "!px-2.5 data-focus-visible:outline-focus-inset",
1925
- "aria-label": translations$1.close[locale],
1926
- children: /*#__PURE__*/ jsx(Close, {})
1927
- })
1928
- ]
1929
- })
1930
- }
1931
- }
1932
- }
1933
- ],
1934
- [
1935
- ButtonContext$1,
1936
- {
1937
- // This is necessary to support multiple close buttons
1938
- slots: {
1939
- // We need to define default slot in order to also support non-slotted buttons (i.e. buttons without slot prop)
1940
- [DEFAULT_SLOT]: {
1941
- className: 'w-fit'
1942
- },
1943
- close: {
1944
- onPress: close,
1945
- className: 'w-fit'
1946
- }
1947
- }
1948
- }
1949
- ]
1950
- ],
1951
- children: children
1699
+ children: children
1700
+ })
1701
+ }),
1702
+ controlledOrUncontrolledFiles.length > 0 && /*#__PURE__*/ jsx("ul", {
1703
+ className: "mt-4 grid max-w-fit gap-y-2",
1704
+ children: controlledOrUncontrolledFiles.map((file, fileIndex)=>{
1705
+ let fileName = file.name;
1706
+ if (fileTriggerProps.acceptDirectory && file.webkitRelativePath !== '') {
1707
+ fileName = file.webkitRelativePath;
1708
+ }
1709
+ const validation = validate?.(file) ?? true;
1710
+ const hasError = validation !== true;
1711
+ return /*#__PURE__*/ jsxs("li", {
1712
+ children: [
1713
+ /*#__PURE__*/ jsxs("div", {
1714
+ className: cx('flex items-center justify-between gap-2 rounded-lg border-2 px-4 py-2', hasError ? 'border-red bg-red-light' : 'border-gray bg-gray-lightest'),
1715
+ children: [
1716
+ fileName,
1717
+ /*#__PURE__*/ jsx("button", {
1718
+ className: cx('self-start', '-m-2 grid h-11 w-11 shrink-0 cursor-pointer place-items-center rounded-xl', // Focus styles
1719
+ 'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
1720
+ onClick: ()=>{
1721
+ // For controlled component
1722
+ onChange?.((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1723
+ // For internal file state
1724
+ setFiles((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1725
+ // Make sure screen readers doesn't loose track of focus
1726
+ // (without this, the focus will be set to the top of the page for screen readers)
1727
+ buttonRef.current?.focus();
1728
+ },
1729
+ "aria-label": translations.remove[locale],
1730
+ type: "button",
1731
+ children: /*#__PURE__*/ jsx(Trash, {})
1732
+ })
1733
+ ]
1734
+ }),
1735
+ hasError && /*#__PURE__*/ jsx(ErrorMessage, {
1736
+ className: "mt-1 block w-full",
1737
+ children: validation
1738
+ })
1739
+ ]
1740
+ }, fileName);
1741
+ })
1742
+ }),
1743
+ (controlledOrUncontrolledFiles.length === 0 || !!errorMessage) && /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1744
+ errorMessage: errorMessage
1952
1745
  })
1953
- })
1746
+ ]
1747
+ })
1954
1748
  });
1955
1749
  };
1956
1750
 
1957
- const tagVariants = cva({
1958
- base: [
1959
- 'relative flex cursor-pointer items-center gap-1 rounded-lg px-2 py-1 font-medium text-sm transition-colors duration-200',
1960
- // Resting
1961
- 'border-2 border-black bg-white text-black',
1962
- //Focus
1963
- 'focus-visible:outline-focus-offset',
1964
- // Hover
1965
- ' data-hovered:bg-sky',
1966
- // Selected
1967
- // Allows removing
1968
- 'data-allows-removing:border-transparent',
1969
- 'data-allows-removing:bg-blue',
1970
- 'data-allows-removing:data-hovered:bg-blue-dark',
1971
- 'data-allows-removing:text-white',
1972
- // Selected
1973
- 'aria-selected:border-transparent',
1974
- 'aria-selected:bg-blue',
1975
- 'aria-selected:data-hovered:bg-blue-dark',
1976
- 'aria-selected:text-white',
1977
- //Icons
1978
- '[&_svg]:h-4 [&_svg]:w-4'
1979
- ]
1980
- });
1981
- /**
1982
- * A group component for Tag components that enables selection and organization of options.
1983
- */ function TagGroup(props) {
1984
- const { onRemove, selectionMode = 'single', className, children, ...restProps } = props;
1985
- return /*#__PURE__*/ jsx(TagGroup$1, {
1986
- ...restProps,
1987
- className: className,
1988
- selectionMode: onRemove ? 'none' : selectionMode,
1989
- onRemove: onRemove,
1990
- children: children
1991
- });
1992
- }
1993
- /**
1994
- * A container component for Tag components within a TagGroup.
1995
- */ function TagList(props) {
1996
- const { className, children, ...restProps } = props;
1997
- return /*#__PURE__*/ jsx(TagList$1, {
1998
- ...restProps,
1999
- className: cx('flex flex-wrap gap-2', className),
2000
- children: children
2001
- });
2002
- }
2003
- /**
2004
- * Interactive tag component for selections, filtering, and categorization.
2005
- */ function Tag(props) {
2006
- const { className, children, ...restProps } = props;
2007
- const textValue = typeof children === 'string' ? children : undefined;
2008
- return /*#__PURE__*/ jsx(Tag$1, {
2009
- className: tagVariants({
2010
- className
2011
- }),
2012
- textValue: textValue,
2013
- ...restProps,
2014
- children: ({ allowsRemoving })=>allowsRemoving ? /*#__PURE__*/ jsxs(Fragment, {
2015
- children: [
2016
- children,
2017
- /*#__PURE__*/ jsx(Button$1, {
2018
- className: "cursor-pointer outline-none after:absolute after:top-0 after:right-0 after:bottom-0 after:left-0",
2019
- slot: "remove",
2020
- children: /*#__PURE__*/ jsx(Close, {
2021
- className: "ml-1"
2022
- })
2023
- })
2024
- ]
2025
- }) : children
1751
+ function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
1752
+ return /*#__PURE__*/ jsx(I18nProvider, {
1753
+ locale: locale,
1754
+ children: navigate ? /*#__PURE__*/ jsx(RouterProvider, {
1755
+ navigate: navigate,
1756
+ useHref: useHref,
1757
+ children: children
1758
+ }) : children
2026
1759
  });
2027
1760
  }
2028
1761
 
@@ -2103,7 +1836,7 @@ const variants = cva({
2103
1836
  variant: 'standard'
2104
1837
  }
2105
1838
  });
2106
- const Hero = ({ variant, className, children })=>{
1839
+ const Hero = ({ variant, className, children, ...rest })=>{
2107
1840
  const variantsClassName = variants({
2108
1841
  variant,
2109
1842
  className
@@ -2131,188 +1864,317 @@ const Hero = ({ variant, className, children })=>{
2131
1864
  ],
2132
1865
  children: /*#__PURE__*/ jsx("div", {
2133
1866
  className: cx(variantsClassName, className),
1867
+ ...rest,
2134
1868
  children: children
2135
1869
  })
2136
1870
  });
2137
1871
  };
2138
1872
 
2139
- const Carousel = ({ className, children, onChange })=>{
2140
- const ref = useRef(null);
2141
- const locale = _useLocale();
2142
- const { previous, next } = translations$1;
2143
- const [scrollTargetIndex, setScrollTargetIndex] = useState(0);
2144
- const [hasReachedScrollStart, setHasReachedScrollStart] = useState(scrollTargetIndex === 0);
2145
- const [hasReachedScrollEnd, setHasReachedScrollEnd] = useState(!ref.current || ref.current.children.length - 1 === scrollTargetIndex);
2146
- useEffect(()=>{
2147
- setHasReachedScrollStart(scrollTargetIndex === 0);
2148
- setHasReachedScrollEnd(!ref.current || ref.current.children.length - 1 === scrollTargetIndex);
2149
- }, [
2150
- scrollTargetIndex
2151
- ]);
2152
- // Keep track of the previous index to determine if the user is scrolling forward or backward
2153
- // This is used to determine which callback to call (onPrev or onNext)
2154
- const prevIndex = useRef(0);
2155
- // Handle scrolling when user clicks the arrow icons
2156
- useUpdateEffect(()=>{
2157
- if (!ref.current) return;
2158
- ref.current.children[scrollTargetIndex]?.scrollIntoView({
2159
- behavior: 'smooth',
2160
- inline: 'start',
2161
- block: 'nearest'
2162
- });
2163
- if (prevIndex.current !== scrollTargetIndex && onChange) {
2164
- onChange({
2165
- index: scrollTargetIndex,
2166
- id: ref.current.children[scrollTargetIndex]?.id,
2167
- prevIndex: prevIndex.current,
2168
- prevId: ref.current.children[prevIndex.current]?.id
2169
- });
2170
- }
2171
- prevIndex.current = scrollTargetIndex;
2172
- }, [
2173
- scrollTargetIndex
2174
- ]);
2175
- const onScroll = useDebouncedCallback((event)=>{
2176
- const target = event.target;
2177
- // Calculate the index of the item that is currently in view
2178
- const newScrollTargetIndex = Array.from(target.children).findIndex((child)=>{
2179
- const rect = child.getBoundingClientRect();
2180
- return rect.left >= 0 && rect.right <= window.innerWidth && rect.top >= 0;
2181
- });
2182
- if (newScrollTargetIndex !== -1) {
2183
- setScrollTargetIndex(newScrollTargetIndex);
2184
- }
2185
- }, 100);
1873
+ /**
1874
+ * A basic link component that extends react-aria-components Link with consistent styling.
1875
+ * Provides accessible focus styles and maintains design system consistency.
1876
+ */ const CustomLink = ({ children, className, ...restProps })=>{
1877
+ return /*#__PURE__*/ jsx(Link, {
1878
+ ...restProps,
1879
+ className: cx(className, 'inline-flex cursor-pointer items-center gap-1 font-medium hover:no-underline focus-visible:outline-current focus-visible:outline-focus-offset [&>svg]:shrink-0'),
1880
+ children: children
1881
+ });
1882
+ };
1883
+
1884
+ const LinkList = ({ className, children, ...restProps })=>{
1885
+ const numberofLinks = Children.count(children);
2186
1886
  return /*#__PURE__*/ jsx("div", {
2187
- "data-slot": "carousel",
2188
- children: /*#__PURE__*/ jsx(Provider, {
2189
- values: [
2190
- [
2191
- CarouselItemsContext,
2192
- {
2193
- ref,
2194
- onScroll
2195
- }
2196
- ],
2197
- [
2198
- ButtonContext,
2199
- {
2200
- slots: {
2201
- prev: {
2202
- 'aria-label': previous[locale],
2203
- onPress: ()=>{
2204
- if (scrollTargetIndex > 0) {
2205
- setScrollTargetIndex((prev)=>prev - 1);
1887
+ className: cx(className, '@container'),
1888
+ ...restProps,
1889
+ children: /*#__PURE__*/ jsx("ul", {
1890
+ className: cx('min-w-fit', // Hide dividers at the top of the list (overflow-y) and prevents arrow icon from overflowing container when animated to the right (overflow-x)
1891
+ 'overflow-hidden', // Add a small gap between items that fits the divider lines (this way the divider line don't take up any space in each item)
1892
+ 'grid auto-rows-max gap-y-0.25', // Gaps for when the list is displayed in multiple columns
1893
+ '@lg:gap-x-12 @md:gap-x-9 @sm:gap-x-4 @xl:gap-x-16', numberofLinks > 5 && [
1894
+ '@xl:grid-cols-2',
1895
+ (numberofLinks === 9 || numberofLinks > 10) && '@4xl:grid-cols-3'
1896
+ ]),
1897
+ children: children
1898
+ })
1899
+ });
1900
+ };
1901
+ const LinkListItem = ({ children, isExternal, className, ...restProps })=>{
1902
+ let Icon = ArrowRight;
1903
+ let iconTransition = 'group-hover:motion-safe:translate-x-1';
1904
+ if (restProps.download) {
1905
+ Icon = Download;
1906
+ iconTransition = 'group-hover:motion-safe:translate-y-1';
1907
+ } else if (isExternal) {
1908
+ iconTransition = 'group-hover:motion-safe:-translate-y-0.5 group-hover:motion-safe:translate-x-0.5';
1909
+ Icon = LinkExternal;
1910
+ }
1911
+ return /*#__PURE__*/ jsx("li", {
1912
+ // Creates divider lines that works in any grid layout and with the focus ring
1913
+ className: "after:-top-0.25 relative p-0.75 after:absolute after:right-0 after:left-0 after:h-0.25 after:w-full after:bg-gray-light",
1914
+ children: /*#__PURE__*/ jsxs(Link, {
1915
+ ...restProps,
1916
+ className: cx(className, 'group paragraph flex cursor-pointer justify-between gap-x-2 py-3.5 font-medium no-underline focus-visible:outline-focus'),
1917
+ children: [
1918
+ /*#__PURE__*/ jsx("span", {
1919
+ children: children
1920
+ }),
1921
+ /*#__PURE__*/ jsx(Icon, {
1922
+ className: cx('shrink-0 motion-safe:transition-transform', iconTransition)
1923
+ })
1924
+ ]
1925
+ })
1926
+ });
1927
+ };
1928
+
1929
+ const DialogTrigger = (props)=>/*#__PURE__*/ jsx(DialogTrigger$1, {
1930
+ ...props
1931
+ });
1932
+ const ModalOverlay = (props)=>/*#__PURE__*/ jsx(ModalOverlay$1, {
1933
+ ...props,
1934
+ isDismissable: true,
1935
+ className: ({ isEntering, isExiting })=>cx('fixed inset-0 z-10 flex min-h-full items-center justify-center overflow-y-auto bg-black/25 p-4 text-center backdrop-blur-sm', isEntering && 'fade-in animate-in duration-300 ease-out', isExiting && 'fade-out animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1936
+ 'motion-reduce:animate-none')
1937
+ });
1938
+ const Modal = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ModalOverlay, {
1939
+ children: /*#__PURE__*/ jsx(Modal$1, {
1940
+ ...restProps,
1941
+ className: ({ isEntering, isExiting })=>cx(className, 'w-full max-w-md overflow-hidden rounded-2xl bg-white p-4 text-left align-middle shadow-xl', isEntering && 'zoom-in-95 animate-in duration-300 ease-out', isExiting && 'zoom-out-95 animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1942
+ 'motion-reduce:animate-none')
1943
+ })
1944
+ });
1945
+ const Dialog = ({ className, children, ...restProps })=>{
1946
+ const locale = _useLocale();
1947
+ return /*#__PURE__*/ jsx(Dialog$1, {
1948
+ ...restProps,
1949
+ className: cx('relative grid gap-y-5 outline-none', // Footer
1950
+ '[&_[data-slot="footer"]]:flex [&_[data-slot="footer"]]:gap-x-2'),
1951
+ children: ({ close })=>/*#__PURE__*/ jsx(Fragment, {
1952
+ children: /*#__PURE__*/ jsx(Provider, {
1953
+ values: [
1954
+ [
1955
+ HeadingContext,
1956
+ {
1957
+ slots: {
1958
+ [DEFAULT_SLOT]: {},
1959
+ title: {
1960
+ className: 'heading-s',
1961
+ _outerWrapper: (children)=>/*#__PURE__*/ jsxs("div", {
1962
+ className: "flex items-center justify-between gap-x-2",
1963
+ children: [
1964
+ children,
1965
+ /*#__PURE__*/ jsx(Button, {
1966
+ slot: "close",
1967
+ variant: "tertiary",
1968
+ className: "!px-2.5 data-focus-visible:outline-focus-inset",
1969
+ "aria-label": translations$1.close[locale],
1970
+ children: /*#__PURE__*/ jsx(Close, {})
1971
+ })
1972
+ ]
1973
+ })
2206
1974
  }
2207
- },
2208
- isDisabled: hasReachedScrollStart
2209
- },
2210
- next: {
2211
- isIconOnly: true,
2212
- 'aria-label': next[locale],
2213
- onPress: ()=>{
2214
- if (!ref.current) return;
2215
- if (scrollTargetIndex < ref.current.children.length - 1) {
2216
- setScrollTargetIndex((prev)=>prev + 1);
1975
+ }
1976
+ }
1977
+ ],
1978
+ [
1979
+ ButtonContext$1,
1980
+ {
1981
+ // This is necessary to support multiple close buttons
1982
+ slots: {
1983
+ // We need to define default slot in order to also support non-slotted buttons (i.e. buttons without slot prop)
1984
+ [DEFAULT_SLOT]: {
1985
+ className: 'w-fit'
1986
+ },
1987
+ close: {
1988
+ onPress: close,
1989
+ className: 'w-fit'
2217
1990
  }
2218
- },
2219
- isDisabled: hasReachedScrollEnd
1991
+ }
2220
1992
  }
2221
- }
2222
- }
2223
- ]
2224
- ],
2225
- children: /*#__PURE__*/ jsxs("div", {
2226
- 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
2227
- '[&: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
2228
- '**:data-[slot="video-loop-button"]:focus-visible:outline-none'),
2229
- children: [
2230
- children,
2231
- /*#__PURE__*/ jsxs(_CarouselControls, {
2232
- children: [
2233
- /*#__PURE__*/ jsx(Button, {
2234
- isIconOnly: true,
2235
- slot: "prev",
2236
- variant: "primary",
2237
- color: "white",
2238
- className: cx('group/carousel-previous', hasReachedScrollStart && 'invisible'),
2239
- children: /*#__PURE__*/ jsx(ChevronLeft, {
2240
- className: "group-hover/carousel-previous:motion-safe:-translate-x-1 transition-transform"
2241
- })
2242
- }),
2243
- /*#__PURE__*/ jsx(Button, {
2244
- isIconOnly: true,
2245
- slot: "next",
2246
- variant: "primary",
2247
- color: "white",
2248
- className: cx('group/carousel-next', hasReachedScrollEnd && 'invisible'),
2249
- children: /*#__PURE__*/ jsx(ChevronRight, {
2250
- className: "transition-transform group-hover/carousel-next:motion-safe:translate-x-1"
2251
- })
2252
- })
2253
1993
  ]
2254
- })
2255
- ]
1994
+ ],
1995
+ children: children
1996
+ })
2256
1997
  })
2257
- })
2258
1998
  });
2259
1999
  };
2260
- /**
2261
- * 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.
2262
- * It is used to render the prev/next buttons in the carousel for now.
2263
- */ const _CarouselControls = ({ children, className })=>/*#__PURE__*/ jsx("div", {
2264
- className: cx(className, 'absolute right-6 bottom-6 flex gap-x-2', // Make it easier to position in full-bleed hero variants (these style have no other side effects)
2265
- 'items-end *:h-fit'),
2266
- "data-slot": "carousel-controls",
2267
- children: children
2268
- });
2269
- const CarouselItemsContext = /*#__PURE__*/ createContext({
2270
- ref: null
2271
- });
2272
- const CarouselItems = ({ className, children })=>/*#__PURE__*/ jsx(CarouselItemsContext.Consumer, {
2273
- children: ({ ref, onScroll })=>/*#__PURE__*/ jsx("div", {
2274
- "data-slot": "carousel-items",
2275
- className: cx(className, [
2276
- 'scrollbar-hidden',
2277
- 'flex',
2278
- 'snap-x',
2279
- 'snap-mandatory',
2280
- 'overflow-x-auto',
2281
- 'outline-none',
2282
- 'rounded-[inherit]'
2283
- ]),
2000
+
2001
+ // This component is based on a copy of ../textfield/TextField, refactoring is TBD: https://github.com/code-obos/grunnmuren/pull/722#issuecomment-1931478786
2002
+ const inputVariants$1 = compose(input, cva({
2003
+ base: '',
2004
+ variants: {
2005
+ textAlign: {
2006
+ right: 'text-right',
2007
+ left: ''
2008
+ },
2009
+ autoWidth: {
2010
+ true: 'max-w-fit',
2011
+ false: ''
2012
+ }
2013
+ }
2014
+ }));
2015
+ function NumberField(props) {
2016
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
2017
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2018
+ // which will override any built in validation
2019
+ const isInvalid = !!errorMessage || _isInvalid;
2020
+ return /*#__PURE__*/ jsxs(NumberField$1, {
2021
+ ...restProps,
2022
+ className: cx(className, formField),
2023
+ isInvalid: isInvalid,
2024
+ children: [
2025
+ label && /*#__PURE__*/ jsx(Label, {
2026
+ children: label
2027
+ }),
2028
+ description && /*#__PURE__*/ jsx(Description, {
2029
+ children: description
2030
+ }),
2031
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
2032
+ className: cx(inputGroup, {
2033
+ 'w-fit': !!size
2034
+ }),
2035
+ children: [
2036
+ leftAddon,
2037
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2038
+ /*#__PURE__*/ jsx(Input, {
2039
+ className: inputVariants$1({
2040
+ textAlign,
2041
+ isGrouped: true,
2042
+ autoWidth: !!size
2043
+ }),
2044
+ ref: ref,
2045
+ size: size
2046
+ }),
2047
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2048
+ rightAddon
2049
+ ]
2050
+ }) : /*#__PURE__*/ jsx(Input, {
2051
+ className: inputVariants$1({
2052
+ textAlign,
2053
+ autoWidth: !!size
2054
+ }),
2284
2055
  ref: ref,
2285
- // When the SnapEvent is supported: https://developer.mozilla.org/en-US/docs/Web/API/SnapEvent
2286
- // We can use the scrollsnapchange event to detect when the user has scrolled to a new item.
2287
- // We can then use Array.from(event.target.children).indexOf(event.snapTargetInline) to calculate the index of the item that is currently in view.
2288
- // Another option is to use the scrollEnd event, when Safiri supports it: https://developer.apple.com/documentation/webkitjs/snap_event/scrollend_event
2289
- onScroll: onScroll,
2290
- children: children
2056
+ size: size
2057
+ }),
2058
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2059
+ errorMessage: errorMessage
2291
2060
  })
2061
+ ]
2292
2062
  });
2293
- const CarouselItem = ({ className, children, id })=>{
2294
- return /*#__PURE__*/ jsx("div", {
2295
- className: cx(className, 'shrink-0 basis-full snap-start'),
2296
- "data-slot": "carousel-item",
2297
- id: id,
2298
- children: /*#__PURE__*/ jsx(Provider, {
2299
- values: [
2300
- [
2301
- MediaContext,
2302
- {
2303
- fit: 'cover',
2304
- className: cx('data-[fit="contain"]:bg-blue-dark', '*:h-full *:w-full', 'aspect-1/1 max-sm:data-[fit="contain"]:*:object-cover sm:aspect-4/3 md:aspect-3/2 lg:aspect-2/1')
2305
- }
2063
+ }
2064
+
2065
+ const defaultClasses = cx([
2066
+ '-ml-2.5 relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
2067
+ // the radio button itself
2068
+ 'before:flex-none before:rounded-full before:border-2 before:border-black',
2069
+ // to vertically align the radio we need to calculate the label's height, which is equal to it's font size multiplied by the line height.
2070
+ // For the ::before psuedo element the line height of the label is always 1em.
2071
+ // When we know the height of the label we use the height of the radio to push it down to align with the label's first line
2072
+ // TODO: 1.75 here is the unit less lineheight, altough we use 1.75rem as the line height, so there is a mismatch here. Revisit this when we've worked on typography in v2. Should this be a CSS custom property instead?
2073
+ 'before:mt-[calc((1em_*_1.75_-_24px)_/_2)] before:h-[24px] before:w-[24px]',
2074
+ // selected
2075
+ 'data-selected:before:border-black data-selected:before:bg-blue data-selected:before:shadow-[inset_0_0_0_4px_rgb(255,255,255)]',
2076
+ // hover
2077
+ 'data-hovered:data-selected:before:border-blue-dark data-hovered:data-invalid:before:bg-red-light data-hovered:data-selected:before:bg-blue-dark data-hovered:before:border-blue data-hovered:before:bg-sky',
2078
+ // focus
2079
+ 'data-focus-visible:before:ring-focus-offset',
2080
+ // invalid - The border is 1 px thicker when invalid. We don't actually want to change the border width, as that causes the element's size to change
2081
+ // so we use an inner outline to artifically pad the border
2082
+ 'data-invalid:data-selected:before:!bg-red data-invalid:before:border-red data-invalid:before:outline data-invalid:before:outline-[3px] data-invalid:before:outline-red data-invalid:before:outline-solid data-invalid:before:outline-offset-[-3px]'
2083
+ ]);
2084
+ function Radio(props) {
2085
+ const { children, className, description, ...restProps } = props;
2086
+ return /*#__PURE__*/ jsx(Radio$1, {
2087
+ ...restProps,
2088
+ className: cx(className, defaultClasses),
2089
+ children: /*#__PURE__*/ jsxs("div", {
2090
+ children: [
2091
+ children,
2092
+ description && /*#__PURE__*/ jsx(Description, {
2093
+ className: "mt-2 block",
2094
+ children: description
2095
+ })
2096
+ ]
2097
+ })
2098
+ });
2099
+ }
2100
+
2101
+ function RadioGroup(props) {
2102
+ const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, value, ...restProps } = props;
2103
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2104
+ // which will override any built in validation
2105
+ const isInvalid = !!errorMessage || _isInvalid;
2106
+ return /*#__PURE__*/ jsxs(RadioGroup$1, {
2107
+ ...restProps,
2108
+ // Tabindex is set to -1 when the value is an empty string, which makes the radio input not focusable
2109
+ value: value === '' ? undefined : value,
2110
+ className: cx(className, 'flex flex-col gap-2'),
2111
+ isInvalid: isInvalid,
2112
+ isRequired: isRequired,
2113
+ children: [
2114
+ label && /*#__PURE__*/ jsx(Label, {
2115
+ children: label
2116
+ }),
2117
+ description && /*#__PURE__*/ jsx(Description, {
2118
+ children: description
2119
+ }),
2120
+ children,
2121
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2122
+ errorMessage: errorMessage
2123
+ })
2124
+ ]
2125
+ });
2126
+ }
2127
+
2128
+ function Select(props) {
2129
+ const { className, children, description, errorMessage, label, isInvalid: _isInvalid, ref, ...restProps } = props;
2130
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2131
+ // which will override any built in validation
2132
+ const isInvalid = !!errorMessage || _isInvalid;
2133
+ return /*#__PURE__*/ jsxs(Select$1, {
2134
+ ...restProps,
2135
+ className: cx(className, formField),
2136
+ isInvalid: isInvalid,
2137
+ children: [
2138
+ label && /*#__PURE__*/ jsx(Label, {
2139
+ children: label
2140
+ }),
2141
+ description && /*#__PURE__*/ jsx(Description, {
2142
+ children: description
2143
+ }),
2144
+ /*#__PURE__*/ jsxs(Button$1, {
2145
+ className: cx(input({
2146
+ focusModifier: 'visible'
2147
+ }), // How to reuse placeholder text?
2148
+ 'inline-flex cursor-default items-center gap-2'),
2149
+ // See https://github.com/adobe/react-spectrum/discussions/4792#discussioncomment-6492305
2150
+ ref: ref,
2151
+ children: [
2152
+ /*#__PURE__*/ jsx(SelectValue, {
2153
+ className: "flex-1 truncate text-left data-[placeholder]:text-[#727070]"
2154
+ }),
2155
+ /*#__PURE__*/ jsx(ChevronDown, {
2156
+ className: dropdown.chevronIcon
2157
+ })
2306
2158
  ]
2307
- ],
2308
- children: children
2309
- })
2159
+ }),
2160
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2161
+ errorMessage: errorMessage
2162
+ }),
2163
+ /*#__PURE__*/ jsx(Popover, {
2164
+ className: dropdown.popover,
2165
+ children: /*#__PURE__*/ jsx(ListBox, {
2166
+ className: dropdown.listbox,
2167
+ children: children
2168
+ })
2169
+ })
2170
+ ]
2310
2171
  });
2311
- };
2172
+ }
2312
2173
 
2313
2174
  function ScrollButton({ direction, onClick, isVisible, hasScrollingOccurred, className, iconClassName }) {
2314
2175
  const Icon = direction === 'left' ? ChevronLeft : ChevronRight;
2315
2176
  return(// biome-ignore lint/a11y/useKeyWithClickEvents: This button is only for mouse interaction to help users scroll. Keyboard and screen reader users can navigate the content directly without needing these scroll helpers.
2177
+ // biome-ignore lint/a11y/noStaticElementInteractions: This button is only for mouse interaction to help users scroll. Keyboard and screen reader users can navigate the content directly without needing these scroll helpers.
2316
2178
  /*#__PURE__*/ jsx("div", {
2317
2179
  onClick: onClick,
2318
2180
  className: cx(// Base scroll button styling
@@ -2375,6 +2237,126 @@ function ScrollButton({ direction, onClick, isVisible, hasScrollingOccurred, cla
2375
2237
  };
2376
2238
  }
2377
2239
 
2240
+ const tableVariants = cva({
2241
+ base: [
2242
+ 'relative'
2243
+ ],
2244
+ variants: {
2245
+ variant: {
2246
+ default: '',
2247
+ 'zebra-striped': ''
2248
+ }
2249
+ }
2250
+ });
2251
+ const tableRowVariants = cva({
2252
+ base: [
2253
+ 'data-focus-visible:outline-focus-inset',
2254
+ 'group-data-[variant=zebra-striped]:odd:bg-white',
2255
+ 'group-data-[variant=zebra-striped]:even:bg-sky-lightest'
2256
+ ]
2257
+ });
2258
+ /**
2259
+ * A container component for displaying tabular data with horizontal scrolling support.
2260
+ */ function Table(props) {
2261
+ const { className, children, variant = 'default', ...restProps } = props;
2262
+ const { scrollContainerRef, canScrollLeft, canScrollRight, hasScrollingOccurred } = useHorizontalScroll();
2263
+ const handleScroll = useCallback((direction)=>{
2264
+ const container = scrollContainerRef.current;
2265
+ if (!container) return;
2266
+ const scrollAmount = container.clientWidth * 0.8;
2267
+ container.scrollBy({
2268
+ left: direction === 'left' ? -scrollAmount : scrollAmount,
2269
+ behavior: 'smooth'
2270
+ });
2271
+ }, [
2272
+ scrollContainerRef
2273
+ ]);
2274
+ return /*#__PURE__*/ jsx("div", {
2275
+ className: tableVariants({
2276
+ className,
2277
+ variant
2278
+ }),
2279
+ children: /*#__PURE__*/ jsxs("div", {
2280
+ className: "relative overflow-hidden",
2281
+ children: [
2282
+ /*#__PURE__*/ jsx(ScrollButton, {
2283
+ direction: "left",
2284
+ onClick: ()=>handleScroll('left'),
2285
+ isVisible: canScrollLeft,
2286
+ hasScrollingOccurred: hasScrollingOccurred,
2287
+ className: "-translate-y-1/2 absolute top-5 z-10 h-11 w-11",
2288
+ iconClassName: "h-5 w-5"
2289
+ }),
2290
+ /*#__PURE__*/ jsx(ScrollButton, {
2291
+ direction: "right",
2292
+ onClick: ()=>handleScroll('right'),
2293
+ isVisible: canScrollRight,
2294
+ hasScrollingOccurred: hasScrollingOccurred,
2295
+ className: "-translate-y-1/2 absolute top-5 z-10 h-11 w-11",
2296
+ iconClassName: "h-5 w-5"
2297
+ }),
2298
+ /*#__PURE__*/ jsx("div", {
2299
+ ref: scrollContainerRef,
2300
+ className: "scrollbar-hidden overflow-x-auto",
2301
+ style: {
2302
+ WebkitOverflowScrolling: 'touch'
2303
+ },
2304
+ children: /*#__PURE__*/ jsx(Table$1, {
2305
+ ...restProps,
2306
+ className: "group w-full min-w-fit",
2307
+ "data-variant": variant,
2308
+ children: children
2309
+ })
2310
+ })
2311
+ ]
2312
+ })
2313
+ });
2314
+ }
2315
+ /**
2316
+ * Container for table column headers.
2317
+ */ function TableHeader({ className, children, ...restProps }) {
2318
+ return /*#__PURE__*/ jsx(TableHeader$1, {
2319
+ ...restProps,
2320
+ className: cx(className, 'border-black border-b'),
2321
+ children: children
2322
+ });
2323
+ }
2324
+ function TableColumn(props) {
2325
+ const { className, children, ...restProps } = props;
2326
+ return /*#__PURE__*/ jsx(Column, {
2327
+ ...restProps,
2328
+ className: cx(className, 'px-4 py-3 text-left font-medium text-black text-sm', 'data-focus-visible:outline-focus-inset', 'min-w-fit whitespace-nowrap'),
2329
+ children: children
2330
+ });
2331
+ }
2332
+ /**
2333
+ * Container for table rows.
2334
+ */ function TableBody({ className, children, ...restProps }) {
2335
+ return /*#__PURE__*/ jsx(TableBody$1, {
2336
+ ...restProps,
2337
+ className: className,
2338
+ children: children
2339
+ });
2340
+ }
2341
+ function TableRow(props) {
2342
+ const { className, children, ...restProps } = props;
2343
+ return /*#__PURE__*/ jsx(Row, {
2344
+ ...restProps,
2345
+ className: tableRowVariants({
2346
+ className
2347
+ }),
2348
+ children: children
2349
+ });
2350
+ }
2351
+ function TableCell(props) {
2352
+ const { className, children, ...restProps } = props;
2353
+ return /*#__PURE__*/ jsx(Cell, {
2354
+ ...restProps,
2355
+ className: cx(className, 'px-4 py-3 text-black text-sm leading-relaxed', 'min-w-fit whitespace-nowrap', 'align-top', 'data-focus-visible:outline-focus-inset'),
2356
+ children: children
2357
+ });
2358
+ }
2359
+
2378
2360
  const tabsVariants = cva({
2379
2361
  base: [
2380
2362
  'grid gap-4'
@@ -2529,180 +2511,244 @@ const tabsVariants = cva({
2529
2511
  });
2530
2512
  }
2531
2513
 
2532
- const tableVariants = cva({
2533
- base: [
2534
- 'relative'
2535
- ],
2536
- variants: {
2537
- variant: {
2538
- default: '',
2539
- 'zebra-striped': ''
2540
- }
2541
- }
2542
- });
2543
- const tableRowVariants = cva({
2514
+ const tagVariants = cva({
2544
2515
  base: [
2545
- 'data-focus-visible:outline-focus-inset',
2546
- 'group-data-[variant=zebra-striped]:odd:bg-white',
2547
- 'group-data-[variant=zebra-striped]:even:bg-sky-lightest'
2516
+ 'relative flex cursor-pointer items-center gap-1 rounded-lg px-2 py-1 font-medium text-sm transition-colors duration-200',
2517
+ // Resting
2518
+ 'border-2 border-black bg-white text-black',
2519
+ //Focus
2520
+ 'focus-visible:outline-focus-offset',
2521
+ // Hover
2522
+ 'data-hovered:bg-sky',
2523
+ // Selected
2524
+ // Allows removing
2525
+ 'data-allows-removing:border-transparent',
2526
+ 'data-allows-removing:bg-blue',
2527
+ 'data-allows-removing:data-hovered:bg-blue-dark',
2528
+ 'data-allows-removing:text-white',
2529
+ // Selected
2530
+ 'aria-selected:border-transparent',
2531
+ 'aria-selected:bg-blue',
2532
+ 'aria-selected:data-hovered:bg-blue-dark',
2533
+ 'aria-selected:text-white',
2534
+ //Icons
2535
+ '[&_svg]:h-4 [&_svg]:w-4'
2548
2536
  ]
2549
2537
  });
2550
2538
  /**
2551
- * A container component for displaying tabular data with horizontal scrolling support.
2552
- */ function Table(props) {
2553
- const { className, children, variant = 'default', ...restProps } = props;
2554
- const { scrollContainerRef, canScrollLeft, canScrollRight, hasScrollingOccurred } = useHorizontalScroll();
2555
- const handleScroll = useCallback((direction)=>{
2556
- const container = scrollContainerRef.current;
2557
- if (!container) return;
2558
- const scrollAmount = container.clientWidth * 0.8;
2559
- container.scrollBy({
2560
- left: direction === 'left' ? -scrollAmount : scrollAmount,
2561
- behavior: 'smooth'
2562
- });
2563
- }, [
2564
- scrollContainerRef
2565
- ]);
2566
- return /*#__PURE__*/ jsx("div", {
2567
- className: tableVariants({
2568
- className,
2569
- variant
2570
- }),
2571
- children: /*#__PURE__*/ jsxs("div", {
2572
- className: "relative overflow-hidden",
2573
- children: [
2574
- /*#__PURE__*/ jsx(ScrollButton, {
2575
- direction: "left",
2576
- onClick: ()=>handleScroll('left'),
2577
- isVisible: canScrollLeft,
2578
- hasScrollingOccurred: hasScrollingOccurred,
2579
- className: "-translate-y-1/2 absolute top-5 z-10 h-11 w-11",
2580
- iconClassName: "h-5 w-5"
2581
- }),
2582
- /*#__PURE__*/ jsx(ScrollButton, {
2583
- direction: "right",
2584
- onClick: ()=>handleScroll('right'),
2585
- isVisible: canScrollRight,
2586
- hasScrollingOccurred: hasScrollingOccurred,
2587
- className: "-translate-y-1/2 absolute top-5 z-10 h-11 w-11",
2588
- iconClassName: "h-5 w-5"
2589
- }),
2590
- /*#__PURE__*/ jsx("div", {
2591
- ref: scrollContainerRef,
2592
- className: "scrollbar-hidden overflow-x-auto",
2593
- style: {
2594
- WebkitOverflowScrolling: 'touch'
2595
- },
2596
- children: /*#__PURE__*/ jsx(Table$1, {
2597
- ...restProps,
2598
- className: "group w-full min-w-fit",
2599
- "data-variant": variant,
2600
- children: children
2601
- })
2602
- })
2603
- ]
2604
- })
2605
- });
2606
- }
2607
- /**
2608
- * Container for table column headers.
2609
- */ function TableHeader({ className, children, ...restProps }) {
2610
- return /*#__PURE__*/ jsx(TableHeader$1, {
2539
+ * A group component for Tag components that enables selection and organization of options.
2540
+ */ function TagGroup(props) {
2541
+ const { onRemove, selectionMode = 'single', className, children, ...restProps } = props;
2542
+ return /*#__PURE__*/ jsx(TagGroup$1, {
2611
2543
  ...restProps,
2612
- className: cx(className, 'border-black border-b'),
2544
+ className: className,
2545
+ selectionMode: onRemove ? 'none' : selectionMode,
2546
+ onRemove: onRemove,
2613
2547
  children: children
2614
2548
  });
2615
2549
  }
2616
- function TableColumn(props) {
2550
+ /**
2551
+ * A container component for Tag components within a TagGroup.
2552
+ */ function TagList(props) {
2617
2553
  const { className, children, ...restProps } = props;
2618
- return /*#__PURE__*/ jsx(Column, {
2554
+ return /*#__PURE__*/ jsx(TagList$1, {
2619
2555
  ...restProps,
2620
- className: cx(className, 'px-4 py-3 text-left font-medium text-black text-sm', 'data-focus-visible:outline-focus-inset', 'min-w-fit whitespace-nowrap'),
2556
+ className: cx('flex flex-wrap gap-2', className),
2621
2557
  children: children
2622
2558
  });
2623
2559
  }
2624
2560
  /**
2625
- * Container for table rows.
2626
- */ function TableBody({ className, children, ...restProps }) {
2627
- return /*#__PURE__*/ jsx(TableBody$1, {
2628
- ...restProps,
2629
- className: className,
2630
- children: children
2631
- });
2632
- }
2633
- function TableRow(props) {
2561
+ * Interactive tag component for selections, filtering, and categorization.
2562
+ */ function Tag(props) {
2634
2563
  const { className, children, ...restProps } = props;
2635
- return /*#__PURE__*/ jsx(Row, {
2636
- ...restProps,
2637
- className: tableRowVariants({
2564
+ const textValue = typeof children === 'string' ? children : undefined;
2565
+ return /*#__PURE__*/ jsx(Tag$1, {
2566
+ className: tagVariants({
2638
2567
  className
2639
2568
  }),
2640
- children: children
2641
- });
2642
- }
2643
- function TableCell(props) {
2644
- const { className, children, ...restProps } = props;
2645
- return /*#__PURE__*/ jsx(Cell, {
2569
+ textValue: textValue,
2646
2570
  ...restProps,
2647
- className: cx(className, 'px-4 py-3 text-black text-sm leading-relaxed', 'min-w-fit whitespace-nowrap', 'align-top', 'data-focus-visible:outline-focus-inset'),
2648
- children: children
2571
+ children: ({ allowsRemoving })=>allowsRemoving ? /*#__PURE__*/ jsxs(Fragment, {
2572
+ children: [
2573
+ children,
2574
+ /*#__PURE__*/ jsx(Button$1, {
2575
+ className: "cursor-pointer outline-none after:absolute after:top-0 after:right-0 after:bottom-0 after:left-0",
2576
+ slot: "remove",
2577
+ children: /*#__PURE__*/ jsx(Close, {
2578
+ className: "ml-1"
2579
+ })
2580
+ })
2581
+ ]
2582
+ }) : children
2649
2583
  });
2650
2584
  }
2651
2585
 
2652
- /**
2653
- * A basic link component that extends react-aria-components Link with consistent styling.
2654
- * Provides accessible focus styles and maintains design system consistency.
2655
- */ const CustomLink = ({ children, className, ...restProps })=>{
2656
- return /*#__PURE__*/ jsx(Link, {
2586
+ function TextArea(props) {
2587
+ const { className, description, errorMessage, label, isInvalid: _isInvalid, rows, ref, ...restProps } = props;
2588
+ const isInvalid = !!errorMessage || _isInvalid;
2589
+ return /*#__PURE__*/ jsxs(TextField$1, {
2657
2590
  ...restProps,
2658
- className: cx(className, 'inline-flex cursor-pointer items-center gap-1 font-medium hover:no-underline focus-visible:outline-current focus-visible:outline-focus-offset [&>svg]:shrink-0'),
2659
- children: children
2591
+ className: cx(className, formField),
2592
+ isInvalid: isInvalid,
2593
+ children: [
2594
+ label && /*#__PURE__*/ jsx(Label, {
2595
+ children: label
2596
+ }),
2597
+ description && /*#__PURE__*/ jsx(Description, {
2598
+ children: description
2599
+ }),
2600
+ /*#__PURE__*/ jsx(TextArea$1, {
2601
+ className: input(),
2602
+ rows: rows,
2603
+ ref: ref
2604
+ }),
2605
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2606
+ errorMessage: errorMessage
2607
+ })
2608
+ ]
2660
2609
  });
2661
- };
2610
+ }
2662
2611
 
2663
- const LinkList = ({ className, children, ...restProps })=>{
2664
- const numberofLinks = Children.count(children);
2665
- return /*#__PURE__*/ jsx("div", {
2666
- className: cx(className, '@container'),
2667
- ...restProps,
2668
- children: /*#__PURE__*/ jsx("ul", {
2669
- className: cx('min-w-fit', // Hide dividers at the top of the list (overflow-y) and prevents arrow icon from overflowing container when animated to the right (overflow-x)
2670
- 'overflow-hidden', // Add a small gap between items that fits the divider lines (this way the divider line don't take up any space in each item)
2671
- 'grid auto-rows-max gap-y-0.25', // Gaps for when the list is displayed in multiple columns
2672
- '@lg:gap-x-12 @md:gap-x-9 @sm:gap-x-4 @xl:gap-x-16', numberofLinks > 5 && [
2673
- '@xl:grid-cols-2',
2674
- (numberofLinks === 9 || numberofLinks > 10) && '@4xl:grid-cols-3'
2675
- ]),
2676
- children: children
2677
- })
2678
- });
2679
- };
2680
- const LinkListItem = ({ children, isExternal, className, ...restProps })=>{
2681
- let Icon = ArrowRight;
2682
- let iconTransition = 'group-hover:motion-safe:translate-x-1';
2683
- if (restProps.download) {
2684
- Icon = Download;
2685
- iconTransition = 'group-hover:motion-safe:translate-y-1';
2686
- } else if (isExternal) {
2687
- iconTransition = 'group-hover:motion-safe:-translate-y-0.5 group-hover:motion-safe:translate-x-0.5';
2688
- Icon = LinkExternal;
2612
+ const inputVariants = compose(input, cva({
2613
+ base: '',
2614
+ variants: {
2615
+ textAlign: {
2616
+ right: 'text-right',
2617
+ left: ''
2618
+ },
2619
+ autoWidth: {
2620
+ true: 'max-w-fit',
2621
+ false: ''
2622
+ }
2689
2623
  }
2690
- return /*#__PURE__*/ jsx("li", {
2691
- // Creates divider lines that works in any grid layout and with the focus ring
2692
- className: "after:-top-0.25 relative p-0.75 after:absolute after:right-0 after:left-0 after:h-0.25 after:w-full after:bg-gray-light",
2693
- children: /*#__PURE__*/ jsxs(Link, {
2694
- ...restProps,
2695
- className: cx(className, 'group paragraph flex cursor-pointer justify-between gap-x-2 py-3.5 font-medium no-underline focus-visible:outline-focus'),
2696
- children: [
2697
- /*#__PURE__*/ jsx("span", {
2698
- children: children
2624
+ }));
2625
+ function TextField(props) {
2626
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
2627
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2628
+ // which will override any built in validation
2629
+ const isInvalid = !!errorMessage || _isInvalid;
2630
+ return /*#__PURE__*/ jsxs(TextField$1, {
2631
+ ...restProps,
2632
+ className: cx(className, formField),
2633
+ isInvalid: isInvalid,
2634
+ children: [
2635
+ label && /*#__PURE__*/ jsx(Label, {
2636
+ children: label
2637
+ }),
2638
+ description && /*#__PURE__*/ jsx(Description, {
2639
+ children: description
2640
+ }),
2641
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
2642
+ className: cx(inputGroup, {
2643
+ 'w-fit': !!size
2699
2644
  }),
2700
- /*#__PURE__*/ jsx(Icon, {
2701
- className: cx('shrink-0 motion-safe:transition-transform', iconTransition)
2645
+ children: [
2646
+ leftAddon,
2647
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2648
+ /*#__PURE__*/ jsx(Input, {
2649
+ className: inputVariants({
2650
+ textAlign,
2651
+ isGrouped: true,
2652
+ autoWidth: !!size
2653
+ }),
2654
+ ref: ref,
2655
+ size: size
2656
+ }),
2657
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2658
+ rightAddon
2659
+ ]
2660
+ }) : /*#__PURE__*/ jsx(Input, {
2661
+ className: inputVariants({
2662
+ textAlign,
2663
+ autoWidth: !!size
2664
+ }),
2665
+ ref: ref,
2666
+ size: size
2667
+ }),
2668
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2669
+ errorMessage: errorMessage
2670
+ })
2671
+ ]
2672
+ });
2673
+ }
2674
+
2675
+ const VideoLoop = ({ src, format, alt, className })=>{
2676
+ // Control the video playback state, so that the user can pause and play the video at will, also control the video autoplay
2677
+ const [shouldPlay, setShouldPlay] = useState(false);
2678
+ // Needed to show the pause button when the video is actually playing (refer to google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes)
2679
+ const [isPlaying, setIsPlaying] = useState(false);
2680
+ // We need to check if the user prefers reduced motion, so that we can prevent the video from autoplaying if so
2681
+ const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(null);
2682
+ const videoRef = useRef(null);
2683
+ useEffect(()=>{
2684
+ const { matches: userPrefersReducedMotion } = matchMedia('(prefers-reduced-motion: reduce)');
2685
+ setUserPrefersReducedMotion(userPrefersReducedMotion);
2686
+ // Autoplay the video if the user does not prefer reduced motion
2687
+ setShouldPlay(!userPrefersReducedMotion);
2688
+ }, []);
2689
+ // Follow google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
2690
+ // "Don't assume a video will play, and don't show a pause button when the video is not actually playing."
2691
+ // "You should always look at the Promise returned by the play function to see if it was rejected:"
2692
+ // This is why we use the promise returned by the play function, and an extra state variable to determine if the video is actually playing or not
2693
+ useEffect(()=>{
2694
+ if (!videoRef.current) return;
2695
+ if (shouldPlay) {
2696
+ videoRef.current.play().then(()=>setIsPlaying(true)).catch(()=>setIsPlaying(false));
2697
+ } else {
2698
+ videoRef.current.pause();
2699
+ setIsPlaying(false);
2700
+ }
2701
+ }, [
2702
+ shouldPlay
2703
+ ]);
2704
+ return /*#__PURE__*/ jsxs("div", {
2705
+ className: cx(className, 'relative', userPrefersReducedMotion === null && 'opacity-0'),
2706
+ children: [
2707
+ /*#__PURE__*/ jsx("video", {
2708
+ "aria-hidden": true,
2709
+ ref: videoRef,
2710
+ // cursor-pointer is not working on the button below, so we add it here for the same effect
2711
+ className: "h-full max-h-[inherit] w-full cursor-pointer rounded-[inherit] object-cover",
2712
+ playsInline: true,
2713
+ loop: userPrefersReducedMotion === false,
2714
+ autoPlay: userPrefersReducedMotion === false,
2715
+ muted: true,
2716
+ onEnded: (event)=>{
2717
+ if (userPrefersReducedMotion) {
2718
+ // Reset the video to the beginning if the user prefers reduced motion, since the video will not loop
2719
+ event.currentTarget.currentTime = 0;
2720
+ setShouldPlay(false);
2721
+ setIsPlaying(false);
2722
+ }
2723
+ },
2724
+ children: /*#__PURE__*/ jsx("source", {
2725
+ src: src,
2726
+ type: `video/${format}`
2702
2727
  })
2703
- ]
2704
- })
2728
+ }),
2729
+ userPrefersReducedMotion !== null && /*#__PURE__*/ jsx("button", {
2730
+ "data-slot": "video-loop-button",
2731
+ "aria-hidden": true,
2732
+ type: "button",
2733
+ onClick: ()=>setShouldPlay((prevState)=>!prevState),
2734
+ className: cx('absolute top-0 right-0 bottom-0 left-0 m-auto grid place-items-center', 'focus-visible:outline-focus focus-visible:outline-focus-offset', 'rounded-[inherit]', // Setting the opacity to 0 before applying the transition below will ensure the button only fades in after the video has started playing
2735
+ shouldPlay && 'opacity-0', isPlaying && [
2736
+ 'transition-opacity duration-200',
2737
+ // Only show the pause button when the video is hovered or focused
2738
+ 'focus-visible:opacity-100',
2739
+ 'hover:opacity-100'
2740
+ ]),
2741
+ children: /*#__PURE__*/ jsx("span", {
2742
+ className: "grid h-12 w-12 place-items-center rounded-full bg-white outline-hidden",
2743
+ children: isPlaying ? /*#__PURE__*/ jsx(PlayerPause, {}) : /*#__PURE__*/ jsx(PlayerPlay, {})
2744
+ })
2745
+ }),
2746
+ alt && /*#__PURE__*/ jsx("p", {
2747
+ className: "sr-only",
2748
+ children: alt
2749
+ })
2750
+ ]
2705
2751
  });
2706
2752
  };
2707
2753
 
2708
- 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, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, DisclosurePanel as UNSAFE_DisclosurePanel, FileUpload as UNSAFE_FileUpload, Hero as UNSAFE_Hero, CustomLink as UNSAFE_Link, LinkList as UNSAFE_LinkList, LinkListItem as UNSAFE_LinkListItem, Modal as UNSAFE_Modal, 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, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, Tabs as UNSAFE_Tabs, Tag as UNSAFE_Tag, TagGroup as UNSAFE_TagGroup, TagList as UNSAFE_TagList, VideoLoop, _useLocale as useLocale };
2754
+ 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, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, 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, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, DisclosurePanel as UNSAFE_DisclosurePanel, FileUpload as UNSAFE_FileUpload, Hero as UNSAFE_Hero, CustomLink as UNSAFE_Link, LinkList as UNSAFE_LinkList, LinkListItem as UNSAFE_LinkListItem, Modal as UNSAFE_Modal, 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, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, Tabs as UNSAFE_Tabs, VideoLoop, _useLocale as useLocale };