@obosbbl/grunnmuren-react 2.0.0-canary.4 → 2.0.0-canary.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,17 +1,209 @@
1
- import { Text, CheckboxContext, Checkbox as Checkbox$1, Label as Label$1, CheckboxGroup as CheckboxGroup$1, FieldError, ListBoxItem, ComboBox, Group, Input, Button as Button$1, Popover, ListBox, RadioGroup as RadioGroup$1, Radio as Radio$1, Select as Select$1, SelectValue, TextField as TextField$1, TextArea as TextArea$1 } from 'react-aria-components';
2
- export { Form, I18nProvider } from 'react-aria-components';
1
+ 'use client';
2
+ import { I18nProvider, RouterProvider, useLocale, useContextProps, Provider, Link, Button as Button$1, Text, CheckboxContext, Checkbox as Checkbox$1, Label as Label$1, FieldError, CheckboxGroup as CheckboxGroup$1, ListBoxItem as ListBoxItem$1, 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 } from 'react-aria-components';
3
+ export { Form } from 'react-aria-components';
3
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import { forwardRef, useState, useRef, useId } from 'react';
5
- import { cva, cx, compose } from 'cva';
6
- import { LoadingSpinner, Check, ChevronDown } from '@obosbbl/grunnmuren-icons-react';
7
- import { mergeRefs } from '@react-aria/utils';
8
- import { u as useClientLayoutEffect } from './useClientLayoutEffect-client-2_5nawgR.js';
5
+ import { createContext, forwardRef, Children, useId, useState } from 'react';
6
+ import { cx, cva, compose } from 'cva';
7
+ import { ChevronDown, LoadingSpinner, Check, Close, InfoCircle, CheckCircle, Warning, CloseCircle, ChevronRight, ChevronLeft } from '@obosbbl/grunnmuren-icons-react';
8
+ import { useLayoutEffect } from '@react-aria/utils';
9
+ import { useProgressBar, useDateFormatter } from 'react-aria';
10
+
11
+ function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
12
+ return /*#__PURE__*/ jsx(I18nProvider, {
13
+ locale: locale,
14
+ children: navigate ? /*#__PURE__*/ jsx(RouterProvider, {
15
+ navigate: navigate,
16
+ useHref: useHref,
17
+ children: children
18
+ }) : children
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Returns the locale set in `<GrunnmurenProvider />`
24
+ */ function _useLocale() {
25
+ // a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
26
+ const locale = useLocale();
27
+ return locale.locale;
28
+ }
29
+
30
+ const HeadingContext = /*#__PURE__*/ createContext({});
31
+ const Heading = (props, ref)=>{
32
+ [props, ref] = useContextProps(props, ref, HeadingContext);
33
+ const { children, level, className, _innerWrapper: innerWrapper, ...restProps } = props;
34
+ const Element = `h${level}`;
35
+ return /*#__PURE__*/ jsx(Element, {
36
+ ...restProps,
37
+ className: className,
38
+ "data-slot": "heading",
39
+ children: innerWrapper ? innerWrapper(children) : children
40
+ });
41
+ };
42
+ const ContentContext = /*#__PURE__*/ createContext({});
43
+ const Content = (props, ref)=>{
44
+ [props, ref] = useContextProps(props, ref, ContentContext);
45
+ const { _outerWrapper: outerWrapper, ...restProps } = props;
46
+ const content = /*#__PURE__*/ jsx("div", {
47
+ ...restProps,
48
+ "data-slot": "content"
49
+ });
50
+ return outerWrapper ? outerWrapper(content) : content;
51
+ };
52
+ const Media = (props)=>/*#__PURE__*/ jsx("div", {
53
+ ...props,
54
+ "data-slot": "media"
55
+ });
56
+ const Footer = (props)=>/*#__PURE__*/ jsx("div", {
57
+ ...props,
58
+ "data-slot": "footer"
59
+ });
60
+
61
+ function Accordion(props, ref) {
62
+ const { children, className, ...restProps } = props;
63
+ const childCount = Children.count(children);
64
+ return /*#__PURE__*/ jsx("div", {
65
+ ...restProps,
66
+ ref: ref,
67
+ className: cx('rounded-lg bg-white', className),
68
+ children: Children.map(children, (child, index)=>/*#__PURE__*/ jsxs(Fragment, {
69
+ children: [
70
+ child,
71
+ index < childCount - 1 && // Margin is added to enable support for containers with a background color
72
+ /*#__PURE__*/ jsx("hr", {
73
+ className: "mx-2 border-gray-light",
74
+ "aria-hidden": true
75
+ })
76
+ ]
77
+ }))
78
+ });
79
+ }
80
+ function AccordionItem(props, ref) {
81
+ const { className, children, defaultOpen = false, isOpen: controlledIsOpen, onOpenChange, ...restProps } = props;
82
+ const contentId = useId();
83
+ const buttonId = useId();
84
+ const isControlled = controlledIsOpen != null;
85
+ // This component has internal state that controls whether it is open or not,
86
+ // regardless if we are controlled or uncontrolled.
87
+ // If we are controlled, we use a layout effect to sync the controlled state
88
+ // with the internal state.
89
+ //
90
+ const [isOpen, setIsOpen] = useState(// If we are controlled, use that open state, otherwise use the uncontrolled
91
+ isControlled ? controlledIsOpen : defaultOpen);
92
+ useLayoutEffect(()=>{
93
+ if (isControlled) {
94
+ setIsOpen(controlledIsOpen);
95
+ }
96
+ }, [
97
+ controlledIsOpen,
98
+ isControlled
99
+ ]);
100
+ const handleOpenChange = ()=>{
101
+ const newOpenState = !isOpen;
102
+ if (!isControlled) {
103
+ setIsOpen(newOpenState);
104
+ }
105
+ // Always call the change handler, even if we're uncontrolled.
106
+ // Easier to add stuff such as tracking etc.
107
+ if (onOpenChange) {
108
+ onOpenChange(newOpenState);
109
+ }
110
+ };
111
+ return /*#__PURE__*/ jsx("div", {
112
+ ...restProps,
113
+ className: cx('relative px-2', className),
114
+ ref: ref,
115
+ "data-open": isOpen,
116
+ children: /*#__PURE__*/ jsx(Provider, {
117
+ values: [
118
+ [
119
+ HeadingContext,
120
+ {
121
+ // Negative margin to strech the button to the entire with of the accordion (to support containers with a background color)
122
+ className: 'font-semibold leading-7 -mx-2 text-base',
123
+ // Supply a default level here to make this typecheck ok. Will be overwritten with the consumers set heading level anyways
124
+ level: 3,
125
+ _innerWrapper: (children)=>/*#__PURE__*/ jsxs("button", {
126
+ "aria-controls": contentId,
127
+ "aria-expanded": isOpen,
128
+ // 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
129
+ className: "flex min-h-[44px] w-full 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",
130
+ id: buttonId,
131
+ onClick: handleOpenChange,
132
+ children: [
133
+ children,
134
+ /*#__PURE__*/ jsx(ChevronDown, {
135
+ className: cx('flex-none transition-transform duration-300 motion-reduce:transition-none', isOpen && 'rotate-180')
136
+ })
137
+ ]
138
+ })
139
+ }
140
+ ],
141
+ [
142
+ ContentContext,
143
+ {
144
+ className: // Uses pseudo element for vertical padding, since that doesn't affect the height when the accordion is closed
145
+ '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',
146
+ role: 'region',
147
+ // @ts-expect-error TODO: remove this expect-error when we're on React 19 https://github.com/facebook/react/issues/17157#issuecomment-2003750544
148
+ inert: isOpen ? undefined : 'true',
149
+ 'aria-labelledby': buttonId,
150
+ _outerWrapper: (children)=>/*#__PURE__*/ jsx("div", {
151
+ 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] '),
152
+ children: children
153
+ })
154
+ }
155
+ ]
156
+ ],
157
+ children: children
158
+ })
159
+ });
160
+ }
161
+ const _Accordion = /*#__PURE__*/ forwardRef(Accordion);
162
+ const _AccordionItem = /*#__PURE__*/ forwardRef(AccordionItem);
163
+
164
+ const badgeVariants = cva({
165
+ base: [
166
+ 'inline-flex w-fit items-center justify-center gap-1.5 rounded-lg [&_svg]:shrink-0'
167
+ ],
168
+ variants: {
169
+ color: {
170
+ 'gray-dark': 'bg-gray-dark text-white',
171
+ mint: 'bg-mint',
172
+ sky: 'bg-sky',
173
+ white: 'bg-white',
174
+ 'blue-dark': 'bg-blue-dark text-white',
175
+ 'green-dark': 'bg-green-dark text-white'
176
+ },
177
+ size: {
178
+ small: 'description px-2 py-0.5 [&_svg]:h-4 [&_svg]:w-4',
179
+ medium: 'description px-2.5 py-1.5 [&_svg]:h-4 [&_svg]:w-4',
180
+ large: 'paragraph px-3 py-2 [&_svg]:h-5 [&_svg]:w-5'
181
+ }
182
+ },
183
+ defaultVariants: {
184
+ size: 'medium'
185
+ }
186
+ });
187
+ function Badge(props, ref) {
188
+ const { className: _className, color, size, ...restProps } = props;
189
+ const className = badgeVariants({
190
+ className: _className,
191
+ color,
192
+ size
193
+ });
194
+ return /*#__PURE__*/ jsx("span", {
195
+ className: className,
196
+ ...restProps,
197
+ ref: ref
198
+ });
199
+ }
200
+ const _Badge = /*#__PURE__*/ forwardRef(Badge);
9
201
 
10
202
  /**
11
203
  * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
12
204
  */ const buttonVariants = cva({
13
205
  base: [
14
- 'inline-flex min-h-[44px] cursor-pointer items-center justify-center whitespace-nowrap rounded-lg font-medium transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2'
206
+ '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 [&:not([data-focus-visible])]:outline-none'
15
207
  ],
16
208
  variants: {
17
209
  /**
@@ -27,17 +219,21 @@ import { u as useClientLayoutEffect } from './useClientLayoutEffect-client-2_5na
27
219
  * Adjusts the color of the button for usage on different backgrounds.
28
220
  * @default green
29
221
  */ color: {
30
- green: 'focus-visible:ring-black',
31
- mint: 'focus-visible:ring-mint focus-visible:ring-offset-green-dark',
32
- white: 'focus-visible:ring-white focus-visible:ring-offset-blue'
222
+ green: 'data-[focus-visible]:outline-focus',
223
+ mint: 'data-[focus-visible]:outline-focus data-[focus-visible]:outline-mint',
224
+ white: 'data-[focus-visible]:outline-focus data-[focus-visible]:outline-white'
33
225
  },
34
226
  /**
35
227
  * When the button is without text, but with a single icon.
36
228
  * @default false
37
229
  */ isIconOnly: {
38
230
  true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
39
- false: // The of-type classes takes care to add spacing when the button is used with icons
40
- 'px-4 py-2 [&>svg]:first-of-type:mr-2.5 [&>svg]:last-of-type:ml-2.5'
231
+ false: 'gap-2.5 px-4 py-2'
232
+ },
233
+ // Make the content of the button transparent to hide it's content, but keep the button width
234
+ isPending: {
235
+ true: 'relative !text-transparent',
236
+ false: null
41
237
  }
42
238
  },
43
239
  compoundVariants: [
@@ -45,96 +241,103 @@ import { u as useClientLayoutEffect } from './useClientLayoutEffect-client-2_5na
45
241
  color: 'green',
46
242
  variant: 'primary',
47
243
  // Darken bg by 20% on hover. The color is manually crafted
48
- className: 'bg-green text-white hover:bg-green-dark active:bg-[#007352]'
244
+ className: 'bg-green text-white hover:bg-green-dark active:bg-[#007352] [&_[role="progressbar"]]:text-white'
49
245
  },
50
246
  {
51
247
  color: 'green',
52
248
  variant: 'secondary',
53
- className: 'bg-white text-black shadow-green hover:bg-green hover:text-white active:bg-green'
249
+ className: 'text-black shadow-green hover:bg-green hover:text-white active:bg-green [&:hover_[role="progressbar"]]:text-white [&_[role="progressbar"]]:text-black'
250
+ },
251
+ {
252
+ color: 'green',
253
+ variant: 'tertiary',
254
+ className: '[&_[role="progressbar"]]:text-black'
54
255
  },
55
256
  {
56
257
  color: 'mint',
57
258
  variant: 'primary',
58
259
  // Darken bg by 20% on hover. The color is manually crafted
59
- className: 'active:[#9ddac6] bg-mint text-black hover:bg-[#8dd4bd]'
260
+ className: 'active:[#9ddac6] bg-mint text-black hover:bg-[#8dd4bd] [&_[role="progressbar"]]:text-black'
60
261
  },
61
262
  {
62
263
  color: 'mint',
63
264
  variant: 'secondary',
64
- className: 'text-mint shadow-mint hover:bg-mint hover:text-black'
265
+ className: 'text-mint shadow-mint hover:bg-mint hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-mint'
65
266
  },
66
267
  {
67
268
  color: 'mint',
68
269
  variant: 'tertiary',
69
- className: 'text-mint'
270
+ className: 'text-mint [&_[role="progressbar"]]:text-mint'
70
271
  },
71
272
  {
72
273
  color: 'white',
73
274
  variant: 'primary',
74
- className: 'bg-white text-black hover:bg-sky active:bg-sky-light'
275
+ className: 'bg-white text-black hover:bg-sky active:bg-sky-light [&_[role="progressbar"]]:text-black'
75
276
  },
76
277
  {
77
278
  color: 'white',
78
279
  variant: 'secondary',
79
- className: 'text-white shadow-white hover:bg-white hover:text-black'
280
+ className: 'text-white shadow-white hover:bg-white hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-white'
80
281
  },
81
282
  {
82
283
  color: 'white',
83
284
  variant: 'tertiary',
84
- className: 'text-white'
285
+ className: 'text-white [&_[role="progressbar"]]:text-white'
85
286
  }
86
287
  ],
87
288
  defaultVariants: {
88
289
  variant: 'primary',
89
290
  color: 'green',
90
- isIconOnly: false
291
+ isIconOnly: false,
292
+ isPending: false
91
293
  }
92
294
  });
93
- function Button(props, forwardedRef) {
94
- const { children, className, color, isIconOnly, isLoading, variant, style, ...restProps } = props;
95
- const [widthOverride, setWidthOverride] = useState();
96
- const ownRef = useRef(null);
97
- const ref = mergeRefs(ownRef, forwardedRef);
98
- useClientLayoutEffect(()=>{
99
- if (isLoading) {
100
- const requestID = window.requestAnimationFrame(()=>{
101
- setWidthOverride(ownRef.current?.getBoundingClientRect()?.width);
102
- });
103
- return ()=>{
104
- setWidthOverride(undefined);
105
- cancelAnimationFrame(requestID);
106
- };
107
- }
108
- }, [
109
- isLoading,
110
- children
111
- ]);
112
- let Component = 'a';
113
- if (props.href == null) {
114
- // If we don't have a href, it's a button, and we add a fallback type button to prevent the button from accidentally submitting when in a form
115
- Component = 'button';
116
- restProps.type ??= 'button';
295
+ function isLinkProps$1(props) {
296
+ return !!props.href;
297
+ }
298
+ const translations$1 = {
299
+ pending: {
300
+ nb: 'venter',
301
+ sv: 'väntar',
302
+ en: 'pending'
117
303
  }
118
- return(// @ts-expect-error TS doesn't agree here taht restProps is safe to spread, because restProps for anchors aren't type compatible with restProps for buttons, but that should be okay here
119
- /*#__PURE__*/ jsx(Component, {
120
- "aria-busy": isLoading ? true : undefined,
121
- className: buttonVariants({
122
- className,
123
- color,
124
- isIconOnly,
125
- variant
126
- }),
304
+ };
305
+ function Button(props, ref) {
306
+ const { children: _children, color, isIconOnly, isLoading, variant, isPending: _isPending, ...restProps } = props;
307
+ const isPending = _isPending || isLoading;
308
+ const className = buttonVariants({
309
+ className: props.className,
310
+ color,
311
+ isIconOnly,
312
+ variant,
313
+ isPending
314
+ });
315
+ const locale = _useLocale();
316
+ const { progressBarProps } = useProgressBar({
317
+ isIndeterminate: true,
318
+ 'aria-label': translations$1.pending[locale]
319
+ });
320
+ const children = isPending ? /*#__PURE__*/ jsxs(Fragment, {
321
+ children: [
322
+ _children,
323
+ /*#__PURE__*/ jsx(LoadingSpinner, {
324
+ className: "absolute m-auto motion-safe:animate-spin",
325
+ ...progressBarProps
326
+ })
327
+ ]
328
+ }) : _children;
329
+ return isLinkProps$1(restProps) ? /*#__PURE__*/ jsx(Link, {
330
+ ...restProps,
331
+ className: className,
127
332
  ref: ref,
128
- style: {
129
- ...style,
130
- width: widthOverride
131
- },
333
+ children: children
334
+ }) : /*#__PURE__*/ jsx(Button$1, {
132
335
  ...restProps,
133
- children: widthOverride ? // remove margin for icon alignment
134
- /*#__PURE__*/ jsx(LoadingSpinner, {
135
- className: "!m-0 mx-auto animate-spin"
136
- }) : children
137
- }));
336
+ className: className,
337
+ isPending: isPending,
338
+ ref: ref,
339
+ children: children
340
+ });
138
341
  }
139
342
  const _Button = /*#__PURE__*/ forwardRef(Button);
140
343
 
@@ -142,20 +345,27 @@ const formField = cx('group flex flex-col gap-2');
142
345
  const formFieldError = cx('w-fit rounded-sm bg-red-light px-2 py-1 text-sm leading-6 text-red');
143
346
  const input = cva({
144
347
  base: [
145
- 'rounded-md px-3 py-2.5 text-sm font-normal leading-6 placeholder-[#727070] outline-none ring-1 ring-black',
348
+ // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
349
+ 'bg-white',
350
+ // Use box-content to enable auto width based on number of characters (size)
351
+ // Setting min-height to prevent the input from collapsing in Safari
352
+ // Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
353
+ 'box-content min-h-6 py-2.5',
354
+ 'rounded-md text-base font-normal leading-6 placeholder-[#727070] outline-none ring-1 ring-black',
146
355
  // invalid styles
147
- 'group-data-[invalid]:ring-2 group-data-[invalid]:ring-red'
356
+ 'group-data-[invalid]:ring-focus group-data-[invalid]:ring-red',
357
+ // Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
358
+ 'appearance-none'
148
359
  ],
149
360
  variants: {
150
361
  // Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
151
362
  focusModifier: {
152
- focus: 'focus:ring-2 group-data-[invalid]:focus:ring',
153
- visible: 'data-[focus-visible]:ring-2 group-data-[invalid]:data-[focus-visible]:ring'
363
+ focus: 'focus:ring-focus group-data-[invalid]:focus:ring',
364
+ visible: 'data-[focus-visible]:ring-focus group-data-[invalid]:data-[focus-visible]:ring'
154
365
  },
155
366
  isGrouped: {
156
- false: '',
157
- //
158
- true: 'flex-1 !ring-0 first:pl-0 last:pr-0'
367
+ false: 'px-3',
368
+ true: 'flex-1 !ring-0'
159
369
  }
160
370
  },
161
371
  defaultVariants: {
@@ -163,10 +373,14 @@ const input = cva({
163
373
  isGrouped: false
164
374
  }
165
375
  });
166
- const inputGroup = cx('inline-flex items-center overflow-hidden rounded-md px-3 ring-1 ring-black focus-within:ring-2 group-data-[invalid]:ring-2 group-data-[invalid]:ring-red group-data-[invalid]:focus-within:ring');
376
+ const inputGroup = cx([
377
+ 'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
378
+ 'group-data-[invalid]:ring-focus group-data-[invalid]:ring-red group-data-[invalid]:focus-within:ring'
379
+ ]);
167
380
  const dropdown = {
168
- popover: cx('min-w-[--trigger-width] overflow-auto rounded-md border border-black bg-white shadow data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:fade-in data-[exiting]:fade-out'),
169
- listbox: cx('text-sm outline-none'),
381
+ popover: cx('min-w-[--trigger-width] overflow-y-auto rounded-md border border-black bg-white shadow data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:fade-in data-[exiting]:fade-out'),
382
+ // overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
383
+ listbox: cx('max-h-[25rem] overflow-x-hidden text-sm outline-none'),
170
384
  chevronIcon: cx('text-base transition-transform duration-150 group-data-[open]:rotate-180 motion-reduce:transition-none')
171
385
  };
172
386
 
@@ -180,22 +394,13 @@ function ErrorMessage(props) {
180
394
  });
181
395
  }
182
396
 
183
- function Description(props) {
184
- const { className, ...restProps } = props;
185
- return /*#__PURE__*/ jsx(Text, {
186
- ...restProps,
187
- className: cx(className, 'text-sm font-light leading-6'),
188
- slot: "description"
189
- });
190
- }
191
-
192
397
  const defaultClasses$1 = cx([
193
- 'group relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4 py-2 leading-7'
398
+ 'group relative left-0 -mx-2.5 inline-flex max-w-fit cursor-pointer items-start gap-4 p-2.5 leading-7'
194
399
  ]);
195
400
  // Pulling this out into it's own component. Will probably export it in the future
196
401
  // so it can be used in other views, outside of an input of type checkbox, like in table rows.
197
402
  function CheckmarkBox() {
198
- return /*#__PURE__*/ jsx("div", {
403
+ return /*#__PURE__*/ jsx("span", {
199
404
  className: cx([
200
405
  'relative left-0 grid flex-none place-content-center rounded-sm border-2 border-black text-white',
201
406
  // 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.
@@ -206,7 +411,7 @@ function CheckmarkBox() {
206
411
  // selected
207
412
  'group-data-[selected]:!border-green group-data-[selected]:!bg-green',
208
413
  // focus
209
- 'group-data-[focus-visible]:ring-2 group-data-[focus-visible]:ring-black group-data-[focus-visible]:ring-offset-[9px]',
414
+ 'group-data-[focus-visible]:outline-focus-offset',
210
415
  // hovered
211
416
  'group-data-[hovered]:border-green group-data-[hovered]:group-data-[invalid]:border-red group-data-[hovered]:bg-green-lightest group-data-[hovered]:group-data-[invalid]:bg-red-light',
212
417
  // 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
@@ -223,7 +428,7 @@ function Checkbox(props, ref) {
223
428
  const id = useId();
224
429
  const descriptionId = 'desc' + id;
225
430
  const errorMessageId = 'error' + id;
226
- const isInvalid = _isInvalid || errorMessage != null;
431
+ const isInvalid = errorMessage != null || _isInvalid;
227
432
  return /*#__PURE__*/ jsx("div", {
228
433
  children: /*#__PURE__*/ jsxs(CheckboxContext.Provider, {
229
434
  value: {
@@ -237,16 +442,15 @@ function Checkbox(props, ref) {
237
442
  isInvalid: isInvalid,
238
443
  ref: ref,
239
444
  children: [
240
- /*#__PURE__*/ jsx("div", {
241
- className: "absolute -left-2.5 top-0 z-10 h-11 w-11"
242
- }),
243
445
  /*#__PURE__*/ jsx(CheckmarkBox, {}),
244
446
  children
245
447
  ]
246
448
  }),
247
- description && /*#__PURE__*/ jsx(Description, {
248
- className: "block",
449
+ 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 */}
450
+ /*#__PURE__*/ jsx("div", {
249
451
  id: descriptionId,
452
+ slot: "description",
453
+ className: "description block",
250
454
  children: description
251
455
  }),
252
456
  errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
@@ -269,9 +473,31 @@ function Label(props) {
269
473
  });
270
474
  }
271
475
 
476
+ function Description(props) {
477
+ const { className, ...restProps } = props;
478
+ return /*#__PURE__*/ jsx(Text, {
479
+ ...restProps,
480
+ className: cx(className, 'description'),
481
+ slot: "description"
482
+ });
483
+ }
484
+
485
+ /**
486
+ * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
487
+ * In other words, this handles controlled and uncontrolled form errors.
488
+ */ function ErrorMessageOrFieldError({ errorMessage }) {
489
+ return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
490
+ children: errorMessage
491
+ }) : /*#__PURE__*/ jsx(FieldError, {
492
+ className: formFieldError
493
+ });
494
+ }
495
+
272
496
  function CheckboxGroup(props, ref) {
273
497
  const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
274
- const isInvalid = _isInvalid || errorMessage != null;
498
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
499
+ // which will override any built in validation
500
+ const isInvalid = errorMessage != null || _isInvalid;
275
501
  return /*#__PURE__*/ jsxs(CheckboxGroup$1, {
276
502
  ...restProps,
277
503
  className: cx(className, 'flex flex-col gap-2'),
@@ -286,28 +512,53 @@ function CheckboxGroup(props, ref) {
286
512
  children: description
287
513
  }),
288
514
  children,
289
- errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
290
- children: errorMessage
515
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
516
+ errorMessage: errorMessage
291
517
  })
292
518
  ]
293
519
  });
294
520
  }
295
521
  const _CheckboxGroup = /*#__PURE__*/ forwardRef(CheckboxGroup);
296
522
 
297
- /**
298
- * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
299
- * In other words, this handles controlled and uncontrolled form errors.
300
- */ function ErrorMessageOrFieldError({ errorMessage }) {
301
- return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
302
- children: errorMessage
303
- }) : /*#__PURE__*/ jsx(FieldError, {
304
- className: formFieldError
523
+ const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
524
+ ...restProps,
525
+ className: cx(dropdown.listbox, className)
526
+ });
527
+ const ListBoxItem = (props)=>{
528
+ let textValue = props.textValue;
529
+ // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
530
+ // Since we use a render function (to handle the selected state) the child is never a string.
531
+ // This condition adds back that behaviour
532
+ if (textValue == null && typeof props.children === 'string') {
533
+ textValue = props.children;
534
+ }
535
+ return /*#__PURE__*/ jsx(ListBoxItem$1, {
536
+ ...props,
537
+ className: cx(props.className, 'flex cursor-pointer px-6 py-3 leading-6 outline-none data-[focused]:bg-sky-lightest'),
538
+ textValue: textValue,
539
+ children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
540
+ children: [
541
+ isSelected && /*#__PURE__*/ jsx(Check, {
542
+ className: "-ml-6 text-base"
543
+ }),
544
+ props.children
545
+ ]
546
+ })
547
+ });
548
+ };
549
+
550
+ function InputAddonDivider() {
551
+ return /*#__PURE__*/ jsx("span", {
552
+ className: "block h-6 w-px flex-none bg-black"
305
553
  });
306
554
  }
307
555
 
308
556
  function Combobox(props, ref) {
309
- const { className, children, description, errorMessage, isLoading, label, isInvalid: _isInvalid, ...restProps } = props;
310
- const isInvalid = _isInvalid || errorMessage != null;
557
+ const { className, children, description, errorMessage, isLoading, isPending: _isPending, label, isInvalid: _isInvalid, ...restProps } = props;
558
+ const isPending = _isPending || isLoading;
559
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
560
+ // which will override any built in validation
561
+ const isInvalid = errorMessage != null || _isInvalid;
311
562
  return /*#__PURE__*/ jsxs(ComboBox, {
312
563
  ...restProps,
313
564
  className: cx(className, formField),
@@ -329,7 +580,7 @@ function Combobox(props, ref) {
329
580
  ref: ref
330
581
  }),
331
582
  /*#__PURE__*/ jsx(Button$1, {
332
- children: isLoading ? /*#__PURE__*/ jsx(LoadingSpinner, {
583
+ children: isPending ? /*#__PURE__*/ jsx(LoadingSpinner, {
333
584
  className: "animate-spin"
334
585
  }) : /*#__PURE__*/ jsx(ChevronDown, {
335
586
  className: dropdown.chevronIcon
@@ -355,33 +606,13 @@ function Combobox(props, ref) {
355
606
  ]
356
607
  });
357
608
  }
358
- const ComboboxItem = (props)=>{
359
- let textValue = props.textValue;
360
- // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
361
- // Since we use a render function (to handle the selected state) the child is never a string.
362
- // This condition adds back that behaviour
363
- if (textValue == null && typeof props.children === 'string') {
364
- textValue = props.children;
365
- }
366
- return /*#__PURE__*/ jsx(ListBoxItem, {
367
- ...props,
368
- className: cx(props.className, 'flex cursor-default px-6 py-2 leading-6 outline-none data-[focused]:bg-sky-lightest'),
369
- textValue: textValue,
370
- children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
371
- children: [
372
- isSelected && /*#__PURE__*/ jsx(Check, {
373
- className: "-ml-6 text-base"
374
- }),
375
- props.children
376
- ]
377
- })
378
- });
379
- };
380
609
  const _Combobox = /*#__PURE__*/ forwardRef(Combobox);
381
610
 
382
611
  function RadioGroup(props, ref) {
383
612
  const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
384
- const isInvalid = _isInvalid || errorMessage != null;
613
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
614
+ // which will override any built in validation
615
+ const isInvalid = errorMessage != null || _isInvalid;
385
616
  return /*#__PURE__*/ jsxs(RadioGroup$1, {
386
617
  ...restProps,
387
618
  className: cx(className, 'flex flex-col gap-2'),
@@ -396,8 +627,8 @@ function RadioGroup(props, ref) {
396
627
  children: description
397
628
  }),
398
629
  children,
399
- errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
400
- children: errorMessage
630
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
631
+ errorMessage: errorMessage
401
632
  })
402
633
  ]
403
634
  });
@@ -405,7 +636,7 @@ function RadioGroup(props, ref) {
405
636
  const _RadioGroup = /*#__PURE__*/ forwardRef(RadioGroup);
406
637
 
407
638
  const defaultClasses = cx([
408
- 'relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2 leading-7',
639
+ 'relative -ml-2.5 inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
409
640
  // the radio button itself
410
641
  'before:flex-none before:rounded-full before:border-2 before:border-black',
411
642
  // 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.
@@ -418,38 +649,35 @@ const defaultClasses = cx([
418
649
  // hover
419
650
  'data-[hovered]:before:border-green data-[hovered]:before:bg-green-lightest data-[hovered]:data-[invalid]:before:bg-red-light',
420
651
  // focus
421
- 'data-[focus-visible]:before:ring data-[focus-visible]:before:ring-black data-[focus-visible]:before:ring-offset-[9px]',
652
+ 'data-[focus-visible]:before:ring-focus-offset',
422
653
  // 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
423
654
  // so we use an inner outline to artifically pad the border
424
655
  'data-[invalid]:before:outline-solid data-[invalid]:before:border-red data-[invalid]:data-[selected]:before:!bg-red data-[invalid]:before:outline data-[invalid]:before:outline-[3px] data-[invalid]:before:outline-offset-[-3px] data-[invalid]:before:outline-red'
425
656
  ]);
426
657
  function Radio(props, ref) {
427
658
  const { children, className, description, ...restProps } = props;
428
- return /*#__PURE__*/ jsxs(Radio$1, {
659
+ return /*#__PURE__*/ jsx(Radio$1, {
429
660
  ...restProps,
430
661
  className: cx(className, defaultClasses),
431
662
  ref: ref,
432
- children: [
433
- /*#__PURE__*/ jsx("div", {
434
- className: "absolute -left-2.5 top-0 z-10 h-11 w-11 "
435
- }),
436
- /*#__PURE__*/ jsxs("div", {
437
- children: [
438
- children,
439
- description && /*#__PURE__*/ jsx(Description, {
440
- className: "mt-2 block",
441
- children: description
442
- })
443
- ]
444
- })
445
- ]
663
+ children: /*#__PURE__*/ jsxs("div", {
664
+ children: [
665
+ children,
666
+ description && /*#__PURE__*/ jsx(Description, {
667
+ className: "mt-2 block",
668
+ children: description
669
+ })
670
+ ]
671
+ })
446
672
  });
447
673
  }
448
674
  const _Radio = /*#__PURE__*/ forwardRef(Radio);
449
675
 
450
676
  function Select(props, ref) {
451
677
  const { className, children, description, errorMessage, label, isInvalid: _isInvalid, ...restProps } = props;
452
- const isInvalid = _isInvalid || errorMessage != null;
678
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
679
+ // which will override any built in validation
680
+ const isInvalid = errorMessage != null || _isInvalid;
453
681
  return /*#__PURE__*/ jsxs(Select$1, {
454
682
  ...restProps,
455
683
  className: cx(className, formField),
@@ -490,33 +718,11 @@ function Select(props, ref) {
490
718
  ]
491
719
  });
492
720
  }
493
- const SelectItem = (props)=>{
494
- let textValue = props.textValue;
495
- // When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
496
- // Since we use a render function (to handle the selected state) the child is never a string.
497
- // This condition adds back that behaviour
498
- if (textValue == null && typeof props.children === 'string') {
499
- textValue = props.children;
500
- }
501
- return /*#__PURE__*/ jsx(ListBoxItem, {
502
- ...props,
503
- className: cx(props.className, 'flex cursor-default px-6 py-2 leading-6 outline-none data-[focused]:bg-sky-lightest'),
504
- textValue: textValue,
505
- children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
506
- children: [
507
- isSelected && /*#__PURE__*/ jsx(Check, {
508
- className: "-ml-6 text-base"
509
- }),
510
- props.children
511
- ]
512
- })
513
- });
514
- };
515
721
  const _Select = /*#__PURE__*/ forwardRef(Select);
516
722
 
517
723
  function TextArea(props, ref) {
518
724
  const { className, description, errorMessage, label, isInvalid: _isInvalid, rows, ...restProps } = props;
519
- const isInvalid = _isInvalid || errorMessage != null;
725
+ const isInvalid = errorMessage != null || _isInvalid;
520
726
  return /*#__PURE__*/ jsxs(TextField$1, {
521
727
  ...restProps,
522
728
  className: cx(className, formField),
@@ -541,18 +747,24 @@ function TextArea(props, ref) {
541
747
  }
542
748
  const _TextArea = /*#__PURE__*/ forwardRef(TextArea);
543
749
 
544
- const inputWithAlignment = compose(input, cva({
750
+ const inputVariants$1 = compose(input, cva({
545
751
  base: '',
546
752
  variants: {
547
753
  textAlign: {
548
754
  right: 'text-right',
549
755
  left: ''
756
+ },
757
+ autoWidth: {
758
+ true: 'max-w-fit',
759
+ false: ''
550
760
  }
551
761
  }
552
762
  }));
553
763
  function TextField(props, ref) {
554
- const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, ...restProps } = props;
555
- const isInvalid = _isInvalid || errorMessage != null;
764
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ...restProps } = props;
765
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
766
+ // which will override any built in validation
767
+ const isInvalid = errorMessage != null || _isInvalid;
556
768
  return /*#__PURE__*/ jsxs(TextField$1, {
557
769
  ...restProps,
558
770
  className: cx(className, formField),
@@ -565,29 +777,96 @@ function TextField(props, ref) {
565
777
  children: description
566
778
  }),
567
779
  leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
568
- className: inputGroup,
780
+ className: cx(inputGroup, {
781
+ 'w-fit': !!size
782
+ }),
569
783
  children: [
570
784
  leftAddon,
571
- withAddonDivider && leftAddon && /*#__PURE__*/ jsx(Divider, {
572
- className: "ml-3"
573
- }),
785
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
574
786
  /*#__PURE__*/ jsx(Input, {
575
- className: inputWithAlignment({
787
+ className: inputVariants$1({
576
788
  textAlign,
577
- isGrouped: true
789
+ isGrouped: true,
790
+ autoWidth: !!size
578
791
  }),
579
- ref: ref
792
+ ref: ref,
793
+ size: size
580
794
  }),
581
- withAddonDivider && rightAddon && /*#__PURE__*/ jsx(Divider, {
582
- className: "mr-3"
795
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
796
+ rightAddon
797
+ ]
798
+ }) : /*#__PURE__*/ jsx(Input, {
799
+ className: inputVariants$1({
800
+ textAlign,
801
+ autoWidth: !!size
802
+ }),
803
+ ref: ref,
804
+ size: size
805
+ }),
806
+ /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
807
+ errorMessage: errorMessage
808
+ })
809
+ ]
810
+ });
811
+ }
812
+ const _TextField = /*#__PURE__*/ forwardRef(TextField);
813
+
814
+ // This component is based on a copy of ../textfield/TextField, refactoring is TBD: https://github.com/code-obos/grunnmuren/pull/722#issuecomment-1931478786
815
+ const inputVariants = compose(input, cva({
816
+ base: '',
817
+ variants: {
818
+ textAlign: {
819
+ right: 'text-right',
820
+ left: ''
821
+ },
822
+ autoWidth: {
823
+ true: 'max-w-fit',
824
+ false: ''
825
+ }
826
+ }
827
+ }));
828
+ function NumberField(props, ref) {
829
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ...restProps } = props;
830
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
831
+ // which will override any built in validation
832
+ const isInvalid = errorMessage != null || _isInvalid;
833
+ return /*#__PURE__*/ jsxs(NumberField$1, {
834
+ ...restProps,
835
+ className: cx(className, formField),
836
+ isInvalid: isInvalid,
837
+ children: [
838
+ label && /*#__PURE__*/ jsx(Label, {
839
+ children: label
840
+ }),
841
+ description && /*#__PURE__*/ jsx(Description, {
842
+ children: description
843
+ }),
844
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
845
+ className: cx(inputGroup, {
846
+ 'w-fit': !!size
847
+ }),
848
+ children: [
849
+ leftAddon,
850
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
851
+ /*#__PURE__*/ jsx(Input, {
852
+ className: inputVariants({
853
+ textAlign,
854
+ isGrouped: true,
855
+ autoWidth: !!size
856
+ }),
857
+ ref: ref,
858
+ size: size
583
859
  }),
860
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
584
861
  rightAddon
585
862
  ]
586
863
  }) : /*#__PURE__*/ jsx(Input, {
587
- className: inputWithAlignment({
588
- textAlign
864
+ className: inputVariants({
865
+ textAlign,
866
+ autoWidth: !!size
589
867
  }),
590
- ref: ref
868
+ ref: ref,
869
+ size: size
591
870
  }),
592
871
  /*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
593
872
  errorMessage: errorMessage
@@ -595,11 +874,326 @@ function TextField(props, ref) {
595
874
  ]
596
875
  });
597
876
  }
598
- function Divider({ className }) {
599
- return /*#__PURE__*/ jsx("span", {
600
- className: cx(className, 'block h-6 w-px flex-none bg-black')
877
+ const _NumberField = /*#__PURE__*/ forwardRef(NumberField);
878
+
879
+ // TODO: add new icons
880
+ const iconMap = {
881
+ info: InfoCircle,
882
+ success: CheckCircle,
883
+ warning: Warning,
884
+ danger: CloseCircle
885
+ };
886
+ const alertVariants = cva({
887
+ base: [
888
+ 'grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-md border-2 px-3 py-2',
889
+ // Heading styles:
890
+ '[&_[data-slot="heading"]]:text-base [&_[data-slot="heading"]]:font-medium [&_[data-slot="heading"]]:leading-7',
891
+ // Content styles:
892
+ '[&:has([data-slot="heading"])_[data-slot="content"]]:col-span-full [&_[data-slot="content"]]:text-sm [&_[data-slot="content"]]:leading-6',
893
+ // Footer styles:
894
+ '[&_[data-slot="footer"]]:col-span-full [&_[data-slot="footer"]]:text-xs [&_[data-slot="footer"]]:font-light [&_[data-slot="footer"]]:leading-6'
895
+ ],
896
+ variants: {
897
+ /**
898
+ * The variant of the alert
899
+ * @default info
900
+ */ variant: {
901
+ info: 'border-[#1A7FA7] bg-sky-light',
902
+ success: 'border-[#0F9B6E] bg-mint-light',
903
+ warning: 'border-[#C57C13] bg-[#FFF2DE]',
904
+ danger: 'border-[#C0385D] bg-red-light'
905
+ }
906
+ },
907
+ defaultVariants: {
908
+ variant: 'info'
909
+ }
910
+ });
911
+ const translations = {
912
+ close: {
913
+ nb: 'Lukk',
914
+ sv: 'Stäng',
915
+ en: 'Close'
916
+ },
917
+ showMore: {
918
+ nb: 'Les mer',
919
+ sv: 'Läs mer',
920
+ en: 'Read more'
921
+ },
922
+ showLess: {
923
+ nb: 'Vis mindre',
924
+ sv: 'Dölj',
925
+ en: 'Show less'
926
+ }
927
+ };
928
+ const Alertbox = ({ children, role, className, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
929
+ const Icon = iconMap[variant];
930
+ const locale = _useLocale();
931
+ const id = useId();
932
+ const [isExpanded, setIsExpanded] = useState(false);
933
+ const isCollapsed = isExpandable && !isExpanded;
934
+ const [isUncontrolledVisible, setIsUncontrolledVisible] = useState(true);
935
+ const isVisible = isDismissed !== undefined ? !isDismissed : isUncontrolledVisible;
936
+ if (!isVisible) return;
937
+ const close = ()=>{
938
+ setIsUncontrolledVisible(false);
939
+ if (onDismiss) onDismiss();
940
+ };
941
+ const isInDevMode = process.env.NODE_ENV !== 'production';
942
+ if (isInDevMode && onDismiss && !isDismissable) {
943
+ console.warn('Passing an `onDismiss` callback without setting the `isDismissable` prop to `true` will not have any effect.');
944
+ }
945
+ if (isInDevMode && !children) {
946
+ console.error('`No children was passed to the <AlertBox/>` component.');
947
+ return;
948
+ }
949
+ const [firstChild, ...restChildren] = Children.toArray(children);
950
+ return /*#__PURE__*/ jsxs("div", {
951
+ className: alertVariants({
952
+ className,
953
+ variant
954
+ }),
955
+ // The role prop is required to force consumers to consider and choose the appropriate alertbox role.
956
+ // role="none" will not have any effect on a div, so it can be omitted.
957
+ role: role === 'none' ? undefined : role,
958
+ children: [
959
+ /*#__PURE__*/ jsx(Icon, {}),
960
+ firstChild,
961
+ isDismissable && /*#__PURE__*/ jsx("button", {
962
+ className: cx('-m-2 grid h-11 w-11 place-items-center rounded-xl', 'focus-visible:outline-focus focus-visible:-outline-offset-8'),
963
+ onClick: close,
964
+ "aria-label": translations.close[locale],
965
+ children: /*#__PURE__*/ jsx(Close, {})
966
+ }),
967
+ isExpandable && /*#__PURE__*/ jsxs("button", {
968
+ className: cx('relative col-span-full row-start-2 -my-3 inline-flex max-w-fit cursor-pointer items-center gap-1 py-3 text-sm leading-6', // Focus styles:
969
+ 'outline-none after:absolute after:bottom-3 after:left-0 after:right-0 after:h-0', 'focus-visible:after:h-[2px] focus-visible:after:bg-black'),
970
+ onClick: ()=>setIsExpanded((prevState)=>!prevState),
971
+ "aria-expanded": isExpanded,
972
+ "aria-controls": id,
973
+ children: [
974
+ isExpanded ? translations.showLess[locale] : translations.showMore[locale],
975
+ /*#__PURE__*/ jsx(ChevronDown, {
976
+ className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
977
+ })
978
+ ]
979
+ }),
980
+ restChildren?.length > 0 && /*#__PURE__*/ jsx("div", {
981
+ className: cx('col-span-full grid gap-y-4', isCollapsed && '[&>*:not([data-slot="footer"])]:hidden'),
982
+ id: id,
983
+ children: restChildren
984
+ })
985
+ ]
986
+ });
987
+ };
988
+
989
+ function Breadcrumbs(props, ref) {
990
+ const { className, children, ...restProps } = props;
991
+ return /*#__PURE__*/ jsx(Breadcrumbs$1, {
992
+ ...restProps,
993
+ className: cx(className, 'flex flex-wrap text-sm leading-6'),
994
+ ref: ref,
995
+ children: children
601
996
  });
602
997
  }
603
- const _TextField = /*#__PURE__*/ forwardRef(TextField);
998
+ const _Breadcrumbs = /*#__PURE__*/ forwardRef(Breadcrumbs);
999
+
1000
+ function Breadcrumb(props, ref) {
1001
+ const { className, children, href, ...restProps } = props;
1002
+ return /*#__PURE__*/ jsxs(Breadcrumb$1, {
1003
+ className: cx(className, 'group flex items-center'),
1004
+ ...restProps,
1005
+ ref: ref,
1006
+ children: [
1007
+ href ? /*#__PURE__*/ jsx(Link, {
1008
+ href: href,
1009
+ // use outline instead of ring for focus marker that can be offset without creating a white background between the focus marker and the element content
1010
+ className: "rounded-sm data-[focus-visible]:outline-focus group-last:no-underline [&:not([data-focus-visible])]:outline-none",
1011
+ children: children
1012
+ }) : children,
1013
+ /*#__PURE__*/ jsx(ChevronRight, {
1014
+ className: "px-1 group-last:hidden"
1015
+ })
1016
+ ]
1017
+ });
1018
+ }
1019
+ const _Breadcrumb = /*#__PURE__*/ forwardRef(Breadcrumb);
1020
+
1021
+ function isLinkProps(props) {
1022
+ return !!props.href;
1023
+ }
1024
+ function Backlink(props, ref) {
1025
+ const { className, children, withUnderline, ...restProps } = props;
1026
+ const Component = isLinkProps(props) ? Link : Button$1;
1027
+ return /*#__PURE__*/ jsxs(Component, {
1028
+ className: cx(className, 'group flex max-w-fit cursor-pointer items-center gap-3 rounded-md p-2.5 no-underline data-[focus-visible]:outline-focus [&:not([data-focus-visible])]:outline-none'),
1029
+ ...restProps,
1030
+ // @ts-expect-error ignore the type of the ref here
1031
+ ref: ref,
1032
+ children: [
1033
+ /*#__PURE__*/ jsx(ChevronLeft, {
1034
+ className: cx('-ml-[0.5em] flex-shrink-0 transition-transform duration-300 group-hover:-translate-x-1')
1035
+ }),
1036
+ /*#__PURE__*/ jsx("span", {
1037
+ children: /*#__PURE__*/ jsx("span", {
1038
+ className: cx('border-b-[1px] border-t-[1px] border-transparent transition-colors duration-300', withUnderline ? 'border-b-black' : 'group-hover:border-b-black'),
1039
+ children: children
1040
+ })
1041
+ })
1042
+ ]
1043
+ });
1044
+ }
1045
+ const _Backlink = /*#__PURE__*/ forwardRef(Backlink);
1046
+
1047
+ const cardVariants = cva({
1048
+ base: [
1049
+ 'group/card',
1050
+ 'rounded-2xl border p-3',
1051
+ 'grid auto-rows-max gap-y-4',
1052
+ 'relative',
1053
+ // **** Heading ****
1054
+ '[&_[data-slot="heading"]]:inline',
1055
+ '[&_[data-slot="heading"]]:heading-s',
1056
+ '[&_[data-slot="heading"]]:leading-6',
1057
+ '[&_[data-slot="heading"]]:w-fit',
1058
+ '[&_[data-slot="heading"]]:text-pretty',
1059
+ // **** Content ****
1060
+ '[&_[data-slot="content"]]:grid [&_[data-slot="content"]]:auto-rows-max [&_[data-slot="content"]]:gap-y-4',
1061
+ // **** Media ****
1062
+ '[&_[data-slot="media"]]:overflow-hidden',
1063
+ '[&_[data-slot="media"]]:rounded-t-2xl',
1064
+ // 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)
1065
+ '[&_[data-slot="media"]]:mx-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))] [&_[data-slot="media"]]:mt-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
1066
+ // Sets the aspect ratio of the media content (width: 100% is necessary to make aspect ratio work in FF)
1067
+ '[&_[data-slot="media"]>*]:aspect-[3/2] [&_[data-slot="media"]>*]:w-full [&_[data-slot="media"]_img]:object-cover',
1068
+ // 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.
1069
+ '[&_[data-slot="media"]>*]:duration-300 [&_[data-slot="media"]>*]:ease-in-out [&_[data-slot="media"]>*]:motion-safe:transition-transform',
1070
+ // **** Card link ****
1071
+ // **** Hover ****
1072
+ // 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)
1073
+ '[&:has([data-slot="card-link"]_a:hover)_[data-slot="media"]>*]:scale-110',
1074
+ // **** Card link in Heading ****
1075
+ '[&:has([data-slot="heading"]_[data-slot="card-link"]:hover)_[data-slot="media"]>*]:scale-110',
1076
+ // Border (bottom/top) is set to transparent to make sure the bottom underline is not visible when the card is hovered
1077
+ // Border top is set to even out the border bottom used for the underline
1078
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:no-underline',
1079
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:border-y-2',
1080
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:border-y-transparent',
1081
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:transition-colors',
1082
+ '[&_[data-slot="heading"]_[data-slot="card-link"]:hover]:border-b-current',
1083
+ // 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
1084
+ '[&_[data-slot="heading"]_[data-slot="card-link"]]:heading-s [&_[data-slot="heading"]_[data-slot="card-link"]]:text-pretty [&_[data-slot="heading"]_[data-slot="card-link"]]:leading-6',
1085
+ // **** Fail-safe for interactive elements ****
1086
+ // Make interactive elements clickable by themselves, while the rest of the card is clickable as a whole
1087
+ // The card is made clickable by a pseudo-element on the heading that covers the entire card
1088
+ '[&:not(:has([data-slot="card-link"]_a))_a:not([data-slot="card-link"])]:relative [&_button]:relative [&_input]:relative',
1089
+ // 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)
1090
+ '[&_[data-slot="card-link"]_a]:static',
1091
+ // Place other interactive on top of the pseudo-element that makes the entire card clickable
1092
+ // by setting a higher z-index than the pseudo-element (which implicitly z-index 0)
1093
+ '[&_a:not([data-slot="card-link"])]:z-[1] [&_button]:z-[1] [&_input]:z-[1]'
1094
+ ],
1095
+ variants: {
1096
+ /**
1097
+ * The variant of the card
1098
+ * @default subtle
1099
+ */ variant: {
1100
+ subtle: [
1101
+ 'border-transparent',
1102
+ // Media styles:
1103
+ '[&_[data-slot="media"]]:rounded-b-2xl'
1104
+ ],
1105
+ outlined: 'border border-black'
1106
+ }
1107
+ },
1108
+ defaultVariants: {
1109
+ variant: 'subtle'
1110
+ }
1111
+ });
1112
+ const Card = ({ children, className: _className, variant, ...restProps })=>{
1113
+ const className = cardVariants({
1114
+ className: _className,
1115
+ variant
1116
+ });
1117
+ return /*#__PURE__*/ jsx("div", {
1118
+ className: className,
1119
+ ...restProps,
1120
+ children: children
1121
+ });
1122
+ };
1123
+ const cardLinkVariants = cva({
1124
+ base: 'w-fit max-w-full',
1125
+ variants: {
1126
+ withHref: {
1127
+ true: [
1128
+ // **** Clickarea ****
1129
+ 'cursor-pointer',
1130
+ 'after:absolute',
1131
+ 'after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
1132
+ 'after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
1133
+ // **** Focus ****
1134
+ 'focus-visible:outline-none',
1135
+ 'data-[focus-visible]:after:outline-focus',
1136
+ 'data-[focus-visible]:after:outline-offset-2',
1137
+ // **** Hover ****
1138
+ // Links are underlined by default, and the underline is removed on hover.
1139
+ // So we make sure that also happens when the user hovers the clickable area.
1140
+ 'hover:no-underline'
1141
+ ],
1142
+ false: [
1143
+ // **** Clickarea ****
1144
+ '[&_a]:after:cursor-pointer',
1145
+ '[&_a]:after:absolute',
1146
+ '[&_a]:after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
1147
+ '[&_a]:after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
1148
+ // **** Focus ****
1149
+ '[&_a[data-focus-visible]]:outline-none',
1150
+ '[&_a[data-focus-visible]]:after:outline-focus',
1151
+ '[&_a[data-focus-visible]]:after:outline-offset-2',
1152
+ // **** Hover ****
1153
+ // Links are underlined by default, and the underline is removed on hover.
1154
+ // So we make sure that also happens when the user hovers the card.
1155
+ // The group-hover ensures that the hover effect also applies when this component is used as a wrapper around a link.
1156
+ '[&_a]:group-hover/card:no-underline'
1157
+ ]
1158
+ }
1159
+ }
1160
+ });
1161
+ /**
1162
+ * A component that creates a clickable area on a card.
1163
+ * It can be used either as a wrapper around a link or as a standalone link.
1164
+ */ const CardLink = ({ className: _className, href, ...restProps })=>{
1165
+ const className = cardLinkVariants({
1166
+ className: _className,
1167
+ withHref: !!href
1168
+ });
1169
+ return href ? /*#__PURE__*/ jsx(Link, {
1170
+ "data-slot": "card-link",
1171
+ ...restProps,
1172
+ href: href,
1173
+ className: className
1174
+ }) : // We can't utilize that the `Link` component from react-aria-components renders as a span if it doesn't have an href,
1175
+ // because it still renders with role="link" and tabindex="0" which makes it focusable.
1176
+ // So we need to render a div instead.
1177
+ /*#__PURE__*/ jsx("div", {
1178
+ "data-slot": "card-link",
1179
+ className: className,
1180
+ ...restProps
1181
+ });
1182
+ };
1183
+
1184
+ /**
1185
+ * A React component that wraps https://react-spectrum.adobe.com/react-aria/useDateFormatter.html
1186
+ * By default it sets the timeZone to `Europe/Berlin` to prevent the server's timezone from affecting
1187
+ * the localized format
1188
+ */ const DateFormatter = ({ options: _options, value, children: render })=>{
1189
+ const options = {
1190
+ timeZone: 'Europe/Berlin',
1191
+ ..._options
1192
+ };
1193
+ const formatter = useDateFormatter(options);
1194
+ const date = typeof value === 'string' ? new Date(value) : value;
1195
+ const formatted = formatter.format(date);
1196
+ return render ? render(formatted) : formatted;
1197
+ };
604
1198
 
605
- export { _Button as Button, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, _Combobox as Combobox, ComboboxItem, _Radio as Radio, _RadioGroup as RadioGroup, _Select as Select, SelectItem, _TextArea as TextArea, _TextField as TextField };
1199
+ export { _Accordion as Accordion, _AccordionItem as AccordionItem, Alertbox, _Backlink as Backlink, _Badge as Badge, _Breadcrumb as Breadcrumb, _Breadcrumbs as Breadcrumbs, _Button as Button, Card, CardLink, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, _Combobox as Combobox, ListBoxItem as ComboboxItem, Content, ContentContext, DateFormatter, Footer, GrunnmurenProvider, Heading, HeadingContext, Media, _NumberField as NumberField, _Radio as Radio, _RadioGroup as RadioGroup, _Select as Select, ListBoxItem as SelectItem, _TextArea as TextArea, _TextField as TextField, _useLocale as useLocale };