@obosbbl/grunnmuren-react 3.0.16 → 3.1.0

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.
@@ -0,0 +1,488 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
5
+ var reactAriaComponents = require('react-aria-components');
6
+ var grunnmurenIconsReact = require('@obosbbl/grunnmuren-icons-react');
7
+ var cva = require('cva');
8
+ var reactAria = require('react-aria');
9
+
10
+ const translations = {
11
+ pending: {
12
+ nb: 'venter',
13
+ sv: 'väntar',
14
+ en: 'pending'
15
+ }};
16
+
17
+ /**
18
+ * Returns the locale set in `<GrunnmurenProvider />`
19
+ */ function _useLocale() {
20
+ // a small wrapper around react-arias useLocale with a simpler return type with only the locales that we actually support
21
+ const locale = reactAriaComponents.useLocale();
22
+ return locale.locale;
23
+ }
24
+
25
+ /**
26
+ * Figma: https://www.figma.com/file/9OvSg0ZXI5E1eQYi7AWiWn/Grunnmuren-2.0-%E2%94%82-Designsystem?node-id=30%3A2574&mode=dev
27
+ */ const buttonVariants = cva.cva({
28
+ base: [
29
+ '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'
30
+ ],
31
+ variants: {
32
+ /**
33
+ * The variant of the button
34
+ * @default primary
35
+ */ variant: {
36
+ primary: 'no-underline',
37
+ // 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
38
+ secondary: 'border-2 border-current no-underline hover:border-transparent',
39
+ tertiary: 'underline hover:no-underline'
40
+ },
41
+ /**
42
+ * Adjusts the color of the button for usage on different backgrounds.
43
+ * @default blue
44
+ */ color: {
45
+ blue: 'focus-visible:outline-focus',
46
+ mint: 'focus-visible:outline-focus focus-visible:outline-mint',
47
+ white: 'focus-visible:outline-focus focus-visible:outline-white'
48
+ },
49
+ /**
50
+ * When the button is without text, but with a single icon.
51
+ * @default false
52
+ */ isIconOnly: {
53
+ true: 'p-2 [&>svg]:h-7 [&>svg]:w-7',
54
+ false: 'gap-2.5 px-4 py-2'
55
+ },
56
+ // Make the content of the button transparent to hide it's content, but keep the button width
57
+ isPending: {
58
+ true: '!text-transparent relative',
59
+ false: null
60
+ }
61
+ },
62
+ compoundVariants: [
63
+ {
64
+ color: 'blue',
65
+ variant: 'primary',
66
+ // Darken bg by 20% on hover. The color is manually crafted
67
+ className: 'bg-blue-dark text-white hover:bg-blue active:bg-[#0536A0] active:text-white [&_[role="progressbar"]]:text-white'
68
+ },
69
+ {
70
+ color: 'blue',
71
+ variant: 'secondary',
72
+ className: 'text-blue-dark hover:border-transparent hover:bg-blue hover:text-blue-dark hover:text-white active:bg-[#0536A0] [&:hover_[role="progressbar"]]:text-white [&_[role="progressbar"]]:text-blue-dark'
73
+ },
74
+ {
75
+ color: 'blue',
76
+ variant: 'tertiary',
77
+ className: '[&_[role="progressbar"]]:text-black'
78
+ },
79
+ {
80
+ color: 'mint',
81
+ variant: 'primary',
82
+ // Darken bg by 20% on hover. The color is manually crafted
83
+ className: 'bg-mint text-black hover:bg-[#8dd4bd] active:[#9ddac6] [&_[role="progressbar"]]:text-black'
84
+ },
85
+ {
86
+ color: 'mint',
87
+ variant: 'secondary',
88
+ className: 'text-mint hover:bg-mint hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-mint'
89
+ },
90
+ {
91
+ color: 'mint',
92
+ variant: 'tertiary',
93
+ className: 'text-mint [&_[role="progressbar"]]:text-mint'
94
+ },
95
+ {
96
+ color: 'white',
97
+ variant: 'primary',
98
+ className: 'bg-white text-black hover:bg-sky active:bg-sky-light [&_[role="progressbar"]]:text-black'
99
+ },
100
+ {
101
+ color: 'white',
102
+ variant: 'secondary',
103
+ className: 'text-white hover:bg-white hover:text-black [&:hover_[role="progressbar"]]:text-black [&_[role="progressbar"]]:text-white'
104
+ },
105
+ {
106
+ color: 'white',
107
+ variant: 'tertiary',
108
+ className: 'text-white [&_[role="progressbar"]]:text-white'
109
+ }
110
+ ],
111
+ defaultVariants: {
112
+ variant: 'primary',
113
+ color: 'blue',
114
+ isIconOnly: false,
115
+ isPending: false
116
+ }
117
+ });
118
+ const ButtonContext = /*#__PURE__*/ react.createContext({});
119
+ function isLinkProps(props) {
120
+ return !!props.href;
121
+ }
122
+ function Button({ ref = null, ...props }) {
123
+ [props, ref] = reactAriaComponents.useContextProps(props, ref, ButtonContext);
124
+ const { children: _children, color, isIconOnly, variant, isPending, ...restProps } = props;
125
+ const className = buttonVariants({
126
+ className: props.className,
127
+ color,
128
+ isIconOnly,
129
+ variant,
130
+ isPending
131
+ });
132
+ const locale = _useLocale();
133
+ const { progressBarProps } = reactAria.useProgressBar({
134
+ isIndeterminate: true,
135
+ 'aria-label': translations.pending[locale]
136
+ });
137
+ const children = isPending ? /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
138
+ children: [
139
+ _children,
140
+ /*#__PURE__*/ jsxRuntime.jsx(grunnmurenIconsReact.LoadingSpinner, {
141
+ className: "absolute m-auto motion-safe:animate-spin",
142
+ ...progressBarProps
143
+ })
144
+ ]
145
+ }) : _children;
146
+ return isLinkProps(restProps) ? /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Link, {
147
+ ...restProps,
148
+ className: className,
149
+ ref: ref,
150
+ children: children
151
+ }) : /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Button, {
152
+ ...restProps,
153
+ className: className,
154
+ isPending: isPending,
155
+ ref: ref,
156
+ children: children
157
+ });
158
+ }
159
+
160
+ const formField = cva.cx('group flex flex-col gap-2');
161
+ const formFieldError = cva.cx('w-fit bg-red-light px-2 py-1 text-red text-sm leading-6', 'group-data-[slot=file-upload]:rounded-lg');
162
+ const input = cva.cva({
163
+ base: [
164
+ // All inputs should always have a white background (this also ensures that type="search" on Safri doesn't get a gray background)
165
+ 'bg-white',
166
+ // Use box-content to enable auto width based on number of characters (size)
167
+ // Setting min-height to prevent the input from collapsing in Safari
168
+ // Combining these with a padding-y as base classes makes it easier to standardize the height (44px) of all inputs
169
+ 'box-content min-h-6 py-2.5',
170
+ 'rounded-md font-normal text-base leading-6 placeholder-[#727070] outline-hidden ring-1 ring-black',
171
+ // invalid styles
172
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red',
173
+ // Fix invisible ring on safari: https://github.com/tailwindlabs/tailwindcss.com/issues/1135
174
+ 'appearance-none'
175
+ ],
176
+ variants: {
177
+ // Focus rings. Can either be :focus or :focus-visible based on the needs of the particular component.
178
+ focusModifier: {
179
+ focus: 'focus:ring-focus group-data-invalid:focus:ring-3 group-data-invalid:focus:ring-red',
180
+ visible: 'data-focus-visible:ring-focus group-data-invalid:data-focus-visible:ring-3 group-data-invalid:data-focus-visible:ring-red'
181
+ },
182
+ isGrouped: {
183
+ false: 'px-3',
184
+ true: '!ring-0 flex-1'
185
+ }
186
+ },
187
+ defaultVariants: {
188
+ focusModifier: 'focus',
189
+ isGrouped: false
190
+ }
191
+ });
192
+ const inputGroup = cva.cx([
193
+ 'inline-flex items-center gap-3 overflow-hidden rounded-md bg-white px-3 text-base ring-1 ring-black focus-within:ring-focus',
194
+ 'group-data-invalid:ring-focus group-data-invalid:ring-red group-data-invalid:focus-within:ring-3 group-data-invalid:focus-within:ring-red'
195
+ ]);
196
+ ({
197
+ popover: cva.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'),
198
+ // overflow-x-hidden is needed to prevent visible vertical scrollbars from overflowing the border radius of the popover
199
+ listbox: cva.cx('max-h-[25rem] overflow-x-hidden text-sm outline-hidden'),
200
+ chevronIcon: cva.cx('text-base transition-transform duration-150 group-data-open:rotate-180 motion-reduce:transition-none')
201
+ });
202
+
203
+ function InputAddonDivider() {
204
+ return /*#__PURE__*/ jsxRuntime.jsx("span", {
205
+ className: "block h-6 w-px flex-none bg-black"
206
+ });
207
+ }
208
+
209
+ function Description(props) {
210
+ const { className, ...restProps } = props;
211
+ return /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Text, {
212
+ ...restProps,
213
+ className: cva.cx(className, 'description'),
214
+ slot: "description"
215
+ });
216
+ }
217
+
218
+ function ErrorMessage(props) {
219
+ const { children, className, ...restProps } = props;
220
+ return /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Text, {
221
+ ...restProps,
222
+ className: cva.cx(className, formFieldError),
223
+ slot: "errorMessage",
224
+ children: children
225
+ });
226
+ }
227
+
228
+ /**
229
+ * This component handles renders a custom error message (if provided), otherwise it falls back to the browser's native validation.
230
+ * In other words, this handles controlled and uncontrolled form errors.
231
+ */ function ErrorMessageOrFieldError({ errorMessage }) {
232
+ return errorMessage ? /*#__PURE__*/ jsxRuntime.jsx(ErrorMessage, {
233
+ children: errorMessage
234
+ }) : /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.FieldError, {
235
+ className: formFieldError
236
+ });
237
+ }
238
+
239
+ function Label(props) {
240
+ const { children, className, ...restProps } = props;
241
+ return /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Label, {
242
+ className: cva.cx(className, 'font-semibold leading-7'),
243
+ ...restProps,
244
+ children: children
245
+ });
246
+ }
247
+
248
+ const inputVariants = cva.compose(input, cva.cva({
249
+ base: '',
250
+ variants: {
251
+ textAlign: {
252
+ right: 'text-right',
253
+ left: ''
254
+ },
255
+ autoWidth: {
256
+ true: 'max-w-fit',
257
+ false: ''
258
+ }
259
+ }
260
+ }));
261
+ function TextField(props) {
262
+ const { className, description, errorMessage, label, leftAddon, isInvalid: _isInvalid, textAlign, rightAddon, withAddonDivider, size, ref, ...restProps } = props;
263
+ // the order of the conditions matter here, because providing a value for isInvalid makes the validation state "controlled",
264
+ // which will override any built in validation
265
+ const isInvalid = !!errorMessage || _isInvalid;
266
+ return /*#__PURE__*/ jsxRuntime.jsxs(reactAriaComponents.TextField, {
267
+ ...restProps,
268
+ className: cva.cx(className, formField),
269
+ isInvalid: isInvalid,
270
+ children: [
271
+ label && /*#__PURE__*/ jsxRuntime.jsx(Label, {
272
+ children: label
273
+ }),
274
+ description && /*#__PURE__*/ jsxRuntime.jsx(Description, {
275
+ children: description
276
+ }),
277
+ leftAddon || rightAddon ? /*#__PURE__*/ jsxRuntime.jsxs(reactAriaComponents.Group, {
278
+ className: cva.cx(inputGroup, {
279
+ 'w-fit': !!size
280
+ }),
281
+ children: [
282
+ leftAddon,
283
+ withAddonDivider && leftAddon && /*#__PURE__*/ jsxRuntime.jsx(InputAddonDivider, {}),
284
+ /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Input, {
285
+ className: inputVariants({
286
+ textAlign,
287
+ isGrouped: true,
288
+ autoWidth: !!size
289
+ }),
290
+ ref: ref,
291
+ size: size
292
+ }),
293
+ withAddonDivider && rightAddon && /*#__PURE__*/ jsxRuntime.jsx(InputAddonDivider, {}),
294
+ rightAddon
295
+ ]
296
+ }) : /*#__PURE__*/ jsxRuntime.jsx(reactAriaComponents.Input, {
297
+ className: inputVariants({
298
+ textAlign,
299
+ autoWidth: !!size
300
+ }),
301
+ ref: ref,
302
+ size: size
303
+ }),
304
+ /*#__PURE__*/ jsxRuntime.jsx(ErrorMessageOrFieldError, {
305
+ errorMessage: errorMessage
306
+ })
307
+ ]
308
+ });
309
+ }
310
+
311
+ const meta = {
312
+ title: 'Form validation'
313
+ };
314
+ const emailErrorMessage = 'Vennligst bruk en .no e-postadresse';
315
+ const Form = (props)=>{
316
+ const [errors, setErrors] = react.useState({});
317
+ const [isPending, setIsPending] = react.useState(false);
318
+ const handleSubmit = async (e)=>{
319
+ e.preventDefault();
320
+ // @ts-expect-error this works..
321
+ const data = Object.fromEntries(new FormData(e.target));
322
+ if (!props.serverValidate) {
323
+ alert(JSON.stringify(data));
324
+ return;
325
+ }
326
+ setIsPending(true);
327
+ // Fake a delay here, so it looks like we're submitting the data to a server
328
+ await new Promise((resolve)=>setTimeout(resolve, 2000));
329
+ setIsPending(false);
330
+ if (!data.email.endsWith('.no')) {
331
+ setErrors({
332
+ email: emailErrorMessage
333
+ });
334
+ } else {
335
+ setErrors({});
336
+ // Do this in a timeout, so the button's loading indicator stops spinning before the alert dialog is displayed
337
+ setTimeout(()=>{
338
+ alert(JSON.stringify(data));
339
+ }, 0);
340
+ }
341
+ };
342
+ return /*#__PURE__*/ jsxRuntime.jsxs(reactAriaComponents.Form, {
343
+ onSubmit: handleSubmit,
344
+ className: "container-prose flex flex-col items-start gap-4",
345
+ validationErrors: errors,
346
+ children: [
347
+ props.children,
348
+ /*#__PURE__*/ jsxRuntime.jsx(Button, {
349
+ type: "submit",
350
+ isPending: isPending,
351
+ children: "Send inn"
352
+ })
353
+ ]
354
+ });
355
+ };
356
+ const nameProps = {
357
+ isRequired: true,
358
+ label: 'Navn',
359
+ name: 'name'
360
+ };
361
+ const emailProps = {
362
+ isRequired: true,
363
+ label: 'Epost',
364
+ type: 'email',
365
+ name: 'email',
366
+ description: /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
367
+ children: [
368
+ "Må være en ",
369
+ /*#__PURE__*/ jsxRuntime.jsx("em", {
370
+ children: ".no"
371
+ }),
372
+ " e-postadresse"
373
+ ]
374
+ })
375
+ };
376
+ const NativeValidation = ()=>{
377
+ return /*#__PURE__*/ jsxRuntime.jsxs(Form, {
378
+ children: [
379
+ /*#__PURE__*/ jsxRuntime.jsx("p", {
380
+ children: "Dette eksemplet bruker kun browserens native validering av skjemaet. Det er dermed mulig å sende inn skjemaet uten at e-postadressen slutter på .no"
381
+ }),
382
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
383
+ ...nameProps
384
+ }),
385
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
386
+ ...emailProps
387
+ })
388
+ ]
389
+ });
390
+ };
391
+ const RealtimeValidation = ()=>{
392
+ return /*#__PURE__*/ jsxRuntime.jsxs(Form, {
393
+ children: [
394
+ /*#__PURE__*/ jsxRuntime.jsxs("p", {
395
+ children: [
396
+ "Dette eksemplet bruker ",
397
+ /*#__PURE__*/ jsxRuntime.jsx("code", {
398
+ children: "validate"
399
+ }),
400
+ " prop-en på skjemaelementene for å implementere realtime validering av skjemaet på klienten. Dette er nyttig for å legge til ekstra validering, selv om skjemaet er",
401
+ ' ',
402
+ /*#__PURE__*/ jsxRuntime.jsx("em", {
403
+ children: "uncontrolled"
404
+ }),
405
+ "."
406
+ ]
407
+ }),
408
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
409
+ ...nameProps
410
+ }),
411
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
412
+ ...emailProps,
413
+ validate: (value)=>value.endsWith('.no') ? null : emailErrorMessage
414
+ })
415
+ ]
416
+ });
417
+ };
418
+ const ServerSideValidation = ()=>{
419
+ return /*#__PURE__*/ jsxRuntime.jsxs(Form, {
420
+ serverValidate: true,
421
+ children: [
422
+ /*#__PURE__*/ jsxRuntime.jsxs("p", {
423
+ children: [
424
+ "Dette eksemplet bruker ",
425
+ /*#__PURE__*/ jsxRuntime.jsx("code", {
426
+ children: "validationErrors"
427
+ }),
428
+ " prop-en på selve skjemaet for å gjøre serverside validering av skjemadatene. Skjemainnsendinger burde alltid valideres på serveren, og det lar oss flytte mye av kompleksiteten til serveren i stedet for klienten, for eksempel dersom vi ønsker å bruke zod for å validere skjemadataene. Kan for eksempel integereres med React server actions."
429
+ ]
430
+ }),
431
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
432
+ ...nameProps
433
+ }),
434
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
435
+ ...emailProps
436
+ })
437
+ ]
438
+ });
439
+ };
440
+ const ControlledValidation = ()=>{
441
+ const [name, setName] = react.useState('');
442
+ const [email, setEmail] = react.useState('');
443
+ let nameError = '';
444
+ if (!name) {
445
+ nameError = 'Fyll ut navn';
446
+ }
447
+ let emailError = '';
448
+ if (!email.endsWith('.no')) {
449
+ emailError = emailErrorMessage;
450
+ }
451
+ return /*#__PURE__*/ jsxRuntime.jsxs(Form, {
452
+ children: [
453
+ /*#__PURE__*/ jsxRuntime.jsxs("p", {
454
+ children: [
455
+ "Dette eksemplet bruker ",
456
+ /*#__PURE__*/ jsxRuntime.jsx("code", {
457
+ children: "errorMesssage"
458
+ }),
459
+ " prop-en på skjemaelementene for å vise feilmeldinger og er nyttig for skjemaer som er såkalt ",
460
+ /*#__PURE__*/ jsxRuntime.jsx("em", {
461
+ children: "controlled"
462
+ }),
463
+ "."
464
+ ]
465
+ }),
466
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
467
+ ...nameProps,
468
+ value: name,
469
+ onChange: setName,
470
+ errorMessage: nameError,
471
+ validationBehavior: "aria"
472
+ }),
473
+ /*#__PURE__*/ jsxRuntime.jsx(TextField, {
474
+ ...emailProps,
475
+ value: email,
476
+ onChange: setEmail,
477
+ errorMessage: emailError,
478
+ validationBehavior: "aria"
479
+ })
480
+ ]
481
+ });
482
+ };
483
+
484
+ exports.ControlledValidation = ControlledValidation;
485
+ exports.NativeValidation = NativeValidation;
486
+ exports.RealtimeValidation = RealtimeValidation;
487
+ exports.ServerSideValidation = ServerSideValidation;
488
+ exports.default = meta;
@@ -0,0 +1,11 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Meta } from '@storybook/react-vite';
3
+
4
+ declare const meta: Meta;
5
+
6
+ declare const NativeValidation: () => react_jsx_runtime.JSX.Element;
7
+ declare const RealtimeValidation: () => react_jsx_runtime.JSX.Element;
8
+ declare const ServerSideValidation: () => react_jsx_runtime.JSX.Element;
9
+ declare const ControlledValidation: () => react_jsx_runtime.JSX.Element;
10
+
11
+ export { ControlledValidation, NativeValidation, RealtimeValidation, ServerSideValidation, meta as default };
@@ -0,0 +1,11 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Meta } from '@storybook/react-vite';
3
+
4
+ declare const meta: Meta;
5
+
6
+ declare const NativeValidation: () => react_jsx_runtime.JSX.Element;
7
+ declare const RealtimeValidation: () => react_jsx_runtime.JSX.Element;
8
+ declare const ServerSideValidation: () => react_jsx_runtime.JSX.Element;
9
+ declare const ControlledValidation: () => react_jsx_runtime.JSX.Element;
10
+
11
+ export { ControlledValidation, NativeValidation, RealtimeValidation, ServerSideValidation, meta as default };