@obosbbl/grunnmuren-react 2.0.0 → 2.0.1
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/README.md +211 -0
- package/dist/index.d.mts +596 -0
- package/dist/index.mjs +1945 -0
- package/package.json +23 -21
- package/dist/Button/Button.d.mts +0 -71
- package/dist/Button/Button.d.ts +0 -71
- package/dist/Button/Button.mjs +0 -86
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 };
|