@obosbbl/grunnmuren-react 3.0.15 → 3.0.16

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.
Files changed (3) hide show
  1. package/dist/index.d.mts +454 -454
  2. package/dist/index.mjs +1594 -1592
  3. package/package.json +2 -2
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, Provider, useLocale, Link, Button as Button$1, Breadcrumb as Breadcrumb$1, Breadcrumbs as Breadcrumbs$1, DEFAULT_SLOT, 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, ButtonContext as ButtonContext$1, DisclosureContext, DisclosureGroupStateContext, useSlottedContext, FormContext, FieldErrorContext, LabelContext, InputContext, I18nProvider, RouterProvider, GroupContext, DialogTrigger as DialogTrigger$1, Modal as Modal$1, Dialog as Dialog$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, TableHeader as TableHeader$1, Column, TableBody as TableBody$1, Row, Cell, Tabs as Tabs$1, TabListStateContext, TabList as TabList$1, Tab as Tab$1, TabPanel as TabPanel$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';
5
+ import { ChevronDown, Close, InfoCircle, CheckCircle, Warning, Error, User, ChevronLeft, ChevronRight, LoadingSpinner, Check, Trash, ArrowRight, Download, LinkExternal, PlayerPause, PlayerPlay } from '@obosbbl/grunnmuren-icons-react';
6
+ import { useLayoutEffect, useUpdateEffect, filterDOMProps, mergeRefs, mergeProps, useObjectRef, useFormReset } from '@react-aria/utils';
7
7
  import { cva, cx, compose } from 'cva';
8
8
  import { createContext, Children, useId, useState, useRef, useEffect, useContext, useCallback } from 'react';
9
9
  import { useProgressBar, useDateFormatter, useFocusRing, useDisclosure, useField } from 'react-aria';
10
+ import { useDebouncedCallback } from 'use-debounce';
10
11
  import { useDisclosureState } from 'react-stately';
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({
@@ -208,43 +189,6 @@ function AccordionItem(props) {
208
189
  });
209
190
  }
210
191
 
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
192
  const translations$1 = {
249
193
  close: {
250
194
  nb: 'Lukk',
@@ -279,1065 +223,1045 @@ const translations$1 = {
279
223
  };
280
224
 
281
225
  /**
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({
226
+ * Returns the locale set in `<GrunnmurenProvider />`
227
+ */ function _useLocale() {
228
+ // a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
229
+ const locale = useLocale();
230
+ return locale.locale;
231
+ }
232
+
233
+ const iconMap = {
234
+ info: InfoCircle,
235
+ success: CheckCircle,
236
+ warning: Warning,
237
+ danger: Error
238
+ };
239
+ const alertVariants = cva({
284
240
  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'
241
+ 'grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-md border-2 px-3 py-2',
242
+ // Heading styles:
243
+ '[&_[data-slot="heading"]]:font-medium [&_[data-slot="heading"]]:text-base [&_[data-slot="heading"]]:leading-7',
244
+ // Content styles:
245
+ '[&:has([data-slot="heading"])_[data-slot="content"]]:col-span-full [&_[data-slot="content"]]:text-sm [&_[data-slot="content"]]:leading-6',
246
+ // Footer styles:
247
+ '[&_[data-slot="footer"]]:col-span-full [&_[data-slot="footer"]]:font-light [&_[data-slot="footer"]]:text-xs [&_[data-slot="footer"]]:leading-6'
286
248
  ],
287
249
  variants: {
288
250
  /**
289
- * The variant of the button
290
- * @default primary
251
+ * The variant of the alert
252
+ * @default info
291
253
  */ variant: {
292
- primary: 'no-underline',
293
- // 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
- secondary: 'border-2 border-current no-underline hover:border-transparent',
295
- tertiary: 'underline hover:no-underline'
296
- },
297
- /**
298
- * Adjusts the color of the button for usage on different backgrounds.
299
- * @default blue
300
- */ color: {
301
- blue: 'focus-visible:outline-focus',
302
- mint: 'focus-visible:outline-focus focus-visible:outline-mint',
303
- white: 'focus-visible:outline-focus focus-visible:outline-white'
304
- },
305
- /**
306
- * When the button is without text, but with a single icon.
307
- * @default false
308
- */ isIconOnly: {
309
- true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
310
- false: 'gap-2.5 px-4 py-2'
311
- },
312
- // Make the content of the button transparent to hide it's content, but keep the button width
313
- isPending: {
314
- true: '!text-transparent relative',
315
- false: null
254
+ info: 'border-[#1A7FA7] bg-sky-light',
255
+ success: 'border-[#0F9B6E] bg-mint-light',
256
+ warning: 'border-[#C57C13] bg-[#FFF2DE]',
257
+ danger: 'border-[#C0385D] bg-red-light'
316
258
  }
317
259
  },
318
- compoundVariants: [
319
- {
320
- color: 'blue',
321
- variant: 'primary',
322
- // Darken bg by 20% on hover. The color is manually crafted
323
- className: 'bg-blue-dark text-white hover:bg-blue active:bg-[#0536A0] active:text-white [&_[role="progressbar"]]:text-white'
324
- },
325
- {
326
- color: 'blue',
327
- variant: 'secondary',
328
- className: 'text-blue-dark hover:border-transparent hover:bg-blue hover:text-blue-dark hover:text-white active:bg-[#0536A0] [&:hover_[role="progressbar"]]:text-white [&_[role="progressbar"]]:text-blue-dark'
329
- },
330
- {
331
- color: 'blue',
332
- variant: 'tertiary',
333
- className: '[&_[role="progressbar"]]:text-black'
334
- },
335
- {
336
- color: 'mint',
337
- variant: 'primary',
338
- // Darken bg by 20% on hover. The color is manually crafted
339
- className: 'bg-mint text-black hover:bg-[#8dd4bd] active:[#9ddac6] [&_[role="progressbar"]]:text-black'
340
- },
341
- {
342
- color: 'mint',
343
- variant: 'secondary',
344
- className: 'text-mint hover:bg-mint hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-mint'
345
- },
346
- {
347
- color: 'mint',
348
- variant: 'tertiary',
349
- className: 'text-mint [&_[role="progressbar"]]:text-mint'
350
- },
351
- {
352
- color: 'white',
353
- variant: 'primary',
354
- className: 'bg-white text-black hover:bg-sky active:bg-sky-light [&_[role="progressbar"]]:text-black'
355
- },
356
- {
357
- color: 'white',
358
- variant: 'secondary',
359
- className: 'text-white hover:bg-white hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-white'
360
- },
361
- {
362
- color: 'white',
363
- variant: 'tertiary',
364
- className: 'text-white [&_[role="progressbar"]]:text-white'
365
- }
366
- ],
367
260
  defaultVariants: {
368
- variant: 'primary',
369
- color: 'blue',
370
- isIconOnly: false,
371
- isPending: false
261
+ variant: 'info'
372
262
  }
373
263
  });
374
- const ButtonContext = /*#__PURE__*/ createContext({});
375
- function isLinkProps$1(props) {
376
- return !!props.href;
377
- }
378
- function Button({ ref = null, ...props }) {
379
- [props, ref] = useContextProps(props, ref, ButtonContext);
380
- const { children: _children, color, isIconOnly, variant, isPending, ...restProps } = props;
381
- const className = buttonVariants({
382
- className: props.className,
383
- color,
384
- isIconOnly,
385
- variant,
386
- isPending
387
- });
264
+ const Alertbox = ({ children, role, className, icon, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
265
+ const Icon = icon ?? iconMap[variant];
388
266
  const locale = _useLocale();
389
- const { progressBarProps } = useProgressBar({
390
- isIndeterminate: true,
391
- 'aria-label': translations$1.pending[locale]
392
- });
393
- const children = isPending ? /*#__PURE__*/ jsxs(Fragment, {
267
+ const id = useId();
268
+ const [isExpanded, setIsExpanded] = useState(false);
269
+ const isCollapsed = isExpandable && !isExpanded;
270
+ const [isUncontrolledVisible, setIsUncontrolledVisible] = useState(true);
271
+ const isVisible = isDismissed !== undefined ? !isDismissed : isUncontrolledVisible;
272
+ if (!isVisible) return;
273
+ const close = ()=>{
274
+ setIsUncontrolledVisible(false);
275
+ if (onDismiss) onDismiss();
276
+ };
277
+ const isInDevMode = process.env.NODE_ENV !== 'production';
278
+ if (isInDevMode && onDismiss && !isDismissable) {
279
+ console.warn('Passing an `onDismiss` callback without setting the `isDismissable` prop to `true` will not have any effect.');
280
+ }
281
+ if (isInDevMode && !children) {
282
+ console.error('`No children was passed to the <AlertBox/>` component.');
283
+ return;
284
+ }
285
+ const [firstChild, ...restChildren] = Children.toArray(children);
286
+ return /*#__PURE__*/ jsxs("div", {
287
+ className: alertVariants({
288
+ className,
289
+ variant
290
+ }),
291
+ // The role prop is required to force consumers to consider and choose the appropriate alertbox role.
292
+ // role="none" will not have any effect on a div, so it can be omitted.
293
+ role: role === 'none' ? undefined : role,
394
294
  children: [
395
- _children,
396
- /*#__PURE__*/ jsx(LoadingSpinner, {
397
- className: "absolute m-auto motion-safe:animate-spin",
398
- ...progressBarProps
295
+ /*#__PURE__*/ jsx(Icon, {}),
296
+ firstChild,
297
+ isDismissable && /*#__PURE__*/ jsx("button", {
298
+ 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'),
299
+ onClick: close,
300
+ "aria-label": translations$1.close[locale],
301
+ type: "button",
302
+ children: /*#__PURE__*/ jsx(Close, {})
303
+ }),
304
+ isExpandable && /*#__PURE__*/ jsxs("button", {
305
+ 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:
306
+ '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'),
307
+ onClick: ()=>setIsExpanded((prevState)=>!prevState),
308
+ "aria-expanded": isExpanded,
309
+ "aria-controls": id,
310
+ type: "button",
311
+ children: [
312
+ isExpanded ? translations$1.showLess[locale] : translations$1.showMore[locale],
313
+ /*#__PURE__*/ jsx(ChevronDown, {
314
+ className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
315
+ })
316
+ ]
317
+ }),
318
+ restChildren?.length > 0 && /*#__PURE__*/ jsx("div", {
319
+ className: cx('col-span-full grid gap-y-4', isCollapsed && '[&>*:not([data-slot="footer"])]:hidden'),
320
+ id: id,
321
+ children: restChildren
399
322
  })
400
323
  ]
401
- }) : _children;
402
- return isLinkProps$1(restProps) ? /*#__PURE__*/ jsx(Link, {
403
- ...restProps,
404
- className: className,
405
- ref: ref,
406
- children: children
407
- }) : /*#__PURE__*/ jsx(Button$1, {
408
- ...restProps,
409
- className: className,
410
- isPending: isPending,
324
+ });
325
+ };
326
+
327
+ const baseClassName = 'h-20 w-20 shrink-0 rounded-full';
328
+ const Avatar = ({ src, alt = '', className, onError, loading = 'lazy', ...rest })=>{
329
+ const [hasError, setHasError] = useState(false);
330
+ const hasValidImage = !hasError && src;
331
+ return hasValidImage ? /*#__PURE__*/ jsx("img", {
332
+ ...rest,
333
+ src: src,
334
+ alt: alt,
335
+ loading: loading,
336
+ className: cx(className, baseClassName, 'object-cover'),
337
+ onError: (event)=>{
338
+ onError?.(event);
339
+ setHasError(true);
340
+ }
341
+ }) : /*#__PURE__*/ jsx("div", {
342
+ className: cx(className, baseClassName, 'grid place-items-center bg-gray-light text-gray-dark'),
343
+ children: /*#__PURE__*/ jsx(User, {
344
+ className: "scale-[2.25]"
345
+ })
346
+ });
347
+ };
348
+
349
+ function isLinkProps$1(props) {
350
+ return !!props.href;
351
+ }
352
+ function Backlink(props) {
353
+ const { className, style, children, withUnderline, ref, ...restProps } = props;
354
+ 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');
355
+ const content = /*#__PURE__*/ jsxs(Fragment, {
356
+ children: [
357
+ /*#__PURE__*/ jsx(ChevronLeft, {
358
+ className: cx('-ml-[0.5em] group-hover:-translate-x-1 shrink-0 transition-transform duration-300')
359
+ }),
360
+ /*#__PURE__*/ jsx("span", {
361
+ children: /*#__PURE__*/ jsx("span", {
362
+ className: cx('border-transparent border-t-[1px] border-b-[1px] transition-colors duration-300', withUnderline ? 'border-b-black' : 'group-hover:border-b-black'),
363
+ children: children
364
+ })
365
+ })
366
+ ]
367
+ });
368
+ if (isLinkProps$1(props)) {
369
+ return /*#__PURE__*/ jsx(Link, {
370
+ ...restProps,
371
+ className: _className,
372
+ style: style,
373
+ ref: ref,
374
+ children: content
375
+ });
376
+ }
377
+ return /*#__PURE__*/ jsx(Button$1, {
378
+ ...restProps,
379
+ className: _className,
380
+ style: style,
411
381
  ref: ref,
412
- children: children
382
+ children: content
413
383
  });
414
384
  }
415
385
 
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({
386
+ const badgeVariants = cva({
419
387
  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'
388
+ 'inline-flex w-fit items-center justify-center gap-1.5 rounded-lg [&_svg]:shrink-0'
431
389
  ],
432
390
  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'
391
+ color: {
392
+ 'gray-dark': 'bg-gray-dark text-white',
393
+ mint: 'bg-mint',
394
+ sky: 'bg-sky',
395
+ white: 'bg-white',
396
+ 'blue-dark': 'bg-blue-dark text-white',
397
+ 'green-dark': 'bg-green-dark text-white'
437
398
  },
438
- isGrouped: {
439
- false: 'px-3',
440
- true: '!ring-0 flex-1'
399
+ size: {
400
+ small: 'description px-2 py-0.5 [&_svg]:h-4 [&_svg]:w-4',
401
+ medium: 'description px-2.5 py-1.5 [&_svg]:h-4 [&_svg]:w-4',
402
+ large: 'paragraph px-3 py-2 [&_svg]:h-5 [&_svg]:w-5'
441
403
  }
442
404
  },
443
405
  defaultVariants: {
444
- focusModifier: 'focus',
445
- isGrouped: false
406
+ size: 'medium'
446
407
  }
447
408
  });
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
409
+ function Badge(props) {
410
+ const { className: _className, color, size, ...restProps } = props;
411
+ const className = badgeVariants({
412
+ className: _className,
413
+ color,
414
+ size
466
415
  });
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
416
  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'),
417
+ className: className,
566
418
  ...restProps,
567
- children: children
419
+ "data-slot": "badge"
568
420
  });
569
421
  }
570
422
 
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, {
423
+ function Breadcrumb(props) {
424
+ const { className, children, href, ...restProps } = props;
425
+ return /*#__PURE__*/ jsxs(Breadcrumb$1, {
426
+ className: cx(className, 'group flex items-center'),
577
427
  ...restProps,
578
- className: cx(className, 'flex flex-col gap-2'),
579
- isInvalid: isInvalid,
580
- isRequired: isRequired,
581
428
  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
429
+ href ? /*#__PURE__*/ jsx(Link, {
430
+ href: href,
431
+ // 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
432
+ className: "rounded-xs focus-visible:outline-focus group-last:no-underline",
433
+ children: children
434
+ }) : children,
435
+ /*#__PURE__*/ jsx(ChevronRight, {
436
+ className: "px-1 group-last:hidden"
591
437
  })
592
438
  ]
593
439
  });
594
440
  }
595
441
 
596
- const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
442
+ function Breadcrumbs(props) {
443
+ const { className, children, ...restProps } = props;
444
+ return /*#__PURE__*/ jsx(Breadcrumbs$1, {
597
445
  ...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')
446
+ className: cx(className, 'flex flex-wrap text-sm leading-6'),
447
+ children: children
629
448
  });
449
+ }
450
+
630
451
  /**
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')
452
+ * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
453
+ */ const buttonVariants = cva({
454
+ base: [
455
+ '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'
456
+ ],
457
+ variants: {
458
+ /**
459
+ * The variant of the button
460
+ * @default primary
461
+ */ variant: {
462
+ primary: 'no-underline',
463
+ // 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
464
+ secondary: 'border-2 border-current no-underline hover:border-transparent',
465
+ tertiary: 'underline hover:no-underline'
466
+ },
467
+ /**
468
+ * Adjusts the color of the button for usage on different backgrounds.
469
+ * @default blue
470
+ */ color: {
471
+ blue: 'focus-visible:outline-focus',
472
+ mint: 'focus-visible:outline-focus focus-visible:outline-mint',
473
+ white: 'focus-visible:outline-focus focus-visible:outline-white'
474
+ },
475
+ /**
476
+ * When the button is without text, but with a single icon.
477
+ * @default false
478
+ */ isIconOnly: {
479
+ true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
480
+ false: 'gap-2.5 px-4 py-2'
481
+ },
482
+ // Make the content of the button transparent to hide it's content, but keep the button width
483
+ isPending: {
484
+ true: '!text-transparent relative',
485
+ false: null
486
+ }
487
+ },
488
+ compoundVariants: [
489
+ {
490
+ color: 'blue',
491
+ variant: 'primary',
492
+ // Darken bg by 20% on hover. The color is manually crafted
493
+ className: 'bg-blue-dark text-white hover:bg-blue active:bg-[#0536A0] active:text-white [&_[role="progressbar"]]:text-white'
494
+ },
495
+ {
496
+ color: 'blue',
497
+ variant: 'secondary',
498
+ className: 'text-blue-dark hover:border-transparent hover:bg-blue hover:text-blue-dark hover:text-white active:bg-[#0536A0] [&:hover_[role="progressbar"]]:text-white [&_[role="progressbar"]]:text-blue-dark'
499
+ },
500
+ {
501
+ color: 'blue',
502
+ variant: 'tertiary',
503
+ className: '[&_[role="progressbar"]]:text-black'
504
+ },
505
+ {
506
+ color: 'mint',
507
+ variant: 'primary',
508
+ // Darken bg by 20% on hover. The color is manually crafted
509
+ className: 'bg-mint text-black hover:bg-[#8dd4bd] active:[#9ddac6] [&_[role="progressbar"]]:text-black'
510
+ },
511
+ {
512
+ color: 'mint',
513
+ variant: 'secondary',
514
+ className: 'text-mint hover:bg-mint hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-mint'
515
+ },
516
+ {
517
+ color: 'mint',
518
+ variant: 'tertiary',
519
+ className: 'text-mint [&_[role="progressbar"]]:text-mint'
520
+ },
521
+ {
522
+ color: 'white',
523
+ variant: 'primary',
524
+ className: 'bg-white text-black hover:bg-sky active:bg-sky-light [&_[role="progressbar"]]:text-black'
525
+ },
526
+ {
527
+ color: 'white',
528
+ variant: 'secondary',
529
+ className: 'text-white hover:bg-white hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-white'
530
+ },
531
+ {
532
+ color: 'white',
533
+ variant: 'tertiary',
534
+ className: 'text-white [&_[role="progressbar"]]:text-white'
535
+ }
536
+ ],
537
+ defaultVariants: {
538
+ variant: 'primary',
539
+ color: 'blue',
540
+ isIconOnly: false,
541
+ isPending: false
542
+ }
543
+ });
544
+ const ButtonContext = /*#__PURE__*/ createContext({});
545
+ function isLinkProps(props) {
546
+ return !!props.href;
547
+ }
548
+ function Button({ ref = null, ...props }) {
549
+ [props, ref] = useContextProps(props, ref, ButtonContext);
550
+ const { children: _children, color, isIconOnly, variant, isPending, ...restProps } = props;
551
+ const className = buttonVariants({
552
+ className: props.className,
553
+ color,
554
+ isIconOnly,
555
+ variant,
556
+ isPending
635
557
  });
636
-
637
- function InputAddonDivider() {
638
- return /*#__PURE__*/ jsx("span", {
639
- className: "block h-6 w-px flex-none bg-black"
558
+ const locale = _useLocale();
559
+ const { progressBarProps } = useProgressBar({
560
+ isIndeterminate: true,
561
+ 'aria-label': translations$1.pending[locale]
640
562
  });
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,
563
+ const children = isPending ? /*#__PURE__*/ jsxs(Fragment, {
652
564
  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
- })
565
+ _children,
566
+ /*#__PURE__*/ jsx(LoadingSpinner, {
567
+ className: "absolute m-auto motion-safe:animate-spin",
568
+ ...progressBarProps
691
569
  })
692
570
  ]
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, {
571
+ }) : _children;
572
+ return isLinkProps(restProps) ? /*#__PURE__*/ jsx(Link, {
702
573
  ...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
- ]
574
+ className: className,
575
+ ref: ref,
576
+ children: children
577
+ }) : /*#__PURE__*/ jsx(Button$1, {
578
+ ...restProps,
579
+ className: className,
580
+ isPending: isPending,
581
+ ref: ref,
582
+ children: children
720
583
  });
721
584
  }
722
585
 
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
- })
586
+ const cardVariants = cva({
587
+ base: [
588
+ 'group/card',
589
+ 'rounded-[inherit]',
590
+ 'border p-3',
591
+ 'flex flex-col gap-y-4',
592
+ 'relative',
593
+ // **** Content ****
594
+ '[&_[data-slot="content"]]:flex [&_[data-slot="content"]]:flex-col [&_[data-slot="content"]]:gap-y-4',
595
+ // **** Media ****
596
+ '[&_[data-slot="media"]_*]:pointer-events-none',
597
+ '[&_[data-slot="media"]]:overflow-hidden',
598
+ '[&_[data-slot="media"]]:relative',
599
+ // Position media at the edges of the card (because of these negative margins the media-element must be a wrapper around the actual image or other media content)
600
+ '[&_[data-slot="media"]]:mx-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))] [&_[data-slot="media"]]:mt-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
601
+ // Sets the aspect ratio of the media content (width: 100% is necessary to make aspect ratio work on images in FF)
602
+ '[&_[data-slot="media"]>*:not([data-slot="badge"])]:aspect-3/2 [&_[data-slot="media"]>img]:w-full [&_[data-slot="media"]>img]:object-cover',
603
+ // Prepare zoom animation for hover effects. The hover effect can also be enabled by classes on the parent component, so it is always prepared here.
604
+ '[&_[data-slot="media"]>*]:duration-300 [&_[data-slot="media"]>*]:ease-in-out [&_[data-slot="media"]>*]:motion-safe:transition-transform',
605
+ // **** Card link ****
606
+ // **** Hover ****
607
+ // Enables the zoom hover effect on media (note that we can't use group-hover/card here, because there might be other clickable elements in the card aside from the heading)
608
+ '[&:has([data-slot="card-link"]_a:hover)_[data-slot="media"]>img]:scale-110',
609
+ '[&:has([data-slot="heading"]_[data-slot="card-link"]:hover)_[data-slot="media"]>img]:scale-110',
610
+ // **** Fail-safe for interactive elements ****
611
+ // Make interactive elements clickable by themselves, while the rest of the card is clickable as a whole
612
+ // The card is made clickable by a pseudo-element on the heading that covers the entire card
613
+ '[&:not(:has([data-slot="card-link"]_a))_a:not([data-slot="card-link"])]:relative [&_button]:relative [&_input]:relative',
614
+ // Our Button component has position: relative by default, so we need to override that if it is used in a CardLink (to make the entire card clickable)
615
+ '[&_[data-slot="card-link"]_a]:static',
616
+ // Place other interactive on top of the pseudo-element that makes the entire card clickable
617
+ // by setting a higher z-index than the pseudo-element (which implicitly z-index 0)
618
+ '[&_a:not([data-slot="card-link"])]:z-[1] [&_button]:z-[1] [&_input]:z-[1]',
619
+ // **** Badge ****
620
+ '[&_[data-slot="media"]_[data-slot="badge"]]:absolute [&_[data-slot="media"]_[data-slot="badge"]]:top-0',
621
+ // Increasing z-index Preserves badge position when media content is hovered (the transform scale effect might otherwise move the badge behind the other media content)
622
+ '[&_[data-slot="media"]_[data-slot="badge"]]:z-[1]',
623
+ // Left aligned - override default corner radius of the badge
624
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tl-2xl',
625
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-br-2xl',
626
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tr-none',
627
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-bl-none',
628
+ // Right aligned - override default corner radius of the badge
629
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tl-none',
630
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-br-none',
631
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tr-2xl',
632
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-bl-2xl',
633
+ // ... and position the badge at the right edge of the media content
634
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:right-0'
635
+ ],
636
+ variants: {
637
+ /**
638
+ * The variant of the card
639
+ * @default subtle
640
+ */ variant: {
641
+ subtle: [
642
+ 'border-transparent',
643
+ // **** Media styles ****
644
+ '[&_[data-slot="media"]]:rounded-2xl'
645
+ ],
646
+ outlined: 'border border-black'
647
+ },
648
+ /**
649
+ * The layout of the card
650
+ * @default vertical
651
+ */ layout: {
652
+ vertical: [
653
+ 'flex-col',
654
+ // **** Media ****
655
+ '[&_[data-slot="media"]]:rounded-t-2xl'
656
+ ],
657
+ horizontal: [
658
+ // Use more gap for horizontal cards that have media
659
+ // Since this does not affect the layout before the flex direction is set (at breakpoint @2xl for Card with Media), we can set it here
660
+ 'has-data-[slot=media]:layout-gap-x not-has-data-[slot=media]:gap-x-4',
661
+ // **** With Media ****
662
+ '[&:has(>[data-slot="media"]:last-child)]:flex-col-reverse',
663
+ 'has-data-[slot=media]:@2xl:!flex-row',
664
+ '*:data-[slot=media]:@2xl:h-fit',
665
+ 'has-data-[slot=media]:*:@2xl:basis-1/2',
666
+ // Position media at the edges of the card
667
+ '*:data-[slot=media]:@2xl:mb-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
668
+ '*:data-[slot=media]:first:@2xl:mr-0',
669
+ '*:data-[slot=media]:last:@2xl:ml-0',
670
+ // Make sure the card link is clickable when the media is on the right side
671
+ // This is necessary because the media content is positioned after the card link in the DOM
672
+ '[&:has(>[data-slot="media"]:last-child)_[data-slot="card-link"]]:z-[1]',
673
+ // **** Without Media ****
674
+ 'not-has-data-[slot=media]:@md:flex-row',
675
+ // Make the layout responsive: when the Content reaches a minimum width of 12rem, the layout switches to vertical. Also makes sure Content takes up the remaining space available.
676
+ 'not-has-data-[slot=media]:**:data-[slot=content]:grow',
677
+ // Make sure svg's etc. are not shrinkable
678
+ '[&>:not([data-slot="content"],[data-slot="media"])]:shrink-0'
754
679
  ]
680
+ }
681
+ },
682
+ defaultVariants: {
683
+ variant: 'subtle',
684
+ layout: 'vertical'
685
+ },
686
+ compoundVariants: [
687
+ {
688
+ variant: 'outlined',
689
+ layout: 'horizontal',
690
+ className: [
691
+ // **** Media ****
692
+ // Some rounded corners are removed when the card is outlined
693
+ '[&_[data-slot="media"]]:rounded-t-2xl',
694
+ '*:data-[slot=media]:first:@2xl:rounded-tr-none *:data-[slot=media]:first:@2xl:rounded-bl-2xl',
695
+ '*:data-[slot=media]:last:@2xl:rounded-tl-none *:data-[slot=media]:last:@2xl:rounded-br-2xl',
696
+ // **** Badge ****
697
+ // Override default corner radius of the badge to match the media border radius
698
+ '[&_[data-slot="media"]:first-child_[data-slot="badge"]:last-child]:@2xl:rounded-tr-none',
699
+ '[&_[data-slot="media"]:last-child_[data-slot="badge"]:first-child]:@2xl:rounded-tl-none'
700
+ ]
701
+ }
702
+ ]
703
+ });
704
+ const Card = ({ children, className, variant, layout, ...restProps })=>{
705
+ const cardClassName = cardVariants({
706
+ variant,
707
+ layout
708
+ });
709
+ return(// The border-radius is set on the outer container to make it act as an invisible wrapper, only used for container queries
710
+ // Since passing the className prop to this container is necessary to make custom styles behave as expected, we need to apply the border-radius here incase the consumer passes a custom background color
711
+ /*#__PURE__*/ jsx("div", {
712
+ ...restProps,
713
+ className: cx(className, '@container rounded-2xl'),
714
+ children: /*#__PURE__*/ jsx("div", {
715
+ className: cardClassName,
716
+ children: /*#__PURE__*/ jsx(Provider, {
717
+ values: [
718
+ [
719
+ HeadingContext,
720
+ {
721
+ size: 's',
722
+ className: cx([
723
+ 'inline',
724
+ 'w-fit',
725
+ 'text-pretty',
726
+ 'hyphens-auto',
727
+ '[word-break:break-word]',
728
+ // **** Card link in Heading ****
729
+ // Border (bottom/top) is set to transparent to make sure the bottom underline is not visible when the card is hovered
730
+ // Border top is set to even out the border bottom used for the underline
731
+ '*:data-[slot="card-link"]:no-underline',
732
+ '*:data-[slot="card-link"]:border-y-2',
733
+ '*:data-[slot="card-link"]:border-y-transparent',
734
+ '*:data-[slot="card-link"]:transition-colors',
735
+ '*:data-[slot="card-link"]:hover:border-b-current',
736
+ // Mimic heading styles for the card link if placed in the heading slot. This is necessary to make the custom underline align with the link text
737
+ '*:data-[slot="card-link"]:font-inherit',
738
+ '*:data-[slot="card-link"]:text-pretty',
739
+ '*:data-[slot="card-link"]:hyphens-auto',
740
+ '*:data-[slot="card-link"]:[word-break:break-word]'
741
+ ])
742
+ }
743
+ ]
744
+ ],
745
+ children: children
746
+ })
755
747
  })
748
+ }));
749
+ };
750
+ const cardLinkVariants = cva({
751
+ base: 'w-fit max-w-full',
752
+ variants: {
753
+ withHref: {
754
+ true: [
755
+ // **** Clickarea ****
756
+ 'cursor-pointer',
757
+ 'after:absolute',
758
+ 'after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
759
+ 'after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
760
+ // **** Focus ****
761
+ 'focus-visible:outline-none',
762
+ 'data-focus-visible:after:outline-focus',
763
+ 'data-focus-visible:after:outline-offset-2',
764
+ // **** Hover ****
765
+ // Links are underlined by default, and the underline is removed on hover.
766
+ // So we make sure that also happens when the user hovers the clickable area.
767
+ 'hover:no-underline'
768
+ ],
769
+ false: [
770
+ // **** Clickarea ****
771
+ '[&_a]:after:cursor-pointer',
772
+ '[&_a]:after:absolute',
773
+ '[&_a]:after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
774
+ '[&_a]:after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
775
+ // **** Focus ****
776
+ '[&_a[data-focus-visible]]:outline-none',
777
+ '[&_a[data-focus-visible]]:after:outline-focus',
778
+ '[&_a[data-focus-visible]]:after:outline-offset-2',
779
+ // **** Hover ****
780
+ // Links are underlined by default, and the underline is removed on hover.
781
+ // So we make sure that also happens when the user hovers the card.
782
+ // The group-hover ensures that the hover effect also applies when this component is used as a wrapper around a link.
783
+ '[&_a]:group-hover/card:no-underline'
784
+ ]
785
+ }
786
+ }
787
+ });
788
+ /**
789
+ * A component that creates a clickable area on a card.
790
+ * It can be used either as a wrapper around a link or as a standalone link.
791
+ */ const CardLink = ({ className: _className, href, ...restProps })=>{
792
+ const className = cardLinkVariants({
793
+ className: _className,
794
+ withHref: !!href
756
795
  });
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, {
796
+ return href ? /*#__PURE__*/ jsx(Link, {
797
+ "data-slot": "card-link",
765
798
  ...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
799
+ href: href,
800
+ className: className
801
+ }) : // We can't utilize that the `Link` component from react-aria-components renders as a span if it doesn't have an href,
802
+ // because it still renders with role="link" and tabindex="0" which makes it focusable.
803
+ // So we need to render a div instead.
804
+ /*#__PURE__*/ jsx("div", {
805
+ ...restProps,
806
+ "data-slot": "card-link",
807
+ className: className
808
+ });
809
+ };
810
+
811
+ const Carousel = ({ className, children, onChange })=>{
812
+ const ref = useRef(null);
813
+ const locale = _useLocale();
814
+ const { previous, next } = translations$1;
815
+ const [scrollTargetIndex, setScrollTargetIndex] = useState(0);
816
+ const [hasReachedScrollStart, setHasReachedScrollStart] = useState(scrollTargetIndex === 0);
817
+ const [hasReachedScrollEnd, setHasReachedScrollEnd] = useState(!ref.current || ref.current.children.length - 1 === scrollTargetIndex);
818
+ useEffect(()=>{
819
+ setHasReachedScrollStart(scrollTargetIndex === 0);
820
+ setHasReachedScrollEnd(!ref.current || ref.current.children.length - 1 === scrollTargetIndex);
821
+ }, [
822
+ scrollTargetIndex
823
+ ]);
824
+ // Keep track of the previous index to determine if the user is scrolling forward or backward
825
+ // This is used to determine which callback to call (onPrev or onNext)
826
+ const prevIndex = useRef(0);
827
+ // Handle scrolling when user clicks the arrow icons
828
+ useUpdateEffect(()=>{
829
+ if (!ref.current) return;
830
+ ref.current.children[scrollTargetIndex]?.scrollIntoView({
831
+ behavior: 'smooth',
832
+ inline: 'start',
833
+ block: 'nearest'
834
+ });
835
+ if (prevIndex.current !== scrollTargetIndex && onChange) {
836
+ onChange({
837
+ index: scrollTargetIndex,
838
+ id: ref.current.children[scrollTargetIndex]?.id,
839
+ prevIndex: prevIndex.current,
840
+ prevId: ref.current.children[prevIndex.current]?.id
841
+ });
842
+ }
843
+ prevIndex.current = scrollTargetIndex;
844
+ }, [
845
+ scrollTargetIndex
846
+ ]);
847
+ const onScroll = useDebouncedCallback((event)=>{
848
+ const target = event.target;
849
+ // Calculate the index of the item that is currently in view
850
+ const newScrollTargetIndex = Array.from(target.children).findIndex((child)=>{
851
+ const rect = child.getBoundingClientRect();
852
+ return rect.left >= 0 && rect.right <= window.innerWidth && rect.top >= 0;
853
+ });
854
+ if (newScrollTargetIndex !== -1) {
855
+ setScrollTargetIndex(newScrollTargetIndex);
856
+ }
857
+ }, 100);
858
+ return /*#__PURE__*/ jsx("div", {
859
+ "data-slot": "carousel",
860
+ children: /*#__PURE__*/ jsx(Provider, {
861
+ values: [
862
+ [
863
+ CarouselItemsContext,
864
+ {
865
+ ref,
866
+ onScroll
867
+ }
868
+ ],
869
+ [
870
+ ButtonContext,
871
+ {
872
+ slots: {
873
+ [DEFAULT_SLOT]: {},
874
+ prev: {
875
+ 'aria-label': previous[locale],
876
+ onPress: ()=>{
877
+ if (scrollTargetIndex > 0) {
878
+ setScrollTargetIndex((prev)=>prev - 1);
879
+ }
880
+ },
881
+ isDisabled: hasReachedScrollStart
882
+ },
883
+ next: {
884
+ isIconOnly: true,
885
+ 'aria-label': next[locale],
886
+ onPress: ()=>{
887
+ if (!ref.current) return;
888
+ if (scrollTargetIndex < ref.current.children.length - 1) {
889
+ setScrollTargetIndex((prev)=>prev + 1);
890
+ }
891
+ },
892
+ isDisabled: hasReachedScrollEnd
893
+ }
894
+ }
895
+ }
896
+ ]
897
+ ],
898
+ children: /*#__PURE__*/ jsxs("div", {
899
+ 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
900
+ '[&: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
901
+ '**:data-[slot="video-loop-button"]:focus-visible:outline-none'),
902
+ children: [
903
+ children,
904
+ /*#__PURE__*/ jsxs(_CarouselControls, {
905
+ children: [
906
+ /*#__PURE__*/ jsx(Button, {
907
+ isIconOnly: true,
908
+ slot: "prev",
909
+ variant: "primary",
910
+ color: "white",
911
+ className: cx('group/carousel-previous', hasReachedScrollStart && 'invisible'),
912
+ children: /*#__PURE__*/ jsx(ChevronLeft, {
913
+ className: "group-hover/carousel-previous:motion-safe:-translate-x-1 transition-transform"
914
+ })
915
+ }),
916
+ /*#__PURE__*/ jsx(Button, {
917
+ isIconOnly: true,
918
+ slot: "next",
919
+ variant: "primary",
920
+ color: "white",
921
+ className: cx('group/carousel-next', hasReachedScrollEnd && 'invisible'),
922
+ children: /*#__PURE__*/ jsx(ChevronRight, {
923
+ className: "transition-transform group-hover/carousel-next:motion-safe:translate-x-1"
924
+ })
925
+ })
926
+ ]
788
927
  })
789
928
  ]
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
929
  })
801
- ]
930
+ })
802
931
  });
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
- ]
932
+ };
933
+ /**
934
+ * 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.
935
+ * It is used to render the prev/next buttons in the carousel for now.
936
+ */ const _CarouselControls = ({ children, className })=>/*#__PURE__*/ jsx("div", {
937
+ 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)
938
+ 'items-end *:h-fit'),
939
+ "data-slot": "carousel-controls",
940
+ children: children
828
941
  });
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
- }),
942
+ const CarouselItemsContext = /*#__PURE__*/ createContext({
943
+ ref: null
944
+ });
945
+ const CarouselItems = ({ className, children })=>/*#__PURE__*/ jsx(CarouselItemsContext.Consumer, {
946
+ children: ({ ref, onScroll })=>/*#__PURE__*/ jsx("div", {
947
+ "data-slot": "carousel-items",
948
+ className: cx(className, [
949
+ 'scrollbar-hidden',
950
+ 'flex',
951
+ 'snap-x',
952
+ 'snap-mandatory',
953
+ 'overflow-x-auto',
954
+ 'outline-none',
955
+ 'rounded-[inherit]'
956
+ ]),
884
957
  ref: ref,
885
- size: size
886
- }),
887
- /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
888
- errorMessage: errorMessage
958
+ // When the SnapEvent is supported: https://developer.mozilla.org/en-US/docs/Web/API/SnapEvent
959
+ // We can use the scrollsnapchange event to detect when the user has scrolled to a new item.
960
+ // We can then use Array.from(event.target.children).indexOf(event.snapTargetInline) to calculate the index of the item that is currently in view.
961
+ // Another option is to use the scrollEnd event, when Safiri supports it: https://developer.apple.com/documentation/webkitjs/snap_event/scrollend_event
962
+ onScroll: onScroll,
963
+ children: children
889
964
  })
890
- ]
891
965
  });
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
966
+ const CarouselItem = ({ className, children, id })=>{
967
+ return /*#__PURE__*/ jsx("div", {
968
+ className: cx(className, 'shrink-0 basis-full snap-start'),
969
+ "data-slot": "carousel-item",
970
+ id: id,
971
+ children: /*#__PURE__*/ jsx(Provider, {
972
+ values: [
973
+ [
974
+ MediaContext,
975
+ {
976
+ fit: 'cover',
977
+ 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')
978
+ }
942
979
  ]
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
- ]
980
+ ],
981
+ children: children
982
+ })
955
983
  });
956
- }
957
-
958
- const iconMap = {
959
- info: InfoCircle,
960
- success: CheckCircle,
961
- warning: Warning,
962
- danger: Error
963
984
  };
964
- const alertVariants = cva({
985
+
986
+ const formField = cx('group flex flex-col gap-2');
987
+ 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');
988
+ const input = cva({
965
989
  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'
990
+ // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
991
+ 'bg-white',
992
+ // Use box-content to enable auto width based on number of characters (size)
993
+ // Setting min-height to prevent the input from collapsing in Safari
994
+ // Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
995
+ 'box-content min-h-6 py-2.5',
996
+ 'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
997
+ // invalid styles
998
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red',
999
+ // Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
1000
+ 'appearance-none'
973
1001
  ],
974
1002
  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'
1003
+ // Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
1004
+ focusModifier: {
1005
+ focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
1006
+ visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
1007
+ },
1008
+ isGrouped: {
1009
+ false: 'px-3',
1010
+ true: '!ring-0 flex-1'
983
1011
  }
984
1012
  },
985
1013
  defaultVariants: {
986
- variant: 'info'
1014
+ focusModifier: 'focus',
1015
+ isGrouped: false
987
1016
  }
988
1017
  });
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
- });
1018
+ const inputGroup = cx([
1019
+ 'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
1020
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
1021
+ ]);
1022
+ const dropdown = {
1023
+ 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'),
1024
+ // overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
1025
+ listbox: cx('max-h-[25rem] overflow-x-hidden text-sm outline-hidden'),
1026
+ chevronIcon: cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
1050
1027
  };
1051
1028
 
1052
- function Breadcrumbs(props) {
1053
- const { className, children, ...restProps } = props;
1054
- return /*#__PURE__*/ jsx(Breadcrumbs$1, {
1029
+ function ErrorMessage(props) {
1030
+ const { children, className, ...restProps } = props;
1031
+ return /*#__PURE__*/ jsx(Text, {
1055
1032
  ...restProps,
1056
- className: cx(className, 'flex flex-wrap text-sm leading-6'),
1033
+ className: cx(className, formFieldError),
1034
+ slot: "errorMessage",
1057
1035
  children: children
1058
1036
  });
1059
1037
  }
1060
1038
 
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'),
1039
+ const defaultClasses$1 = cx([
1040
+ 'group -mx-2.5 relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4 p-2.5 leading-7'
1041
+ ]);
1042
+ // Pulling this out into it's own component. Will probably export it in the future
1043
+ // so it can be used in other views, outside of an input of type checkbox, like in table rows.
1044
+ function CheckmarkBox() {
1045
+ return /*#__PURE__*/ jsx("span", {
1046
+ className: cx([
1047
+ 'relative left-0 grid flex-none place-content-center rounded-sm border-2 border-black text-white',
1048
+ // 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.
1049
+ // For the ::before psuedo element the line height of the label is always 1em.
1050
+ // 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
1051
+ // 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?
1052
+ 'mt-[calc((1em_*_1.75_-_24px)_/_2)] h-[24px] w-[24px]',
1053
+ // selected
1054
+ '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',
1055
+ 'group-data-selected:group-not-data-hovered:group-data-invalid:border-red group-data-selected:group-not-data-hovered:group-data-invalid:bg-red',
1056
+ // focus
1057
+ 'group-data-focus-visible:outline-focus-offset',
1058
+ // hovered
1059
+ 'group-data-hovered:group-data-invalid:bg-red-light',
1060
+ 'group-data-hovered:border-blue',
1061
+ 'group-data-hovered:bg-sky',
1062
+ 'group-data-hovered:group-data-selected:group-not-data-invalid:border-blue-dark',
1063
+ 'group-data-hovered:group-data-selected:group-not-data-invalid:bg-blue-dark',
1064
+ // 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
1065
+ // so we use an inner shadow of 1 px instead to pad the actual border
1066
+ 'group-data-invalid:border-red group-data-invalid:shadow-[inset_0_0_0_1px] group-data-invalid:shadow-red'
1067
+ ]),
1068
+ children: /*#__PURE__*/ jsx(Check, {
1069
+ className: "h-full w-full opacity-0 group-data-invalid:group-data-hovered:group-data-selected:text-red group-data-selected:opacity-100"
1070
+ })
1071
+ });
1072
+ }
1073
+ function Checkbox(props) {
1074
+ const { children, className, description, errorMessage, isInvalid: _isInvalid, ...restProps } = props;
1075
+ const id = useId();
1076
+ const descriptionId = `desc${id}`;
1077
+ const errorMessageId = `error${id}`;
1078
+ const isInvalid = !!errorMessage || _isInvalid;
1079
+ return /*#__PURE__*/ jsx("div", {
1080
+ children: /*#__PURE__*/ jsxs(CheckboxContext.Provider, {
1081
+ value: {
1082
+ 'aria-describedby': description ? descriptionId : undefined,
1083
+ 'aria-errormessage': errorMessage ? errorMessageId : undefined
1084
+ },
1085
+ children: [
1086
+ /*#__PURE__*/ jsxs(Checkbox$1, {
1087
+ ...restProps,
1088
+ className: cx(className, defaultClasses$1),
1089
+ isInvalid: isInvalid,
1090
+ children: [
1091
+ /*#__PURE__*/ jsx(CheckmarkBox, {}),
1092
+ children
1093
+ ]
1094
+ }),
1095
+ 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 */}
1096
+ /*#__PURE__*/ jsx("div", {
1097
+ id: descriptionId,
1098
+ slot: "description",
1099
+ className: "description block",
1100
+ children: description
1101
+ }),
1102
+ errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
1103
+ className: "mt-2 block",
1104
+ id: errorMessageId,
1105
+ children: errorMessage
1106
+ })
1107
+ ]
1108
+ })
1109
+ });
1110
+ }
1111
+
1112
+ function Description(props) {
1113
+ const { className, ...restProps } = props;
1114
+ return /*#__PURE__*/ jsx(Text, {
1065
1115
  ...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
- ]
1116
+ className: cx(className, 'description'),
1117
+ slot: "description"
1077
1118
  });
1078
1119
  }
1079
1120
 
1080
- function isLinkProps(props) {
1081
- return !!props.href;
1121
+ /**
1122
+ * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
1123
+ * In other words, this handles controlled and uncontrolled form errors.
1124
+ */ function ErrorMessageOrFieldError({ errorMessage }) {
1125
+ return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
1126
+ children: errorMessage
1127
+ }) : /*#__PURE__*/ jsx(FieldError, {
1128
+ className: formFieldError
1129
+ });
1082
1130
  }
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
- ]
1131
+
1132
+ function Label(props) {
1133
+ const { children, className, ...restProps } = props;
1134
+ return /*#__PURE__*/ jsx(Label$1, {
1135
+ className: cx(className, 'font-semibold leading-7'),
1136
+ ...restProps,
1137
+ children: children
1098
1138
  });
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, {
1139
+ }
1140
+
1141
+ function CheckboxGroup(props) {
1142
+ const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
1143
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
1144
+ // which will override any built in validation
1145
+ const isInvalid = !!errorMessage || _isInvalid;
1146
+ return /*#__PURE__*/ jsxs(CheckboxGroup$1, {
1109
1147
  ...restProps,
1110
- className: _className,
1111
- style: style,
1112
- ref: ref,
1113
- children: content
1148
+ className: cx(className, 'flex flex-col gap-2'),
1149
+ isInvalid: isInvalid,
1150
+ isRequired: isRequired,
1151
+ children: [
1152
+ label && /*#__PURE__*/ jsx(Label, {
1153
+ children: label
1154
+ }),
1155
+ description && /*#__PURE__*/ jsx(Description, {
1156
+ children: description
1157
+ }),
1158
+ children,
1159
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1160
+ errorMessage: errorMessage
1161
+ })
1162
+ ]
1114
1163
  });
1115
1164
  }
1116
1165
 
1117
- const cardVariants = cva({
1118
- base: [
1119
- 'group/card',
1120
- 'rounded-[inherit]',
1121
- 'border p-3',
1122
- 'flex flex-col gap-y-4',
1123
- 'relative',
1124
- // **** Content ****
1125
- '[&_[data-slot="content"]]:flex [&_[data-slot="content"]]:flex-col [&_[data-slot="content"]]:gap-y-4',
1126
- // **** Media ****
1127
- '[&_[data-slot="media"]_*]:pointer-events-none',
1128
- '[&_[data-slot="media"]]:overflow-hidden',
1129
- '[&_[data-slot="media"]]:relative',
1130
- // Position media at the edges of the card (because of these negative margins the media-element must be a wrapper around the actual image or other media content)
1131
- '[&_[data-slot="media"]]:mx-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))] [&_[data-slot="media"]]:mt-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
1132
- // Sets the aspect ratio of the media content (width: 100% is necessary to make aspect ratio work on images in FF)
1133
- '[&_[data-slot="media"]>*:not([data-slot="badge"])]:aspect-3/2 [&_[data-slot="media"]>img]:w-full [&_[data-slot="media"]>img]:object-cover',
1134
- // Prepare zoom animation for hover effects. The hover effect can also be enabled by classes on the parent component, so it is always prepared here.
1135
- '[&_[data-slot="media"]>*]:duration-300 [&_[data-slot="media"]>*]:ease-in-out [&_[data-slot="media"]>*]:motion-safe:transition-transform',
1136
- // **** Card link ****
1137
- // **** Hover ****
1138
- // Enables the zoom hover effect on media (note that we can't use group-hover/card here, because there might be other clickable elements in the card aside from the heading)
1139
- '[&:has([data-slot="card-link"]_a:hover)_[data-slot="media"]>img]:scale-110',
1140
- '[&:has([data-slot="heading"]_[data-slot="card-link"]:hover)_[data-slot="media"]>img]:scale-110',
1141
- // **** Fail-safe for interactive elements ****
1142
- // Make interactive elements clickable by themselves, while the rest of the card is clickable as a whole
1143
- // The card is made clickable by a pseudo-element on the heading that covers the entire card
1144
- '[&:not(:has([data-slot="card-link"]_a))_a:not([data-slot="card-link"])]:relative [&_button]:relative [&_input]:relative',
1145
- // Our Button component has position: relative by default, so we need to override that if it is used in a CardLink (to make the entire card clickable)
1146
- '[&_[data-slot="card-link"]_a]:static',
1147
- // Place other interactive on top of the pseudo-element that makes the entire card clickable
1148
- // by setting a higher z-index than the pseudo-element (which implicitly z-index 0)
1149
- '[&_a:not([data-slot="card-link"])]:z-[1] [&_button]:z-[1] [&_input]:z-[1]',
1150
- // **** Badge ****
1151
- '[&_[data-slot="media"]_[data-slot="badge"]]:absolute [&_[data-slot="media"]_[data-slot="badge"]]:top-0',
1152
- // Increasing z-index Preserves badge position when media content is hovered (the transform scale effect might otherwise move the badge behind the other media content)
1153
- '[&_[data-slot="media"]_[data-slot="badge"]]:z-[1]',
1154
- // Left aligned - override default corner radius of the badge
1155
- '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tl-2xl',
1156
- '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-br-2xl',
1157
- '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tr-none',
1158
- '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-bl-none',
1159
- // Right aligned - override default corner radius of the badge
1160
- '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tl-none',
1161
- '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-br-none',
1162
- '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tr-2xl',
1163
- '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-bl-2xl',
1164
- // ... and position the badge at the right edge of the media content
1165
- '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:right-0'
1166
- ],
1167
- variants: {
1168
- /**
1169
- * The variant of the card
1170
- * @default subtle
1171
- */ variant: {
1172
- subtle: [
1173
- 'border-transparent',
1174
- // **** Media styles ****
1175
- '[&_[data-slot="media"]]:rounded-2xl'
1176
- ],
1177
- outlined: 'border border-black'
1178
- },
1179
- /**
1180
- * The layout of the card
1181
- * @default vertical
1182
- */ layout: {
1183
- vertical: [
1184
- 'flex-col',
1185
- // **** Media ****
1186
- '[&_[data-slot="media"]]:rounded-t-2xl'
1187
- ],
1188
- horizontal: [
1189
- // Use more gap for horizontal cards that have media
1190
- // Since this does not affect the layout before the flex direction is set (at breakpoint @2xl for Card with Media), we can set it here
1191
- 'has-data-[slot=media]:layout-gap-x not-has-data-[slot=media]:gap-x-4',
1192
- // **** With Media ****
1193
- '[&:has(>[data-slot="media"]:last-child)]:flex-col-reverse',
1194
- 'has-data-[slot=media]:@2xl:!flex-row',
1195
- '*:data-[slot=media]:@2xl:h-fit',
1196
- 'has-data-[slot=media]:*:@2xl:basis-1/2',
1197
- // Position media at the edges of the card
1198
- '*:data-[slot=media]:@2xl:mb-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
1199
- '*:data-[slot=media]:first:@2xl:mr-0',
1200
- '*:data-[slot=media]:last:@2xl:ml-0',
1201
- // Make sure the card link is clickable when the media is on the right side
1202
- // This is necessary because the media content is positioned after the card link in the DOM
1203
- '[&:has(>[data-slot="media"]:last-child)_[data-slot="card-link"]]:z-[1]',
1204
- // **** Without Media ****
1205
- 'not-has-data-[slot=media]:@md:flex-row',
1206
- // Make the layout responsive: when the Content reaches a minimum width of 12rem, the layout switches to vertical. Also makes sure Content takes up the remaining space available.
1207
- 'not-has-data-[slot=media]:**:data-[slot=content]:grow',
1208
- // Make sure svg's etc. are not shrinkable
1209
- '[&>:not([data-slot="content"],[data-slot="media"])]:shrink-0'
1210
- ]
1211
- }
1212
- },
1213
- defaultVariants: {
1214
- variant: 'subtle',
1215
- layout: 'vertical'
1216
- },
1217
- compoundVariants: [
1218
- {
1219
- variant: 'outlined',
1220
- layout: 'horizontal',
1221
- className: [
1222
- // **** Media ****
1223
- // Some rounded corners are removed when the card is outlined
1224
- '[&_[data-slot="media"]]:rounded-t-2xl',
1225
- '*:data-[slot=media]:first:@2xl:rounded-tr-none *:data-[slot=media]:first:@2xl:rounded-bl-2xl',
1226
- '*:data-[slot=media]:last:@2xl:rounded-tl-none *:data-[slot=media]:last:@2xl:rounded-br-2xl',
1227
- // **** Badge ****
1228
- // Override default corner radius of the badge to match the media border radius
1229
- '[&_[data-slot="media"]:first-child_[data-slot="badge"]:last-child]:@2xl:rounded-tr-none',
1230
- '[&_[data-slot="media"]:last-child_[data-slot="badge"]:first-child]:@2xl:rounded-tl-none'
1231
- ]
1232
- }
1233
- ]
1234
- });
1235
- const Card = ({ children, className, variant, layout, ...restProps })=>{
1236
- const cardClassName = cardVariants({
1237
- variant,
1238
- layout
1166
+ function InputAddonDivider() {
1167
+ return /*#__PURE__*/ jsx("span", {
1168
+ className: "block h-6 w-px flex-none bg-black"
1239
1169
  });
1240
- return(// The border-radius is set on the outer container to make it act as an invisible wrapper, only used for container queries
1241
- // Since passing the className prop to this container is necessary to make custom styles behave as expected, we need to apply the border-radius here incase the consumer passes a custom background color
1242
- /*#__PURE__*/ jsx("div", {
1170
+ }
1171
+
1172
+ const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
1243
1173
  ...restProps,
1244
- className: cx(className, '@container rounded-2xl'),
1245
- children: /*#__PURE__*/ jsx("div", {
1246
- className: cardClassName,
1247
- children: /*#__PURE__*/ jsx(Provider, {
1248
- values: [
1249
- [
1250
- HeadingContext,
1251
- {
1252
- size: 's',
1253
- className: cx([
1254
- 'inline',
1255
- 'w-fit',
1256
- 'text-pretty',
1257
- 'hyphens-auto',
1258
- '[word-break:break-word]',
1259
- // **** Card link in Heading ****
1260
- // Border (bottom/top) is set to transparent to make sure the bottom underline is not visible when the card is hovered
1261
- // Border top is set to even out the border bottom used for the underline
1262
- '*:data-[slot="card-link"]:no-underline',
1263
- '*:data-[slot="card-link"]:border-y-2',
1264
- '*:data-[slot="card-link"]:border-y-transparent',
1265
- '*:data-[slot="card-link"]:transition-colors',
1266
- '*:data-[slot="card-link"]:hover:border-b-current',
1267
- // Mimic heading styles for the card link if placed in the heading slot. This is necessary to make the custom underline align with the link text
1268
- '*:data-[slot="card-link"]:font-inherit',
1269
- '*:data-[slot="card-link"]:text-pretty',
1270
- '*:data-[slot="card-link"]:hyphens-auto',
1271
- '*:data-[slot="card-link"]:[word-break:break-word]'
1272
- ])
1273
- }
1274
- ]
1275
- ],
1276
- children: children
1174
+ className: cx(dropdown.listbox, className)
1175
+ });
1176
+ const ListBoxItem = (props)=>{
1177
+ let textValue = props.textValue;
1178
+ // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
1179
+ // Since we use a render function (to handle the selected state) the child is never a string.
1180
+ // This condition adds back that behaviour
1181
+ if (textValue == null && typeof props.children === 'string') {
1182
+ textValue = props.children;
1183
+ }
1184
+ return /*#__PURE__*/ jsx(ListBoxItem$1, {
1185
+ ...props,
1186
+ className: cx(props.className, 'flex cursor-pointer px-6 py-3 leading-6 outline-none data-focused:bg-sky-lightest'),
1187
+ textValue: textValue,
1188
+ children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
1189
+ children: [
1190
+ isSelected && /*#__PURE__*/ jsx(Check, {
1191
+ className: "-ml-6 text-base"
1192
+ }),
1193
+ props.children
1194
+ ]
1277
1195
  })
1278
- })
1279
- }));
1196
+ });
1280
1197
  };
1281
- const cardLinkVariants = cva({
1282
- base: 'w-fit max-w-full',
1283
- variants: {
1284
- withHref: {
1285
- true: [
1286
- // **** Clickarea ****
1287
- 'cursor-pointer',
1288
- 'after:absolute',
1289
- 'after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
1290
- 'after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
1291
- // **** Focus ****
1292
- 'focus-visible:outline-none',
1293
- 'data-focus-visible:after:outline-focus',
1294
- 'data-focus-visible:after:outline-offset-2',
1295
- // **** Hover ****
1296
- // Links are underlined by default, and the underline is removed on hover.
1297
- // So we make sure that also happens when the user hovers the clickable area.
1298
- 'hover:no-underline'
1299
- ],
1300
- false: [
1301
- // **** Clickarea ****
1302
- '[&_a]:after:cursor-pointer',
1303
- '[&_a]:after:absolute',
1304
- '[&_a]:after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
1305
- '[&_a]:after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
1306
- // **** Focus ****
1307
- '[&_a[data-focus-visible]]:outline-none',
1308
- '[&_a[data-focus-visible]]:after:outline-focus',
1309
- '[&_a[data-focus-visible]]:after:outline-offset-2',
1310
- // **** Hover ****
1311
- // Links are underlined by default, and the underline is removed on hover.
1312
- // So we make sure that also happens when the user hovers the card.
1313
- // The group-hover ensures that the hover effect also applies when this component is used as a wrapper around a link.
1314
- '[&_a]:group-hover/card:no-underline'
1315
- ]
1316
- }
1317
- }
1318
- });
1319
1198
  /**
1320
- * A component that creates a clickable area on a card.
1321
- * It can be used either as a wrapper around a link or as a standalone link.
1322
- */ const CardLink = ({ className: _className, href, ...restProps })=>{
1323
- const className = cardLinkVariants({
1324
- className: _className,
1325
- withHref: !!href
1326
- });
1327
- return href ? /*#__PURE__*/ jsx(Link, {
1328
- "data-slot": "card-link",
1199
+ * This component can be used to group items in a listbox
1200
+ */ const ListBoxSection = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBoxSection$1, {
1329
1201
  ...restProps,
1330
- href: href,
1331
- className: className
1332
- }) : // We can't utilize that the `Link` component from react-aria-components renders as a span if it doesn't have an href,
1333
- // because it still renders with role="link" and tabindex="0" which makes it focusable.
1334
- // So we need to render a div instead.
1335
- /*#__PURE__*/ jsx("div", {
1202
+ // The :not(:first-child) selector adds extra spacing to all the options, but not the section (group) headings
1203
+ // This way we get the desired extra indent on all options within a group
1204
+ className: cx(className, 'pb-1 [&>:not(:first-child)]:pl-10')
1205
+ });
1206
+ /**
1207
+ * This component can be used to label grouped items in a `ListBoxSection` with a heading
1208
+ */ const ListBoxHeader = (props)=>/*#__PURE__*/ jsx(Header, {
1209
+ ...props,
1210
+ className: cx(props.className, 'mx-6 cursor-default py-2 font-medium text-blue-dark leading-6')
1211
+ });
1212
+
1213
+ function Combobox(props) {
1214
+ const { className, children, description, errorMessage, isPending, label, isInvalid: _isInvalid, ref, ...restProps } = props;
1215
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
1216
+ // which will override any built in validation
1217
+ const isInvalid = !!errorMessage || _isInvalid;
1218
+ return /*#__PURE__*/ jsxs(ComboBox, {
1336
1219
  ...restProps,
1337
- "data-slot": "card-link",
1338
- className: className
1220
+ className: cx(className, formField),
1221
+ isInvalid: isInvalid,
1222
+ children: [
1223
+ label && /*#__PURE__*/ jsx(Label, {
1224
+ children: label
1225
+ }),
1226
+ description && /*#__PURE__*/ jsx(Description, {
1227
+ children: description
1228
+ }),
1229
+ /*#__PURE__*/ jsxs(Group, {
1230
+ className: inputGroup,
1231
+ children: [
1232
+ /*#__PURE__*/ jsx(Input, {
1233
+ className: input({
1234
+ isGrouped: true
1235
+ }),
1236
+ ref: ref
1237
+ }),
1238
+ /*#__PURE__*/ jsx(Button$1, {
1239
+ children: isPending ? /*#__PURE__*/ jsx(LoadingSpinner, {
1240
+ className: "animate-spin"
1241
+ }) : /*#__PURE__*/ jsx(ChevronDown, {
1242
+ className: dropdown.chevronIcon
1243
+ })
1244
+ })
1245
+ ]
1246
+ }),
1247
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1248
+ errorMessage: errorMessage
1249
+ }),
1250
+ /*#__PURE__*/ jsx(Popover, {
1251
+ // FIXME: The trigger width doesn't include the padding of the group, so for now we have to apply this workaround.
1252
+ // Also... the combobox border gets a pixel wider when focused, so we account for that as well when calculating the width
1253
+ // and the offset.
1254
+ // The input gutter should probably be moved to a theme variable instead of using the hardcoded value as here.
1255
+ className: cx(dropdown.popover, 'min-w-[calc(var(--trigger-width)+26px)]'),
1256
+ crossOffset: -13,
1257
+ children: /*#__PURE__*/ jsx(ListBox, {
1258
+ className: dropdown.listbox,
1259
+ children: children
1260
+ })
1261
+ })
1262
+ ]
1339
1263
  });
1340
- };
1264
+ }
1341
1265
 
1342
1266
  /**
1343
1267
  * A React component that wraps https://react-spectrum.adobe.com/react-aria/useDateFormatter.html
@@ -1354,85 +1278,6 @@ const cardLinkVariants = cva({
1354
1278
  return render ? render(formatted) : formatted;
1355
1279
  };
1356
1280
 
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);
1365
- 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);
1370
- }, []);
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));
1379
- } else {
1380
- videoRef.current.pause();
1381
- setIsPlaying(false);
1382
- }
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);
1404
- }
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'
1422
- ]),
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
1431
- })
1432
- ]
1433
- });
1434
- };
1435
-
1436
1281
  const disclosureButtonVariants = cva({
1437
1282
  base: [
1438
1283
  'inline-flex cursor-pointer items-center justify-between rounded-lg focus-visible:outline-current focus-visible:outline-focus',
@@ -1831,198 +1676,42 @@ const FileUpload = ({ children, files: _files, onChange, validate, isInvalid: _i
1831
1676
  onClick: ()=>{
1832
1677
  // For controlled component
1833
1678
  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
1679
+ // For internal file state
1680
+ setFiles((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1681
+ // Make sure screen readers doesn't loose track of focus
1682
+ // (without this, the focus will be set to the top of the page for screen readers)
1683
+ buttonRef.current?.focus();
1684
+ },
1685
+ "aria-label": translations.remove[locale],
1686
+ type: "button",
1687
+ children: /*#__PURE__*/ jsx(Trash, {})
1688
+ })
1689
+ ]
1690
+ }),
1691
+ hasError && /*#__PURE__*/ jsx(ErrorMessage, {
1692
+ className: "mt-1 block w-full",
1693
+ children: validation
1694
+ })
1695
+ ]
1696
+ }, fileName);
1697
+ })
1698
+ }),
1699
+ (controlledOrUncontrolledFiles.length === 0 || !!errorMessage) && /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1700
+ errorMessage: errorMessage
1952
1701
  })
1953
- })
1702
+ ]
1703
+ })
1954
1704
  });
1955
1705
  };
1956
1706
 
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
1707
+ function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
1708
+ return /*#__PURE__*/ jsx(I18nProvider, {
1709
+ locale: locale,
1710
+ children: navigate ? /*#__PURE__*/ jsx(RouterProvider, {
1711
+ navigate: navigate,
1712
+ useHref: useHref,
1713
+ children: children
1714
+ }) : children
2026
1715
  });
2027
1716
  }
2028
1717
 
@@ -2136,183 +1825,312 @@ const Hero = ({ variant, className, children })=>{
2136
1825
  });
2137
1826
  };
2138
1827
 
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);
1828
+ /**
1829
+ * A basic link component that extends react-aria-components Link with consistent styling.
1830
+ * Provides accessible focus styles and maintains design system consistency.
1831
+ */ const CustomLink = ({ children, className, ...restProps })=>{
1832
+ return /*#__PURE__*/ jsx(Link, {
1833
+ ...restProps,
1834
+ 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'),
1835
+ children: children
1836
+ });
1837
+ };
1838
+
1839
+ const LinkList = ({ className, children, ...restProps })=>{
1840
+ const numberofLinks = Children.count(children);
2186
1841
  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);
1842
+ className: cx(className, '@container'),
1843
+ ...restProps,
1844
+ children: /*#__PURE__*/ jsx("ul", {
1845
+ 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)
1846
+ '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)
1847
+ 'grid auto-rows-max gap-y-0.25', // Gaps for when the list is displayed in multiple columns
1848
+ '@lg:gap-x-12 @md:gap-x-9 @sm:gap-x-4 @xl:gap-x-16', numberofLinks > 5 && [
1849
+ '@xl:grid-cols-2',
1850
+ (numberofLinks === 9 || numberofLinks > 10) && '@4xl:grid-cols-3'
1851
+ ]),
1852
+ children: children
1853
+ })
1854
+ });
1855
+ };
1856
+ const LinkListItem = ({ children, isExternal, className, ...restProps })=>{
1857
+ let Icon = ArrowRight;
1858
+ let iconTransition = 'group-hover:motion-safe:translate-x-1';
1859
+ if (restProps.download) {
1860
+ Icon = Download;
1861
+ iconTransition = 'group-hover:motion-safe:translate-y-1';
1862
+ } else if (isExternal) {
1863
+ iconTransition = 'group-hover:motion-safe:-translate-y-0.5 group-hover:motion-safe:translate-x-0.5';
1864
+ Icon = LinkExternal;
1865
+ }
1866
+ return /*#__PURE__*/ jsx("li", {
1867
+ // Creates divider lines that works in any grid layout and with the focus ring
1868
+ 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",
1869
+ children: /*#__PURE__*/ jsxs(Link, {
1870
+ ...restProps,
1871
+ className: cx(className, 'group paragraph flex cursor-pointer justify-between gap-x-2 py-3.5 font-medium no-underline focus-visible:outline-focus'),
1872
+ children: [
1873
+ /*#__PURE__*/ jsx("span", {
1874
+ children: children
1875
+ }),
1876
+ /*#__PURE__*/ jsx(Icon, {
1877
+ className: cx('shrink-0 motion-safe:transition-transform', iconTransition)
1878
+ })
1879
+ ]
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
+ })
2206
1930
  }
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);
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'
2217
1946
  }
2218
- },
2219
- isDisabled: hasReachedScrollEnd
1947
+ }
2220
1948
  }
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
1949
  ]
2254
- })
2255
- ]
1950
+ ],
1951
+ children: children
1952
+ })
2256
1953
  })
2257
- })
2258
1954
  });
2259
1955
  };
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
- ]),
1956
+
1957
+ // This component is based on a copy of ../textfield/TextField, refactoring is TBD: https://github.com/code-obos/grunnmuren/pull/722#issuecomment-1931478786
1958
+ const inputVariants$1 = compose(input, cva({
1959
+ base: '',
1960
+ variants: {
1961
+ textAlign: {
1962
+ right: 'text-right',
1963
+ left: ''
1964
+ },
1965
+ autoWidth: {
1966
+ true: 'max-w-fit',
1967
+ false: ''
1968
+ }
1969
+ }
1970
+ }));
1971
+ function NumberField(props) {
1972
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
1973
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
1974
+ // which will override any built in validation
1975
+ const isInvalid = !!errorMessage || _isInvalid;
1976
+ return /*#__PURE__*/ jsxs(NumberField$1, {
1977
+ ...restProps,
1978
+ className: cx(className, formField),
1979
+ isInvalid: isInvalid,
1980
+ children: [
1981
+ label && /*#__PURE__*/ jsx(Label, {
1982
+ children: label
1983
+ }),
1984
+ description && /*#__PURE__*/ jsx(Description, {
1985
+ children: description
1986
+ }),
1987
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
1988
+ className: cx(inputGroup, {
1989
+ 'w-fit': !!size
1990
+ }),
1991
+ children: [
1992
+ leftAddon,
1993
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
1994
+ /*#__PURE__*/ jsx(Input, {
1995
+ className: inputVariants$1({
1996
+ textAlign,
1997
+ isGrouped: true,
1998
+ autoWidth: !!size
1999
+ }),
2000
+ ref: ref,
2001
+ size: size
2002
+ }),
2003
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2004
+ rightAddon
2005
+ ]
2006
+ }) : /*#__PURE__*/ jsx(Input, {
2007
+ className: inputVariants$1({
2008
+ textAlign,
2009
+ autoWidth: !!size
2010
+ }),
2284
2011
  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
2012
+ size: size
2013
+ }),
2014
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2015
+ errorMessage: errorMessage
2291
2016
  })
2017
+ ]
2292
2018
  });
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
- }
2019
+ }
2020
+
2021
+ const defaultClasses = cx([
2022
+ '-ml-2.5 relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
2023
+ // the radio button itself
2024
+ 'before:flex-none before:rounded-full before:border-2 before:border-black',
2025
+ // 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.
2026
+ // For the ::before psuedo element the line height of the label is always 1em.
2027
+ // 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
2028
+ // 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?
2029
+ 'before:mt-[calc((1em_*_1.75_-_24px)_/_2)] before:h-[24px] before:w-[24px]',
2030
+ // selected
2031
+ 'data-selected:before:border-black data-selected:before:bg-blue data-selected:before:shadow-[inset_0_0_0_4px_rgb(255,255,255)]',
2032
+ // hover
2033
+ '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',
2034
+ // focus
2035
+ 'data-focus-visible:before:ring-focus-offset',
2036
+ // 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
2037
+ // so we use an inner outline to artifically pad the border
2038
+ '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]'
2039
+ ]);
2040
+ function Radio(props) {
2041
+ const { children, className, description, ...restProps } = props;
2042
+ return /*#__PURE__*/ jsx(Radio$1, {
2043
+ ...restProps,
2044
+ className: cx(className, defaultClasses),
2045
+ children: /*#__PURE__*/ jsxs("div", {
2046
+ children: [
2047
+ children,
2048
+ description && /*#__PURE__*/ jsx(Description, {
2049
+ className: "mt-2 block",
2050
+ children: description
2051
+ })
2052
+ ]
2053
+ })
2054
+ });
2055
+ }
2056
+
2057
+ function RadioGroup(props) {
2058
+ const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, value, ...restProps } = props;
2059
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2060
+ // which will override any built in validation
2061
+ const isInvalid = !!errorMessage || _isInvalid;
2062
+ return /*#__PURE__*/ jsxs(RadioGroup$1, {
2063
+ ...restProps,
2064
+ // Tabindex is set to -1 when the value is an empty string, which makes the radio input not focusable
2065
+ value: value === '' ? undefined : value,
2066
+ className: cx(className, 'flex flex-col gap-2'),
2067
+ isInvalid: isInvalid,
2068
+ isRequired: isRequired,
2069
+ children: [
2070
+ label && /*#__PURE__*/ jsx(Label, {
2071
+ children: label
2072
+ }),
2073
+ description && /*#__PURE__*/ jsx(Description, {
2074
+ children: description
2075
+ }),
2076
+ children,
2077
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2078
+ errorMessage: errorMessage
2079
+ })
2080
+ ]
2081
+ });
2082
+ }
2083
+
2084
+ function Select(props) {
2085
+ const { className, children, description, errorMessage, label, isInvalid: _isInvalid, ref, ...restProps } = props;
2086
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2087
+ // which will override any built in validation
2088
+ const isInvalid = !!errorMessage || _isInvalid;
2089
+ return /*#__PURE__*/ jsxs(Select$1, {
2090
+ ...restProps,
2091
+ className: cx(className, formField),
2092
+ isInvalid: isInvalid,
2093
+ children: [
2094
+ label && /*#__PURE__*/ jsx(Label, {
2095
+ children: label
2096
+ }),
2097
+ description && /*#__PURE__*/ jsx(Description, {
2098
+ children: description
2099
+ }),
2100
+ /*#__PURE__*/ jsxs(Button$1, {
2101
+ className: cx(input({
2102
+ focusModifier: 'visible'
2103
+ }), // How to reuse placeholder text?
2104
+ 'inline-flex cursor-default items-center gap-2'),
2105
+ // See https://github.com/adobe/react-spectrum/discussions/4792#discussioncomment-6492305
2106
+ ref: ref,
2107
+ children: [
2108
+ /*#__PURE__*/ jsx(SelectValue, {
2109
+ className: "flex-1 truncate text-left data-[placeholder]:text-[#727070]"
2110
+ }),
2111
+ /*#__PURE__*/ jsx(ChevronDown, {
2112
+ className: dropdown.chevronIcon
2113
+ })
2306
2114
  ]
2307
- ],
2308
- children: children
2309
- })
2115
+ }),
2116
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2117
+ errorMessage: errorMessage
2118
+ }),
2119
+ /*#__PURE__*/ jsx(Popover, {
2120
+ className: dropdown.popover,
2121
+ children: /*#__PURE__*/ jsx(ListBox, {
2122
+ className: dropdown.listbox,
2123
+ children: children
2124
+ })
2125
+ })
2126
+ ]
2310
2127
  });
2311
- };
2128
+ }
2312
2129
 
2313
2130
  function ScrollButton({ direction, onClick, isVisible, hasScrollingOccurred, className, iconClassName }) {
2314
2131
  const Icon = direction === 'left' ? ChevronLeft : ChevronRight;
2315
2132
  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.
2133
+ // 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
2134
  /*#__PURE__*/ jsx("div", {
2317
2135
  onClick: onClick,
2318
2136
  className: cx(// Base scroll button styling
@@ -2375,6 +2193,126 @@ function ScrollButton({ direction, onClick, isVisible, hasScrollingOccurred, cla
2375
2193
  };
2376
2194
  }
2377
2195
 
2196
+ const tableVariants = cva({
2197
+ base: [
2198
+ 'relative'
2199
+ ],
2200
+ variants: {
2201
+ variant: {
2202
+ default: '',
2203
+ 'zebra-striped': ''
2204
+ }
2205
+ }
2206
+ });
2207
+ const tableRowVariants = cva({
2208
+ base: [
2209
+ 'data-focus-visible:outline-focus-inset',
2210
+ 'group-data-[variant=zebra-striped]:odd:bg-white',
2211
+ 'group-data-[variant=zebra-striped]:even:bg-sky-lightest'
2212
+ ]
2213
+ });
2214
+ /**
2215
+ * A container component for displaying tabular data with horizontal scrolling support.
2216
+ */ function Table(props) {
2217
+ const { className, children, variant = 'default', ...restProps } = props;
2218
+ const { scrollContainerRef, canScrollLeft, canScrollRight, hasScrollingOccurred } = useHorizontalScroll();
2219
+ const handleScroll = useCallback((direction)=>{
2220
+ const container = scrollContainerRef.current;
2221
+ if (!container) return;
2222
+ const scrollAmount = container.clientWidth * 0.8;
2223
+ container.scrollBy({
2224
+ left: direction === 'left' ? -scrollAmount : scrollAmount,
2225
+ behavior: 'smooth'
2226
+ });
2227
+ }, [
2228
+ scrollContainerRef
2229
+ ]);
2230
+ return /*#__PURE__*/ jsx("div", {
2231
+ className: tableVariants({
2232
+ className,
2233
+ variant
2234
+ }),
2235
+ children: /*#__PURE__*/ jsxs("div", {
2236
+ className: "relative overflow-hidden",
2237
+ children: [
2238
+ /*#__PURE__*/ jsx(ScrollButton, {
2239
+ direction: "left",
2240
+ onClick: ()=>handleScroll('left'),
2241
+ isVisible: canScrollLeft,
2242
+ hasScrollingOccurred: hasScrollingOccurred,
2243
+ className: "-translate-y-1/2 absolute top-5 z-10 h-11 w-11",
2244
+ iconClassName: "h-5 w-5"
2245
+ }),
2246
+ /*#__PURE__*/ jsx(ScrollButton, {
2247
+ direction: "right",
2248
+ onClick: ()=>handleScroll('right'),
2249
+ isVisible: canScrollRight,
2250
+ hasScrollingOccurred: hasScrollingOccurred,
2251
+ className: "-translate-y-1/2 absolute top-5 z-10 h-11 w-11",
2252
+ iconClassName: "h-5 w-5"
2253
+ }),
2254
+ /*#__PURE__*/ jsx("div", {
2255
+ ref: scrollContainerRef,
2256
+ className: "scrollbar-hidden overflow-x-auto",
2257
+ style: {
2258
+ WebkitOverflowScrolling: 'touch'
2259
+ },
2260
+ children: /*#__PURE__*/ jsx(Table$1, {
2261
+ ...restProps,
2262
+ className: "group w-full min-w-fit",
2263
+ "data-variant": variant,
2264
+ children: children
2265
+ })
2266
+ })
2267
+ ]
2268
+ })
2269
+ });
2270
+ }
2271
+ /**
2272
+ * Container for table column headers.
2273
+ */ function TableHeader({ className, children, ...restProps }) {
2274
+ return /*#__PURE__*/ jsx(TableHeader$1, {
2275
+ ...restProps,
2276
+ className: cx(className, 'border-black border-b'),
2277
+ children: children
2278
+ });
2279
+ }
2280
+ function TableColumn(props) {
2281
+ const { className, children, ...restProps } = props;
2282
+ return /*#__PURE__*/ jsx(Column, {
2283
+ ...restProps,
2284
+ 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'),
2285
+ children: children
2286
+ });
2287
+ }
2288
+ /**
2289
+ * Container for table rows.
2290
+ */ function TableBody({ className, children, ...restProps }) {
2291
+ return /*#__PURE__*/ jsx(TableBody$1, {
2292
+ ...restProps,
2293
+ className: className,
2294
+ children: children
2295
+ });
2296
+ }
2297
+ function TableRow(props) {
2298
+ const { className, children, ...restProps } = props;
2299
+ return /*#__PURE__*/ jsx(Row, {
2300
+ ...restProps,
2301
+ className: tableRowVariants({
2302
+ className
2303
+ }),
2304
+ children: children
2305
+ });
2306
+ }
2307
+ function TableCell(props) {
2308
+ const { className, children, ...restProps } = props;
2309
+ return /*#__PURE__*/ jsx(Cell, {
2310
+ ...restProps,
2311
+ 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'),
2312
+ children: children
2313
+ });
2314
+ }
2315
+
2378
2316
  const tabsVariants = cva({
2379
2317
  base: [
2380
2318
  'grid gap-4'
@@ -2529,179 +2467,243 @@ const tabsVariants = cva({
2529
2467
  });
2530
2468
  }
2531
2469
 
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({
2470
+ const tagVariants = cva({
2544
2471
  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'
2472
+ 'relative flex cursor-pointer items-center gap-1 rounded-lg px-2 py-1 font-medium text-sm transition-colors duration-200',
2473
+ // Resting
2474
+ 'border-2 border-black bg-white text-black',
2475
+ //Focus
2476
+ 'focus-visible:outline-focus-offset',
2477
+ // Hover
2478
+ 'data-hovered:bg-sky',
2479
+ // Selected
2480
+ // Allows removing
2481
+ 'data-allows-removing:border-transparent',
2482
+ 'data-allows-removing:bg-blue',
2483
+ 'data-allows-removing:data-hovered:bg-blue-dark',
2484
+ 'data-allows-removing:text-white',
2485
+ // Selected
2486
+ 'aria-selected:border-transparent',
2487
+ 'aria-selected:bg-blue',
2488
+ 'aria-selected:data-hovered:bg-blue-dark',
2489
+ 'aria-selected:text-white',
2490
+ //Icons
2491
+ '[&_svg]:h-4 [&_svg]:w-4'
2548
2492
  ]
2549
2493
  });
2550
2494
  /**
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, {
2495
+ * A group component for Tag components that enables selection and organization of options.
2496
+ */ function TagGroup(props) {
2497
+ const { onRemove, selectionMode = 'single', className, children, ...restProps } = props;
2498
+ return /*#__PURE__*/ jsx(TagGroup$1, {
2611
2499
  ...restProps,
2612
- className: cx(className, 'border-black border-b'),
2500
+ className: className,
2501
+ selectionMode: onRemove ? 'none' : selectionMode,
2502
+ onRemove: onRemove,
2613
2503
  children: children
2614
2504
  });
2615
2505
  }
2616
- function TableColumn(props) {
2506
+ /**
2507
+ * A container component for Tag components within a TagGroup.
2508
+ */ function TagList(props) {
2617
2509
  const { className, children, ...restProps } = props;
2618
- return /*#__PURE__*/ jsx(Column, {
2510
+ return /*#__PURE__*/ jsx(TagList$1, {
2619
2511
  ...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'),
2512
+ className: cx('flex flex-wrap gap-2', className),
2621
2513
  children: children
2622
2514
  });
2623
2515
  }
2624
2516
  /**
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) {
2517
+ * Interactive tag component for selections, filtering, and categorization.
2518
+ */ function Tag(props) {
2634
2519
  const { className, children, ...restProps } = props;
2635
- return /*#__PURE__*/ jsx(Row, {
2636
- ...restProps,
2637
- className: tableRowVariants({
2520
+ const textValue = typeof children === 'string' ? children : undefined;
2521
+ return /*#__PURE__*/ jsx(Tag$1, {
2522
+ className: tagVariants({
2638
2523
  className
2639
2524
  }),
2640
- children: children
2641
- });
2642
- }
2643
- function TableCell(props) {
2644
- const { className, children, ...restProps } = props;
2645
- return /*#__PURE__*/ jsx(Cell, {
2525
+ textValue: textValue,
2646
2526
  ...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
2527
+ children: ({ allowsRemoving })=>allowsRemoving ? /*#__PURE__*/ jsxs(Fragment, {
2528
+ children: [
2529
+ children,
2530
+ /*#__PURE__*/ jsx(Button$1, {
2531
+ className: "cursor-pointer outline-none after:absolute after:top-0 after:right-0 after:bottom-0 after:left-0",
2532
+ slot: "remove",
2533
+ children: /*#__PURE__*/ jsx(Close, {
2534
+ className: "ml-1"
2535
+ })
2536
+ })
2537
+ ]
2538
+ }) : children
2649
2539
  });
2650
2540
  }
2651
2541
 
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, {
2542
+ function TextArea(props) {
2543
+ const { className, description, errorMessage, label, isInvalid: _isInvalid, rows, ref, ...restProps } = props;
2544
+ const isInvalid = !!errorMessage || _isInvalid;
2545
+ return /*#__PURE__*/ jsxs(TextField$1, {
2657
2546
  ...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
2547
+ className: cx(className, formField),
2548
+ isInvalid: isInvalid,
2549
+ children: [
2550
+ label && /*#__PURE__*/ jsx(Label, {
2551
+ children: label
2552
+ }),
2553
+ description && /*#__PURE__*/ jsx(Description, {
2554
+ children: description
2555
+ }),
2556
+ /*#__PURE__*/ jsx(TextArea$1, {
2557
+ className: input(),
2558
+ rows: rows,
2559
+ ref: ref
2560
+ }),
2561
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2562
+ errorMessage: errorMessage
2563
+ })
2564
+ ]
2660
2565
  });
2661
- };
2566
+ }
2662
2567
 
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;
2568
+ const inputVariants = compose(input, cva({
2569
+ base: '',
2570
+ variants: {
2571
+ textAlign: {
2572
+ right: 'text-right',
2573
+ left: ''
2574
+ },
2575
+ autoWidth: {
2576
+ true: 'max-w-fit',
2577
+ false: ''
2578
+ }
2689
2579
  }
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
2580
+ }));
2581
+ function TextField(props) {
2582
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
2583
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
2584
+ // which will override any built in validation
2585
+ const isInvalid = !!errorMessage || _isInvalid;
2586
+ return /*#__PURE__*/ jsxs(TextField$1, {
2587
+ ...restProps,
2588
+ className: cx(className, formField),
2589
+ isInvalid: isInvalid,
2590
+ children: [
2591
+ label && /*#__PURE__*/ jsx(Label, {
2592
+ children: label
2593
+ }),
2594
+ description && /*#__PURE__*/ jsx(Description, {
2595
+ children: description
2596
+ }),
2597
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
2598
+ className: cx(inputGroup, {
2599
+ 'w-fit': !!size
2699
2600
  }),
2700
- /*#__PURE__*/ jsx(Icon, {
2701
- className: cx('shrink-0 motion-safe:transition-transform', iconTransition)
2601
+ children: [
2602
+ leftAddon,
2603
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2604
+ /*#__PURE__*/ jsx(Input, {
2605
+ className: inputVariants({
2606
+ textAlign,
2607
+ isGrouped: true,
2608
+ autoWidth: !!size
2609
+ }),
2610
+ ref: ref,
2611
+ size: size
2612
+ }),
2613
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
2614
+ rightAddon
2615
+ ]
2616
+ }) : /*#__PURE__*/ jsx(Input, {
2617
+ className: inputVariants({
2618
+ textAlign,
2619
+ autoWidth: !!size
2620
+ }),
2621
+ ref: ref,
2622
+ size: size
2623
+ }),
2624
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
2625
+ errorMessage: errorMessage
2626
+ })
2627
+ ]
2628
+ });
2629
+ }
2630
+
2631
+ const VideoLoop = ({ src, format, alt, className })=>{
2632
+ // Control the video playback state, so that the user can pause and play the video at will, also control the video autoplay
2633
+ const [shouldPlay, setShouldPlay] = useState(false);
2634
+ // 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)
2635
+ const [isPlaying, setIsPlaying] = useState(false);
2636
+ // We need to check if the user prefers reduced motion, so that we can prevent the video from autoplaying if so
2637
+ const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(null);
2638
+ const videoRef = useRef(null);
2639
+ useEffect(()=>{
2640
+ const { matches: userPrefersReducedMotion } = matchMedia('(prefers-reduced-motion: reduce)');
2641
+ setUserPrefersReducedMotion(userPrefersReducedMotion);
2642
+ // Autoplay the video if the user does not prefer reduced motion
2643
+ setShouldPlay(!userPrefersReducedMotion);
2644
+ }, []);
2645
+ // Follow google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
2646
+ // "Don't assume a video will play, and don't show a pause button when the video is not actually playing."
2647
+ // "You should always look at the Promise returned by the play function to see if it was rejected:"
2648
+ // 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
2649
+ useEffect(()=>{
2650
+ if (!videoRef.current) return;
2651
+ if (shouldPlay) {
2652
+ videoRef.current.play().then(()=>setIsPlaying(true)).catch(()=>setIsPlaying(false));
2653
+ } else {
2654
+ videoRef.current.pause();
2655
+ setIsPlaying(false);
2656
+ }
2657
+ }, [
2658
+ shouldPlay
2659
+ ]);
2660
+ return /*#__PURE__*/ jsxs("div", {
2661
+ className: cx(className, 'relative', userPrefersReducedMotion === null && 'opacity-0'),
2662
+ children: [
2663
+ /*#__PURE__*/ jsx("video", {
2664
+ "aria-hidden": true,
2665
+ ref: videoRef,
2666
+ // cursor-pointer is not working on the button below, so we add it here for the same effect
2667
+ className: "h-full max-h-[inherit] w-full cursor-pointer rounded-[inherit] object-cover",
2668
+ playsInline: true,
2669
+ loop: userPrefersReducedMotion === false,
2670
+ autoPlay: userPrefersReducedMotion === false,
2671
+ muted: true,
2672
+ onEnded: (event)=>{
2673
+ if (userPrefersReducedMotion) {
2674
+ // Reset the video to the beginning if the user prefers reduced motion, since the video will not loop
2675
+ event.currentTarget.currentTime = 0;
2676
+ setShouldPlay(false);
2677
+ setIsPlaying(false);
2678
+ }
2679
+ },
2680
+ children: /*#__PURE__*/ jsx("source", {
2681
+ src: src,
2682
+ type: `video/${format}`
2702
2683
  })
2703
- ]
2704
- })
2684
+ }),
2685
+ userPrefersReducedMotion !== null && /*#__PURE__*/ jsx("button", {
2686
+ "data-slot": "video-loop-button",
2687
+ "aria-hidden": true,
2688
+ type: "button",
2689
+ onClick: ()=>setShouldPlay((prevState)=>!prevState),
2690
+ 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
2691
+ shouldPlay && 'opacity-0', isPlaying && [
2692
+ 'transition-opacity duration-200',
2693
+ // Only show the pause button when the video is hovered or focused
2694
+ 'focus-visible:opacity-100',
2695
+ 'hover:opacity-100'
2696
+ ]),
2697
+ children: /*#__PURE__*/ jsx("span", {
2698
+ className: "grid h-12 w-12 place-items-center rounded-full bg-white outline-hidden",
2699
+ children: isPlaying ? /*#__PURE__*/ jsx(PlayerPause, {}) : /*#__PURE__*/ jsx(PlayerPlay, {})
2700
+ })
2701
+ }),
2702
+ alt && /*#__PURE__*/ jsx("p", {
2703
+ className: "sr-only",
2704
+ children: alt
2705
+ })
2706
+ ]
2705
2707
  });
2706
2708
  };
2707
2709