@obosbbl/grunnmuren-react 2.0.0-canary.5 → 2.0.0-canary.50
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 +122 -10
- package/dist/index.d.mts +507 -79
- package/dist/index.mjs +1160 -141
- package/package.json +9 -6
- package/dist/Button-client-wuoyidfi.js +0 -144
package/dist/index.mjs
CHANGED
|
@@ -1,29 +1,383 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export {
|
|
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 } from 'react-aria-components';
|
|
3
|
+
export { Form, DisclosureGroup as UNSAFE_DisclosureGroup } from 'react-aria-components';
|
|
4
4
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
-
import {
|
|
5
|
+
import { ChevronDown, LoadingSpinner, Check, Close, InfoCircle, CheckCircle, Warning, Error, ChevronRight, ChevronLeft, PlayerPause, PlayerPlay } from '@obosbbl/grunnmuren-icons-react';
|
|
6
|
+
import { useLayoutEffect, filterDOMProps } from '@react-aria/utils';
|
|
6
7
|
import { cx, cva, compose } from 'cva';
|
|
7
|
-
import {
|
|
8
|
+
import { createContext, forwardRef, Children, useId, useState, useRef, useEffect, useContext } from 'react';
|
|
9
|
+
import { useProgressBar, useDateFormatter, useDisclosure } from 'react-aria';
|
|
10
|
+
import { useDisclosureState } from 'react-stately';
|
|
11
|
+
|
|
12
|
+
function GrunnmurenProvider({ children, locale = 'nb', navigate, useHref }) {
|
|
13
|
+
return /*#__PURE__*/ jsx(I18nProvider, {
|
|
14
|
+
locale: locale,
|
|
15
|
+
children: navigate ? /*#__PURE__*/ jsx(RouterProvider, {
|
|
16
|
+
navigate: navigate,
|
|
17
|
+
useHref: useHref,
|
|
18
|
+
children: children
|
|
19
|
+
}) : children
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the locale set in `<GrunnmurenProvider />`
|
|
25
|
+
*/ function _useLocale() {
|
|
26
|
+
// a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
|
|
27
|
+
const locale = useLocale();
|
|
28
|
+
return locale.locale;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const HeadingContext = /*#__PURE__*/ createContext({});
|
|
32
|
+
const Heading = (props, ref)=>{
|
|
33
|
+
// biome-ignore lint/style/noParameterAssign: fix when removing refs for React 19
|
|
34
|
+
[props, ref] = useContextProps(props, ref, HeadingContext);
|
|
35
|
+
const { children, level, className, _innerWrapper: innerWrapper, ...restProps } = props;
|
|
36
|
+
const Element = `h${level}`;
|
|
37
|
+
return /*#__PURE__*/ jsx(Element, {
|
|
38
|
+
...restProps,
|
|
39
|
+
className: className,
|
|
40
|
+
"data-slot": "heading",
|
|
41
|
+
children: innerWrapper ? innerWrapper(children) : children
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const ContentContext = /*#__PURE__*/ createContext({});
|
|
45
|
+
const Content = (props, ref)=>{
|
|
46
|
+
// biome-ignore lint/style/noParameterAssign: fix when removing refs for React 19
|
|
47
|
+
[props, ref] = useContextProps(props, ref, ContentContext);
|
|
48
|
+
const { _outerWrapper: outerWrapper, ...restProps } = props;
|
|
49
|
+
const content = /*#__PURE__*/ jsx("div", {
|
|
50
|
+
...restProps,
|
|
51
|
+
"data-slot": "content"
|
|
52
|
+
});
|
|
53
|
+
return outerWrapper ? outerWrapper(content) : content;
|
|
54
|
+
};
|
|
55
|
+
const Media = (props)=>/*#__PURE__*/ jsx("div", {
|
|
56
|
+
...props,
|
|
57
|
+
"data-slot": "media"
|
|
58
|
+
});
|
|
59
|
+
const Caption = ({ className, ...restProps })=>/*#__PURE__*/ jsx("div", {
|
|
60
|
+
...restProps,
|
|
61
|
+
className: cx('description', className),
|
|
62
|
+
"data-slot": "caption"
|
|
63
|
+
});
|
|
64
|
+
const Footer = (props)=>/*#__PURE__*/ jsx("div", {
|
|
65
|
+
...props,
|
|
66
|
+
"data-slot": "footer"
|
|
67
|
+
});
|
|
68
|
+
const _Heading = /*#__PURE__*/ forwardRef(Heading);
|
|
69
|
+
const _Content = /*#__PURE__*/ forwardRef(Content);
|
|
70
|
+
|
|
71
|
+
function Accordion(props, ref) {
|
|
72
|
+
const { children, className, ...restProps } = props;
|
|
73
|
+
const childCount = Children.count(children);
|
|
74
|
+
return /*#__PURE__*/ jsx("div", {
|
|
75
|
+
...restProps,
|
|
76
|
+
ref: ref,
|
|
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, ref) {
|
|
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
|
+
ref: ref,
|
|
125
|
+
"data-open": isOpen,
|
|
126
|
+
children: /*#__PURE__*/ jsx(Provider, {
|
|
127
|
+
values: [
|
|
128
|
+
[
|
|
129
|
+
HeadingContext,
|
|
130
|
+
{
|
|
131
|
+
// Negative margin to strech the button to the entire with of the accordion (to support containers with a background color)
|
|
132
|
+
className: 'font-semibold leading-7 -mx-2 text-base',
|
|
133
|
+
// Supply a default level here to make this typecheck ok. Will be overwritten with the consumers set heading level anyways
|
|
134
|
+
level: 3,
|
|
135
|
+
_innerWrapper: (children)=>/*#__PURE__*/ jsxs("button", {
|
|
136
|
+
"aria-controls": contentId,
|
|
137
|
+
"aria-expanded": isOpen,
|
|
138
|
+
// 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
|
|
139
|
+
className: "flex min-h-[44px] w-full items-center justify-between gap-1.5 rounded-lg px-2 py-3.5 text-left focus-visible:outline-focus focus-visible:outline-focus-inset",
|
|
140
|
+
id: buttonId,
|
|
141
|
+
onClick: handleOpenChange,
|
|
142
|
+
type: "button",
|
|
143
|
+
children: [
|
|
144
|
+
children,
|
|
145
|
+
/*#__PURE__*/ jsx(ChevronDown, {
|
|
146
|
+
className: cx('flex-none transition-transform duration-300 motion-reduce:transition-none', isOpen && 'rotate-180')
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
ContentContext,
|
|
154
|
+
{
|
|
155
|
+
className: // Uses pseudo element for vertical padding, since that doesn't affect the height when the accordion is closed
|
|
156
|
+
'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',
|
|
157
|
+
role: 'region',
|
|
158
|
+
// @ts-expect-error TODO: remove this expect-error when we're on React 19 https://github.com/facebook/react/issues/17157#issuecomment-2003750544
|
|
159
|
+
inert: isOpen ? undefined : 'true',
|
|
160
|
+
'aria-labelledby': buttonId,
|
|
161
|
+
_outerWrapper: (children)=>/*#__PURE__*/ jsx("div", {
|
|
162
|
+
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]'),
|
|
163
|
+
children: children
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
],
|
|
168
|
+
children: children
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const _Accordion = /*#__PURE__*/ forwardRef(Accordion);
|
|
173
|
+
const _AccordionItem = /*#__PURE__*/ forwardRef(AccordionItem);
|
|
174
|
+
|
|
175
|
+
const badgeVariants = cva({
|
|
176
|
+
base: [
|
|
177
|
+
'inline-flex w-fit items-center justify-center gap-1.5 rounded-lg [&_svg]:shrink-0'
|
|
178
|
+
],
|
|
179
|
+
variants: {
|
|
180
|
+
color: {
|
|
181
|
+
'gray-dark': 'bg-gray-dark text-white',
|
|
182
|
+
mint: 'bg-mint',
|
|
183
|
+
sky: 'bg-sky',
|
|
184
|
+
white: 'bg-white',
|
|
185
|
+
'blue-dark': 'bg-blue-dark text-white',
|
|
186
|
+
'green-dark': 'bg-green-dark text-white'
|
|
187
|
+
},
|
|
188
|
+
size: {
|
|
189
|
+
small: 'description px-2 py-0.5 [&_svg]:h-4 [&_svg]:w-4',
|
|
190
|
+
medium: 'description px-2.5 py-1.5 [&_svg]:h-4 [&_svg]:w-4',
|
|
191
|
+
large: 'paragraph px-3 py-2 [&_svg]:h-5 [&_svg]:w-5'
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
defaultVariants: {
|
|
195
|
+
size: 'medium'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
function Badge(props, ref) {
|
|
199
|
+
const { className: _className, color, size, ...restProps } = props;
|
|
200
|
+
const className = badgeVariants({
|
|
201
|
+
className: _className,
|
|
202
|
+
color,
|
|
203
|
+
size
|
|
204
|
+
});
|
|
205
|
+
return /*#__PURE__*/ jsx("span", {
|
|
206
|
+
className: className,
|
|
207
|
+
...restProps,
|
|
208
|
+
ref: ref,
|
|
209
|
+
"data-slot": "badge"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const _Badge = /*#__PURE__*/ forwardRef(Badge);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
|
|
216
|
+
*/ const buttonVariants = cva({
|
|
217
|
+
base: [
|
|
218
|
+
'inline-flex min-h-[44px] cursor-pointer items-center justify-center whitespace-nowrap rounded-lg font-medium transition-colors duration-200 focus-visible:outline-focus-offset [&:not([data-focus-visible])]:outline-none'
|
|
219
|
+
],
|
|
220
|
+
variants: {
|
|
221
|
+
/**
|
|
222
|
+
* The variant of the button
|
|
223
|
+
* @default primary
|
|
224
|
+
*/ variant: {
|
|
225
|
+
primary: 'no-underline',
|
|
226
|
+
// 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
|
|
227
|
+
secondary: 'no-underline shadow-[inset_0_0_0_2px]',
|
|
228
|
+
tertiary: 'underline hover:no-underline'
|
|
229
|
+
},
|
|
230
|
+
/**
|
|
231
|
+
* Adjusts the color of the button for usage on different backgrounds.
|
|
232
|
+
* @default green
|
|
233
|
+
*/ color: {
|
|
234
|
+
green: 'data-[focus-visible]:outline-focus',
|
|
235
|
+
mint: 'data-[focus-visible]:outline-focus data-[focus-visible]:outline-mint',
|
|
236
|
+
white: 'data-[focus-visible]:outline-focus data-[focus-visible]:outline-white'
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* When the button is without text, but with a single icon.
|
|
240
|
+
* @default false
|
|
241
|
+
*/ isIconOnly: {
|
|
242
|
+
true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
|
|
243
|
+
false: 'gap-2.5 px-4 py-2'
|
|
244
|
+
},
|
|
245
|
+
// Make the content of the button transparent to hide it's content, but keep the button width
|
|
246
|
+
isPending: {
|
|
247
|
+
true: '!text-transparent relative',
|
|
248
|
+
false: null
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
compoundVariants: [
|
|
252
|
+
{
|
|
253
|
+
color: 'green',
|
|
254
|
+
variant: 'primary',
|
|
255
|
+
// Darken bg by 20% on hover. The color is manually crafted
|
|
256
|
+
className: 'bg-green text-white hover:bg-green-dark active:bg-[#007352] [&_[role="progressbar"]]:text-white'
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
color: 'green',
|
|
260
|
+
variant: 'secondary',
|
|
261
|
+
className: 'text-black shadow-green hover:bg-green hover:text-white active:bg-green [&:hover_[role="progressbar"]]:text-white [&_[role="progressbar"]]:text-black'
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
color: 'green',
|
|
265
|
+
variant: 'tertiary',
|
|
266
|
+
className: '[&_[role="progressbar"]]:text-black'
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
color: 'mint',
|
|
270
|
+
variant: 'primary',
|
|
271
|
+
// Darken bg by 20% on hover. The color is manually crafted
|
|
272
|
+
className: 'bg-mint text-black hover:bg-[#8dd4bd] active:[#9ddac6] [&_[role="progressbar"]]:text-black'
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
color: 'mint',
|
|
276
|
+
variant: 'secondary',
|
|
277
|
+
className: 'text-mint shadow-mint hover:bg-mint hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-mint'
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
color: 'mint',
|
|
281
|
+
variant: 'tertiary',
|
|
282
|
+
className: 'text-mint [&_[role="progressbar"]]:text-mint'
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
color: 'white',
|
|
286
|
+
variant: 'primary',
|
|
287
|
+
className: 'bg-white text-black hover:bg-sky active:bg-sky-light [&_[role="progressbar"]]:text-black'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
color: 'white',
|
|
291
|
+
variant: 'secondary',
|
|
292
|
+
className: 'text-white shadow-white hover:bg-white hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-white'
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
color: 'white',
|
|
296
|
+
variant: 'tertiary',
|
|
297
|
+
className: 'text-white [&_[role="progressbar"]]:text-white'
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
defaultVariants: {
|
|
301
|
+
variant: 'primary',
|
|
302
|
+
color: 'green',
|
|
303
|
+
isIconOnly: false,
|
|
304
|
+
isPending: false
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
function isLinkProps$1(props) {
|
|
308
|
+
return !!props.href;
|
|
309
|
+
}
|
|
310
|
+
const translations$1 = {
|
|
311
|
+
pending: {
|
|
312
|
+
nb: 'venter',
|
|
313
|
+
sv: 'väntar',
|
|
314
|
+
en: 'pending'
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function Button(props, ref) {
|
|
318
|
+
const { children: _children, color, isIconOnly, isLoading, variant, isPending: _isPending, ...restProps } = props;
|
|
319
|
+
const isPending = _isPending || isLoading;
|
|
320
|
+
const className = buttonVariants({
|
|
321
|
+
className: props.className,
|
|
322
|
+
color,
|
|
323
|
+
isIconOnly,
|
|
324
|
+
variant,
|
|
325
|
+
isPending
|
|
326
|
+
});
|
|
327
|
+
const locale = _useLocale();
|
|
328
|
+
const { progressBarProps } = useProgressBar({
|
|
329
|
+
isIndeterminate: true,
|
|
330
|
+
'aria-label': translations$1.pending[locale]
|
|
331
|
+
});
|
|
332
|
+
const children = isPending ? /*#__PURE__*/ jsxs(Fragment, {
|
|
333
|
+
children: [
|
|
334
|
+
_children,
|
|
335
|
+
/*#__PURE__*/ jsx(LoadingSpinner, {
|
|
336
|
+
className: "absolute m-auto motion-safe:animate-spin",
|
|
337
|
+
...progressBarProps
|
|
338
|
+
})
|
|
339
|
+
]
|
|
340
|
+
}) : _children;
|
|
341
|
+
return isLinkProps$1(restProps) ? /*#__PURE__*/ jsx(Link, {
|
|
342
|
+
...restProps,
|
|
343
|
+
className: className,
|
|
344
|
+
ref: ref,
|
|
345
|
+
children: children
|
|
346
|
+
}) : /*#__PURE__*/ jsx(Button$1, {
|
|
347
|
+
...restProps,
|
|
348
|
+
className: className,
|
|
349
|
+
isPending: isPending,
|
|
350
|
+
ref: ref,
|
|
351
|
+
children: children
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const _Button = /*#__PURE__*/ forwardRef(Button);
|
|
8
355
|
|
|
9
356
|
const formField = cx('group flex flex-col gap-2');
|
|
10
|
-
const formFieldError = cx('w-fit rounded-sm bg-red-light px-2 py-1 text-sm leading-6
|
|
357
|
+
const formFieldError = cx('w-fit rounded-sm bg-red-light px-2 py-1 text-red text-sm leading-6');
|
|
11
358
|
const input = cva({
|
|
12
359
|
base: [
|
|
13
|
-
|
|
360
|
+
// All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
|
|
361
|
+
'bg-white',
|
|
362
|
+
// Use box-content to enable auto width based on number of characters (size)
|
|
363
|
+
// Setting min-height to prevent the input from collapsing in Safari
|
|
364
|
+
// Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
|
|
365
|
+
'box-content min-h-6 py-2.5',
|
|
366
|
+
'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-none ring-1 ring-black',
|
|
14
367
|
// invalid styles
|
|
15
|
-
'group-data-[invalid]:ring-
|
|
368
|
+
'group-data-[invalid]:ring-focus group-data-[invalid]:ring-red',
|
|
369
|
+
// Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
|
|
370
|
+
'appearance-none'
|
|
16
371
|
],
|
|
17
372
|
variants: {
|
|
18
373
|
// Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
|
|
19
374
|
focusModifier: {
|
|
20
|
-
focus: 'focus:ring-
|
|
21
|
-
visible: 'data-[focus-visible]:ring-
|
|
375
|
+
focus: 'focus:ring-focus group-data-[invalid]:focus:ring',
|
|
376
|
+
visible: 'data-[focus-visible]:ring-focus group-data-[invalid]:data-[focus-visible]:ring'
|
|
22
377
|
},
|
|
23
378
|
isGrouped: {
|
|
24
|
-
false: '',
|
|
25
|
-
|
|
26
|
-
true: 'flex-1 !ring-0 first:pl-0 last:pr-0'
|
|
379
|
+
false: 'px-3',
|
|
380
|
+
true: '!ring-0 flex-1'
|
|
27
381
|
}
|
|
28
382
|
},
|
|
29
383
|
defaultVariants: {
|
|
@@ -31,10 +385,14 @@ const input = cva({
|
|
|
31
385
|
isGrouped: false
|
|
32
386
|
}
|
|
33
387
|
});
|
|
34
|
-
const inputGroup = cx(
|
|
388
|
+
const inputGroup = cx([
|
|
389
|
+
'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
|
|
390
|
+
'group-data-[invalid]:ring-focus group-data-[invalid]:ring-red group-data-[invalid]:focus-within:ring'
|
|
391
|
+
]);
|
|
35
392
|
const dropdown = {
|
|
36
|
-
popover: cx('min-w-[--trigger-width] overflow-auto rounded-md border border-black bg-white shadow data-[entering]:animate-in data-[exiting]:animate-out
|
|
37
|
-
|
|
393
|
+
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 data-[entering]:animate-in data-[exiting]:animate-out'),
|
|
394
|
+
// overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
|
|
395
|
+
listbox: cx('max-h-[25rem] overflow-x-hidden text-sm outline-none'),
|
|
38
396
|
chevronIcon: cx('text-base transition-transform duration-150 group-data-[open]:rotate-180 motion-reduce:transition-none')
|
|
39
397
|
};
|
|
40
398
|
|
|
@@ -48,22 +406,13 @@ function ErrorMessage(props) {
|
|
|
48
406
|
});
|
|
49
407
|
}
|
|
50
408
|
|
|
51
|
-
function Description(props) {
|
|
52
|
-
const { className, ...restProps } = props;
|
|
53
|
-
return /*#__PURE__*/ jsx(Text, {
|
|
54
|
-
...restProps,
|
|
55
|
-
className: cx(className, 'text-sm font-light leading-6'),
|
|
56
|
-
slot: "description"
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
409
|
const defaultClasses$1 = cx([
|
|
61
|
-
'group relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4
|
|
410
|
+
'group -mx-2.5 relative left-0 inline-flex max-w-fit cursor-pointer items-start gap-4 p-2.5 leading-7'
|
|
62
411
|
]);
|
|
63
412
|
// Pulling this out into it's own component. Will probably export it in the future
|
|
64
413
|
// so it can be used in other views, outside of an input of type checkbox, like in table rows.
|
|
65
414
|
function CheckmarkBox() {
|
|
66
|
-
return /*#__PURE__*/ jsx("
|
|
415
|
+
return /*#__PURE__*/ jsx("span", {
|
|
67
416
|
className: cx([
|
|
68
417
|
'relative left-0 grid flex-none place-content-center rounded-sm border-2 border-black text-white',
|
|
69
418
|
// 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.
|
|
@@ -74,12 +423,12 @@ function CheckmarkBox() {
|
|
|
74
423
|
// selected
|
|
75
424
|
'group-data-[selected]:!border-green group-data-[selected]:!bg-green',
|
|
76
425
|
// focus
|
|
77
|
-
'group-data-[focus-visible]:
|
|
426
|
+
'group-data-[focus-visible]:outline-focus-offset',
|
|
78
427
|
// hovered
|
|
79
|
-
'group-data-[hovered]:border-
|
|
428
|
+
'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',
|
|
80
429
|
// 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
|
|
81
430
|
// so we use an inner shadow of 1 px instead to pad the actual border
|
|
82
|
-
'group-data-[invalid]:
|
|
431
|
+
'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'
|
|
83
432
|
]),
|
|
84
433
|
children: /*#__PURE__*/ jsx(Check, {
|
|
85
434
|
className: "h-full w-full opacity-0 group-data-[selected]:opacity-100"
|
|
@@ -89,9 +438,9 @@ function CheckmarkBox() {
|
|
|
89
438
|
function Checkbox(props, ref) {
|
|
90
439
|
const { children, className, description, errorMessage, isInvalid: _isInvalid, ...restProps } = props;
|
|
91
440
|
const id = useId();
|
|
92
|
-
const descriptionId =
|
|
93
|
-
const errorMessageId =
|
|
94
|
-
const isInvalid =
|
|
441
|
+
const descriptionId = `desc${id}`;
|
|
442
|
+
const errorMessageId = `error${id}`;
|
|
443
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
95
444
|
return /*#__PURE__*/ jsx("div", {
|
|
96
445
|
children: /*#__PURE__*/ jsxs(CheckboxContext.Provider, {
|
|
97
446
|
value: {
|
|
@@ -105,16 +454,15 @@ function Checkbox(props, ref) {
|
|
|
105
454
|
isInvalid: isInvalid,
|
|
106
455
|
ref: ref,
|
|
107
456
|
children: [
|
|
108
|
-
/*#__PURE__*/ jsx("div", {
|
|
109
|
-
className: "absolute -left-2.5 top-0 z-10 h-11 w-11"
|
|
110
|
-
}),
|
|
111
457
|
/*#__PURE__*/ jsx(CheckmarkBox, {}),
|
|
112
458
|
children
|
|
113
459
|
]
|
|
114
460
|
}),
|
|
115
|
-
description &&
|
|
116
|
-
|
|
461
|
+
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 */}
|
|
462
|
+
/*#__PURE__*/ jsx("div", {
|
|
117
463
|
id: descriptionId,
|
|
464
|
+
slot: "description",
|
|
465
|
+
className: "description block",
|
|
118
466
|
children: description
|
|
119
467
|
}),
|
|
120
468
|
errorMessage && /*#__PURE__*/ jsx(ErrorMessage, {
|
|
@@ -128,6 +476,26 @@ function Checkbox(props, ref) {
|
|
|
128
476
|
}
|
|
129
477
|
const _Checkbox = /*#__PURE__*/ forwardRef(Checkbox);
|
|
130
478
|
|
|
479
|
+
function Description(props) {
|
|
480
|
+
const { className, ...restProps } = props;
|
|
481
|
+
return /*#__PURE__*/ jsx(Text, {
|
|
482
|
+
...restProps,
|
|
483
|
+
className: cx(className, 'description'),
|
|
484
|
+
slot: "description"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
|
|
490
|
+
* In other words, this handles controlled and uncontrolled form errors.
|
|
491
|
+
*/ function ErrorMessageOrFieldError({ errorMessage }) {
|
|
492
|
+
return errorMessage ? /*#__PURE__*/ jsx(ErrorMessage, {
|
|
493
|
+
children: errorMessage
|
|
494
|
+
}) : /*#__PURE__*/ jsx(FieldError, {
|
|
495
|
+
className: formFieldError
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
131
499
|
function Label(props) {
|
|
132
500
|
const { children, className, ...restProps } = props;
|
|
133
501
|
return /*#__PURE__*/ jsx(Label$1, {
|
|
@@ -139,7 +507,9 @@ function Label(props) {
|
|
|
139
507
|
|
|
140
508
|
function CheckboxGroup(props, ref) {
|
|
141
509
|
const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
|
|
142
|
-
|
|
510
|
+
// the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
|
|
511
|
+
// which will override any built in validation
|
|
512
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
143
513
|
return /*#__PURE__*/ jsxs(CheckboxGroup$1, {
|
|
144
514
|
...restProps,
|
|
145
515
|
className: cx(className, 'flex flex-col gap-2'),
|
|
@@ -154,28 +524,67 @@ function CheckboxGroup(props, ref) {
|
|
|
154
524
|
children: description
|
|
155
525
|
}),
|
|
156
526
|
children,
|
|
157
|
-
|
|
158
|
-
|
|
527
|
+
/*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
|
|
528
|
+
errorMessage: errorMessage
|
|
159
529
|
})
|
|
160
530
|
]
|
|
161
531
|
});
|
|
162
532
|
}
|
|
163
533
|
const _CheckboxGroup = /*#__PURE__*/ forwardRef(CheckboxGroup);
|
|
164
534
|
|
|
535
|
+
const ListBox = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBox$1, {
|
|
536
|
+
...restProps,
|
|
537
|
+
className: cx(dropdown.listbox, className)
|
|
538
|
+
});
|
|
539
|
+
const ListBoxItem = (props)=>{
|
|
540
|
+
let textValue = props.textValue;
|
|
541
|
+
// When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
|
|
542
|
+
// Since we use a render function (to handle the selected state) the child is never a string.
|
|
543
|
+
// This condition adds back that behaviour
|
|
544
|
+
if (textValue == null && typeof props.children === 'string') {
|
|
545
|
+
textValue = props.children;
|
|
546
|
+
}
|
|
547
|
+
return /*#__PURE__*/ jsx(ListBoxItem$1, {
|
|
548
|
+
...props,
|
|
549
|
+
className: cx(props.className, 'flex cursor-pointer px-6 py-3 leading-6 outline-none data-[focused]:bg-sky-lightest'),
|
|
550
|
+
textValue: textValue,
|
|
551
|
+
children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
|
|
552
|
+
children: [
|
|
553
|
+
isSelected && /*#__PURE__*/ jsx(Check, {
|
|
554
|
+
className: "-ml-6 text-base"
|
|
555
|
+
}),
|
|
556
|
+
props.children
|
|
557
|
+
]
|
|
558
|
+
})
|
|
559
|
+
});
|
|
560
|
+
};
|
|
165
561
|
/**
|
|
166
|
-
* This component
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
562
|
+
* This component can be used to group items in a listbox
|
|
563
|
+
*/ const ListBoxSection = ({ className, ...restProps })=>/*#__PURE__*/ jsx(ListBoxSection$1, {
|
|
564
|
+
...restProps,
|
|
565
|
+
// The :not(:first-child) selector adds extra spacing to all the options, but not the section (group) headings
|
|
566
|
+
// This way we get the desired extra indent on all options within a group
|
|
567
|
+
className: cx(className, 'pb-1 [&>:not(:first-child)]:pl-10')
|
|
568
|
+
});
|
|
569
|
+
/**
|
|
570
|
+
* This component can be used to label grouped items in a `ListBoxSection` with a heading
|
|
571
|
+
*/ const ListBoxHeader = (props)=>/*#__PURE__*/ jsx(Header, {
|
|
572
|
+
...props,
|
|
573
|
+
className: cx(props.className, 'mx-6 cursor-default py-2 font-medium text-blue-dark leading-6')
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
function InputAddonDivider() {
|
|
577
|
+
return /*#__PURE__*/ jsx("span", {
|
|
578
|
+
className: "block h-6 w-px flex-none bg-black"
|
|
173
579
|
});
|
|
174
580
|
}
|
|
175
581
|
|
|
176
582
|
function Combobox(props, ref) {
|
|
177
|
-
const { className, children, description, errorMessage, isLoading, label, isInvalid: _isInvalid, ...restProps } = props;
|
|
178
|
-
const
|
|
583
|
+
const { className, children, description, errorMessage, isLoading, isPending: _isPending, label, isInvalid: _isInvalid, ...restProps } = props;
|
|
584
|
+
const isPending = _isPending || isLoading;
|
|
585
|
+
// the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
|
|
586
|
+
// which will override any built in validation
|
|
587
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
179
588
|
return /*#__PURE__*/ jsxs(ComboBox, {
|
|
180
589
|
...restProps,
|
|
181
590
|
className: cx(className, formField),
|
|
@@ -196,8 +605,8 @@ function Combobox(props, ref) {
|
|
|
196
605
|
}),
|
|
197
606
|
ref: ref
|
|
198
607
|
}),
|
|
199
|
-
/*#__PURE__*/ jsx(Button, {
|
|
200
|
-
children:
|
|
608
|
+
/*#__PURE__*/ jsx(Button$1, {
|
|
609
|
+
children: isPending ? /*#__PURE__*/ jsx(LoadingSpinner, {
|
|
201
610
|
className: "animate-spin"
|
|
202
611
|
}) : /*#__PURE__*/ jsx(ChevronDown, {
|
|
203
612
|
className: dropdown.chevronIcon
|
|
@@ -223,33 +632,13 @@ function Combobox(props, ref) {
|
|
|
223
632
|
]
|
|
224
633
|
});
|
|
225
634
|
}
|
|
226
|
-
const ComboboxItem = (props)=>{
|
|
227
|
-
let textValue = props.textValue;
|
|
228
|
-
// When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
|
|
229
|
-
// Since we use a render function (to handle the selected state) the child is never a string.
|
|
230
|
-
// This condition adds back that behaviour
|
|
231
|
-
if (textValue == null && typeof props.children === 'string') {
|
|
232
|
-
textValue = props.children;
|
|
233
|
-
}
|
|
234
|
-
return /*#__PURE__*/ jsx(ListBoxItem, {
|
|
235
|
-
...props,
|
|
236
|
-
className: cx(props.className, 'flex cursor-default px-6 py-2 leading-6 outline-none data-[focused]:bg-sky-lightest'),
|
|
237
|
-
textValue: textValue,
|
|
238
|
-
children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
|
|
239
|
-
children: [
|
|
240
|
-
isSelected && /*#__PURE__*/ jsx(Check, {
|
|
241
|
-
className: "-ml-6 text-base"
|
|
242
|
-
}),
|
|
243
|
-
props.children
|
|
244
|
-
]
|
|
245
|
-
})
|
|
246
|
-
});
|
|
247
|
-
};
|
|
248
635
|
const _Combobox = /*#__PURE__*/ forwardRef(Combobox);
|
|
249
636
|
|
|
250
637
|
function RadioGroup(props, ref) {
|
|
251
638
|
const { children, className, description, errorMessage, label, isRequired, isInvalid: _isInvalid, ...restProps } = props;
|
|
252
|
-
|
|
639
|
+
// the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
|
|
640
|
+
// which will override any built in validation
|
|
641
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
253
642
|
return /*#__PURE__*/ jsxs(RadioGroup$1, {
|
|
254
643
|
...restProps,
|
|
255
644
|
className: cx(className, 'flex flex-col gap-2'),
|
|
@@ -264,8 +653,8 @@ function RadioGroup(props, ref) {
|
|
|
264
653
|
children: description
|
|
265
654
|
}),
|
|
266
655
|
children,
|
|
267
|
-
|
|
268
|
-
|
|
656
|
+
/*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
|
|
657
|
+
errorMessage: errorMessage
|
|
269
658
|
})
|
|
270
659
|
]
|
|
271
660
|
});
|
|
@@ -273,7 +662,7 @@ function RadioGroup(props, ref) {
|
|
|
273
662
|
const _RadioGroup = /*#__PURE__*/ forwardRef(RadioGroup);
|
|
274
663
|
|
|
275
664
|
const defaultClasses = cx([
|
|
276
|
-
'relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2 leading-7',
|
|
665
|
+
'-ml-2.5 relative inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
|
|
277
666
|
// the radio button itself
|
|
278
667
|
'before:flex-none before:rounded-full before:border-2 before:border-black',
|
|
279
668
|
// 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.
|
|
@@ -284,40 +673,37 @@ const defaultClasses = cx([
|
|
|
284
673
|
// selected
|
|
285
674
|
'data-[selected]:before:border-black data-[selected]:before:bg-green data-[selected]:before:shadow-[inset_0_0_0_4px_rgb(255,255,255)]',
|
|
286
675
|
// hover
|
|
287
|
-
'data-[hovered]:
|
|
676
|
+
'data-[hovered]:data-[invalid]:before:bg-red-light data-[hovered]:before:border-green data-[hovered]:before:bg-green-lightest',
|
|
288
677
|
// focus
|
|
289
|
-
'data-[focus-visible]:before:ring
|
|
678
|
+
'data-[focus-visible]:before:ring-focus-offset',
|
|
290
679
|
// 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
|
|
291
680
|
// so we use an inner outline to artifically pad the border
|
|
292
|
-
'data-[invalid]:
|
|
681
|
+
'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]'
|
|
293
682
|
]);
|
|
294
683
|
function Radio(props, ref) {
|
|
295
684
|
const { children, className, description, ...restProps } = props;
|
|
296
|
-
return /*#__PURE__*/
|
|
685
|
+
return /*#__PURE__*/ jsx(Radio$1, {
|
|
297
686
|
...restProps,
|
|
298
687
|
className: cx(className, defaultClasses),
|
|
299
688
|
ref: ref,
|
|
300
|
-
children:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
children: description
|
|
310
|
-
})
|
|
311
|
-
]
|
|
312
|
-
})
|
|
313
|
-
]
|
|
689
|
+
children: /*#__PURE__*/ jsxs("div", {
|
|
690
|
+
children: [
|
|
691
|
+
children,
|
|
692
|
+
description && /*#__PURE__*/ jsx(Description, {
|
|
693
|
+
className: "mt-2 block",
|
|
694
|
+
children: description
|
|
695
|
+
})
|
|
696
|
+
]
|
|
697
|
+
})
|
|
314
698
|
});
|
|
315
699
|
}
|
|
316
700
|
const _Radio = /*#__PURE__*/ forwardRef(Radio);
|
|
317
701
|
|
|
318
702
|
function Select(props, ref) {
|
|
319
703
|
const { className, children, description, errorMessage, label, isInvalid: _isInvalid, ...restProps } = props;
|
|
320
|
-
|
|
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;
|
|
321
707
|
return /*#__PURE__*/ jsxs(Select$1, {
|
|
322
708
|
...restProps,
|
|
323
709
|
className: cx(className, formField),
|
|
@@ -329,7 +715,7 @@ function Select(props, ref) {
|
|
|
329
715
|
description && /*#__PURE__*/ jsx(Description, {
|
|
330
716
|
children: description
|
|
331
717
|
}),
|
|
332
|
-
/*#__PURE__*/ jsxs(Button, {
|
|
718
|
+
/*#__PURE__*/ jsxs(Button$1, {
|
|
333
719
|
className: cx(input({
|
|
334
720
|
focusModifier: 'visible'
|
|
335
721
|
}), // How to reuse placeholder text?
|
|
@@ -358,33 +744,11 @@ function Select(props, ref) {
|
|
|
358
744
|
]
|
|
359
745
|
});
|
|
360
746
|
}
|
|
361
|
-
const SelectItem = (props)=>{
|
|
362
|
-
let textValue = props.textValue;
|
|
363
|
-
// When the ListBoxItem child isn't a string we have to set textValue for keyboard completion to work.
|
|
364
|
-
// Since we use a render function (to handle the selected state) the child is never a string.
|
|
365
|
-
// This condition adds back that behaviour
|
|
366
|
-
if (textValue == null && typeof props.children === 'string') {
|
|
367
|
-
textValue = props.children;
|
|
368
|
-
}
|
|
369
|
-
return /*#__PURE__*/ jsx(ListBoxItem, {
|
|
370
|
-
...props,
|
|
371
|
-
className: cx(props.className, 'flex cursor-default px-6 py-2 leading-6 outline-none data-[focused]:bg-sky-lightest'),
|
|
372
|
-
textValue: textValue,
|
|
373
|
-
children: ({ isSelected })=>/*#__PURE__*/ jsxs(Fragment, {
|
|
374
|
-
children: [
|
|
375
|
-
isSelected && /*#__PURE__*/ jsx(Check, {
|
|
376
|
-
className: "-ml-6 text-base"
|
|
377
|
-
}),
|
|
378
|
-
props.children
|
|
379
|
-
]
|
|
380
|
-
})
|
|
381
|
-
});
|
|
382
|
-
};
|
|
383
747
|
const _Select = /*#__PURE__*/ forwardRef(Select);
|
|
384
748
|
|
|
385
749
|
function TextArea(props, ref) {
|
|
386
750
|
const { className, description, errorMessage, label, isInvalid: _isInvalid, rows, ...restProps } = props;
|
|
387
|
-
const isInvalid =
|
|
751
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
388
752
|
return /*#__PURE__*/ jsxs(TextField$1, {
|
|
389
753
|
...restProps,
|
|
390
754
|
className: cx(className, formField),
|
|
@@ -409,18 +773,24 @@ function TextArea(props, ref) {
|
|
|
409
773
|
}
|
|
410
774
|
const _TextArea = /*#__PURE__*/ forwardRef(TextArea);
|
|
411
775
|
|
|
412
|
-
const
|
|
776
|
+
const inputVariants$1 = compose(input, cva({
|
|
413
777
|
base: '',
|
|
414
778
|
variants: {
|
|
415
779
|
textAlign: {
|
|
416
780
|
right: 'text-right',
|
|
417
781
|
left: ''
|
|
782
|
+
},
|
|
783
|
+
autoWidth: {
|
|
784
|
+
true: 'max-w-fit',
|
|
785
|
+
false: ''
|
|
418
786
|
}
|
|
419
787
|
}
|
|
420
788
|
}));
|
|
421
789
|
function TextField(props, ref) {
|
|
422
|
-
const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, ...restProps } = props;
|
|
423
|
-
|
|
790
|
+
const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ...restProps } = props;
|
|
791
|
+
// the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
|
|
792
|
+
// which will override any built in validation
|
|
793
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
424
794
|
return /*#__PURE__*/ jsxs(TextField$1, {
|
|
425
795
|
...restProps,
|
|
426
796
|
className: cx(className, formField),
|
|
@@ -433,29 +803,96 @@ function TextField(props, ref) {
|
|
|
433
803
|
children: description
|
|
434
804
|
}),
|
|
435
805
|
leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
|
|
436
|
-
className: inputGroup,
|
|
806
|
+
className: cx(inputGroup, {
|
|
807
|
+
'w-fit': !!size
|
|
808
|
+
}),
|
|
437
809
|
children: [
|
|
438
810
|
leftAddon,
|
|
439
|
-
withAddonDivider && leftAddon && /*#__PURE__*/ jsx(
|
|
440
|
-
className: "ml-3"
|
|
441
|
-
}),
|
|
811
|
+
withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
|
|
442
812
|
/*#__PURE__*/ jsx(Input, {
|
|
443
|
-
className:
|
|
813
|
+
className: inputVariants$1({
|
|
444
814
|
textAlign,
|
|
445
|
-
isGrouped: true
|
|
815
|
+
isGrouped: true,
|
|
816
|
+
autoWidth: !!size
|
|
446
817
|
}),
|
|
447
|
-
ref: ref
|
|
818
|
+
ref: ref,
|
|
819
|
+
size: size
|
|
448
820
|
}),
|
|
449
|
-
withAddonDivider && rightAddon && /*#__PURE__*/ jsx(
|
|
450
|
-
|
|
821
|
+
withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
|
|
822
|
+
rightAddon
|
|
823
|
+
]
|
|
824
|
+
}) : /*#__PURE__*/ jsx(Input, {
|
|
825
|
+
className: inputVariants$1({
|
|
826
|
+
textAlign,
|
|
827
|
+
autoWidth: !!size
|
|
828
|
+
}),
|
|
829
|
+
ref: ref,
|
|
830
|
+
size: size
|
|
831
|
+
}),
|
|
832
|
+
/*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
|
|
833
|
+
errorMessage: errorMessage
|
|
834
|
+
})
|
|
835
|
+
]
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const _TextField = /*#__PURE__*/ forwardRef(TextField);
|
|
839
|
+
|
|
840
|
+
// This component is based on a copy of ../textfield/TextField, refactoring is TBD: https://github.com/code-obos/grunnmuren/pull/722#issuecomment-1931478786
|
|
841
|
+
const inputVariants = compose(input, cva({
|
|
842
|
+
base: '',
|
|
843
|
+
variants: {
|
|
844
|
+
textAlign: {
|
|
845
|
+
right: 'text-right',
|
|
846
|
+
left: ''
|
|
847
|
+
},
|
|
848
|
+
autoWidth: {
|
|
849
|
+
true: 'max-w-fit',
|
|
850
|
+
false: ''
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}));
|
|
854
|
+
function NumberField(props, ref) {
|
|
855
|
+
const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ...restProps } = props;
|
|
856
|
+
// the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
|
|
857
|
+
// which will override any built in validation
|
|
858
|
+
const isInvalid = errorMessage != null || _isInvalid;
|
|
859
|
+
return /*#__PURE__*/ jsxs(NumberField$1, {
|
|
860
|
+
...restProps,
|
|
861
|
+
className: cx(className, formField),
|
|
862
|
+
isInvalid: isInvalid,
|
|
863
|
+
children: [
|
|
864
|
+
label && /*#__PURE__*/ jsx(Label, {
|
|
865
|
+
children: label
|
|
866
|
+
}),
|
|
867
|
+
description && /*#__PURE__*/ jsx(Description, {
|
|
868
|
+
children: description
|
|
869
|
+
}),
|
|
870
|
+
leftAddon || rightAddon ? /*#__PURE__*/ jsxs(Group, {
|
|
871
|
+
className: cx(inputGroup, {
|
|
872
|
+
'w-fit': !!size
|
|
873
|
+
}),
|
|
874
|
+
children: [
|
|
875
|
+
leftAddon,
|
|
876
|
+
withAddonDivider && leftAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
|
|
877
|
+
/*#__PURE__*/ jsx(Input, {
|
|
878
|
+
className: inputVariants({
|
|
879
|
+
textAlign,
|
|
880
|
+
isGrouped: true,
|
|
881
|
+
autoWidth: !!size
|
|
882
|
+
}),
|
|
883
|
+
ref: ref,
|
|
884
|
+
size: size
|
|
451
885
|
}),
|
|
886
|
+
withAddonDivider && rightAddon && /*#__PURE__*/ jsx(InputAddonDivider, {}),
|
|
452
887
|
rightAddon
|
|
453
888
|
]
|
|
454
889
|
}) : /*#__PURE__*/ jsx(Input, {
|
|
455
|
-
className:
|
|
456
|
-
textAlign
|
|
890
|
+
className: inputVariants({
|
|
891
|
+
textAlign,
|
|
892
|
+
autoWidth: !!size
|
|
457
893
|
}),
|
|
458
|
-
ref: ref
|
|
894
|
+
ref: ref,
|
|
895
|
+
size: size
|
|
459
896
|
}),
|
|
460
897
|
/*#__PURE__*/ jsx(ErrorMessageOrFieldError, {
|
|
461
898
|
errorMessage: errorMessage
|
|
@@ -463,11 +900,593 @@ function TextField(props, ref) {
|
|
|
463
900
|
]
|
|
464
901
|
});
|
|
465
902
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
903
|
+
const _NumberField = /*#__PURE__*/ forwardRef(NumberField);
|
|
904
|
+
|
|
905
|
+
const iconMap = {
|
|
906
|
+
info: InfoCircle,
|
|
907
|
+
success: CheckCircle,
|
|
908
|
+
warning: Warning,
|
|
909
|
+
danger: Error
|
|
910
|
+
};
|
|
911
|
+
const alertVariants = cva({
|
|
912
|
+
base: [
|
|
913
|
+
'grid grid-cols-[auto_1fr_auto] items-center gap-2 rounded-md border-2 px-3 py-2',
|
|
914
|
+
// Heading styles:
|
|
915
|
+
'[&_[data-slot="heading"]]:font-medium [&_[data-slot="heading"]]:text-base [&_[data-slot="heading"]]:leading-7',
|
|
916
|
+
// Content styles:
|
|
917
|
+
'[&:has([data-slot="heading"])_[data-slot="content"]]:col-span-full [&_[data-slot="content"]]:text-sm [&_[data-slot="content"]]:leading-6',
|
|
918
|
+
// Footer styles:
|
|
919
|
+
'[&_[data-slot="footer"]]:col-span-full [&_[data-slot="footer"]]:font-light [&_[data-slot="footer"]]:text-xs [&_[data-slot="footer"]]:leading-6'
|
|
920
|
+
],
|
|
921
|
+
variants: {
|
|
922
|
+
/**
|
|
923
|
+
* The variant of the alert
|
|
924
|
+
* @default info
|
|
925
|
+
*/ variant: {
|
|
926
|
+
info: 'border-[#1A7FA7] bg-sky-light',
|
|
927
|
+
success: 'border-[#0F9B6E] bg-mint-light',
|
|
928
|
+
warning: 'border-[#C57C13] bg-[#FFF2DE]',
|
|
929
|
+
danger: 'border-[#C0385D] bg-red-light'
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
defaultVariants: {
|
|
933
|
+
variant: 'info'
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
const translations = {
|
|
937
|
+
close: {
|
|
938
|
+
nb: 'Lukk',
|
|
939
|
+
sv: 'Stäng',
|
|
940
|
+
en: 'Close'
|
|
941
|
+
},
|
|
942
|
+
showMore: {
|
|
943
|
+
nb: 'Les mer',
|
|
944
|
+
sv: 'Läs mer',
|
|
945
|
+
en: 'Read more'
|
|
946
|
+
},
|
|
947
|
+
showLess: {
|
|
948
|
+
nb: 'Vis mindre',
|
|
949
|
+
sv: 'Dölj',
|
|
950
|
+
en: 'Show less'
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
const Alertbox = ({ children, role, className, icon, variant = 'info', isDismissable = false, isDismissed, onDismiss, isExpandable })=>{
|
|
954
|
+
const Icon = icon ?? iconMap[variant];
|
|
955
|
+
const locale = _useLocale();
|
|
956
|
+
const id = useId();
|
|
957
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
958
|
+
const isCollapsed = isExpandable && !isExpanded;
|
|
959
|
+
const [isUncontrolledVisible, setIsUncontrolledVisible] = useState(true);
|
|
960
|
+
const isVisible = isDismissed !== undefined ? !isDismissed : isUncontrolledVisible;
|
|
961
|
+
if (!isVisible) return;
|
|
962
|
+
const close = ()=>{
|
|
963
|
+
setIsUncontrolledVisible(false);
|
|
964
|
+
if (onDismiss) onDismiss();
|
|
965
|
+
};
|
|
966
|
+
const isInDevMode = process.env.NODE_ENV !== 'production';
|
|
967
|
+
if (isInDevMode && onDismiss && !isDismissable) {
|
|
968
|
+
console.warn('Passing an `onDismiss` callback without setting the `isDismissable` prop to `true` will not have any effect.');
|
|
969
|
+
}
|
|
970
|
+
if (isInDevMode && !children) {
|
|
971
|
+
console.error('`No children was passed to the <AlertBox/>` component.');
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const [firstChild, ...restChildren] = Children.toArray(children);
|
|
975
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
976
|
+
className: alertVariants({
|
|
977
|
+
className,
|
|
978
|
+
variant
|
|
979
|
+
}),
|
|
980
|
+
// The role prop is required to force consumers to consider and choose the appropriate alertbox role.
|
|
981
|
+
// role="none" will not have any effect on a div, so it can be omitted.
|
|
982
|
+
role: role === 'none' ? undefined : role,
|
|
983
|
+
children: [
|
|
984
|
+
/*#__PURE__*/ jsx(Icon, {}),
|
|
985
|
+
firstChild,
|
|
986
|
+
isDismissable && /*#__PURE__*/ jsx("button", {
|
|
987
|
+
className: cx('-m-2 grid h-11 w-11 place-items-center rounded-xl', 'focus-visible:-outline-offset-8 focus-visible:outline-focus'),
|
|
988
|
+
onClick: close,
|
|
989
|
+
"aria-label": translations.close[locale],
|
|
990
|
+
type: "button",
|
|
991
|
+
children: /*#__PURE__*/ jsx(Close, {})
|
|
992
|
+
}),
|
|
993
|
+
isExpandable && /*#__PURE__*/ jsxs("button", {
|
|
994
|
+
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:
|
|
995
|
+
'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'),
|
|
996
|
+
onClick: ()=>setIsExpanded((prevState)=>!prevState),
|
|
997
|
+
"aria-expanded": isExpanded,
|
|
998
|
+
"aria-controls": id,
|
|
999
|
+
type: "button",
|
|
1000
|
+
children: [
|
|
1001
|
+
isExpanded ? translations.showLess[locale] : translations.showMore[locale],
|
|
1002
|
+
/*#__PURE__*/ jsx(ChevronDown, {
|
|
1003
|
+
className: cx('transition-transform duration-150 motion-reduce:transition-none', isExpanded && 'rotate-180')
|
|
1004
|
+
})
|
|
1005
|
+
]
|
|
1006
|
+
}),
|
|
1007
|
+
restChildren?.length > 0 && /*#__PURE__*/ jsx("div", {
|
|
1008
|
+
className: cx('col-span-full grid gap-y-4', isCollapsed && '[&>*:not([data-slot="footer"])]:hidden'),
|
|
1009
|
+
id: id,
|
|
1010
|
+
children: restChildren
|
|
1011
|
+
})
|
|
1012
|
+
]
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
function Breadcrumbs(props, ref) {
|
|
1017
|
+
const { className, children, ...restProps } = props;
|
|
1018
|
+
return /*#__PURE__*/ jsx(Breadcrumbs$1, {
|
|
1019
|
+
...restProps,
|
|
1020
|
+
className: cx(className, 'flex flex-wrap text-sm leading-6'),
|
|
1021
|
+
ref: ref,
|
|
1022
|
+
children: children
|
|
469
1023
|
});
|
|
470
1024
|
}
|
|
471
|
-
const
|
|
1025
|
+
const _Breadcrumbs = /*#__PURE__*/ forwardRef(Breadcrumbs);
|
|
1026
|
+
|
|
1027
|
+
function Breadcrumb(props, ref) {
|
|
1028
|
+
const { className, children, href, ...restProps } = props;
|
|
1029
|
+
return /*#__PURE__*/ jsxs(Breadcrumb$1, {
|
|
1030
|
+
className: cx(className, 'group flex items-center'),
|
|
1031
|
+
...restProps,
|
|
1032
|
+
ref: ref,
|
|
1033
|
+
children: [
|
|
1034
|
+
href ? /*#__PURE__*/ jsx(Link, {
|
|
1035
|
+
href: href,
|
|
1036
|
+
// use outline instead of ring for focus marker that can be offset without creating a white background between the focus marker and the element content
|
|
1037
|
+
className: "rounded-sm group-last:no-underline data-[focus-visible]:outline-focus [&:not([data-focus-visible])]:outline-none",
|
|
1038
|
+
children: children
|
|
1039
|
+
}) : children,
|
|
1040
|
+
/*#__PURE__*/ jsx(ChevronRight, {
|
|
1041
|
+
className: "px-1 group-last:hidden"
|
|
1042
|
+
})
|
|
1043
|
+
]
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const _Breadcrumb = /*#__PURE__*/ forwardRef(Breadcrumb);
|
|
1047
|
+
|
|
1048
|
+
function isLinkProps(props) {
|
|
1049
|
+
return !!props.href;
|
|
1050
|
+
}
|
|
1051
|
+
function Backlink(props, ref) {
|
|
1052
|
+
const { className, children, withUnderline, ...restProps } = props;
|
|
1053
|
+
const Component = isLinkProps(props) ? Link : Button$1;
|
|
1054
|
+
return /*#__PURE__*/ jsxs(Component, {
|
|
1055
|
+
className: cx(className, 'group flex max-w-fit cursor-pointer items-center gap-3 rounded-md p-2.5 no-underline data-[focus-visible]:outline-focus [&:not([data-focus-visible])]:outline-none'),
|
|
1056
|
+
...restProps,
|
|
1057
|
+
// @ts-expect-error ignore the type of the ref here
|
|
1058
|
+
ref: ref,
|
|
1059
|
+
children: [
|
|
1060
|
+
/*#__PURE__*/ jsx(ChevronLeft, {
|
|
1061
|
+
className: cx('-ml-[0.5em] group-hover:-translate-x-1 flex-shrink-0 transition-transform duration-300')
|
|
1062
|
+
}),
|
|
1063
|
+
/*#__PURE__*/ jsx("span", {
|
|
1064
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
1065
|
+
className: cx('border-transparent border-t-[1px] border-b-[1px] transition-colors duration-300', withUnderline ? 'border-b-black' : 'group-hover:border-b-black'),
|
|
1066
|
+
children: children
|
|
1067
|
+
})
|
|
1068
|
+
})
|
|
1069
|
+
]
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
const _Backlink = /*#__PURE__*/ forwardRef(Backlink);
|
|
1073
|
+
|
|
1074
|
+
const cardVariants = cva({
|
|
1075
|
+
base: [
|
|
1076
|
+
'group/card',
|
|
1077
|
+
'rounded-2xl border p-3',
|
|
1078
|
+
'flex gap-y-4',
|
|
1079
|
+
'relative',
|
|
1080
|
+
// **** Heading ****
|
|
1081
|
+
'[&_[data-slot="heading"]]:inline',
|
|
1082
|
+
'[&_[data-slot="heading"]]:heading-s',
|
|
1083
|
+
'[&_[data-slot="heading"]]:leading-6',
|
|
1084
|
+
'[&_[data-slot="heading"]]:w-fit',
|
|
1085
|
+
'[&_[data-slot="heading"]]:text-pretty',
|
|
1086
|
+
'[&_[data-slot="heading"]]:hyphens-auto',
|
|
1087
|
+
'[&_[data-slot="heading"]]:[word-break:break-word]',
|
|
1088
|
+
// **** Content ****
|
|
1089
|
+
'[&_[data-slot="content"]]:flex [&_[data-slot="content"]]:flex-col [&_[data-slot="content"]]:gap-y-4',
|
|
1090
|
+
// **** Media ****
|
|
1091
|
+
'[&_[data-slot="media"]]:overflow-hidden',
|
|
1092
|
+
'[&_[data-slot="media"]]:relative',
|
|
1093
|
+
// 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)
|
|
1094
|
+
'[&_[data-slot="media"]]:mx-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))] [&_[data-slot="media"]]:mt-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
|
|
1095
|
+
// Sets the aspect ratio of the media content (width: 100% is necessary to make aspect ratio work in FF)
|
|
1096
|
+
'[&_[data-slot="media"]>*:not([data-slot="badge"])]:aspect-[3/2] [&_[data-slot="media"]>*:not([data-slot="badge"])]:w-full [&_[data-slot="media"]_img]:object-cover',
|
|
1097
|
+
// 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.
|
|
1098
|
+
'[&_[data-slot="media"]>*]:duration-300 [&_[data-slot="media"]>*]:ease-in-out [&_[data-slot="media"]>*]:motion-safe:transition-transform',
|
|
1099
|
+
// **** Card link ****
|
|
1100
|
+
// **** Hover ****
|
|
1101
|
+
// 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)
|
|
1102
|
+
'[&:has([data-slot="card-link"]_a:hover)_[data-slot="media"]>*:not([data-slot="badge"])]:scale-110',
|
|
1103
|
+
// **** Card link in Heading ****
|
|
1104
|
+
'[&:has([data-slot="heading"]_[data-slot="card-link"]:hover)_[data-slot="media"]>*:not([data-slot="badge"])]:scale-110',
|
|
1105
|
+
// Border (bottom/top) is set to transparent to make sure the bottom underline is not visible when the card is hovered
|
|
1106
|
+
// Border top is set to even out the border bottom used for the underline
|
|
1107
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:no-underline',
|
|
1108
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:border-y-2',
|
|
1109
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:border-y-transparent',
|
|
1110
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:transition-colors',
|
|
1111
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]:hover]:border-b-current',
|
|
1112
|
+
// 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
|
|
1113
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:heading-s',
|
|
1114
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:leading-6',
|
|
1115
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:text-pretty',
|
|
1116
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:hyphens-auto',
|
|
1117
|
+
'[&_[data-slot="heading"]_[data-slot="card-link"]]:[word-break:break-word]',
|
|
1118
|
+
// **** Fail-safe for interactive elements ****
|
|
1119
|
+
// Make interactive elements clickable by themselves, while the rest of the card is clickable as a whole
|
|
1120
|
+
// The card is made clickable by a pseudo-element on the heading that covers the entire card
|
|
1121
|
+
'[&:not(:has([data-slot="card-link"]_a))_a:not([data-slot="card-link"])]:relative [&_button]:relative [&_input]:relative',
|
|
1122
|
+
// 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)
|
|
1123
|
+
'[&_[data-slot="card-link"]_a]:static',
|
|
1124
|
+
// Place other interactive on top of the pseudo-element that makes the entire card clickable
|
|
1125
|
+
// by setting a higher z-index than the pseudo-element (which implicitly z-index 0)
|
|
1126
|
+
'[&_a:not([data-slot="card-link"])]:z-[1] [&_button]:z-[1] [&_input]:z-[1]',
|
|
1127
|
+
// **** Badge ****
|
|
1128
|
+
'[&_[data-slot="media"]_[data-slot="badge"]]:absolute [&_[data-slot="media"]_[data-slot="badge"]]:top-0',
|
|
1129
|
+
// 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)
|
|
1130
|
+
'[&_[data-slot="media"]_[data-slot="badge"]]:z-[1]',
|
|
1131
|
+
// Left aligned - override default corner radius of the badge
|
|
1132
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tl-2xl',
|
|
1133
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-br-2xl',
|
|
1134
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-tr-none',
|
|
1135
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:first-child]:rounded-bl-none',
|
|
1136
|
+
// Right aligned - override default corner radius of the badge
|
|
1137
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tl-none',
|
|
1138
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-br-none',
|
|
1139
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-tr-2xl',
|
|
1140
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:last-child]:rounded-bl-2xl',
|
|
1141
|
+
// ... and position the badge at the right edge of the media content
|
|
1142
|
+
'[&_[data-slot="media"]_[data-slot="badge"]:last-child]:right-0'
|
|
1143
|
+
],
|
|
1144
|
+
variants: {
|
|
1145
|
+
/**
|
|
1146
|
+
* The variant of the card
|
|
1147
|
+
* @default subtle
|
|
1148
|
+
*/ variant: {
|
|
1149
|
+
subtle: [
|
|
1150
|
+
'border-transparent',
|
|
1151
|
+
// **** Media styles ****
|
|
1152
|
+
'[&_[data-slot="media"]]:rounded-2xl'
|
|
1153
|
+
],
|
|
1154
|
+
outlined: 'border border-black'
|
|
1155
|
+
},
|
|
1156
|
+
/**
|
|
1157
|
+
* The layout of the card
|
|
1158
|
+
* @default vertical
|
|
1159
|
+
*/ layout: {
|
|
1160
|
+
vertical: [
|
|
1161
|
+
'flex-col',
|
|
1162
|
+
// **** Media ****
|
|
1163
|
+
'[&_[data-slot="media"]]:rounded-t-2xl'
|
|
1164
|
+
],
|
|
1165
|
+
horizontal: [
|
|
1166
|
+
'gap-x-4',
|
|
1167
|
+
// **** With Media ****
|
|
1168
|
+
'[&:has(>[data-slot="media"]:first-child)]:flex-col',
|
|
1169
|
+
'[&:has(>[data-slot="media"]:last-child)]:flex-col-reverse',
|
|
1170
|
+
'[&:has(>[data-slot="media"])]:md:!flex-row',
|
|
1171
|
+
'[&:has(>[data-slot="media"])>*]:md:basis-1/2',
|
|
1172
|
+
// Position media at the edges of the card
|
|
1173
|
+
'[&_[data-slot="media"]]:md:mb-[calc(theme(space.3)*-1-theme(borderWidth.DEFAULT))]',
|
|
1174
|
+
'[&_[data-slot="media"]:first-child]:md:mr-0',
|
|
1175
|
+
'[&_[data-slot="media"]:last-child]:md:ml-0',
|
|
1176
|
+
// Make sure the card link is clickable when the media is on the right side
|
|
1177
|
+
// This i necessary because the media content is positioned after the card link in the DOM
|
|
1178
|
+
'[&:has(>[data-slot="media"]:last-child)_[data-slot="card-link"]]:z-[1]',
|
|
1179
|
+
// **** Without Media ****
|
|
1180
|
+
'[&:not(:has(>[data-slot="media"]))]:flex-row',
|
|
1181
|
+
// Make the layout responsive: when the Content reaches a minimum width of 18rem, the layout switches to vertical. Also makes sure Content takes up the remaining space available.
|
|
1182
|
+
'[&: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-[18rem]',
|
|
1183
|
+
// Make sure svg's etc. are not shrinkable
|
|
1184
|
+
'[&>:not([data-slot="content"],[data-slot="media"])]:shrink-0'
|
|
1185
|
+
]
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
defaultVariants: {
|
|
1189
|
+
variant: 'subtle',
|
|
1190
|
+
layout: 'vertical'
|
|
1191
|
+
},
|
|
1192
|
+
compoundVariants: [
|
|
1193
|
+
{
|
|
1194
|
+
variant: 'outlined',
|
|
1195
|
+
layout: 'horizontal',
|
|
1196
|
+
className: [
|
|
1197
|
+
// **** Media ****
|
|
1198
|
+
// Some rounded corners are removed when the card is outlined
|
|
1199
|
+
'[&_[data-slot="media"]]:rounded-t-2xl',
|
|
1200
|
+
'[&_[data-slot="media"]:first-child]:md:rounded-tr-none [&_[data-slot="media"]:first-child]:md:rounded-bl-2xl',
|
|
1201
|
+
'[&_[data-slot="media"]:last-child]:md:rounded-tl-none [&_[data-slot="media"]:last-child]:md:rounded-br-2xl',
|
|
1202
|
+
// **** Badge ****
|
|
1203
|
+
// Override default corner radius of the badge to match the media border radius
|
|
1204
|
+
'[&_[data-slot="media"]:first-child_[data-slot="badge"]:last-child]:md:rounded-tr-none',
|
|
1205
|
+
'[&_[data-slot="media"]:last-child_[data-slot="badge"]:first-child]:md:rounded-tl-none'
|
|
1206
|
+
]
|
|
1207
|
+
}
|
|
1208
|
+
]
|
|
1209
|
+
});
|
|
1210
|
+
const Card = ({ children, className: _className, variant, layout, ...restProps })=>{
|
|
1211
|
+
const className = cardVariants({
|
|
1212
|
+
className: _className,
|
|
1213
|
+
variant,
|
|
1214
|
+
layout
|
|
1215
|
+
});
|
|
1216
|
+
return /*#__PURE__*/ jsx("div", {
|
|
1217
|
+
className: className,
|
|
1218
|
+
...restProps,
|
|
1219
|
+
children: children
|
|
1220
|
+
});
|
|
1221
|
+
};
|
|
1222
|
+
const cardLinkVariants = cva({
|
|
1223
|
+
base: 'w-fit max-w-full',
|
|
1224
|
+
variants: {
|
|
1225
|
+
withHref: {
|
|
1226
|
+
true: [
|
|
1227
|
+
// **** Clickarea ****
|
|
1228
|
+
'cursor-pointer',
|
|
1229
|
+
'after:absolute',
|
|
1230
|
+
'after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
|
|
1231
|
+
'after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
|
|
1232
|
+
// **** Focus ****
|
|
1233
|
+
'focus-visible:outline-none',
|
|
1234
|
+
'data-[focus-visible]:after:outline-focus',
|
|
1235
|
+
'data-[focus-visible]:after:outline-offset-2',
|
|
1236
|
+
// **** Hover ****
|
|
1237
|
+
// Links are underlined by default, and the underline is removed on hover.
|
|
1238
|
+
// So we make sure that also happens when the user hovers the clickable area.
|
|
1239
|
+
'hover:no-underline'
|
|
1240
|
+
],
|
|
1241
|
+
false: [
|
|
1242
|
+
// **** Clickarea ****
|
|
1243
|
+
'[&_a]:after:cursor-pointer',
|
|
1244
|
+
'[&_a]:after:absolute',
|
|
1245
|
+
'[&_a]:after:inset-[calc(theme(borderWidth.DEFAULT)*-1)]',
|
|
1246
|
+
'[&_a]:after:rounded-[calc(theme(borderRadius.2xl)-theme(borderWidth.DEFAULT))]',
|
|
1247
|
+
// **** Focus ****
|
|
1248
|
+
'[&_a[data-focus-visible]]:outline-none',
|
|
1249
|
+
'[&_a[data-focus-visible]]:after:outline-focus',
|
|
1250
|
+
'[&_a[data-focus-visible]]:after:outline-offset-2',
|
|
1251
|
+
// **** Hover ****
|
|
1252
|
+
// Links are underlined by default, and the underline is removed on hover.
|
|
1253
|
+
// So we make sure that also happens when the user hovers the card.
|
|
1254
|
+
// The group-hover ensures that the hover effect also applies when this component is used as a wrapper around a link.
|
|
1255
|
+
'[&_a]:group-hover/card:no-underline'
|
|
1256
|
+
]
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
/**
|
|
1261
|
+
* A component that creates a clickable area on a card.
|
|
1262
|
+
* It can be used either as a wrapper around a link or as a standalone link.
|
|
1263
|
+
*/ const CardLink = ({ className: _className, href, ...restProps })=>{
|
|
1264
|
+
const className = cardLinkVariants({
|
|
1265
|
+
className: _className,
|
|
1266
|
+
withHref: !!href
|
|
1267
|
+
});
|
|
1268
|
+
return href ? /*#__PURE__*/ jsx(Link, {
|
|
1269
|
+
"data-slot": "card-link",
|
|
1270
|
+
...restProps,
|
|
1271
|
+
href: href,
|
|
1272
|
+
className: className
|
|
1273
|
+
}) : // We can't utilize that the `Link` component from react-aria-components renders as a span if it doesn't have an href,
|
|
1274
|
+
// because it still renders with role="link" and tabindex="0" which makes it focusable.
|
|
1275
|
+
// So we need to render a div instead.
|
|
1276
|
+
/*#__PURE__*/ jsx("div", {
|
|
1277
|
+
...restProps,
|
|
1278
|
+
"data-slot": "card-link",
|
|
1279
|
+
className: className
|
|
1280
|
+
});
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* A React component that wraps https://react-spectrum.adobe.com/react-aria/useDateFormatter.html
|
|
1285
|
+
* By default it sets the timeZone to `Europe/Berlin` to prevent the server's timezone from affecting
|
|
1286
|
+
* the localized format
|
|
1287
|
+
*/ const DateFormatter = ({ options: _options, value, children: render })=>{
|
|
1288
|
+
const options = {
|
|
1289
|
+
timeZone: 'Europe/Berlin',
|
|
1290
|
+
..._options
|
|
1291
|
+
};
|
|
1292
|
+
const formatter = useDateFormatter(options);
|
|
1293
|
+
const date = typeof value === 'string' ? new Date(value) : value;
|
|
1294
|
+
const formatted = formatter.format(date);
|
|
1295
|
+
return render ? render(formatted) : formatted;
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
const VideoLoop = ({ src, format, alt, className })=>{
|
|
1299
|
+
// Control the video playback state, so that the user can pause and play the video at will, also control the video autoplay
|
|
1300
|
+
const [shouldPlay, setShouldPlay] = useState(false);
|
|
1301
|
+
// 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)
|
|
1302
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
1303
|
+
// We need to check if the user prefers reduced motion, so that we can prevent the video from autoplaying if so
|
|
1304
|
+
const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(null);
|
|
1305
|
+
const videoRef = useRef(null);
|
|
1306
|
+
useEffect(()=>{
|
|
1307
|
+
const { matches: userPrefersReducedMotion } = matchMedia('(prefers-reduced-motion: reduce)');
|
|
1308
|
+
setUserPrefersReducedMotion(userPrefersReducedMotion);
|
|
1309
|
+
// Autoplay the video if the user does not prefer reduced motion
|
|
1310
|
+
setShouldPlay(!userPrefersReducedMotion);
|
|
1311
|
+
}, []);
|
|
1312
|
+
// Follow google's autoplay policy: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
|
1313
|
+
// "Don't assume a video will play, and don't show a pause button when the video is not actually playing."
|
|
1314
|
+
// "You should always look at the Promise returned by the play function to see if it was rejected:"
|
|
1315
|
+
// 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
|
|
1316
|
+
useEffect(()=>{
|
|
1317
|
+
if (!videoRef.current) return;
|
|
1318
|
+
if (shouldPlay) {
|
|
1319
|
+
videoRef.current.play().then(()=>setIsPlaying(true)).catch(()=>setIsPlaying(false));
|
|
1320
|
+
} else {
|
|
1321
|
+
videoRef.current.pause();
|
|
1322
|
+
setIsPlaying(false);
|
|
1323
|
+
}
|
|
1324
|
+
}, [
|
|
1325
|
+
shouldPlay
|
|
1326
|
+
]);
|
|
1327
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
1328
|
+
className: cx(className, 'relative', userPrefersReducedMotion === null && 'opacity-0'),
|
|
1329
|
+
children: [
|
|
1330
|
+
/*#__PURE__*/ jsx("video", {
|
|
1331
|
+
"aria-hidden": true,
|
|
1332
|
+
ref: videoRef,
|
|
1333
|
+
// cursor-pointer is not working on the button below, so we add it here for the same effect
|
|
1334
|
+
className: "h-full w-full cursor-pointer object-cover",
|
|
1335
|
+
playsInline: true,
|
|
1336
|
+
loop: userPrefersReducedMotion === false,
|
|
1337
|
+
autoPlay: userPrefersReducedMotion === false,
|
|
1338
|
+
muted: true,
|
|
1339
|
+
onEnded: (event)=>{
|
|
1340
|
+
if (userPrefersReducedMotion) {
|
|
1341
|
+
// Reset the video to the beginning if the user prefers reduced motion, since the video will not loop
|
|
1342
|
+
event.currentTarget.currentTime = 0;
|
|
1343
|
+
setShouldPlay(false);
|
|
1344
|
+
setIsPlaying(false);
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
children: /*#__PURE__*/ jsx("source", {
|
|
1348
|
+
src: src,
|
|
1349
|
+
type: `video/${format}`
|
|
1350
|
+
})
|
|
1351
|
+
}),
|
|
1352
|
+
userPrefersReducedMotion !== null && /*#__PURE__*/ jsx("button", {
|
|
1353
|
+
"aria-hidden": true,
|
|
1354
|
+
type: "button",
|
|
1355
|
+
onClick: ()=>setShouldPlay((prevState)=>!prevState),
|
|
1356
|
+
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
|
|
1357
|
+
shouldPlay && 'opacity-0', isPlaying && [
|
|
1358
|
+
'transition-opacity duration-200',
|
|
1359
|
+
// Only show the pause button when the video is hovered or focused
|
|
1360
|
+
'focus-visible:opacity-100',
|
|
1361
|
+
'hover:opacity-100'
|
|
1362
|
+
]),
|
|
1363
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
1364
|
+
className: "grid h-12 w-12 place-items-center rounded-full bg-white outline-none",
|
|
1365
|
+
children: isPlaying ? /*#__PURE__*/ jsx(PlayerPause, {}) : /*#__PURE__*/ jsx(PlayerPlay, {})
|
|
1366
|
+
})
|
|
1367
|
+
}),
|
|
1368
|
+
alt && /*#__PURE__*/ jsx("p", {
|
|
1369
|
+
className: "sr-only",
|
|
1370
|
+
children: alt
|
|
1371
|
+
})
|
|
1372
|
+
]
|
|
1373
|
+
});
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
const disclosureButtonVariants = cva({
|
|
1377
|
+
base: [
|
|
1378
|
+
'inline-flex cursor-pointer items-center justify-between rounded-lg outline-none data-[focus-visible]:outline-focus',
|
|
1379
|
+
// Ensure a minimum click area of 44x44px, while making it look like it only has the size of the content
|
|
1380
|
+
'-m-2.5 p-2.5 data-[focus-visible]:outline-offset-[-0.625rem]'
|
|
1381
|
+
],
|
|
1382
|
+
variants: {
|
|
1383
|
+
withChevron: {
|
|
1384
|
+
true: '[&[aria-expanded="true"]_svg]:rotate-180',
|
|
1385
|
+
false: null
|
|
1386
|
+
},
|
|
1387
|
+
/**
|
|
1388
|
+
* When the button is without text, but with a single icon.
|
|
1389
|
+
* @default false
|
|
1390
|
+
*/ isIconOnly: {
|
|
1391
|
+
true: '[&>svg]:h-7 [&>svg]:w-7',
|
|
1392
|
+
false: 'gap-2.5'
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
defaultVariants: {
|
|
1396
|
+
withChevron: false,
|
|
1397
|
+
isIconOnly: false
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
const DisclosureButton = ({ className, withChevron, isIconOnly, children, ref: _ref, ...restProps })=>{
|
|
1401
|
+
const [props, ref] = useContextProps(restProps, _ref, ButtonContext);
|
|
1402
|
+
return /*#__PURE__*/ jsxs(Button$1, {
|
|
1403
|
+
...props,
|
|
1404
|
+
ref: ref,
|
|
1405
|
+
className: disclosureButtonVariants({
|
|
1406
|
+
className,
|
|
1407
|
+
withChevron,
|
|
1408
|
+
isIconOnly
|
|
1409
|
+
}),
|
|
1410
|
+
children: [
|
|
1411
|
+
children,
|
|
1412
|
+
withChevron && /*#__PURE__*/ jsx(ChevronDown, {
|
|
1413
|
+
className: "flex-none transition-transform duration-300 motion-reduce:transition-none"
|
|
1414
|
+
})
|
|
1415
|
+
]
|
|
1416
|
+
});
|
|
1417
|
+
};
|
|
1418
|
+
const Disclosure = ({ ref: _ref, children, ..._props })=>{
|
|
1419
|
+
const [props, ref] = useContextProps(_props, _ref, DisclosureContext);
|
|
1420
|
+
const groupState = useContext(DisclosureGroupStateContext);
|
|
1421
|
+
let { id, ...otherProps } = props;
|
|
1422
|
+
const defaultId = useId();
|
|
1423
|
+
id ||= defaultId;
|
|
1424
|
+
const isExpanded = groupState ? groupState.expandedKeys.has(id) : props.isExpanded;
|
|
1425
|
+
const state = useDisclosureState({
|
|
1426
|
+
...props,
|
|
1427
|
+
isExpanded,
|
|
1428
|
+
onExpandedChange (isExpanded) {
|
|
1429
|
+
if (groupState) {
|
|
1430
|
+
groupState.toggleKey(id);
|
|
1431
|
+
}
|
|
1432
|
+
props.onExpandedChange?.(isExpanded);
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
const isDisabled = props.isDisabled || groupState?.isDisabled || false;
|
|
1436
|
+
const domProps = filterDOMProps(otherProps);
|
|
1437
|
+
const panelRef = useRef(null);
|
|
1438
|
+
const { buttonProps, panelProps } = useDisclosure({
|
|
1439
|
+
...props,
|
|
1440
|
+
isExpanded,
|
|
1441
|
+
isDisabled
|
|
1442
|
+
}, state, panelRef);
|
|
1443
|
+
const { role: _, ...propsWithoutRole } = panelProps;
|
|
1444
|
+
return /*#__PURE__*/ jsx(Provider, {
|
|
1445
|
+
values: [
|
|
1446
|
+
[
|
|
1447
|
+
DisclosureContext,
|
|
1448
|
+
state
|
|
1449
|
+
],
|
|
1450
|
+
[
|
|
1451
|
+
ButtonContext,
|
|
1452
|
+
buttonProps
|
|
1453
|
+
],
|
|
1454
|
+
[
|
|
1455
|
+
DisclosurePanelContext,
|
|
1456
|
+
{
|
|
1457
|
+
...propsWithoutRole,
|
|
1458
|
+
ref: panelRef
|
|
1459
|
+
}
|
|
1460
|
+
]
|
|
1461
|
+
],
|
|
1462
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
1463
|
+
...domProps,
|
|
1464
|
+
className: otherProps.className,
|
|
1465
|
+
ref: ref,
|
|
1466
|
+
"data-expanded": state.isExpanded || undefined,
|
|
1467
|
+
"data-disabled": isDisabled || undefined,
|
|
1468
|
+
children: children
|
|
1469
|
+
})
|
|
1470
|
+
});
|
|
1471
|
+
};
|
|
1472
|
+
const DisclosurePanelContext = /*#__PURE__*/ createContext({});
|
|
1473
|
+
const DisclosurePanel = ({ ref: _ref, ..._props })=>{
|
|
1474
|
+
const disclosureContext = useContext(DisclosureContext);
|
|
1475
|
+
const [props, ref] = useContextProps(_props, _ref, DisclosurePanelContext);
|
|
1476
|
+
const { role: _role = 'group', className, ...restProps } = props;
|
|
1477
|
+
const ariaLabelledby = _props['aria-labelledby'] ?? restProps['aria-labelledby'];
|
|
1478
|
+
const isWithoutRole = _role === 'none';
|
|
1479
|
+
const role = isWithoutRole ? undefined : _role;
|
|
1480
|
+
return /*#__PURE__*/ jsx("div", {
|
|
1481
|
+
className: cx('grid transition-all duration-300 after:relative after:block after:h-0 after:transition-all after:duration-300 motion-reduce:transition-none', disclosureContext?.isExpanded ? 'grid-rows-[1fr] after:h-3.5' : 'grid-rows-[0fr]'),
|
|
1482
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
1483
|
+
...restProps,
|
|
1484
|
+
ref: ref,
|
|
1485
|
+
className: cx(className, 'relative overflow-hidden [content-visibility:visible] before:relative before:block before:h-1.5 after:relative after:block after:h-1.5'),
|
|
1486
|
+
role: role,
|
|
1487
|
+
"aria-labelledby": isWithoutRole ? undefined : ariaLabelledby
|
|
1488
|
+
})
|
|
1489
|
+
});
|
|
1490
|
+
};
|
|
472
1491
|
|
|
473
|
-
export { _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, _Combobox as Combobox, ComboboxItem, _Radio as Radio, _RadioGroup as RadioGroup, _Select as Select, SelectItem, _TextArea as TextArea, _TextField as TextField };
|
|
1492
|
+
export { _Accordion as Accordion, _AccordionItem as AccordionItem, Alertbox, _Backlink as Backlink, _Badge as Badge, _Breadcrumb as Breadcrumb, _Breadcrumbs as Breadcrumbs, _Button as Button, Caption, Card, CardLink, _Checkbox as Checkbox, _CheckboxGroup as CheckboxGroup, _Combobox as Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, _Content as Content, ContentContext, DateFormatter, Footer, GrunnmurenProvider, _Heading as Heading, HeadingContext, Media, _NumberField as NumberField, _Radio as Radio, _RadioGroup as RadioGroup, _Select as Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, _TextArea as TextArea, _TextField as TextField, Disclosure as UNSAFE_Disclosure, DisclosureButton as UNSAFE_DisclosureButton, DisclosurePanel as UNSAFE_DisclosurePanel, VideoLoop, _useLocale as useLocale };
|