@obosbbl/grunnmuren-react 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1945 @@
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, 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 } from 'react-aria-components';
3
+ export { Form, DisclosureGroup as UNSAFE_DisclosureGroup } from 'react-aria-components';
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 } from '@obosbbl/grunnmuren-icons-react';
6
+ import { useLayoutEffect, filterDOMProps, mergeRefs, mergeProps, useObjectRef, useFormReset, useUpdateEffect } from '@react-aria/utils';
7
+ import { cx, cva, compose } from 'cva';
8
+ import { createContext, Children, useId, useState, useRef, useEffect, useContext, useCallback } from 'react';
9
+ import { useProgressBar, useDateFormatter, useFocusRing, useDisclosure, useField } from 'react-aria';
10
+ import { useDisclosureState } from 'react-stately';
11
+ import { useFormValidation } from '@react-aria/form';
12
+ import { useFormValidationState } from '@react-stately/form';
13
+ import { useControlledState } from '@react-stately/utils';
14
+ import { PressResponder } from '@react-aria/interactions';
15
+
16
+ function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
17
+ return /*#__PURE__*/ jsx(I18nProvider, {
18
+ locale: locale,
19
+ children: navigate ? /*#__PURE__*/ jsx(RouterProvider, {
20
+ navigate: navigate,
21
+ useHref: useHref,
22
+ children: children
23
+ }) : children
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Returns the locale set in `<GrunnmurenProvider />`
29
+ */ function _useLocale() {
30
+ // a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
31
+ const locale = useLocale();
32
+ return locale.locale;
33
+ }
34
+
35
+ const HeadingContext = /*#__PURE__*/ createContext({});
36
+ const Heading = ({ ref = null, ...props })=>{
37
+ [props, ref] = useContextProps(props, ref, HeadingContext);
38
+ const { children, level, className, _innerWrapper: innerWrapper, _outerWrapper: outerWrapper, ...restProps } = props;
39
+ const Element = `h${level}`;
40
+ const content = /*#__PURE__*/ jsx(Element, {
41
+ ...restProps,
42
+ className: className,
43
+ "data-slot": "heading",
44
+ children: innerWrapper ? innerWrapper(children) : children
45
+ });
46
+ return outerWrapper ? outerWrapper(content) : content;
47
+ };
48
+ const ContentContext = /*#__PURE__*/ createContext({});
49
+ const Content = ({ ref = null, ...props })=>{
50
+ [props, ref] = useContextProps(props, ref, ContentContext);
51
+ const { _outerWrapper: outerWrapper, ...restProps } = props;
52
+ const content = /*#__PURE__*/ jsx("div", {
53
+ ...restProps,
54
+ "data-slot": "content"
55
+ });
56
+ return outerWrapper ? outerWrapper(content) : content;
57
+ };
58
+ const Media = (props)=>/*#__PURE__*/ jsx("div", {
59
+ ...props,
60
+ "data-slot": "media"
61
+ });
62
+ const Caption = ({ className, ...restProps })=>/*#__PURE__*/ jsx("div", {
63
+ ...restProps,
64
+ className: cx('description', className),
65
+ "data-slot": "caption"
66
+ });
67
+ const Footer = (props)=>/*#__PURE__*/ jsx("div", {
68
+ ...props,
69
+ "data-slot": "footer"
70
+ });
71
+
72
+ function Accordion(props) {
73
+ const { children, className, ...restProps } = props;
74
+ const childCount = Children.count(children);
75
+ return /*#__PURE__*/ jsx("div", {
76
+ ...restProps,
77
+ className: cx('rounded-lg bg-white', className),
78
+ children: Children.map(children, (child, index)=>/*#__PURE__*/ jsxs(Fragment, {
79
+ children: [
80
+ child,
81
+ index < childCount - 1 && // Margin is added to enable support for containers with a background color
82
+ /*#__PURE__*/ jsx("hr", {
83
+ className: "mx-2 border-gray-light",
84
+ "aria-hidden": true
85
+ })
86
+ ]
87
+ }))
88
+ });
89
+ }
90
+ function AccordionItem(props) {
91
+ const { className, children, defaultOpen = false, isOpen: controlledIsOpen, onOpenChange, ...restProps } = props;
92
+ const contentId = useId();
93
+ const buttonId = useId();
94
+ const isControlled = controlledIsOpen != null;
95
+ // This component has internal state that controls whether it is open or not,
96
+ // regardless if we are controlled or uncontrolled.
97
+ // If we are controlled, we use a layout effect to sync the controlled state
98
+ // with the internal state.
99
+ //
100
+ const [isOpen, setIsOpen] = useState(// If we are controlled, use that open state, otherwise use the uncontrolled
101
+ isControlled ? controlledIsOpen : defaultOpen);
102
+ useLayoutEffect(()=>{
103
+ if (isControlled) {
104
+ setIsOpen(controlledIsOpen);
105
+ }
106
+ }, [
107
+ controlledIsOpen,
108
+ isControlled
109
+ ]);
110
+ const handleOpenChange = ()=>{
111
+ const newOpenState = !isOpen;
112
+ if (!isControlled) {
113
+ setIsOpen(newOpenState);
114
+ }
115
+ // Always call the change handler, even if we're uncontrolled.
116
+ // Easier to add stuff such as tracking etc.
117
+ if (onOpenChange) {
118
+ onOpenChange(newOpenState);
119
+ }
120
+ };
121
+ return /*#__PURE__*/ jsx("div", {
122
+ ...restProps,
123
+ className: cx('relative px-2', className),
124
+ "data-open": isOpen,
125
+ children: /*#__PURE__*/ jsx(Provider, {
126
+ values: [
127
+ [
128
+ HeadingContext,
129
+ {
130
+ // Negative margin to strech the button to the entire with of the accordion (to support containers with a background color)
131
+ className: 'font-semibold leading-7 -mx-2 text-base',
132
+ // Supply a default level here to make this typecheck ok. Will be overwritten with the consumers set heading level anyways
133
+ level: 3,
134
+ _innerWrapper: (children)=>/*#__PURE__*/ jsxs("button", {
135
+ "aria-controls": contentId,
136
+ "aria-expanded": isOpen,
137
+ // Use outline with offset as focus indicator, this does not cover the left mint border on the expanded content and works with or without a background color on the accordion container
138
+ className: "flex min-h-[44px] w-full cursor-pointer items-center justify-between gap-1.5 rounded-lg px-2 py-3.5 text-left focus-visible:outline-focus focus-visible:outline-focus-inset",
139
+ id: buttonId,
140
+ onClick: handleOpenChange,
141
+ type: "button",
142
+ children: [
143
+ children,
144
+ /*#__PURE__*/ jsx(ChevronDown, {
145
+ className: cx('flex-none transition-transform duration-300 motion-reduce:transition-none', isOpen && 'rotate-180')
146
+ })
147
+ ]
148
+ })
149
+ }
150
+ ],
151
+ [
152
+ ContentContext,
153
+ {
154
+ className: // Uses pseudo element for vertical padding, since that doesn't affect the height when the accordion is closed
155
+ 'text-sm font-light leading-6 px-3.5 relative overflow-hidden border-mint border-l-[3px] before:relative before:block before:h-1.5 after:relative after:block after:h-1.5',
156
+ role: 'region',
157
+ inert: isOpen,
158
+ 'aria-labelledby': buttonId,
159
+ _outerWrapper: (children)=>/*#__PURE__*/ jsx("div", {
160
+ className: cx('grid transition-all duration-300 after:relative after:block after:h-0 after:transition-all after:duration-300 motion-reduce:transition-none', isOpen ? 'grid-rows-[1fr] after:h-3.5' : 'grid-rows-[0fr]'),
161
+ children: children
162
+ })
163
+ }
164
+ ]
165
+ ],
166
+ children: children
167
+ })
168
+ });
169
+ }
170
+
171
+ const badgeVariants = cva({
172
+ base: [
173
+ 'inline-flex w-fit items-center justify-center gap-1.5 rounded-lg [&_svg]:shrink-0'
174
+ ],
175
+ variants: {
176
+ color: {
177
+ 'gray-dark': 'bg-gray-dark text-white',
178
+ mint: 'bg-mint',
179
+ sky: 'bg-sky',
180
+ white: 'bg-white',
181
+ 'blue-dark': 'bg-blue-dark text-white',
182
+ 'green-dark': 'bg-green-dark text-white'
183
+ },
184
+ size: {
185
+ small: 'description px-2 py-0.5 [&_svg]:h-4 [&_svg]:w-4',
186
+ medium: 'description px-2.5 py-1.5 [&_svg]:h-4 [&_svg]:w-4',
187
+ large: 'paragraph px-3 py-2 [&_svg]:h-5 [&_svg]:w-5'
188
+ }
189
+ },
190
+ defaultVariants: {
191
+ size: 'medium'
192
+ }
193
+ });
194
+ function Badge(props) {
195
+ const { className: _className, color, size, ...restProps } = props;
196
+ const className = badgeVariants({
197
+ className: _className,
198
+ color,
199
+ size
200
+ });
201
+ return /*#__PURE__*/ jsx("span", {
202
+ className: className,
203
+ ...restProps,
204
+ "data-slot": "badge"
205
+ });
206
+ }
207
+
208
+ const translations$1 = {
209
+ close: {
210
+ nb: 'Lukk',
211
+ sv: 'Stäng',
212
+ en: 'Close'
213
+ },
214
+ pending: {
215
+ nb: 'venter',
216
+ sv: 'väntar',
217
+ en: 'pending'
218
+ },
219
+ showMore: {
220
+ nb: 'Les mer',
221
+ sv: 'Läs mer',
222
+ en: 'Read more'
223
+ },
224
+ showLess: {
225
+ nb: 'Vis mindre',
226
+ sv: 'Dölj',
227
+ en: 'Show less'
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
233
+ */ const buttonVariants = cva({
234
+ base: [
235
+ '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'
236
+ ],
237
+ variants: {
238
+ /**
239
+ * The variant of the button
240
+ * @default primary
241
+ */ variant: {
242
+ primary: 'no-underline',
243
+ // 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
244
+ secondary: 'no-underline shadow-[inset_0_0_0_2px]',
245
+ tertiary: 'underline hover:no-underline'
246
+ },
247
+ /**
248
+ * Adjusts the color of the button for usage on different backgrounds.
249
+ * @default green
250
+ */ color: {
251
+ green: 'focus-visible:outline-focus',
252
+ mint: 'focus-visible:outline-focus focus-visible:outline-mint',
253
+ white: 'focus-visible:outline-focus focus-visible:outline-white'
254
+ },
255
+ /**
256
+ * When the button is without text, but with a single icon.
257
+ * @default false
258
+ */ isIconOnly: {
259
+ true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
260
+ false: 'gap-2.5 px-4 py-2'
261
+ },
262
+ // Make the content of the button transparent to hide it's content, but keep the button width
263
+ isPending: {
264
+ true: '!text-transparent relative',
265
+ false: null
266
+ }
267
+ },
268
+ compoundVariants: [
269
+ {
270
+ color: 'green',
271
+ variant: 'primary',
272
+ // Darken bg by 20% on hover. The color is manually crafted
273
+ className: 'bg-green text-white hover:bg-green-dark active:bg-[#007352] [&_[role="progressbar"]]:text-white'
274
+ },
275
+ {
276
+ color: 'green',
277
+ variant: 'secondary',
278
+ className: 'text-black shadow-green hover:bg-green hover:text-white active:bg-green [&:hover_[role="progressbar"]]:text-white [&_[role="progressbar"]]:text-black'
279
+ },
280
+ {
281
+ color: 'green',
282
+ variant: 'tertiary',
283
+ className: '[&_[role="progressbar"]]:text-black'
284
+ },
285
+ {
286
+ color: 'mint',
287
+ variant: 'primary',
288
+ // Darken bg by 20% on hover. The color is manually crafted
289
+ className: 'bg-mint text-black hover:bg-[#8dd4bd] active:[#9ddac6] [&_[role="progressbar"]]:text-black'
290
+ },
291
+ {
292
+ color: 'mint',
293
+ variant: 'secondary',
294
+ className: 'text-mint shadow-mint hover:bg-mint hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-mint'
295
+ },
296
+ {
297
+ color: 'mint',
298
+ variant: 'tertiary',
299
+ className: 'text-mint [&_[role="progressbar"]]:text-mint'
300
+ },
301
+ {
302
+ color: 'white',
303
+ variant: 'primary',
304
+ className: 'bg-white text-black hover:bg-sky active:bg-sky-light [&_[role="progressbar"]]:text-black'
305
+ },
306
+ {
307
+ color: 'white',
308
+ variant: 'secondary',
309
+ className: 'text-white shadow-white hover:bg-white hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-white'
310
+ },
311
+ {
312
+ color: 'white',
313
+ variant: 'tertiary',
314
+ className: 'text-white [&_[role="progressbar"]]:text-white'
315
+ }
316
+ ],
317
+ defaultVariants: {
318
+ variant: 'primary',
319
+ color: 'green',
320
+ isIconOnly: false,
321
+ isPending: false
322
+ }
323
+ });
324
+ function isLinkProps$1(props) {
325
+ return !!props.href;
326
+ }
327
+ function Button(props) {
328
+ const { children: _children, color, isIconOnly, isLoading, variant, isPending: _isPending, ref, ...restProps } = props;
329
+ const isPending = _isPending || isLoading;
330
+ const className = buttonVariants({
331
+ className: props.className,
332
+ color,
333
+ isIconOnly,
334
+ variant,
335
+ isPending
336
+ });
337
+ const locale = _useLocale();
338
+ const { progressBarProps } = useProgressBar({
339
+ isIndeterminate: true,
340
+ 'aria-label': translations$1.pending[locale]
341
+ });
342
+ const children = isPending ? /*#__PURE__*/ jsxs(Fragment, {
343
+ children: [
344
+ _children,
345
+ /*#__PURE__*/ jsx(LoadingSpinner, {
346
+ className: "absolute m-auto motion-safe:animate-spin",
347
+ ...progressBarProps
348
+ })
349
+ ]
350
+ }) : _children;
351
+ return isLinkProps$1(restProps) ? /*#__PURE__*/ jsx(Link, {
352
+ ...restProps,
353
+ className: className,
354
+ ref: ref,
355
+ children: children
356
+ }) : /*#__PURE__*/ jsx(Button$1, {
357
+ ...restProps,
358
+ className: className,
359
+ isPending: isPending,
360
+ ref: ref,
361
+ children: children
362
+ });
363
+ }
364
+
365
+ const formField = cx('group flex flex-col gap-2');
366
+ 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');
367
+ const input = cva({
368
+ base: [
369
+ // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
370
+ 'bg-white',
371
+ // Use box-content to enable auto width based on number of characters (size)
372
+ // Setting min-height to prevent the input from collapsing in Safari
373
+ // Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
374
+ 'box-content min-h-6 py-2.5',
375
+ 'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
376
+ // invalid styles
377
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red',
378
+ // Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
379
+ 'appearance-none'
380
+ ],
381
+ variants: {
382
+ // Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
383
+ focusModifier: {
384
+ focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
385
+ visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
386
+ },
387
+ isGrouped: {
388
+ false: 'px-3',
389
+ true: '!ring-0 flex-1'
390
+ }
391
+ },
392
+ defaultVariants: {
393
+ focusModifier: 'focus',
394
+ isGrouped: false
395
+ }
396
+ });
397
+ const inputGroup = cx([
398
+ 'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
399
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
400
+ ]);
401
+ const dropdown = {
402
+ 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'),
403
+ // overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
404
+ listbox: cx('max-h-[25rem] overflow-x-hidden text-sm outline-hidden'),
405
+ chevronIcon: cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
406
+ };
407
+
408
+ function ErrorMessage(props) {
409
+ const { children, className, ...restProps } = props;
410
+ return /*#__PURE__*/ jsx(Text, {
411
+ ...restProps,
412
+ className: cx(className, formFieldError),
413
+ slot: "errorMessage",
414
+ children: children
415
+ });
416
+ }
417
+
418
+ const defaultClasses$1 = cx([
419
+ 'group -mx-2.5 relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4 p-2.5 leading-7'
420
+ ]);
421
+ // Pulling this out into it's own component. Will probably export it in the future
422
+ // so it can be used in other views, outside of an input of type checkbox, like in table rows.
423
+ function CheckmarkBox() {
424
+ return /*#__PURE__*/ jsx("span", {
425
+ className: cx([
426
+ 'relative left-0 grid flex-none place-content-center rounded-sm border-2 border-black text-white',
427
+ // 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.
428
+ // For the ::before psuedo element the line height of the label is always 1em.
429
+ // 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
430
+ // 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?
431
+ 'mt-[calc((1em_*_1.75_-_24px)_/_2)] h-[24px] w-[24px]',
432
+ // selected
433
+ 'group-data-selected:!border-green group-data-selected:!bg-green',
434
+ // focus
435
+ 'group-data-focus-visible:outline-focus-offset',
436
+ // hovered
437
+ 'group-data-hovered:group-data-invalid:border-red group-data-hovered:group-data-invalid:bg-red-light group-data-hovered:border-green group-data-hovered:bg-green-lightest',
438
+ // 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
439
+ // so we use an inner shadow of 1 px instead to pad the actual border
440
+ 'group-data-invalid:group-data-selected:shadow-none group-data-invalid:border-red group-data-invalid:shadow-[inset_0_0_0_1px] group-data-invalid:shadow-red'
441
+ ]),
442
+ children: /*#__PURE__*/ jsx(Check, {
443
+ className: "h-full w-full opacity-0 group-data-selected:opacity-100"
444
+ })
445
+ });
446
+ }
447
+ function Checkbox(props) {
448
+ const { children, className, description, errorMessage, isInvalid: _isInvalid, ...restProps } = props;
449
+ const id = useId();
450
+ const descriptionId = `desc${id}`;
451
+ const errorMessageId = `error${id}`;
452
+ const isInvalid = errorMessage != null || _isInvalid;
453
+ return /*#__PURE__*/ jsx("div", {
454
+ children: /*#__PURE__*/ jsxs(CheckboxContext.Provider, {
455
+ value: {
456
+ 'aria-describedby': description ? descriptionId : undefined,
457
+ 'aria-errormessage': errorMessage ? errorMessageId : undefined
458
+ },
459
+ children: [
460
+ /*#__PURE__*/ jsxs(Checkbox$1, {
461
+ ...restProps,
462
+ className: cx(className, defaultClasses$1),
463
+ isInvalid: isInvalid,
464
+ children: [
465
+ /*#__PURE__*/ jsx(CheckmarkBox, {}),
466
+ children
467
+ ]
468
+ }),
469
+ 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 */}
470
+ /*#__PURE__*/ jsx("div", {
471
+ id: descriptionId,
472
+ slot: "description",
473
+ className: "description block",
474
+ children: description
475
+ }),
476
+ errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
477
+ className: "mt-2 block",
478
+ id: errorMessageId,
479
+ children: errorMessage
480
+ })
481
+ ]
482
+ })
483
+ });
484
+ }
485
+
486
+ function Description(props) {
487
+ const { className, ...restProps } = props;
488
+ return /*#__PURE__*/ jsx(Text, {
489
+ ...restProps,
490
+ className: cx(className, 'description'),
491
+ slot: "description"
492
+ });
493
+ }
494
+
495
+ /**
496
+ * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
497
+ * In other words, this handles controlled and uncontrolled form errors.
498
+ */ function ErrorMessageOrFieldError({ errorMessage }) {
499
+ return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
500
+ children: errorMessage
501
+ }) : /*#__PURE__*/ jsx(FieldError, {
502
+ className: formFieldError
503
+ });
504
+ }
505
+
506
+ function Label(props) {
507
+ const { children, className, ...restProps } = props;
508
+ return /*#__PURE__*/ jsx(Label$1, {
509
+ className: cx(className, 'font-semibold leading-7'),
510
+ ...restProps,
511
+ children: children
512
+ });
513
+ }
514
+
515
+ function CheckboxGroup(props) {
516
+ const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
517
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
518
+ // which will override any built in validation
519
+ const isInvalid = errorMessage != null || _isInvalid;
520
+ return /*#__PURE__*/ jsxs(CheckboxGroup$1, {
521
+ ...restProps,
522
+ className: cx(className, 'flex flex-col gap-2'),
523
+ isInvalid: isInvalid,
524
+ isRequired: isRequired,
525
+ children: [
526
+ label && /*#__PURE__*/ jsx(Label, {
527
+ children: label
528
+ }),
529
+ description && /*#__PURE__*/ jsx(Description, {
530
+ children: description
531
+ }),
532
+ children,
533
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
534
+ errorMessage: errorMessage
535
+ })
536
+ ]
537
+ });
538
+ }
539
+
540
+ const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
541
+ ...restProps,
542
+ className: cx(dropdown.listbox, className)
543
+ });
544
+ const ListBoxItem = (props)=>{
545
+ let textValue = props.textValue;
546
+ // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
547
+ // Since we use a render function (to handle the selected state) the child is never a string.
548
+ // This condition adds back that behaviour
549
+ if (textValue == null && typeof props.children === 'string') {
550
+ textValue = props.children;
551
+ }
552
+ return /*#__PURE__*/ jsx(ListBoxItem$1, {
553
+ ...props,
554
+ className: cx(props.className, 'flex cursor-pointer px-6 py-3 leading-6 outline-none data-focused:bg-sky-lightest'),
555
+ textValue: textValue,
556
+ children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
557
+ children: [
558
+ isSelected && /*#__PURE__*/ jsx(Check, {
559
+ className: "-ml-6 text-base"
560
+ }),
561
+ props.children
562
+ ]
563
+ })
564
+ });
565
+ };
566
+ /**
567
+ * This component can be used to group items in a listbox
568
+ */ const ListBoxSection = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBoxSection$1, {
569
+ ...restProps,
570
+ // The :not(:first-child) selector adds extra spacing to all the options, but not the section (group) headings
571
+ // This way we get the desired extra indent on all options within a group
572
+ className: cx(className, 'pb-1 [&>:not(:first-child)]:pl-10')
573
+ });
574
+ /**
575
+ * This component can be used to label grouped items in a `ListBoxSection` with a heading
576
+ */ const ListBoxHeader = (props)=>/*#__PURE__*/ jsx(Header, {
577
+ ...props,
578
+ className: cx(props.className, 'mx-6 cursor-default py-2 font-medium text-blue-dark leading-6')
579
+ });
580
+
581
+ function InputAddonDivider() {
582
+ return /*#__PURE__*/ jsx("span", {
583
+ className: "block h-6 w-px flex-none bg-black"
584
+ });
585
+ }
586
+
587
+ function Combobox(props) {
588
+ const { className, children, description, errorMessage, isLoading, isPending: _isPending, label, isInvalid: _isInvalid, ref, ...restProps } = props;
589
+ const isPending = _isPending || isLoading;
590
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
591
+ // which will override any built in validation
592
+ const isInvalid = errorMessage != null || _isInvalid;
593
+ return /*#__PURE__*/ jsxs(ComboBox, {
594
+ ...restProps,
595
+ className: cx(className, formField),
596
+ isInvalid: isInvalid,
597
+ children: [
598
+ label && /*#__PURE__*/ jsx(Label, {
599
+ children: label
600
+ }),
601
+ description && /*#__PURE__*/ jsx(Description, {
602
+ children: description
603
+ }),
604
+ /*#__PURE__*/ jsxs(Group, {
605
+ className: inputGroup,
606
+ children: [
607
+ /*#__PURE__*/ jsx(Input, {
608
+ className: input({
609
+ isGrouped: true
610
+ }),
611
+ ref: ref
612
+ }),
613
+ /*#__PURE__*/ jsx(Button$1, {
614
+ children: isPending ? /*#__PURE__*/ jsx(LoadingSpinner, {
615
+ className: "animate-spin"
616
+ }) : /*#__PURE__*/ jsx(ChevronDown, {
617
+ className: dropdown.chevronIcon
618
+ })
619
+ })
620
+ ]
621
+ }),
622
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
623
+ errorMessage: errorMessage
624
+ }),
625
+ /*#__PURE__*/ jsx(Popover, {
626
+ // FIXME: The trigger width doesn't include the padding of the group, so for now we have to apply this workaround.
627
+ // Also... the combobox border gets a pixel wider when focused, so we account for that as well when calculating the width
628
+ // and the offset.
629
+ // The input gutter should probably be moved to a theme variable instead of using the hardcoded value as here.
630
+ className: cx(dropdown.popover, 'min-w-[calc(var(--trigger-width)+26px)]'),
631
+ crossOffset: -13,
632
+ children: /*#__PURE__*/ jsx(ListBox, {
633
+ className: dropdown.listbox,
634
+ children: children
635
+ })
636
+ })
637
+ ]
638
+ });
639
+ }
640
+
641
+ function RadioGroup(props) {
642
+ const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
643
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
644
+ // which will override any built in validation
645
+ const isInvalid = errorMessage != null || _isInvalid;
646
+ return /*#__PURE__*/ jsxs(RadioGroup$1, {
647
+ ...restProps,
648
+ className: cx(className, 'flex flex-col gap-2'),
649
+ isInvalid: isInvalid,
650
+ isRequired: isRequired,
651
+ children: [
652
+ label && /*#__PURE__*/ jsx(Label, {
653
+ children: label
654
+ }),
655
+ description && /*#__PURE__*/ jsx(Description, {
656
+ children: description
657
+ }),
658
+ children,
659
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
660
+ errorMessage: errorMessage
661
+ })
662
+ ]
663
+ });
664
+ }
665
+
666
+ const defaultClasses = cx([
667
+ '-ml-2.5 relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
668
+ // the radio button itself
669
+ 'before:flex-none before:rounded-full before:border-2 before:border-black',
670
+ // 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.
671
+ // For the ::before psuedo element the line height of the label is always 1em.
672
+ // 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
673
+ // 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?
674
+ 'before:mt-[calc((1em_*_1.75_-_24px)_/_2)] before:h-[24px] before:w-[24px]',
675
+ // selected
676
+ 'data-selected:before:border-black data-selected:before:bg-green data-selected:before:shadow-[inset_0_0_0_4px_rgb(255,255,255)]',
677
+ // hover
678
+ 'data-hovered:data-invalid:before:bg-red-light data-hovered:before:border-green data-hovered:before:bg-green-lightest',
679
+ // focus
680
+ 'data-focus-visible:before:ring-focus-offset',
681
+ // 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
682
+ // so we use an inner outline to artifically pad the border
683
+ '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]'
684
+ ]);
685
+ function Radio(props) {
686
+ const { children, className, description, ...restProps } = props;
687
+ return /*#__PURE__*/ jsx(Radio$1, {
688
+ ...restProps,
689
+ className: cx(className, defaultClasses),
690
+ children: /*#__PURE__*/ jsxs("div", {
691
+ children: [
692
+ children,
693
+ description && /*#__PURE__*/ jsx(Description, {
694
+ className: "mt-2 block",
695
+ children: description
696
+ })
697
+ ]
698
+ })
699
+ });
700
+ }
701
+
702
+ function Select(props) {
703
+ const { className, children, description, errorMessage, label, isInvalid: _isInvalid, ref, ...restProps } = props;
704
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
705
+ // which will override any built in validation
706
+ const isInvalid = errorMessage != null || _isInvalid;
707
+ return /*#__PURE__*/ jsxs(Select$1, {
708
+ ...restProps,
709
+ className: cx(className, formField),
710
+ isInvalid: isInvalid,
711
+ children: [
712
+ label && /*#__PURE__*/ jsx(Label, {
713
+ children: label
714
+ }),
715
+ description && /*#__PURE__*/ jsx(Description, {
716
+ children: description
717
+ }),
718
+ /*#__PURE__*/ jsxs(Button$1, {
719
+ className: cx(input({
720
+ focusModifier: 'visible'
721
+ }), // How to reuse placeholder text?
722
+ 'inline-flex cursor-default items-center gap-2'),
723
+ // See https://github.com/adobe/react-spectrum/discussions/4792#discussioncomment-6492305
724
+ ref: ref,
725
+ children: [
726
+ /*#__PURE__*/ jsx(SelectValue, {
727
+ className: "flex-1 truncate text-left data-[placeholder]:text-[#727070]"
728
+ }),
729
+ /*#__PURE__*/ jsx(ChevronDown, {
730
+ className: dropdown.chevronIcon
731
+ })
732
+ ]
733
+ }),
734
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
735
+ errorMessage: errorMessage
736
+ }),
737
+ /*#__PURE__*/ jsx(Popover, {
738
+ className: dropdown.popover,
739
+ children: /*#__PURE__*/ jsx(ListBox, {
740
+ className: dropdown.listbox,
741
+ children: children
742
+ })
743
+ })
744
+ ]
745
+ });
746
+ }
747
+
748
+ function TextArea(props) {
749
+ const { className, description, errorMessage, label, isInvalid: _isInvalid, rows, ref, ...restProps } = props;
750
+ const isInvalid = errorMessage != null || _isInvalid;
751
+ return /*#__PURE__*/ jsxs(TextField$1, {
752
+ ...restProps,
753
+ className: cx(className, formField),
754
+ isInvalid: isInvalid,
755
+ children: [
756
+ label && /*#__PURE__*/ jsx(Label, {
757
+ children: label
758
+ }),
759
+ description && /*#__PURE__*/ jsx(Description, {
760
+ children: description
761
+ }),
762
+ /*#__PURE__*/ jsx(TextArea$1, {
763
+ className: input(),
764
+ rows: rows,
765
+ ref: ref
766
+ }),
767
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
768
+ errorMessage: errorMessage
769
+ })
770
+ ]
771
+ });
772
+ }
773
+
774
+ const inputVariants$1 = compose(input, cva({
775
+ base: '',
776
+ variants: {
777
+ textAlign: {
778
+ right: 'text-right',
779
+ left: ''
780
+ },
781
+ autoWidth: {
782
+ true: 'max-w-fit',
783
+ false: ''
784
+ }
785
+ }
786
+ }));
787
+ function TextField(props) {
788
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
789
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
790
+ // which will override any built in validation
791
+ const isInvalid = errorMessage != null || _isInvalid;
792
+ return /*#__PURE__*/ jsxs(TextField$1, {
793
+ ...restProps,
794
+ className: cx(className, formField),
795
+ isInvalid: isInvalid,
796
+ children: [
797
+ label && /*#__PURE__*/ jsx(Label, {
798
+ children: label
799
+ }),
800
+ description && /*#__PURE__*/ jsx(Description, {
801
+ children: description
802
+ }),
803
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
804
+ className: cx(inputGroup, {
805
+ 'w-fit': !!size
806
+ }),
807
+ children: [
808
+ leftAddon,
809
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
810
+ /*#__PURE__*/ jsx(Input, {
811
+ className: inputVariants$1({
812
+ textAlign,
813
+ isGrouped: true,
814
+ autoWidth: !!size
815
+ }),
816
+ ref: ref,
817
+ size: size
818
+ }),
819
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
820
+ rightAddon
821
+ ]
822
+ }) : /*#__PURE__*/ jsx(Input, {
823
+ className: inputVariants$1({
824
+ textAlign,
825
+ autoWidth: !!size
826
+ }),
827
+ ref: ref,
828
+ size: size
829
+ }),
830
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
831
+ errorMessage: errorMessage
832
+ })
833
+ ]
834
+ });
835
+ }
836
+
837
+ // This component is based on a copy of ../textfield/TextField, refactoring is TBD: https://github.com/code-obos/grunnmuren/pull/722#issuecomment-1931478786
838
+ const inputVariants = compose(input, cva({
839
+ base: '',
840
+ variants: {
841
+ textAlign: {
842
+ right: 'text-right',
843
+ left: ''
844
+ },
845
+ autoWidth: {
846
+ true: 'max-w-fit',
847
+ false: ''
848
+ }
849
+ }
850
+ }));
851
+ function NumberField(props) {
852
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
853
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
854
+ // which will override any built in validation
855
+ const isInvalid = errorMessage != null || _isInvalid;
856
+ return /*#__PURE__*/ jsxs(NumberField$1, {
857
+ ...restProps,
858
+ className: cx(className, formField),
859
+ isInvalid: isInvalid,
860
+ children: [
861
+ label && /*#__PURE__*/ jsx(Label, {
862
+ children: label
863
+ }),
864
+ description && /*#__PURE__*/ jsx(Description, {
865
+ children: description
866
+ }),
867
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
868
+ className: cx(inputGroup, {
869
+ 'w-fit': !!size
870
+ }),
871
+ children: [
872
+ leftAddon,
873
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
874
+ /*#__PURE__*/ jsx(Input, {
875
+ className: inputVariants({
876
+ textAlign,
877
+ isGrouped: true,
878
+ autoWidth: !!size
879
+ }),
880
+ ref: ref,
881
+ size: size
882
+ }),
883
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
884
+ rightAddon
885
+ ]
886
+ }) : /*#__PURE__*/ jsx(Input, {
887
+ className: inputVariants({
888
+ textAlign,
889
+ autoWidth: !!size
890
+ }),
891
+ ref: ref,
892
+ size: size
893
+ }),
894
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
895
+ errorMessage: errorMessage
896
+ })
897
+ ]
898
+ });
899
+ }
900
+
901
+ const iconMap = {
902
+ info: InfoCircle,
903
+ success: CheckCircle,
904
+ warning: Warning,
905
+ danger: Error
906
+ };
907
+ const alertVariants = cva({
908
+ base: [
909
+ 'grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-md border-2 px-3 py-2',
910
+ // Heading styles:
911
+ '[&_[data-slot="heading"]]:font-medium [&_[data-slot="heading"]]:text-base [&_[data-slot="heading"]]:leading-7',
912
+ // Content styles:
913
+ '[&:has([data-slot="heading"])_[data-slot="content"]]:col-span-full [&_[data-slot="content"]]:text-sm [&_[data-slot="content"]]:leading-6',
914
+ // Footer styles:
915
+ '[&_[data-slot="footer"]]:col-span-full [&_[data-slot="footer"]]:font-light [&_[data-slot="footer"]]:text-xs [&_[data-slot="footer"]]:leading-6'
916
+ ],
917
+ variants: {
918
+ /**
919
+ * The variant of the alert
920
+ * @default info
921
+ */ variant: {
922
+ info: 'border-[#1A7FA7] bg-sky-light',
923
+ success: 'border-[#0F9B6E] bg-mint-light',
924
+ warning: 'border-[#C57C13] bg-[#FFF2DE]',
925
+ danger: 'border-[#C0385D] bg-red-light'
926
+ }
927
+ },
928
+ defaultVariants: {
929
+ variant: 'info'
930
+ }
931
+ });
932
+ const Alertbox = ({ children, role, className, icon, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
933
+ const Icon = icon ?? iconMap[variant];
934
+ const locale = _useLocale();
935
+ const id = useId();
936
+ const [isExpanded, setIsExpanded] = useState(false);
937
+ const isCollapsed = isExpandable && !isExpanded;
938
+ const [isUncontrolledVisible, setIsUncontrolledVisible] = useState(true);
939
+ const isVisible = isDismissed !== undefined ? !isDismissed : isUncontrolledVisible;
940
+ if (!isVisible) return;
941
+ const close = ()=>{
942
+ setIsUncontrolledVisible(false);
943
+ if (onDismiss) onDismiss();
944
+ };
945
+ const isInDevMode = process.env.NODE_ENV !== 'production';
946
+ if (isInDevMode && onDismiss && !isDismissable) {
947
+ console.warn('Passing an `onDismiss` callback without setting the `isDismissable` prop to `true` will not have any effect.');
948
+ }
949
+ if (isInDevMode && !children) {
950
+ console.error('`No children was passed to the <AlertBox/>` component.');
951
+ return;
952
+ }
953
+ const [firstChild, ...restChildren] = Children.toArray(children);
954
+ return /*#__PURE__*/ jsxs("div", {
955
+ className: alertVariants({
956
+ className,
957
+ variant
958
+ }),
959
+ // The role prop is required to force consumers to consider and choose the appropriate alertbox role.
960
+ // role="none" will not have any effect on a div, so it can be omitted.
961
+ role: role === 'none' ? undefined : role,
962
+ children: [
963
+ /*#__PURE__*/ jsx(Icon, {}),
964
+ firstChild,
965
+ isDismissable && /*#__PURE__*/ jsx("button", {
966
+ 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'),
967
+ onClick: close,
968
+ "aria-label": translations$1.close[locale],
969
+ type: "button",
970
+ children: /*#__PURE__*/ jsx(Close, {})
971
+ }),
972
+ isExpandable && /*#__PURE__*/ jsxs("button", {
973
+ 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:
974
+ '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'),
975
+ onClick: ()=>setIsExpanded((prevState)=>!prevState),
976
+ "aria-expanded": isExpanded,
977
+ "aria-controls": id,
978
+ type: "button",
979
+ children: [
980
+ isExpanded ? translations$1.showLess[locale] : translations$1.showMore[locale],
981
+ /*#__PURE__*/ jsx(ChevronDown, {
982
+ className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
983
+ })
984
+ ]
985
+ }),
986
+ restChildren?.length > 0 && /*#__PURE__*/ jsx("div", {
987
+ className: cx('col-span-full grid gap-y-4', isCollapsed && '[&>*:not([data-slot="footer"])]:hidden'),
988
+ id: id,
989
+ children: restChildren
990
+ })
991
+ ]
992
+ });
993
+ };
994
+
995
+ function Breadcrumbs(props) {
996
+ const { className, children, ...restProps } = props;
997
+ return /*#__PURE__*/ jsx(Breadcrumbs$1, {
998
+ ...restProps,
999
+ className: cx(className, 'flex flex-wrap text-sm leading-6'),
1000
+ children: children
1001
+ });
1002
+ }
1003
+
1004
+ function Breadcrumb(props) {
1005
+ const { className, children, href, ...restProps } = props;
1006
+ return /*#__PURE__*/ jsxs(Breadcrumb$1, {
1007
+ className: cx(className, 'group flex items-center'),
1008
+ ...restProps,
1009
+ children: [
1010
+ href ? /*#__PURE__*/ jsx(Link, {
1011
+ href: href,
1012
+ // 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
1013
+ className: "rounded-xs focus-visible:outline-focus group-last:no-underline",
1014
+ children: children
1015
+ }) : children,
1016
+ /*#__PURE__*/ jsx(ChevronRight, {
1017
+ className: "px-1 group-last:hidden"
1018
+ })
1019
+ ]
1020
+ });
1021
+ }
1022
+
1023
+ function isLinkProps(props) {
1024
+ return !!props.href;
1025
+ }
1026
+ function Backlink(props) {
1027
+ const { className, style, children, withUnderline, ref, ...restProps } = props;
1028
+ 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');
1029
+ const content = /*#__PURE__*/ jsxs(Fragment, {
1030
+ children: [
1031
+ /*#__PURE__*/ jsx(ChevronLeft, {
1032
+ className: cx('-ml-[0.5em] group-hover:-translate-x-1 shrink-0 transition-transform duration-300')
1033
+ }),
1034
+ /*#__PURE__*/ jsx("span", {
1035
+ children: /*#__PURE__*/ jsx("span", {
1036
+ className: cx('border-transparent border-t-[1px] border-b-[1px] transition-colors duration-300', withUnderline ? 'border-b-black' : 'group-hover:border-b-black'),
1037
+ children: children
1038
+ })
1039
+ })
1040
+ ]
1041
+ });
1042
+ if (isLinkProps(props)) {
1043
+ return /*#__PURE__*/ jsx(Link, {
1044
+ ...restProps,
1045
+ className: _className,
1046
+ style: style,
1047
+ ref: ref,
1048
+ children: content
1049
+ });
1050
+ }
1051
+ return /*#__PURE__*/ jsx(Button$1, {
1052
+ ...restProps,
1053
+ className: _className,
1054
+ style: style,
1055
+ ref: ref,
1056
+ children: content
1057
+ });
1058
+ }
1059
+
1060
+ const cardVariants = cva({
1061
+ base: [
1062
+ 'group/card',
1063
+ 'rounded-2xl border p-3',
1064
+ 'flex gap-y-4',
1065
+ 'relative',
1066
+ // **** Heading ****
1067
+ '[&_[data-slot="heading"]]:inline',
1068
+ '[&_[data-slot="heading"]]:heading-s',
1069
+ '[&_[data-slot="heading"]]:leading-6',
1070
+ '[&_[data-slot="heading"]]:w-fit',
1071
+ '[&_[data-slot="heading"]]:text-pretty',
1072
+ '[&_[data-slot="heading"]]:hyphens-auto',
1073
+ '[&_[data-slot="heading"]]:[word-break:break-word]',
1074
+ // **** Content ****
1075
+ '[&_[data-slot="content"]]:flex [&_[data-slot="content"]]:flex-col [&_[data-slot="content"]]:gap-y-4',
1076
+ // **** Media ****
1077
+ '[&_[data-slot="media"]_*]:pointer-events-none',
1078
+ '[&_[data-slot="media"]]:overflow-hidden',
1079
+ '[&_[data-slot="media"]]:relative',
1080
+ // 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)
1081
+ '[&_[data-slot="media"]]:mx-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))] [&_[data-slot="media"]]:mt-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
1082
+ // Sets the aspect ratio of the media content (width: 100% is necessary to make aspect ratio work on images in FF)
1083
+ '[&_[data-slot="media"]>*:not([data-slot="badge"])]:aspect-3/2 [&_[data-slot="media"]>img]:w-full [&_[data-slot="media"]>img]:object-cover',
1084
+ // 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.
1085
+ '[&_[data-slot="media"]>*]:duration-300 [&_[data-slot="media"]>*]:ease-in-out [&_[data-slot="media"]>*]:motion-safe:transition-transform',
1086
+ // **** Card link ****
1087
+ // **** Hover ****
1088
+ // 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)
1089
+ '[&:has([data-slot="card-link"]_a:hover)_[data-slot="media"]>img]:scale-110',
1090
+ // **** Card link in Heading ****
1091
+ '[&:has([data-slot="heading"]_[data-slot="card-link"]:hover)_[data-slot="media"]>img]:scale-110',
1092
+ // Border (bottom/top) is set to transparent to make sure the bottom underline is not visible when the card is hovered
1093
+ // Border top is set to even out the border bottom used for the underline
1094
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:no-underline',
1095
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:border-y-2',
1096
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:border-y-transparent',
1097
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:transition-colors',
1098
+ '[&_[data-slot="heading"]_[data-slot="card-link"]:hover]:border-b-current',
1099
+ // 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
1100
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:heading-s',
1101
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:leading-6',
1102
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:text-pretty',
1103
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:hyphens-auto',
1104
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:[word-break:break-word]',
1105
+ // **** Fail-safe for interactive elements ****
1106
+ // Make interactive elements clickable by themselves, while the rest of the card is clickable as a whole
1107
+ // The card is made clickable by a pseudo-element on the heading that covers the entire card
1108
+ '[&:not(:has([data-slot="card-link"]_a))_a:not([data-slot="card-link"])]:relative [&_button]:relative [&_input]:relative',
1109
+ // 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)
1110
+ '[&_[data-slot="card-link"]_a]:static',
1111
+ // Place other interactive on top of the pseudo-element that makes the entire card clickable
1112
+ // by setting a higher z-index than the pseudo-element (which implicitly z-index 0)
1113
+ '[&_a:not([data-slot="card-link"])]:z-[1] [&_button]:z-[1] [&_input]:z-[1]',
1114
+ // **** Badge ****
1115
+ '[&_[data-slot="media"]_[data-slot="badge"]]:absolute [&_[data-slot="media"]_[data-slot="badge"]]:top-0',
1116
+ // 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)
1117
+ '[&_[data-slot="media"]_[data-slot="badge"]]:z-[1]',
1118
+ // Left aligned - override default corner radius of the badge
1119
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tl-2xl',
1120
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-br-2xl',
1121
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tr-none',
1122
+ '[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-bl-none',
1123
+ // Right aligned - override default corner radius of the badge
1124
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tl-none',
1125
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-br-none',
1126
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tr-2xl',
1127
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-bl-2xl',
1128
+ // ... and position the badge at the right edge of the media content
1129
+ '[&_[data-slot="media"]_[data-slot="badge"]:last-child]:right-0'
1130
+ ],
1131
+ variants: {
1132
+ /**
1133
+ * The variant of the card
1134
+ * @default subtle
1135
+ */ variant: {
1136
+ subtle: [
1137
+ 'border-transparent',
1138
+ // **** Media styles ****
1139
+ '[&_[data-slot="media"]]:rounded-2xl'
1140
+ ],
1141
+ outlined: 'border border-black'
1142
+ },
1143
+ /**
1144
+ * The layout of the card
1145
+ * @default vertical
1146
+ */ layout: {
1147
+ vertical: [
1148
+ 'flex-col',
1149
+ // **** Media ****
1150
+ '[&_[data-slot="media"]]:rounded-t-2xl'
1151
+ ],
1152
+ horizontal: [
1153
+ 'gap-x-4',
1154
+ // **** With Media ****
1155
+ '[&:has(>[data-slot="media"]:first-child)]:flex-col',
1156
+ '[&:has(>[data-slot="media"]:last-child)]:flex-col-reverse',
1157
+ '[&:has(>[data-slot="media"])]:md:!flex-row',
1158
+ '[&_[data-slot="media"]]:md:h-fit',
1159
+ '[&:has(>[data-slot="media"])>*]:md:basis-1/2',
1160
+ // Position media at the edges of the card
1161
+ '[&_[data-slot="media"]]:md:mb-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
1162
+ '[&_[data-slot="media"]:first-child]:md:mr-0',
1163
+ '[&_[data-slot="media"]:last-child]:md:ml-0',
1164
+ // Make sure the card link is clickable when the media is on the right side
1165
+ // This i necessary because the media content is positioned after the card link in the DOM
1166
+ '[&:has(>[data-slot="media"]:last-child)_[data-slot="card-link"]]:z-[1]',
1167
+ // **** Without Media ****
1168
+ '[&:not(:has(>[data-slot="media"]))]:flex-row',
1169
+ // 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.
1170
+ '[&:not(:has(>[data-slot="media"]))]:flex-wrap [&:not(:has(>[data-slot="media"]))_[data-slot="content"]]:grow [&:not(:has(>[data-slot="media"]))_[data-slot="content"]]:basis-[12rem]',
1171
+ // Make sure svg's etc. are not shrinkable
1172
+ '[&>:not([data-slot="content"],[data-slot="media"])]:shrink-0'
1173
+ ]
1174
+ }
1175
+ },
1176
+ defaultVariants: {
1177
+ variant: 'subtle',
1178
+ layout: 'vertical'
1179
+ },
1180
+ compoundVariants: [
1181
+ {
1182
+ variant: 'outlined',
1183
+ layout: 'horizontal',
1184
+ className: [
1185
+ // **** Media ****
1186
+ // Some rounded corners are removed when the card is outlined
1187
+ '[&_[data-slot="media"]]:rounded-t-2xl',
1188
+ '[&_[data-slot="media"]:first-child]:md:rounded-tr-none [&_[data-slot="media"]:first-child]:md:rounded-bl-2xl',
1189
+ '[&_[data-slot="media"]:last-child]:md:rounded-tl-none [&_[data-slot="media"]:last-child]:md:rounded-br-2xl',
1190
+ // **** Badge ****
1191
+ // Override default corner radius of the badge to match the media border radius
1192
+ '[&_[data-slot="media"]:first-child_[data-slot="badge"]:last-child]:md:rounded-tr-none',
1193
+ '[&_[data-slot="media"]:last-child_[data-slot="badge"]:first-child]:md:rounded-tl-none'
1194
+ ]
1195
+ }
1196
+ ]
1197
+ });
1198
+ const Card = ({ children, className: _className, variant, layout, ...restProps })=>{
1199
+ const className = cardVariants({
1200
+ className: _className,
1201
+ variant,
1202
+ layout
1203
+ });
1204
+ return /*#__PURE__*/ jsx("div", {
1205
+ className: className,
1206
+ ...restProps,
1207
+ children: children
1208
+ });
1209
+ };
1210
+ const cardLinkVariants = cva({
1211
+ base: 'w-fit max-w-full',
1212
+ variants: {
1213
+ withHref: {
1214
+ true: [
1215
+ // **** Clickarea ****
1216
+ 'cursor-pointer',
1217
+ 'after:absolute',
1218
+ 'after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
1219
+ 'after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
1220
+ // **** Focus ****
1221
+ 'focus-visible:outline-none',
1222
+ 'data-focus-visible:after:outline-focus',
1223
+ 'data-focus-visible:after:outline-offset-2',
1224
+ // **** Hover ****
1225
+ // Links are underlined by default, and the underline is removed on hover.
1226
+ // So we make sure that also happens when the user hovers the clickable area.
1227
+ 'hover:no-underline'
1228
+ ],
1229
+ false: [
1230
+ // **** Clickarea ****
1231
+ '[&_a]:after:cursor-pointer',
1232
+ '[&_a]:after:absolute',
1233
+ '[&_a]:after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
1234
+ '[&_a]:after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
1235
+ // **** Focus ****
1236
+ '[&_a[data-focus-visible]]:outline-none',
1237
+ '[&_a[data-focus-visible]]:after:outline-focus',
1238
+ '[&_a[data-focus-visible]]:after:outline-offset-2',
1239
+ // **** Hover ****
1240
+ // Links are underlined by default, and the underline is removed on hover.
1241
+ // So we make sure that also happens when the user hovers the card.
1242
+ // The group-hover ensures that the hover effect also applies when this component is used as a wrapper around a link.
1243
+ '[&_a]:group-hover/card:no-underline'
1244
+ ]
1245
+ }
1246
+ }
1247
+ });
1248
+ /**
1249
+ * A component that creates a clickable area on a card.
1250
+ * It can be used either as a wrapper around a link or as a standalone link.
1251
+ */ const CardLink = ({ className: _className, href, ...restProps })=>{
1252
+ const className = cardLinkVariants({
1253
+ className: _className,
1254
+ withHref: !!href
1255
+ });
1256
+ return href ? /*#__PURE__*/ jsx(Link, {
1257
+ "data-slot": "card-link",
1258
+ ...restProps,
1259
+ href: href,
1260
+ className: className
1261
+ }) : // We can't utilize that the `Link` component from react-aria-components renders as a span if it doesn't have an href,
1262
+ // because it still renders with role="link" and tabindex="0" which makes it focusable.
1263
+ // So we need to render a div instead.
1264
+ /*#__PURE__*/ jsx("div", {
1265
+ ...restProps,
1266
+ "data-slot": "card-link",
1267
+ className: className
1268
+ });
1269
+ };
1270
+
1271
+ /**
1272
+ * A React component that wraps https://react-spectrum.adobe.com/react-aria/useDateFormatter.html
1273
+ * By default it sets the timeZone to `Europe/Berlin` to prevent the server's timezone from affecting
1274
+ * the localized format
1275
+ */ const DateFormatter = ({ options: _options, value, children: render })=>{
1276
+ const options = {
1277
+ timeZone: 'Europe/Berlin',
1278
+ ..._options
1279
+ };
1280
+ const formatter = useDateFormatter(options);
1281
+ const date = typeof value === 'string' ? new Date(value) : value;
1282
+ const formatted = formatter.format(date);
1283
+ return render ? render(formatted) : formatted;
1284
+ };
1285
+
1286
+ const VideoLoop = ({ src, format, alt, className })=>{
1287
+ // Control the video playback state, so that the user can pause and play the video at will, also control the video autoplay
1288
+ const [shouldPlay, setShouldPlay] = useState(false);
1289
+ // 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)
1290
+ const [isPlaying, setIsPlaying] = useState(false);
1291
+ // We need to check if the user prefers reduced motion, so that we can prevent the video from autoplaying if so
1292
+ const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(null);
1293
+ const videoRef = useRef(null);
1294
+ useEffect(()=>{
1295
+ const { matches: userPrefersReducedMotion } = matchMedia('(prefers-reduced-motion: reduce)');
1296
+ setUserPrefersReducedMotion(userPrefersReducedMotion);
1297
+ // Autoplay the video if the user does not prefer reduced motion
1298
+ setShouldPlay(!userPrefersReducedMotion);
1299
+ }, []);
1300
+ // Follow google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
1301
+ // "Don't assume a video will play, and don't show a pause button when the video is not actually playing."
1302
+ // "You should always look at the Promise returned by the play function to see if it was rejected:"
1303
+ // 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
1304
+ useEffect(()=>{
1305
+ if (!videoRef.current) return;
1306
+ if (shouldPlay) {
1307
+ videoRef.current.play().then(()=>setIsPlaying(true)).catch(()=>setIsPlaying(false));
1308
+ } else {
1309
+ videoRef.current.pause();
1310
+ setIsPlaying(false);
1311
+ }
1312
+ }, [
1313
+ shouldPlay
1314
+ ]);
1315
+ return /*#__PURE__*/ jsxs("div", {
1316
+ className: cx(className, 'relative', userPrefersReducedMotion === null && 'opacity-0'),
1317
+ children: [
1318
+ /*#__PURE__*/ jsx("video", {
1319
+ "aria-hidden": true,
1320
+ ref: videoRef,
1321
+ // cursor-pointer is not working on the button below, so we add it here for the same effect
1322
+ className: "h-full w-full cursor-pointer object-cover",
1323
+ playsInline: true,
1324
+ loop: userPrefersReducedMotion === false,
1325
+ autoPlay: userPrefersReducedMotion === false,
1326
+ muted: true,
1327
+ onEnded: (event)=>{
1328
+ if (userPrefersReducedMotion) {
1329
+ // Reset the video to the beginning if the user prefers reduced motion, since the video will not loop
1330
+ event.currentTarget.currentTime = 0;
1331
+ setShouldPlay(false);
1332
+ setIsPlaying(false);
1333
+ }
1334
+ },
1335
+ children: /*#__PURE__*/ jsx("source", {
1336
+ src: src,
1337
+ type: `video/${format}`
1338
+ })
1339
+ }),
1340
+ userPrefersReducedMotion !== null && /*#__PURE__*/ jsx("button", {
1341
+ "aria-hidden": true,
1342
+ type: "button",
1343
+ onClick: ()=>setShouldPlay((prevState)=>!prevState),
1344
+ 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', // Setting the opacity to 0 before applying the transition below will ensure the button only fades in after the video has started playing
1345
+ shouldPlay && 'opacity-0', isPlaying && [
1346
+ 'transition-opacity duration-200',
1347
+ // Only show the pause button when the video is hovered or focused
1348
+ 'focus-visible:opacity-100',
1349
+ 'hover:opacity-100'
1350
+ ]),
1351
+ children: /*#__PURE__*/ jsx("span", {
1352
+ className: "grid h-12 w-12 place-items-center rounded-full bg-white outline-hidden",
1353
+ children: isPlaying ? /*#__PURE__*/ jsx(PlayerPause, {}) : /*#__PURE__*/ jsx(PlayerPlay, {})
1354
+ })
1355
+ }),
1356
+ alt && /*#__PURE__*/ jsx("p", {
1357
+ className: "sr-only",
1358
+ children: alt
1359
+ })
1360
+ ]
1361
+ });
1362
+ };
1363
+
1364
+ const disclosureButtonVariants = cva({
1365
+ base: [
1366
+ 'inline-flex cursor-pointer items-center justify-between rounded-lg focus-visible:outline-current focus-visible:outline-focus',
1367
+ // Ensure a minimum click area of 44x44px, while making it look like it only has the size of the content
1368
+ '-m-2.5 p-2.5 focus-visible:outline-offset-[-0.625rem]'
1369
+ ],
1370
+ variants: {
1371
+ withChevron: {
1372
+ true: '[&[aria-expanded="true"]_svg]:rotate-180',
1373
+ false: null
1374
+ },
1375
+ /**
1376
+ * When the button is without text, but with a single icon.
1377
+ * @default false
1378
+ */ isIconOnly: {
1379
+ true: '[&>svg]:h-7 [&>svg]:w-7',
1380
+ false: 'gap-2.5'
1381
+ }
1382
+ },
1383
+ defaultVariants: {
1384
+ withChevron: false,
1385
+ isIconOnly: false
1386
+ }
1387
+ });
1388
+ const DisclosureButton = ({ className, withChevron, isIconOnly, children, ref: _ref, ...restProps })=>{
1389
+ const [props, ref] = useContextProps(restProps, _ref, ButtonContext);
1390
+ return /*#__PURE__*/ jsxs(Button$1, {
1391
+ ...props,
1392
+ ref: ref,
1393
+ className: disclosureButtonVariants({
1394
+ className,
1395
+ withChevron,
1396
+ isIconOnly
1397
+ }),
1398
+ slot: "trigger",
1399
+ children: [
1400
+ children,
1401
+ withChevron && /*#__PURE__*/ jsx(ChevronDown, {
1402
+ className: "flex-none transition-transform duration-300 motion-reduce:transition-none"
1403
+ })
1404
+ ]
1405
+ });
1406
+ };
1407
+ const DisclosureStateContext = /*#__PURE__*/ createContext(null);
1408
+ const Disclosure = ({ ref: _ref, children, ..._props })=>{
1409
+ const [props, ref] = useContextProps(_props, _ref, DisclosureContext);
1410
+ const groupState = useContext(DisclosureGroupStateContext);
1411
+ let { id, ...otherProps } = props;
1412
+ const defaultId = useId();
1413
+ id ||= defaultId;
1414
+ const isExpanded = groupState ? groupState.expandedKeys.has(id) : props.isExpanded;
1415
+ const state = useDisclosureState({
1416
+ ...props,
1417
+ isExpanded,
1418
+ onExpandedChange (isExpanded) {
1419
+ if (groupState) {
1420
+ groupState.toggleKey(id);
1421
+ }
1422
+ props.onExpandedChange?.(isExpanded);
1423
+ }
1424
+ });
1425
+ const isDisabled = props.isDisabled || groupState?.isDisabled || false;
1426
+ const domProps = filterDOMProps(otherProps);
1427
+ const { isFocusVisible: isFocusVisibleWithin } = useFocusRing({
1428
+ within: true
1429
+ });
1430
+ const panelRef = useRef(null);
1431
+ const { buttonProps, panelProps } = useDisclosure({
1432
+ ...props,
1433
+ isExpanded,
1434
+ isDisabled
1435
+ }, state, panelRef);
1436
+ const { role: _, ...propsWithoutRole } = panelProps;
1437
+ return /*#__PURE__*/ jsx(Provider, {
1438
+ values: [
1439
+ [
1440
+ DisclosureContext,
1441
+ state
1442
+ ],
1443
+ [
1444
+ ButtonContext,
1445
+ {
1446
+ slots: {
1447
+ [DEFAULT_SLOT]: {},
1448
+ trigger: buttonProps
1449
+ }
1450
+ }
1451
+ ],
1452
+ [
1453
+ DisclosurePanelContext,
1454
+ {
1455
+ ...propsWithoutRole,
1456
+ panelRef
1457
+ }
1458
+ ],
1459
+ [
1460
+ DisclosureStateContext,
1461
+ state
1462
+ ]
1463
+ ],
1464
+ children: /*#__PURE__*/ jsx("div", {
1465
+ ...domProps,
1466
+ className: otherProps.className,
1467
+ ref: ref,
1468
+ "data-focus-visible-within": isFocusVisibleWithin || undefined,
1469
+ "data-expanded": state.isExpanded || undefined,
1470
+ "data-disabled": isDisabled || undefined,
1471
+ children: typeof children === 'function' ? children({
1472
+ isExpanded: state.isExpanded,
1473
+ isFocusVisibleWithin,
1474
+ isDisabled,
1475
+ state,
1476
+ defaultChildren: null
1477
+ }) : children
1478
+ })
1479
+ });
1480
+ };
1481
+ const DisclosurePanelContext = /*#__PURE__*/ createContext({});
1482
+ const DisclosurePanel = ({ ref, children, ...props })=>{
1483
+ const disclosureContext = useContext(DisclosureContext);
1484
+ const { panelProps, panelRef } = useContext(DisclosurePanelContext);
1485
+ const { role: _role = 'group', className, ...restProps } = props;
1486
+ const ariaLabelledby = props['aria-labelledby'] ?? restProps['aria-labelledby'];
1487
+ const isWithoutRole = _role === 'none';
1488
+ const role = isWithoutRole ? undefined : _role;
1489
+ const { isFocusVisible: isFocusVisibleWithin, focusProps: focusWithinProps } = useFocusRing({
1490
+ within: true
1491
+ });
1492
+ const domProps = filterDOMProps(props);
1493
+ return /*#__PURE__*/ jsx("div", {
1494
+ className: cx('grid transition-all duration-300 motion-reduce:transition-none', disclosureContext?.isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'),
1495
+ children: /*#__PURE__*/ jsx("div", {
1496
+ className: "overflow-hidden",
1497
+ children: /*#__PURE__*/ jsx("div", {
1498
+ ref: mergeRefs(ref, panelRef),
1499
+ ...mergeProps(panelProps, focusWithinProps),
1500
+ ...restProps,
1501
+ ...domProps,
1502
+ "data-focus-visible-within": isFocusVisibleWithin || undefined,
1503
+ className: cx(className, '[content-visibility:visible]'),
1504
+ role: role,
1505
+ "aria-labelledby": isWithoutRole ? undefined : ariaLabelledby,
1506
+ children: /*#__PURE__*/ jsx(Provider, {
1507
+ values: [
1508
+ // Reset the context to avoid passing the same context to children, in case of nested Disclosures
1509
+ [
1510
+ DisclosureContext,
1511
+ null
1512
+ ],
1513
+ [
1514
+ ButtonContext,
1515
+ null
1516
+ ]
1517
+ ],
1518
+ children: children
1519
+ })
1520
+ })
1521
+ })
1522
+ });
1523
+ };
1524
+
1525
+ /**
1526
+ * A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
1527
+ */ const FileTrigger = (props)=>{
1528
+ const { onSelect, acceptedFileTypes, allowsMultiple, defaultCamera, children, acceptDirectory, ref, isInvalid, isRequired, name, value, ...rest } = props;
1529
+ const inputRef = useObjectRef(ref);
1530
+ return /*#__PURE__*/ jsxs(Fragment, {
1531
+ children: [
1532
+ /*#__PURE__*/ jsx(PressResponder, {
1533
+ onPress: ()=>{
1534
+ if (inputRef.current?.value) {
1535
+ inputRef.current.value = '';
1536
+ }
1537
+ inputRef.current?.click();
1538
+ },
1539
+ children: children
1540
+ }),
1541
+ /*#__PURE__*/ jsx(Input, {
1542
+ ...rest,
1543
+ required: isRequired,
1544
+ "aria-invalid": isInvalid,
1545
+ "data-invalid": isInvalid,
1546
+ "data-rac": true,
1547
+ name: Array.isArray(name) ? name.join(' ') : name,
1548
+ type: "file",
1549
+ ref: inputRef,
1550
+ accept: acceptedFileTypes?.toString(),
1551
+ onChange: (e)=>onSelect?.(e.target.files),
1552
+ capture: defaultCamera,
1553
+ multiple: allowsMultiple,
1554
+ // @ts-expect-error
1555
+ webkitdirectory: acceptDirectory ? '' : undefined,
1556
+ // This is a work around to prevent error in the console when attempting to submit a form with a required and empty file input
1557
+ // RAC uses display: none, which prevents the file input from being focused.
1558
+ // What we do instead is to hide it visually using custom CSS, so that the native HTML validation messages are still hidden. Which is why
1559
+ // we don't use the sr-only class.
1560
+ className: "absolute left-[-1000vw] opacity-0",
1561
+ // Finally, we add aria-hidden to prevent the file input from being read by screen readers
1562
+ "aria-hidden": true,
1563
+ // Prevent focus trap when tabbing (since focus is delegated to the button)
1564
+ tabIndex: -1
1565
+ })
1566
+ ]
1567
+ });
1568
+ };
1569
+
1570
+ const translations = {
1571
+ remove: {
1572
+ nb: 'Fjern',
1573
+ sv: 'Ta bort',
1574
+ en: 'Remove'
1575
+ }
1576
+ };
1577
+ /**
1578
+ * Converts an array of files to a DataTransfer object which can be used as a FileList.
1579
+ * This is necessary for setting the files on a native file input.
1580
+ * @param files An array of files
1581
+ * @returns The files as a DataTransfer object which can be used as a FileList
1582
+ */ function filesToDataTransfer(files) {
1583
+ const dataTransfer = new DataTransfer();
1584
+ for (const file of files){
1585
+ dataTransfer.items.add(file);
1586
+ }
1587
+ return dataTransfer.files;
1588
+ }
1589
+ /**
1590
+ * Generates unique file names for files with the same original name.
1591
+ * Any duplicate files will have a number in parentheses appended to their name.
1592
+ * @param files An array of files
1593
+ * @returns An array of files with unique names
1594
+ */ function uniqueFileNames(files) {
1595
+ const fileNameCounts = {};
1596
+ return files.map((file)=>{
1597
+ const fileName = file.name;
1598
+ // Filter out the extension and any trailing numbers in parentheses
1599
+ const baseName = fileName.replace(/\s*\(\d+\)|(\.[^.]+)$/g, '');
1600
+ // Extract the file extension
1601
+ const extension = fileName.match(/(\.[^.]+)$/)?.[0] || '';
1602
+ if (!fileNameCounts[baseName]) {
1603
+ // Extract any number from the file name (if any, otherwise default to 0)
1604
+ const baseNameCount = Number.parseInt(fileName.match(/\((\d+)\)/)?.[1] ?? '0');
1605
+ fileNameCounts[baseName] = baseNameCount;
1606
+ }
1607
+ fileNameCounts[baseName]++;
1608
+ if (fileNameCounts[baseName] > 1) {
1609
+ return new File([
1610
+ file
1611
+ ], // Follow the pattern of adding a number in parentheses to the base name (e.g. "file (1).txt")
1612
+ `${baseName} (${fileNameCounts[baseName] - 1})${extension}`);
1613
+ }
1614
+ return file;
1615
+ });
1616
+ }
1617
+ const FileUpload = ({ children, files: _files, onChange, validate, isInvalid: _isInvalid, errorMessage, isRequired, allowsMultiple, ref, ...fileTriggerProps })=>{
1618
+ const [files, setFiles] = useState(_files ?? []);
1619
+ const isInvalid = !!errorMessage || _isInvalid;
1620
+ const id = useId();
1621
+ const locale = _useLocale();
1622
+ const _inputRef = useRef(null);
1623
+ const inputRef = ref ?? _inputRef;
1624
+ const slottedContext = useSlottedContext(FormContext) || {};
1625
+ const validationBehavior = fileTriggerProps.validationBehavior ?? slottedContext.validationBehavior ?? 'native';
1626
+ const validateFiles = useCallback((files)=>{
1627
+ if (validate === undefined) return true;
1628
+ const errors = [];
1629
+ for (const file of files){
1630
+ if (!validate) continue;
1631
+ const validation = validate(file);
1632
+ if (typeof validation === 'string') errors.push(validation);
1633
+ }
1634
+ if (errors.length === 0) return true;
1635
+ return errors;
1636
+ }, [
1637
+ validate
1638
+ ]);
1639
+ const validationState = useFormValidationState({
1640
+ ...fileTriggerProps,
1641
+ validationBehavior,
1642
+ validate: validateFiles,
1643
+ isRequired,
1644
+ isInvalid,
1645
+ value: _files ?? files ?? null
1646
+ });
1647
+ const controlledOrUncontrolledFiles = // Use controlled files if they are provided, otherwise use internal files - but map them to File objects (remove validation prop)
1648
+ _files ?? files;
1649
+ useEffect(()=>{
1650
+ // Keep the native file input in sync with the internal file state
1651
+ if (inputRef.current) {
1652
+ inputRef.current.files = filesToDataTransfer(controlledOrUncontrolledFiles);
1653
+ }
1654
+ }, [
1655
+ controlledOrUncontrolledFiles,
1656
+ inputRef
1657
+ ]);
1658
+ const { fieldProps } = useField({
1659
+ ...fileTriggerProps,
1660
+ validate: validateFiles,
1661
+ validationBehavior,
1662
+ isInvalid,
1663
+ errorMessage
1664
+ });
1665
+ const [value, setValue] = useControlledState(_files, [], onChange);
1666
+ useFormReset(inputRef, value, setValue);
1667
+ useFormValidation({
1668
+ ...fileTriggerProps,
1669
+ validationBehavior,
1670
+ validate: validateFiles,
1671
+ isRequired,
1672
+ isInvalid
1673
+ }, validationState, inputRef);
1674
+ const buttonRef = useRef(null);
1675
+ const { displayValidation } = validationState;
1676
+ useUpdateEffect(()=>{
1677
+ // Fixes a bug where validation state is not reset after being set to customError
1678
+ // This happens if the file upload ends up in an invalid state and then is emptied: the old valdiations is still lingering
1679
+ if (controlledOrUncontrolledFiles.length === 0 && validationState.displayValidation.validationDetails.customError) {
1680
+ validationState.commitValidation();
1681
+ }
1682
+ }, [
1683
+ controlledOrUncontrolledFiles
1684
+ ]);
1685
+ return /*#__PURE__*/ jsx(Provider, {
1686
+ values: [
1687
+ [
1688
+ FieldErrorContext,
1689
+ displayValidation
1690
+ ]
1691
+ ],
1692
+ children: /*#__PURE__*/ jsxs("div", {
1693
+ "data-slot": "file-upload",
1694
+ className: "group grid w-72 max-w-full gap-2",
1695
+ "data-required": isRequired,
1696
+ children: [
1697
+ /*#__PURE__*/ jsx(Provider, {
1698
+ values: [
1699
+ [
1700
+ LabelContext,
1701
+ {
1702
+ htmlFor: id
1703
+ }
1704
+ ],
1705
+ [
1706
+ ButtonContext,
1707
+ {
1708
+ // The button acts as the trigger for the file input, which is why we connect the label to the button id
1709
+ id,
1710
+ // Needed for RAC auto-focusing behavior to work
1711
+ ref: buttonRef,
1712
+ className: 'w-fit'
1713
+ }
1714
+ ],
1715
+ [
1716
+ InputContext,
1717
+ fieldProps
1718
+ ]
1719
+ ],
1720
+ children: /*#__PURE__*/ jsx(FileTrigger, {
1721
+ ...fileTriggerProps,
1722
+ isRequired: isRequired,
1723
+ allowsMultiple: allowsMultiple,
1724
+ onSelect: (selectedFiles)=>{
1725
+ if (selectedFiles === null) return;
1726
+ const newFiles = Array.from(selectedFiles);
1727
+ // For controlled component
1728
+ onChange?.((prevFiles)=>allowsMultiple ? uniqueFileNames(prevFiles.concat(newFiles)) : newFiles);
1729
+ // For internal file state
1730
+ setFiles((prevFiles)=>allowsMultiple ? uniqueFileNames(prevFiles.concat(newFiles)) : newFiles);
1731
+ },
1732
+ isInvalid: isInvalid || validationState.displayValidation.isInvalid,
1733
+ ref: inputRef,
1734
+ // Delegate focus to the button when the hidden file input is focused (for RAC auto-focusing behavior)
1735
+ onFocus: ()=>buttonRef.current?.focus(),
1736
+ children: children
1737
+ })
1738
+ }),
1739
+ /*#__PURE__*/ jsx("ul", {
1740
+ className: "mt-4 grid gap-y-2",
1741
+ children: controlledOrUncontrolledFiles.map((file, fileIndex)=>{
1742
+ let fileName = file.name;
1743
+ if (fileTriggerProps.acceptDirectory && file.webkitRelativePath !== '') {
1744
+ fileName = file.webkitRelativePath;
1745
+ }
1746
+ const validation = validate?.(file) ?? true;
1747
+ const hasError = validation !== true;
1748
+ return /*#__PURE__*/ jsxs("li", {
1749
+ children: [
1750
+ /*#__PURE__*/ jsxs("div", {
1751
+ className: cx('flex items-center justify-between gap-2 rounded-lg border-2 px-4 py-2', hasError ? 'border-red bg-red-light' : 'border-gray bg-gray-lightest'),
1752
+ children: [
1753
+ fileName,
1754
+ ' ',
1755
+ /*#__PURE__*/ jsx("button", {
1756
+ className: cx('-m-2 grid h-11 w-11 shrink-0 cursor-pointer place-items-center rounded-xl', // Focus styles
1757
+ 'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
1758
+ onClick: ()=>{
1759
+ // For controlled component
1760
+ onChange?.((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1761
+ // For internal file state
1762
+ setFiles((prevFiles)=>prevFiles.filter((_, index)=>index !== fileIndex));
1763
+ // Make sure screen readers doesn't loose track of focus
1764
+ // (without this, the focus will be set to the top of the page for screen readers)
1765
+ buttonRef.current?.focus();
1766
+ },
1767
+ "aria-label": translations.remove[locale],
1768
+ type: "button",
1769
+ children: /*#__PURE__*/ jsx(Trash, {})
1770
+ })
1771
+ ]
1772
+ }),
1773
+ hasError && /*#__PURE__*/ jsx(ErrorMessage, {
1774
+ className: "mt-1 block w-full",
1775
+ children: validation
1776
+ })
1777
+ ]
1778
+ }, fileName);
1779
+ })
1780
+ }),
1781
+ (controlledOrUncontrolledFiles.length === 0 || !!errorMessage) && /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
1782
+ errorMessage: errorMessage
1783
+ })
1784
+ ]
1785
+ })
1786
+ });
1787
+ };
1788
+
1789
+ const baseClassName = 'h-20 w-20 shrink-0 rounded-full';
1790
+ const Avatar = ({ src, alt = '', className, onError, loading = 'lazy', ...rest })=>{
1791
+ const [hasError, setHasError] = useState(false);
1792
+ const hasValidImage = !hasError && src;
1793
+ return hasValidImage ? /*#__PURE__*/ jsx("img", {
1794
+ ...rest,
1795
+ src: src,
1796
+ alt: alt,
1797
+ loading: loading,
1798
+ className: cx(className, baseClassName, 'object-cover'),
1799
+ onError: (event)=>{
1800
+ onError?.(event);
1801
+ setHasError(true);
1802
+ }
1803
+ }) : /*#__PURE__*/ jsx("div", {
1804
+ className: cx(className, baseClassName, 'grid place-items-center bg-gray-light text-gray-dark'),
1805
+ children: /*#__PURE__*/ jsx(User, {
1806
+ className: "scale-[2.25]"
1807
+ })
1808
+ });
1809
+ };
1810
+
1811
+ const DialogTrigger = (props)=>/*#__PURE__*/ jsx(DialogTrigger$1, {
1812
+ ...props
1813
+ });
1814
+ const ModalOverlay = (props)=>/*#__PURE__*/ jsx(ModalOverlay$1, {
1815
+ ...props,
1816
+ isDismissable: true,
1817
+ 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
1818
+ 'motion-reduce:animate-none')
1819
+ });
1820
+ const Modal = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ModalOverlay, {
1821
+ children: /*#__PURE__*/ jsx(Modal$1, {
1822
+ ...restProps,
1823
+ 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
1824
+ 'motion-reduce:animate-none')
1825
+ })
1826
+ });
1827
+ const Dialog = ({ className, children, ...restProps })=>{
1828
+ const locale = _useLocale();
1829
+ return /*#__PURE__*/ jsx(Dialog$1, {
1830
+ ...restProps,
1831
+ className: cx('relative grid gap-y-5 outline-none', // Footer
1832
+ '[&_[data-slot="footer"]]:flex [&_[data-slot="footer"]]:gap-x-2'),
1833
+ children: ({ close })=>/*#__PURE__*/ jsx(Fragment, {
1834
+ children: /*#__PURE__*/ jsx(Provider, {
1835
+ values: [
1836
+ [
1837
+ HeadingContext,
1838
+ {
1839
+ slots: {
1840
+ [DEFAULT_SLOT]: {},
1841
+ title: {
1842
+ className: 'heading-s',
1843
+ _outerWrapper: (children)=>/*#__PURE__*/ jsxs("div", {
1844
+ className: "flex items-center justify-between gap-x-2",
1845
+ children: [
1846
+ children,
1847
+ /*#__PURE__*/ jsx(Button, {
1848
+ slot: "close" // RAC Dialog suppors one close button out of the box, so we utilize that here. For other close buttons we use ButtonContext
1849
+ ,
1850
+ variant: "tertiary",
1851
+ className: "!px-2.5 data-focus-visible:outline-focus-inset",
1852
+ "aria-label": translations$1.close[locale],
1853
+ children: /*#__PURE__*/ jsx(Close, {})
1854
+ })
1855
+ ]
1856
+ })
1857
+ }
1858
+ }
1859
+ }
1860
+ ],
1861
+ [
1862
+ ButtonContext,
1863
+ {
1864
+ // This is necessary to support multiple close buttons
1865
+ slots: {
1866
+ // We need to define default slot in order to also support non-slotted buttons (i.e. buttons without slot prop)
1867
+ [DEFAULT_SLOT]: {
1868
+ className: 'w-fit'
1869
+ },
1870
+ close: {
1871
+ onPress: close,
1872
+ className: 'w-fit'
1873
+ }
1874
+ }
1875
+ }
1876
+ ]
1877
+ ],
1878
+ children: children
1879
+ })
1880
+ })
1881
+ });
1882
+ };
1883
+
1884
+ const tagVariants = cva({
1885
+ base: [
1886
+ 'relative flex cursor-pointer items-center gap-1 rounded-lg px-2 py-1 font-medium text-sm transition-colors duration-200',
1887
+ //Focus
1888
+ 'focus-visible:outline-focus-offset',
1889
+ //Border
1890
+ 'border-2 border-blue-dark',
1891
+ //Backgrounds
1892
+ 'data-hovered:!bg-sky bg-white text-black aria-selected:bg-sky-light data-allows-removing:bg-sky-light',
1893
+ //Icons
1894
+ '[&_svg]:h-4 [&_svg]:w-4'
1895
+ ]
1896
+ });
1897
+ /**
1898
+ * A group component for Tag components that enables selection and organization of options.
1899
+ */ function TagGroup(props) {
1900
+ const { onRemove, selectionMode = 'single', className, children, ...restProps } = props;
1901
+ return /*#__PURE__*/ jsx(TagGroup$1, {
1902
+ ...restProps,
1903
+ className: className,
1904
+ selectionMode: onRemove ? 'none' : selectionMode,
1905
+ onRemove: onRemove,
1906
+ children: children
1907
+ });
1908
+ }
1909
+ /**
1910
+ * A container component for Tag components within a TagGroup.
1911
+ */ function TagList(props) {
1912
+ const { className, children, ...restProps } = props;
1913
+ return /*#__PURE__*/ jsx(TagList$1, {
1914
+ ...restProps,
1915
+ className: cx('flex flex-wrap gap-2', className),
1916
+ children: children
1917
+ });
1918
+ }
1919
+ /**
1920
+ * Interactive tag component for selections, filtering, and categorization.
1921
+ */ function Tag(props) {
1922
+ const { className, children, ...restProps } = props;
1923
+ const textValue = typeof children === 'string' ? children : undefined;
1924
+ return /*#__PURE__*/ jsx(Tag$1, {
1925
+ className: tagVariants({
1926
+ className
1927
+ }),
1928
+ textValue: textValue,
1929
+ ...restProps,
1930
+ children: ({ allowsRemoving })=>allowsRemoving ? /*#__PURE__*/ jsxs(Fragment, {
1931
+ children: [
1932
+ children,
1933
+ /*#__PURE__*/ jsx(Button$1, {
1934
+ className: "cursor-pointer outline-none after:absolute after:top-0 after:right-0 after:bottom-0 after:left-0",
1935
+ slot: "remove",
1936
+ children: /*#__PURE__*/ jsx(Close, {
1937
+ className: "ml-1"
1938
+ })
1939
+ })
1940
+ ]
1941
+ }) : children
1942
+ });
1943
+ }
1944
+
1945
+ export { Accordion, AccordionItem, Alertbox, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, Media, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, TextArea, TextField, Avatar as UNSAFE_Avatar, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, DisclosurePanel as UNSAFE_DisclosurePanel, FileUpload as UNSAFE_FileUpload, Modal as UNSAFE_Modal, Tag as UNSAFE_Tag, TagGroup as UNSAFE_TagGroup, TagList as UNSAFE_TagList, VideoLoop, _useLocale as useLocale };