@obosbbl/grunnmuren-react 3.6.0 → 3.8.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.
package/README.md CHANGED
@@ -1,19 +1,33 @@
1
1
  # @obosbbl/grunnmuren-react
2
2
 
3
- [![npm canary version](https://img.shields.io/npm/v/@obosbbl%2Fgrunnmuren-react/canary.svg)](https://www.npmjs.com/package/@obosbbl/grunnmuren-react)
3
+ [![npm version](https://img.shields.io/npm/v/@obosbbl%2Fgrunnmuren-react.svg)](https://www.npmjs.com/package/@obosbbl/grunnmuren-react)
4
4
 
5
5
  Grunnmuren React components.
6
6
 
7
7
  ## Install
8
8
 
9
+ `@obosbbl/grunnmuren-react` declares `@obosbbl/grunnmuren-tailwind` as a peer dependency — the components rely on Tailwind tokens and utilities defined in that package. Install both:
10
+
9
11
  ```sh
10
12
  # npm
11
- npm install @obosbbl/grunnmuren-react@canary
13
+ npm install @obosbbl/grunnmuren-react @obosbbl/grunnmuren-tailwind
12
14
 
13
15
  # pnpm
14
- pnpm add @obosbbl/grunnmuren-react@canary
16
+ pnpm add @obosbbl/grunnmuren-react @obosbbl/grunnmuren-tailwind
15
17
  ```
16
18
 
19
+ Then import the Tailwind preset in your global stylesheet. The preset already does `@import 'tailwindcss';` internally, so you should **not** import Tailwind separately:
20
+
21
+ ```css
22
+ /* globals.css */
23
+ @import '@obosbbl/grunnmuren-tailwind';
24
+
25
+ /* Register the React package as a Tailwind source so the utilities used by the components are emitted. */
26
+ @source "../../node_modules/@obosbbl/grunnmuren-react/dist";
27
+ ```
28
+
29
+ See [`@obosbbl/grunnmuren-tailwind`](../tailwind/) for the full preset documentation.
30
+
17
31
  ## Setup
18
32
 
19
33
  ### Internationalization
@@ -200,8 +214,6 @@ The plugin works with several different bundlers. See [React Aria's bundle size
200
214
 
201
215
  ## Usage
202
216
 
203
- Before you start using the components you need to configure the [Tailwind preset](../tailwind/). Remember to add this package to the content scan.
204
-
205
217
  ```js
206
218
  import { Button } from '@obosbbl/grunnmuren-react';
207
219
 
@@ -215,11 +215,13 @@ function Button({ ref = null, ...props }) {
215
215
  return isLinkProps(restProps) ? /*#__PURE__*/ jsxRuntime.jsx(Link.Link, {
216
216
  ...restProps,
217
217
  className: className,
218
+ "data-slot": "button",
218
219
  ref: ref,
219
220
  children: children
220
221
  }) : /*#__PURE__*/ jsxRuntime.jsx(Button$1.Button, {
221
222
  ...restProps,
222
223
  className: className,
224
+ "data-slot": "button",
223
225
  isPending: isPending,
224
226
  ref: ref,
225
227
  children: children
@@ -213,11 +213,13 @@ function Button({ ref = null, ...props }) {
213
213
  return isLinkProps(restProps) ? /*#__PURE__*/ jsx(Link, {
214
214
  ...restProps,
215
215
  className: className,
216
+ "data-slot": "button",
216
217
  ref: ref,
217
218
  children: children
218
219
  }) : /*#__PURE__*/ jsx(Button$1, {
219
220
  ...restProps,
220
221
  className: className,
222
+ "data-slot": "button",
221
223
  isPending: isPending,
222
224
  ref: ref,
223
225
  children: children
@@ -307,7 +307,11 @@ const oneColumnLayout = [
307
307
  // Aligns <Content> and any element beside it (e.g. <Media>, <Badge>, <CTA> etc.) to the bottom of the <Content> container
308
308
  'lg:items-end'
309
309
  ];
310
- const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-[1/1] sm:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-4/3 md:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-3/2';
310
+ // Use `not-has-[video]` (tag selector) so that aspect-ratio rules also skip for video players
311
+ // that don't expose `data-slot="video"` (e.g., MuxPlayer).
312
+ // Aspect-ratio is applied to the `<Media>` element itself (not the img inside) so that the
313
+ // Media box fills the grid column and consumers don't need `!important` overrides when nesting.
314
+ const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot=media]:not-has-[video]:aspect-[1/1] sm:*:data-[slot=media]:not-has-[video]:aspect-4/3 md:*:data-[slot=media]:not-has-[video]:aspect-3/2';
311
315
  const variants = cva.cva({
312
316
  base: [
313
317
  'container px-0',
@@ -332,7 +336,7 @@ const variants = cva.cva({
332
336
  standard: [
333
337
  oneColumnLayout,
334
338
  nonFullBleedAspectRatiosForSmallScreens,
335
- 'lg:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-2/1'
339
+ 'lg:*:data-[slot=media]:not-has-[video]:aspect-2/1'
336
340
  ],
337
341
  'full-bleed': [
338
342
  oneColumnLayout,
@@ -361,7 +365,7 @@ const variants = cva.cva({
361
365
  'lg:*:data-[slot=content]:gap-y-7',
362
366
  nonFullBleedAspectRatiosForSmallScreens,
363
367
  // Set media aspect ratio to 1:1 (square)
364
- 'lg:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-square'
368
+ 'lg:*:data-[slot=media]:not-has-[video]:aspect-square'
365
369
  ]
366
370
  }
367
371
  },
@@ -373,6 +377,8 @@ const variants = cva.cva({
373
377
  ],
374
378
  className: [
375
379
  '*:data-[slot=media]:*:rounded-3xl',
380
+ // Make non-video media (image/picture) fill the aspect-ratio-constrained Media box
381
+ '*:data-[slot=media]:not-has-[video]:*:size-full',
376
382
  '*:data-[slot=carousel]:relative **:data-[slot=carousel-container]:rounded-3xl **:data-[slot=carousel-controls]:absolute **:data-[slot=carousel-controls]:right-4 **:data-[slot=carousel-controls]:bottom-4'
377
383
  ]
378
384
  }
@@ -305,7 +305,11 @@ const oneColumnLayout = [
305
305
  // Aligns <Content> and any element beside it (e.g. <Media>, <Badge>, <CTA> etc.) to the bottom of the <Content> container
306
306
  'lg:items-end'
307
307
  ];
308
- const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-[1/1] sm:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-4/3 md:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-3/2';
308
+ // Use `not-has-[video]` (tag selector) so that aspect-ratio rules also skip for video players
309
+ // that don't expose `data-slot="video"` (e.g., MuxPlayer).
310
+ // Aspect-ratio is applied to the `<Media>` element itself (not the img inside) so that the
311
+ // Media box fills the grid column and consumers don't need `!important` overrides when nesting.
312
+ const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot=media]:not-has-[video]:aspect-[1/1] sm:*:data-[slot=media]:not-has-[video]:aspect-4/3 md:*:data-[slot=media]:not-has-[video]:aspect-3/2';
309
313
  const variants = cva({
310
314
  base: [
311
315
  'container px-0',
@@ -330,7 +334,7 @@ const variants = cva({
330
334
  standard: [
331
335
  oneColumnLayout,
332
336
  nonFullBleedAspectRatiosForSmallScreens,
333
- 'lg:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-2/1'
337
+ 'lg:*:data-[slot=media]:not-has-[video]:aspect-2/1'
334
338
  ],
335
339
  'full-bleed': [
336
340
  oneColumnLayout,
@@ -359,7 +363,7 @@ const variants = cva({
359
363
  'lg:*:data-[slot=content]:gap-y-7',
360
364
  nonFullBleedAspectRatiosForSmallScreens,
361
365
  // Set media aspect ratio to 1:1 (square)
362
- 'lg:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-square'
366
+ 'lg:*:data-[slot=media]:not-has-[video]:aspect-square'
363
367
  ]
364
368
  }
365
369
  },
@@ -371,6 +375,8 @@ const variants = cva({
371
375
  ],
372
376
  className: [
373
377
  '*:data-[slot=media]:*:rounded-3xl',
378
+ // Make non-video media (image/picture) fill the aspect-ratio-constrained Media box
379
+ '*:data-[slot=media]:not-has-[video]:*:size-full',
374
380
  '*:data-[slot=carousel]:relative **:data-[slot=carousel-container]:rounded-3xl **:data-[slot=carousel-controls]:absolute **:data-[slot=carousel-controls]:right-4 **:data-[slot=carousel-controls]:bottom-4'
375
381
  ]
376
382
  }
package/dist/index.d.mts CHANGED
@@ -17,13 +17,13 @@ import { CheckboxProps as CheckboxProps$1 } from 'react-aria-components/Checkbox
17
17
  import { CheckboxGroupProps as CheckboxGroupProps$1 } from 'react-aria-components/CheckboxGroup';
18
18
  import { ComboBoxProps } from 'react-aria-components/ComboBox';
19
19
  import { DateFormatterOptions } from 'react-aria/useDateFormatter';
20
+ import { ModalOverlayProps as ModalOverlayProps$1 } from 'react-aria-components/Modal';
20
21
  import { FileTriggerProps as FileTriggerProps$1 } from 'react-aria-components/FileTrigger';
21
22
  import { useFormValidationState } from 'react-stately/private/form/useFormValidationState';
22
23
  import { TextProps } from 'react-aria-components/Text';
23
24
  import { LabelProps } from 'react-aria-components/Label';
24
25
  export { LabelProps } from 'react-aria-components/Label';
25
26
  import { DialogProps as DialogProps$1, DialogTriggerProps as DialogTriggerProps$1 } from 'react-aria-components/Dialog';
26
- import { ModalOverlayProps as ModalOverlayProps$1 } from 'react-aria-components/Modal';
27
27
  import { NumberFieldProps as NumberFieldProps$1 } from 'react-aria-components/NumberField';
28
28
  import { RadioProps as RadioProps$1, RadioGroupProps as RadioGroupProps$1 } from 'react-aria-components/RadioGroup';
29
29
  import { SelectProps as SelectProps$1 } from 'react-aria-components/Select';
@@ -495,6 +495,27 @@ type DateFormatterProps = {
495
495
  */
496
496
  declare const DateFormatter: ({ options: _options, value, children: render }: DateFormatterProps) => ReactNode;
497
497
 
498
+ declare const drawerVariants: (props?: ({
499
+ placement?: "right" | "left" | "top" | "bottom" | undefined;
500
+ isEntering?: boolean | undefined;
501
+ isExiting?: boolean | undefined;
502
+ } & ({
503
+ class?: cva.ClassValue;
504
+ className?: never;
505
+ } | {
506
+ class?: never;
507
+ className?: cva.ClassValue;
508
+ })) | undefined) => string;
509
+ type DrawerProps = Omit<ModalOverlayProps$1, 'isDismissable' | 'style'> & Pick<VariantProps<typeof drawerVariants>, 'placement'> & {
510
+ /** Additional style properties for the element. */
511
+ style?: React.CSSProperties;
512
+ /** @default 10 Controls the z-index of the drawer overlay */
513
+ zIndex?: number;
514
+ /** @default true Makes the drawer dismissable */
515
+ isDismissable?: boolean;
516
+ };
517
+ declare const Drawer: ({ isDismissable, isOpen, onOpenChange, defaultOpen, className, zIndex, placement, style, ...restProps }: DrawerProps) => react_jsx_runtime.JSX.Element;
518
+
498
519
  type FormValidationProps<T> = Parameters<typeof useFormValidationState<T>>[0];
499
520
  type FileTriggerProps = Partial<Omit<FormValidationProps<File>, 'value'>> & FileTriggerProps$1 & Omit<HTMLAttributes<HTMLInputElement>, 'onSelect' | 'onChange' | 'required' | 'className'> & {
500
521
  ref?: RefObject<HTMLInputElement | null>;
@@ -640,6 +661,25 @@ type NumberFieldProps = {
640
661
  } & Omit<NumberFieldProps$1, 'className' | 'isReadOnly' | 'isDisabled' | 'children' | 'style' | 'hideStepper'>;
641
662
  declare function NumberField(props: NumberFieldProps): react_jsx_runtime.JSX.Element;
642
663
 
664
+ type PaginationProps = {
665
+ /** The current page (1-indexed). */
666
+ page: number;
667
+ /** The total number of pages. */
668
+ count: number;
669
+ /** Given a page number, returns the href for navigating to that page. The
670
+ * value is set as the `href` attribute on the rendered link, enabling
671
+ * right-click, middle-click and SEO. Client-side routing is set up via
672
+ * `routerOptions` on `<GrunnmurenProvider />`. */
673
+ getItemHref: (page: number) => string;
674
+ /** Optional callback fired when the user activates a page link. Useful for
675
+ * keeping local state in sync with the URL. Navigation still happens via
676
+ * the link's href. */
677
+ onChange?: (page: number) => void;
678
+ /** Additional class name for the root `<ol>`. */
679
+ className?: string;
680
+ };
681
+ declare const Pagination: (props: PaginationProps) => react_jsx_runtime.JSX.Element;
682
+
643
683
  type RadioProps = {
644
684
  children: React.ReactNode;
645
685
  /** Additional CSS className for the element. */
@@ -922,5 +962,5 @@ type VideoLoopProps = {
922
962
  };
923
963
  declare const VideoLoop: ({ src, format, alt, className }: VideoLoopProps) => react_jsx_runtime.JSX.Element;
924
964
 
925
- export { Accordion, AccordionItem, Alertbox, Avatar, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, ButtonContext, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, Disclosure, DisclosureButton, DisclosurePanel, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, Link, LinkList, LinkListContainer, LinkListContext, LinkListItem, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, Tab, TabList, TabPanel, Tabs, Tag, TagGroup, TagList, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselButton as UNSAFE_CarouselButton, CarouselContext as UNSAFE_CarouselContext, CarouselControls as UNSAFE_CarouselControls, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, CarouselItemsContainer as UNSAFE_CarouselItemsContainer, CarouselItemsContainer as UNSAFE_CarouselItemsContainerProps, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, FileUpload as UNSAFE_FileUpload, Hero as UNSAFE_Hero, HeroContext as UNSAFE_HeroContext, Modal as UNSAFE_Modal, ResizableTableContainer as UNSAFE_ResizableTableContainer, Step as UNSAFE_Step, Stepper as UNSAFE_Stepper, Table as UNSAFE_Table, TableBody as UNSAFE_TableBody, TableCell as UNSAFE_TableCell, TableColumn as UNSAFE_TableColumn, TableColumnResizer as UNSAFE_TableColumnResizer, UNSAFE_TableContainer, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, VideoLoop, _useLocale as useLocale };
926
- export type { AccordionItemProps, AccordionProps, Props as AlertboxProps, AvatarProps, BacklinkProps, BadgeProps, BreadcrumbProps, BreadcrumbsProps, ButtonProps, CaptionProps, CardLinkProps, CardProps, CheckboxGroupProps, CheckboxProps, ComboboxProps, ContentProps, DateFormatterProps, DescriptionProps, DisclosureButtonProps, DisclosurePanelProps, DisclosureProps, ErrorMessageProps, FooterProps, GrunnmurenProviderProps, HeadingProps, LinkListContainerProps, LinkListContextValue, LinkListItemProps, LinkListProps, LinkProps, Locale, MediaProps, NumberFieldProps, RadioGroupProps, RadioProps, SelectProps, TabListProps, TabPanelProps, TabProps, TabsProps, TagGroupProps, TagListProps, TagProps, TextAreaProps, TextFieldProps, CarouselButtonProps as UNSAFE_CarouselButtonProps, CarouselContextValue as UNSAFE_CarouselContextValue, CarouselControlsProps as UNSAFE_CarouselControlsProps, CarouselItemProps as UNSAFE_CarouselItemProps, CarouselItemsProps as UNSAFE_CarouselItemsProps, CarouselProps as UNSAFE_CarouselProps, CarouselElement as UNSAFE_CarouselRef, DialogProps as UNSAFE_DialogProps, DialogTriggerProps as UNSAFE_DialogTriggerProps, FileUploadProps as UNSAFE_FileUploadProps, HeroContextValue as UNSAFE_HeroContextValue, HeroProps as UNSAFE_HeroProps, ModalProps as UNSAFE_ModalProps, ResizableTableContainerProps as UNSAFE_ResizableTableContainerProps, StepProps as UNSAFE_StepProps, StepperProps as UNSAFE_StepperProps, TableBodyProps as UNSAFE_TableBodyProps, TableCellProps as UNSAFE_TableCellProps, TableColumnProps as UNSAFE_TableColumnProps, TableColumnResizerProps as UNSAFE_TableColumnResizerProps, UNSAFE_TableContainerProps, TableHeaderProps as UNSAFE_TableHeaderProps, TableProps as UNSAFE_TableProps, TableRowProps as UNSAFE_TableRowProps };
965
+ export { Accordion, AccordionItem, Alertbox, Avatar, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, ButtonContext, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, Disclosure, DisclosureButton, DisclosurePanel, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Hero, HeroContext, Label, Link, LinkList, LinkListContainer, LinkListContext, LinkListItem, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, Tab, TabList, TabPanel, Tabs, Tag, TagGroup, TagList, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselButton as UNSAFE_CarouselButton, CarouselContext as UNSAFE_CarouselContext, CarouselControls as UNSAFE_CarouselControls, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, CarouselItemsContainer as UNSAFE_CarouselItemsContainer, CarouselItemsContainer as UNSAFE_CarouselItemsContainerProps, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, Drawer as UNSAFE_Drawer, FileUpload as UNSAFE_FileUpload, Modal as UNSAFE_Modal, Pagination as UNSAFE_Pagination, ResizableTableContainer as UNSAFE_ResizableTableContainer, Step as UNSAFE_Step, Stepper as UNSAFE_Stepper, Table as UNSAFE_Table, TableBody as UNSAFE_TableBody, TableCell as UNSAFE_TableCell, TableColumn as UNSAFE_TableColumn, TableColumnResizer as UNSAFE_TableColumnResizer, UNSAFE_TableContainer, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, VideoLoop, _useLocale as useLocale };
966
+ export type { AccordionItemProps, AccordionProps, Props as AlertboxProps, AvatarProps, BacklinkProps, BadgeProps, BreadcrumbProps, BreadcrumbsProps, ButtonProps, CaptionProps, CardLinkProps, CardProps, CheckboxGroupProps, CheckboxProps, ComboboxProps, ContentProps, DateFormatterProps, DescriptionProps, DisclosureButtonProps, DisclosurePanelProps, DisclosureProps, ErrorMessageProps, FooterProps, GrunnmurenProviderProps, HeadingProps, HeroContextValue, HeroProps, LinkListContainerProps, LinkListContextValue, LinkListItemProps, LinkListProps, LinkProps, Locale, MediaProps, NumberFieldProps, RadioGroupProps, RadioProps, SelectProps, TabListProps, TabPanelProps, TabProps, TabsProps, TagGroupProps, TagListProps, TagProps, TextAreaProps, TextFieldProps, CarouselButtonProps as UNSAFE_CarouselButtonProps, CarouselContextValue as UNSAFE_CarouselContextValue, CarouselControlsProps as UNSAFE_CarouselControlsProps, CarouselItemProps as UNSAFE_CarouselItemProps, CarouselItemsProps as UNSAFE_CarouselItemsProps, CarouselProps as UNSAFE_CarouselProps, CarouselElement as UNSAFE_CarouselRef, DialogProps as UNSAFE_DialogProps, DialogTriggerProps as UNSAFE_DialogTriggerProps, DrawerProps as UNSAFE_DrawerProps, FileUploadProps as UNSAFE_FileUploadProps, ModalProps as UNSAFE_ModalProps, PaginationProps as UNSAFE_PaginationProps, ResizableTableContainerProps as UNSAFE_ResizableTableContainerProps, StepProps as UNSAFE_StepProps, StepperProps as UNSAFE_StepperProps, TableBodyProps as UNSAFE_TableBodyProps, TableCellProps as UNSAFE_TableCellProps, TableColumnProps as UNSAFE_TableColumnProps, TableColumnResizerProps as UNSAFE_TableColumnResizerProps, UNSAFE_TableContainerProps, TableHeaderProps as UNSAFE_TableHeaderProps, TableProps as UNSAFE_TableProps, TableRowProps as UNSAFE_TableRowProps };
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { RouterProvider } from 'react-aria-components';
3
3
  export { Form, Group } from 'react-aria-components';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { cva, cx, compose } from 'cva';
6
- import { createContext, useContext, useId, useRef, Children, useState, useEffect, useMemo, useCallback, useImperativeHandle, isValidElement, use, cloneElement } from 'react';
6
+ import { createContext, useContext, useId, useRef, Children, useState, useEffect, useMemo, useCallback, useImperativeHandle, isValidElement, useLayoutEffect, use, cloneElement } from 'react';
7
7
  import { useContextProps, Provider, DEFAULT_SLOT, useSlottedContext } from 'react-aria-components/slots';
8
8
  import { ChevronDown, Error, Warning, CheckCircle, InfoCircle, Close, User, ChevronLeft, Download, LinkExternal, ArrowRight, ChevronRight, LoadingSpinner, Check, Trash, Edit, PlayerPause, PlayerPlay } from '@obosbbl/grunnmuren-icons-react';
9
9
  import { ButtonContext as ButtonContext$1, Button as Button$1 } from 'react-aria-components/Button';
@@ -397,6 +397,21 @@ const translations$1 = {
397
397
  sv: 'Nästa',
398
398
  en: 'Next'
399
399
  },
400
+ pagination: {
401
+ nb: 'Paginering',
402
+ sv: 'Paginering',
403
+ en: 'Pagination'
404
+ },
405
+ previousPage: {
406
+ nb: 'Forrige side',
407
+ sv: 'Föregående sida',
408
+ en: 'Previous page'
409
+ },
410
+ nextPage: {
411
+ nb: 'Neste side',
412
+ sv: 'Nästa sida',
413
+ en: 'Next page'
414
+ },
400
415
  externalLink: {
401
416
  nb: '(ekstern lenke)',
402
417
  sv: '(extern länk)',
@@ -945,11 +960,13 @@ function Button({ ref = null, ...props }) {
945
960
  return isLinkProps(restProps) ? /*#__PURE__*/ jsx(Link$1, {
946
961
  ...restProps,
947
962
  className: className,
963
+ "data-slot": "button",
948
964
  ref: ref,
949
965
  children: children
950
966
  }) : /*#__PURE__*/ jsx(Button$1, {
951
967
  ...restProps,
952
968
  className: className,
969
+ "data-slot": "button",
953
970
  isPending: isPending,
954
971
  ref: ref,
955
972
  children: children
@@ -1188,7 +1205,11 @@ const oneColumnLayout = [
1188
1205
  // Aligns <Content> and any element beside it (e.g. <Media>, <Badge>, <CTA> etc.) to the bottom of the <Content> container
1189
1206
  'lg:items-end'
1190
1207
  ];
1191
- const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-[1/1] sm:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-4/3 md:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-3/2';
1208
+ // Use `not-has-[video]` (tag selector) so that aspect-ratio rules also skip for video players
1209
+ // that don't expose `data-slot="video"` (e.g., MuxPlayer).
1210
+ // Aspect-ratio is applied to the `<Media>` element itself (not the img inside) so that the
1211
+ // Media box fills the grid column and consumers don't need `!important` overrides when nesting.
1212
+ const nonFullBleedAspectRatiosForSmallScreens = '*:data-[slot=media]:not-has-[video]:aspect-[1/1] sm:*:data-[slot=media]:not-has-[video]:aspect-4/3 md:*:data-[slot=media]:not-has-[video]:aspect-3/2';
1192
1213
  const variants = cva({
1193
1214
  base: [
1194
1215
  'container px-0',
@@ -1213,7 +1234,7 @@ const variants = cva({
1213
1234
  standard: [
1214
1235
  oneColumnLayout,
1215
1236
  nonFullBleedAspectRatiosForSmallScreens,
1216
- 'lg:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-2/1'
1237
+ 'lg:*:data-[slot=media]:not-has-[video]:aspect-2/1'
1217
1238
  ],
1218
1239
  'full-bleed': [
1219
1240
  oneColumnLayout,
@@ -1242,7 +1263,7 @@ const variants = cva({
1242
1263
  'lg:*:data-[slot=content]:gap-y-7',
1243
1264
  nonFullBleedAspectRatiosForSmallScreens,
1244
1265
  // Set media aspect ratio to 1:1 (square)
1245
- 'lg:*:data-[slot=media]:not-has-data-[slot=video]:*:aspect-square'
1266
+ 'lg:*:data-[slot=media]:not-has-[video]:aspect-square'
1246
1267
  ]
1247
1268
  }
1248
1269
  },
@@ -1254,6 +1275,8 @@ const variants = cva({
1254
1275
  ],
1255
1276
  className: [
1256
1277
  '*:data-[slot=media]:*:rounded-3xl',
1278
+ // Make non-video media (image/picture) fill the aspect-ratio-constrained Media box
1279
+ '*:data-[slot=media]:not-has-[video]:*:size-full',
1257
1280
  '*:data-[slot=carousel]:relative **:data-[slot=carousel-container]:rounded-3xl **:data-[slot=carousel-controls]:absolute **:data-[slot=carousel-controls]:right-4 **:data-[slot=carousel-controls]:bottom-4'
1258
1281
  ]
1259
1282
  }
@@ -1349,11 +1372,12 @@ const Modal = ({ isDismissable = true, isOpen, onOpenChange, defaultOpen, classN
1349
1372
  onOpenChange: onOpenChange,
1350
1373
  defaultOpen: defaultOpen,
1351
1374
  isDismissable: isDismissable,
1375
+ isKeyboardDismissDisabled: !isDismissable,
1352
1376
  zIndex: zIndex,
1353
1377
  fullscreen: fullscreen,
1354
1378
  children: /*#__PURE__*/ jsx(Modal$1, {
1355
1379
  ...restProps,
1356
- className: ({ isEntering, isExiting })=>cx(className, 'overflow-auto bg-white text-left shadow-xl', fullscreen ? 'fixed inset-0' : 'w-full max-w-md rounded-2xl p-4 align-middle', isEntering && 'zoom-in-95 animate-in duration-300 ease-out', isExiting && 'zoom-out-95 animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1380
+ className: ({ isEntering, isExiting })=>cx(className, 'overflow-auto bg-white text-left shadow-xl', fullscreen ? 'fixed inset-0' : 'w-full max-w-md rounded-2xl align-middle', isEntering && 'zoom-in-95 animate-in duration-300 ease-out', isExiting && 'zoom-out-95 animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
1357
1381
  'motion-reduce:animate-none')
1358
1382
  })
1359
1383
  })
@@ -1361,7 +1385,7 @@ const Modal = ({ isDismissable = true, isOpen, onOpenChange, defaultOpen, classN
1361
1385
  };
1362
1386
  const Dialog = ({ className, children, ...restProps })=>/*#__PURE__*/ jsx(Dialog$1, {
1363
1387
  ...restProps,
1364
- className: cx(className, 'relative flex flex-col gap-y-5 outline-none', // Footer
1388
+ className: cx(className, 'relative flex flex-col gap-y-5 p-4 outline-none', // Footer
1365
1389
  '**:data-[slot="footer"]:flex **:data-[slot="footer"]:gap-x-2'),
1366
1390
  children: ({ close })=>/*#__PURE__*/ jsx(Provider, {
1367
1391
  values: [
@@ -2051,6 +2075,123 @@ function Combobox(props) {
2051
2075
  return render ? render(formatted) : formatted;
2052
2076
  };
2053
2077
 
2078
+ const drawerVariants = cva({
2079
+ base: [
2080
+ 'fixed overflow-auto bg-white text-left shadow-xl',
2081
+ 'motion-reduce:animate-none'
2082
+ ],
2083
+ variants: {
2084
+ placement: {
2085
+ right: 'top-0 right-0 h-dvh w-full max-w-md rounded-l-2xl',
2086
+ left: 'top-0 left-0 h-dvh w-full max-w-md rounded-r-2xl',
2087
+ top: 'inset-x-0 top-0 max-h-[80dvh] w-full rounded-b-2xl',
2088
+ bottom: 'inset-x-0 bottom-0 max-h-[80dvh] w-full rounded-t-2xl'
2089
+ },
2090
+ isEntering: {
2091
+ true: 'animate-in duration-300 ease-out'
2092
+ },
2093
+ isExiting: {
2094
+ true: 'animate-out duration-200 ease-in'
2095
+ }
2096
+ },
2097
+ compoundVariants: [
2098
+ {
2099
+ placement: 'right',
2100
+ isEntering: true,
2101
+ className: 'slide-in-from-right'
2102
+ },
2103
+ {
2104
+ placement: 'right',
2105
+ isExiting: true,
2106
+ className: 'slide-out-to-right'
2107
+ },
2108
+ {
2109
+ placement: 'left',
2110
+ isEntering: true,
2111
+ className: 'slide-in-from-left'
2112
+ },
2113
+ {
2114
+ placement: 'left',
2115
+ isExiting: true,
2116
+ className: 'slide-out-to-left'
2117
+ },
2118
+ {
2119
+ placement: 'top',
2120
+ isEntering: true,
2121
+ className: 'slide-in-from-top'
2122
+ },
2123
+ {
2124
+ placement: 'top',
2125
+ isExiting: true,
2126
+ className: 'slide-out-to-top'
2127
+ },
2128
+ {
2129
+ placement: 'bottom',
2130
+ isEntering: true,
2131
+ className: 'slide-in-from-bottom'
2132
+ },
2133
+ {
2134
+ placement: 'bottom',
2135
+ isExiting: true,
2136
+ className: 'slide-out-to-bottom'
2137
+ }
2138
+ ]
2139
+ });
2140
+ const Drawer = ({ isDismissable = true, isOpen, onOpenChange, defaultOpen, className, zIndex = 10, placement = 'right', style = {}, ...restProps })=>{
2141
+ const locale = _useLocale();
2142
+ return /*#__PURE__*/ jsx(Provider, {
2143
+ values: [
2144
+ [
2145
+ HeadingContext,
2146
+ {
2147
+ slots: {
2148
+ [DEFAULT_SLOT]: {},
2149
+ title: {
2150
+ className: 'heading-s',
2151
+ _outerWrapper: (children)=>/*#__PURE__*/ jsxs("div", {
2152
+ className: "flex items-center justify-between gap-x-2",
2153
+ children: [
2154
+ children,
2155
+ isDismissable && /*#__PURE__*/ jsx(Button, {
2156
+ slot: "close",
2157
+ variant: "tertiary",
2158
+ className: "data-focus-visible:outline-focus-inset px-2.5!",
2159
+ "aria-label": translations$1.close[locale],
2160
+ onPress: ()=>onOpenChange?.(false),
2161
+ children: /*#__PURE__*/ jsx(Close, {})
2162
+ })
2163
+ ]
2164
+ })
2165
+ }
2166
+ }
2167
+ }
2168
+ ]
2169
+ ],
2170
+ children: /*#__PURE__*/ jsx(ModalOverlay, {
2171
+ isOpen: isOpen,
2172
+ onOpenChange: onOpenChange,
2173
+ defaultOpen: defaultOpen,
2174
+ isDismissable: isDismissable,
2175
+ isKeyboardDismissDisabled: !isDismissable,
2176
+ style: {
2177
+ zIndex,
2178
+ ...style
2179
+ },
2180
+ className: ({ isEntering, isExiting })=>cx('fixed inset-0 bg-black/25 backdrop-blur-sm', isEntering && 'fade-in animate-in duration-300 ease-out', isExiting && 'fade-out animate-out duration-200 ease-in', // Using the motion-safe class does not work, so we use motion-reduce to overwrite instead
2181
+ 'motion-reduce:animate-none'),
2182
+ children: /*#__PURE__*/ jsx(Modal$1, {
2183
+ ...restProps,
2184
+ className: ({ isEntering, isExiting })=>drawerVariants({
2185
+ placement,
2186
+ isEntering,
2187
+ isExiting,
2188
+ className
2189
+ })
2190
+ })
2191
+ })
2192
+ });
2193
+ };
2194
+
2054
2195
  /**
2055
2196
  * A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress.
2056
2197
  */ const FileTrigger = (props)=>{
@@ -2453,6 +2594,157 @@ function NumberField(props) {
2453
2594
  });
2454
2595
  }
2455
2596
 
2597
+ // Lives here, not in `translations`, because it interpolates the hidden range
2598
+ // — the static-string `translations` map doesn't support function values.
2599
+ const HIDDEN_PAGES_LABEL = {
2600
+ nb: (start, end)=>start === end ? `Skjuler side ${start}` : `Skjuler side ${start} til ${end}`,
2601
+ sv: (start, end)=>start === end ? `Döljer sida ${start}` : `Döljer sida ${start} till ${end}`,
2602
+ en: (start, end)=>start === end ? `Hides page ${start}` : `Hides pages ${start} to ${end}`
2603
+ };
2604
+ // Number of pages shown on each side of the current page on desktop. On
2605
+ // narrow containers, CSS hides the outermost visible pages via the
2606
+ // `data-outer` attribute so the entire pagination fits a 320px viewport
2607
+ // without changing the rendered DOM.
2608
+ const SIBLING_COUNT = 2;
2609
+ /**
2610
+ * Returns the page numbers (1-indexed) that should be rendered between the
2611
+ * always-visible first page and the next/prev buttons. Mirrors v1's ellipsis
2612
+ * behaviour: only a single (left) ellipsis is ever shown, and the last page
2613
+ * is not guaranteed to be rendered.
2614
+ *
2615
+ * Page 1 is filtered out because it's rendered separately as a fixed slot —
2616
+ * keeping it here would duplicate when the special-case extension lands the
2617
+ * window on it.
2618
+ */ function getVisiblePages(currentPage, pageCount) {
2619
+ const end = Math.min(Math.max(2 + SIBLING_COUNT * 2, currentPage + SIBLING_COUNT), pageCount);
2620
+ let start = Math.max(Math.min(currentPage - SIBLING_COUNT, end - SIBLING_COUNT * 2), 1);
2621
+ // When `start` lands exactly SIBLING_COUNT pages past page 1, we have room
2622
+ // to render one extra page to the left without needing an ellipsis.
2623
+ if (start - SIBLING_COUNT === 0) {
2624
+ start = start - 1;
2625
+ }
2626
+ const pages = Array.from({
2627
+ length: end - start
2628
+ }, (_, i)=>start + i + 1);
2629
+ return pages.filter((p)=>p > 1);
2630
+ }
2631
+ /**
2632
+ * Returns the indices of the two visible pages farthest from `currentPage`.
2633
+ * These get marked with `data-outer` so the CSS container query can hide them
2634
+ * on narrow viewports, dropping the slot count from 8 to 6 without changing
2635
+ * the rendered DOM. Returns an empty set when there's nothing to gain.
2636
+ */ function getOuterIndices(visiblePages, currentPage) {
2637
+ if (visiblePages.length <= 3) {
2638
+ return new Set();
2639
+ }
2640
+ return new Set(visiblePages.map((p, i)=>({
2641
+ i,
2642
+ distance: Math.abs(p - currentPage)
2643
+ })).sort((a, b)=>b.distance - a.distance).slice(0, 2).map((entry)=>entry.i));
2644
+ }
2645
+ const Pagination = (props)=>{
2646
+ const { page, count, getItemHref, onChange, className } = props;
2647
+ const locale = _useLocale();
2648
+ const currentPage = Math.max(1, Math.min(page, Math.max(1, count)));
2649
+ const pageCount = Math.max(1, count);
2650
+ const visiblePages = getVisiblePages(currentPage, pageCount);
2651
+ const outerIndices = getOuterIndices(visiblePages, currentPage);
2652
+ // Show the left ellipsis whenever there's a gap between page 1 and the
2653
+ // first visible page. This matches v1 for SIBLING_COUNT=2.
2654
+ const showLeftEllipsis = visiblePages.length > 0 && visiblePages[0] > 2;
2655
+ const canGoPrev = currentPage > 1;
2656
+ const canGoNext = currentPage < pageCount;
2657
+ // Prev/next switches element type (<a> with href ↔ <button> without) when
2658
+ // crossing a boundary. React replaces the DOM node, so the browser blurs
2659
+ // the focused element — restore it manually after the re-render.
2660
+ const prevButtonRef = useRef(null);
2661
+ const nextButtonRef = useRef(null);
2662
+ const restoreFocusToRef = useRef(null);
2663
+ useLayoutEffect(()=>{
2664
+ if (restoreFocusToRef.current === 'prev') {
2665
+ prevButtonRef.current?.focus();
2666
+ } else if (restoreFocusToRef.current === 'next') {
2667
+ nextButtonRef.current?.focus();
2668
+ }
2669
+ restoreFocusToRef.current = null;
2670
+ }, [
2671
+ currentPage
2672
+ ]);
2673
+ return /*#__PURE__*/ jsxs("ol", {
2674
+ "aria-label": translations$1.pagination[locale],
2675
+ className: cx('gm-pagination', className),
2676
+ children: [
2677
+ /*#__PURE__*/ jsx("li", {
2678
+ children: /*#__PURE__*/ jsx(Button, {
2679
+ "aria-disabled": !canGoPrev,
2680
+ "aria-label": translations$1.previousPage[locale],
2681
+ color: "white",
2682
+ href: canGoPrev ? getItemHref(currentPage - 1) : undefined,
2683
+ isIconOnly: true,
2684
+ onPress: canGoPrev ? ()=>{
2685
+ restoreFocusToRef.current = 'prev';
2686
+ onChange?.(currentPage - 1);
2687
+ } : undefined,
2688
+ ref: prevButtonRef,
2689
+ children: /*#__PURE__*/ jsx(ChevronLeft, {})
2690
+ })
2691
+ }),
2692
+ /*#__PURE__*/ jsx(PageItem, {
2693
+ getItemHref: getItemHref,
2694
+ isActive: currentPage === 1,
2695
+ onChange: onChange,
2696
+ page: 1
2697
+ }),
2698
+ showLeftEllipsis && /*#__PURE__*/ jsxs("li", {
2699
+ "data-slot": "ellipsis",
2700
+ children: [
2701
+ /*#__PURE__*/ jsx("span", {
2702
+ "aria-hidden": "true",
2703
+ children: "…"
2704
+ }),
2705
+ /*#__PURE__*/ jsx("span", {
2706
+ children: HIDDEN_PAGES_LABEL[locale](2, visiblePages[0] - 1)
2707
+ })
2708
+ ]
2709
+ }),
2710
+ visiblePages.map((p, i)=>/*#__PURE__*/ jsx(PageItem, {
2711
+ getItemHref: getItemHref,
2712
+ isActive: p === currentPage,
2713
+ isOuter: outerIndices.has(i),
2714
+ onChange: onChange,
2715
+ page: p
2716
+ }, p)),
2717
+ /*#__PURE__*/ jsx("li", {
2718
+ children: /*#__PURE__*/ jsx(Button, {
2719
+ "aria-disabled": !canGoNext,
2720
+ "aria-label": translations$1.nextPage[locale],
2721
+ color: "white",
2722
+ href: canGoNext ? getItemHref(currentPage + 1) : undefined,
2723
+ isIconOnly: true,
2724
+ onPress: canGoNext ? ()=>{
2725
+ restoreFocusToRef.current = 'next';
2726
+ onChange?.(currentPage + 1);
2727
+ } : undefined,
2728
+ ref: nextButtonRef,
2729
+ children: /*#__PURE__*/ jsx(ChevronRight, {})
2730
+ })
2731
+ })
2732
+ ]
2733
+ });
2734
+ };
2735
+ const PageItem = ({ page, isActive, isOuter, getItemHref, onChange })=>{
2736
+ return /*#__PURE__*/ jsx("li", {
2737
+ "data-outer": isOuter || undefined,
2738
+ children: /*#__PURE__*/ jsx(Button, {
2739
+ "aria-current": isActive ? 'page' : undefined,
2740
+ color: isActive ? 'blue' : 'white',
2741
+ href: getItemHref(page),
2742
+ onPress: ()=>onChange?.(page),
2743
+ children: page
2744
+ })
2745
+ });
2746
+ };
2747
+
2456
2748
  const defaultClasses = cx([
2457
2749
  'relative -ml-2.5 inline-flex max-w-fit cursor-pointer items-start gap-4 py-2.5 pl-2.5 leading-7',
2458
2750
  // the radio button itself
@@ -3358,4 +3650,4 @@ const VideoLoop = ({ src, format, alt, className })=>{
3358
3650
  });
3359
3651
  };
3360
3652
 
3361
- export { Accordion, AccordionItem, Alertbox, Avatar, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, ButtonContext, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, Disclosure, DisclosureButton, DisclosurePanel, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Label, Link, LinkList, LinkListContainer, LinkListContext, LinkListItem, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, Tab, TabList, TabPanel, Tabs, Tag, TagGroup, TagList, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselButton as UNSAFE_CarouselButton, CarouselContext as UNSAFE_CarouselContext, CarouselControls as UNSAFE_CarouselControls, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, CarouselItemsContainer as UNSAFE_CarouselItemsContainer, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, FileUpload as UNSAFE_FileUpload, Hero as UNSAFE_Hero, HeroContext as UNSAFE_HeroContext, Modal as UNSAFE_Modal, ResizableTableContainer as UNSAFE_ResizableTableContainer, Step as UNSAFE_Step, Stepper as UNSAFE_Stepper, Table as UNSAFE_Table, TableBody as UNSAFE_TableBody, TableCell as UNSAFE_TableCell, TableColumn as UNSAFE_TableColumn, TableColumnResizer as UNSAFE_TableColumnResizer, UNSAFE_TableContainer, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, VideoLoop, _useLocale as useLocale };
3653
+ export { Accordion, AccordionItem, Alertbox, Avatar, Backlink, Badge, Breadcrumb, Breadcrumbs, Button, ButtonContext, Caption, Card, CardLink, Checkbox, CheckboxGroup, Combobox, ListBoxHeader as ComboboxHeader, ListBoxItem as ComboboxItem, ListBoxSection as ComboboxSection, Content, ContentContext, DateFormatter, Description, Disclosure, DisclosureButton, DisclosurePanel, DisclosureStateContext, ErrorMessage, Footer, GrunnmurenProvider, Heading, HeadingContext, Hero, HeroContext, Label, Link, LinkList, LinkListContainer, LinkListContext, LinkListItem, Media, MediaContext, NumberField, Radio, RadioGroup, Select, ListBoxHeader as SelectHeader, ListBoxItem as SelectItem, ListBoxSection as SelectSection, Tab, TabList, TabPanel, Tabs, Tag, TagGroup, TagList, TextArea, TextField, Carousel as UNSAFE_Carousel, CarouselButton as UNSAFE_CarouselButton, CarouselContext as UNSAFE_CarouselContext, CarouselControls as UNSAFE_CarouselControls, CarouselItem as UNSAFE_CarouselItem, CarouselItems as UNSAFE_CarouselItems, CarouselItemsContainer as UNSAFE_CarouselItemsContainer, Dialog as UNSAFE_Dialog, DialogTrigger as UNSAFE_DialogTrigger, Drawer as UNSAFE_Drawer, FileUpload as UNSAFE_FileUpload, Modal as UNSAFE_Modal, Pagination as UNSAFE_Pagination, ResizableTableContainer as UNSAFE_ResizableTableContainer, Step as UNSAFE_Step, Stepper as UNSAFE_Stepper, Table as UNSAFE_Table, TableBody as UNSAFE_TableBody, TableCell as UNSAFE_TableCell, TableColumn as UNSAFE_TableColumn, TableColumnResizer as UNSAFE_TableColumnResizer, UNSAFE_TableContainer, TableHeader as UNSAFE_TableHeader, TableRow as UNSAFE_TableRow, VideoLoop, _useLocale as useLocale };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obosbbl/grunnmuren-react",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Grunnmuren components in React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,14 +26,15 @@
26
26
  "react-aria-components": "^1.17.0",
27
27
  "react-stately": "^3.46.0",
28
28
  "use-debounce": "^10.0.4",
29
- "@obosbbl/grunnmuren-icons-react": "^2.2.0"
29
+ "@obosbbl/grunnmuren-icons-react": "^2.2.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^24.0.0",
33
33
  "tailwindcss": "4.2.2"
34
34
  },
35
35
  "peerDependencies": {
36
- "react": "^19"
36
+ "react": "^19",
37
+ "@obosbbl/grunnmuren-tailwind": "^2.4.11"
37
38
  },
38
39
  "scripts": {
39
40
  "build": "bunchee"