@vygruppen/spor-react 13.1.3 → 13.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vygruppen/spor-react",
3
3
  "type": "module",
4
- "version": "13.1.3",
4
+ "version": "13.2.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -64,8 +64,8 @@
64
64
  "react-dom": "19.2.3",
65
65
  "tsup": "^7.2.0",
66
66
  "typescript": "^5.7.3",
67
- "@vygruppen/eslint-config": "2.2.0",
68
- "@vygruppen/tsconfig": "0.1.1"
67
+ "@vygruppen/tsconfig": "0.1.1",
68
+ "@vygruppen/eslint-config": "2.2.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": ">=19.0.0",
@@ -1,94 +1,156 @@
1
1
  "use client";
2
- import { CheckboxCard, CheckboxCardRootProps, Span } from "@chakra-ui/react";
3
- import { CloseOutline24Icon } from "@vygruppen/spor-icon-react";
4
- import React from "react";
2
+ import {
3
+ RadioCard as ChakraRadioCard,
4
+ RecipeVariantProps,
5
+ Span,
6
+ useSlotRecipe,
7
+ } from "@chakra-ui/react";
8
+ import { createContext, useContext, useId } from "react";
5
9
 
6
- type CheckBoxIcon = {
7
- default: React.ReactNode;
8
- checked: React.ReactNode;
9
- };
10
-
11
- export type ChoiceChipProps = Omit<
12
- CheckboxCardRootProps,
13
- "onCheckedChange" | "checked"
14
- > & {
15
- icon?: CheckBoxIcon;
16
- onCheckedChange?: (checked: boolean) => void;
17
- checked?: boolean;
18
- };
10
+ import { choiceChipSlotRecipe } from "@/theme/slot-recipes/choice-chip";
19
11
 
20
12
  /**
21
- * Choice chips are checkboxes that look like selectable buttons.
13
+ * Choice chips are radio buttons that look like selectable buttons, allowing only one selection at a time.
22
14
  *
23
15
  * Choice chips are available in four different sizes - `xs`, `sm`, `md` and `lg`.
24
16
  *
25
17
  * ```tsx
26
- * <Stack flexDirection="row">
27
- * <ChoiceChip size="lg">Bus</ChoiceChip>
28
- * <ChoiceChip size="lg">Train</ChoiceChip>
29
- * </Stack>
18
+ * <ChoiceChipGroup defaultValue="economy">
19
+ * <ChoiceChip value="economy">Economy</ChoiceChip>
20
+ * <ChoiceChip value="business">Business</ChoiceChip>
21
+ * <ChoiceChip value="first-class">First Class</ChoiceChip>
22
+ * </ChoiceChipGroup>
30
23
  * ```
31
24
  *
32
- * There are also three different chipType - `icon`, `choice` and `filter`.
33
- *
34
- * ```tsx
35
- * <Stack flexDirection="row">
36
- * <ChoiceChip chipType="icon" icon={<Bus24Icon />}>Bus</ChoiceChip>
37
- * <ChoiceChip chipType="choice" icon={<Bus24Icon />}>Bus</ChoiceChip>
38
- * <ChoiceChip chipType="filter" icon={<Bus24Icon />}>Bus</ChoiceChip>
39
- * </Stack>
40
- *
41
25
  * There are also three different variants - `core`, `accent` and `floating`.
42
26
  *
43
27
  * ```tsx
44
- * <Stack flexDirection="row">
45
- * <ChoiceChip variant="core">Bus</ChoiceChip>
46
- * <ChoiceChip variant="accent">Boat</ChoiceChip>
47
- * <ChoiceChip variant="floating">Train</ChoiceChip>
48
- * </Stack>
28
+ * <>
29
+ * <ChoiceChipGroup defaultValue="bus" variant="core">
30
+ * <ChoiceChip value="bus">Bus</ChoiceChip>
31
+ * </ChoiceChipGroup>
32
+ * <ChoiceChipGroup defaultValue="bus" variant="accent">
33
+ * <ChoiceChip value="bus">Bus</ChoiceChip>
34
+ * </ChoiceChipGroup>
35
+ * <ChoiceChipGroup defaultValue="bus" variant="floating">
36
+ * <ChoiceChip value="bus">Bus</ChoiceChip>
37
+ * </ChoiceChipGroup>
38
+ * </>
49
39
  * ```
40
+ *
41
+ * @see https://spor.vy.no/components/choice-chip
50
42
  */
51
43
 
44
+ type RadioCardVariantProps = RecipeVariantProps<typeof choiceChipSlotRecipe>;
45
+
46
+ type Variant = Pick<RadioCardRootProps, "variant" | "size">;
47
+
48
+ const ChoiceChipContext = createContext<Variant>({
49
+ variant: "core",
50
+ size: "sm",
51
+ });
52
+ const useChoiceChipContext = () => useContext(ChoiceChipContext);
53
+
54
+ type CheckBoxIcon = {
55
+ default: React.ReactNode;
56
+ checked: React.ReactNode;
57
+ };
58
+
59
+ type RadioCardItemProps = Exclude<
60
+ ChakraRadioCard.ItemProps,
61
+ "colorPalette" | "indicator" | "variant" | "size" | "addon"
62
+ > &
63
+ RadioCardVariantProps & {
64
+ inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
65
+ ariaLabel?: string;
66
+ icon?: CheckBoxIcon;
67
+ };
68
+
52
69
  export const ChoiceChip = ({
53
70
  ref,
54
- children,
55
- icon,
56
- onCheckedChange,
57
- ...rootProps
58
- }: ChoiceChipProps & {
71
+ ...props
72
+ }: RadioCardItemProps & {
59
73
  ref?: React.Ref<HTMLInputElement>;
60
74
  }) => {
75
+ const { children, inputProps, icon, variant, size, css, ...rest } = props;
76
+ const { variant: contextVariant, size: contextSize } = useChoiceChipContext();
77
+
78
+ const uniqueId = useId();
79
+ const itemControlId = `radio-card-item-control-${uniqueId}`;
80
+
81
+ const inputHasAriaLabel =
82
+ inputProps?.["aria-labelledby"] || inputProps?.["aria-label"];
83
+
84
+ const finalVariant = variant ?? contextVariant;
85
+ const finalSize = size ?? contextSize;
86
+
87
+ const recipe = useSlotRecipe({ key: "choiceChip" });
88
+ const styles = recipe({
89
+ variant: finalVariant,
90
+ size: finalSize,
91
+ });
92
+
93
+ return (
94
+ <ChakraRadioCard.Item {...rest} css={{ ...css, ...styles.item }}>
95
+ <ChakraRadioCard.ItemHiddenInput
96
+ aria-labelledby={
97
+ inputHasAriaLabel ? inputProps?.["aria-labelledby"] : itemControlId
98
+ }
99
+ ref={ref}
100
+ {...inputProps}
101
+ />
102
+ <ChakraRadioCard.ItemControl
103
+ id={itemControlId}
104
+ aria-hidden
105
+ css={styles.itemControl}
106
+ >
107
+ <ChakraRadioCard.ItemContext>
108
+ {({ checked }) => (
109
+ <ChakraRadioCard.Label css={styles.label}>
110
+ {checked
111
+ ? icon?.checked && <Span>{icon.checked}</Span>
112
+ : icon?.default && <Span>{icon.default}</Span>}
113
+ {children}
114
+ </ChakraRadioCard.Label>
115
+ )}
116
+ </ChakraRadioCard.ItemContext>
117
+ </ChakraRadioCard.ItemControl>
118
+ </ChakraRadioCard.Item>
119
+ );
120
+ };
121
+
122
+ type RadioCardRootProps = Exclude<
123
+ ChakraRadioCard.RootProps,
124
+ "variant" | "size"
125
+ > &
126
+ RadioCardVariantProps & {
127
+ children: React.ReactNode;
128
+ gap?: string | number;
129
+ direction?: "row" | "column";
130
+ display?: string;
131
+ };
132
+
133
+ export const ChoiceChipGroup = ({
134
+ ref,
135
+ ...props
136
+ }: RadioCardRootProps & {
137
+ ref?: React.Ref<HTMLDivElement>;
138
+ }) => {
139
+ const { children, variant, size, css, ...rest } = props;
140
+ const recipe = useSlotRecipe({ key: "choiceChip" });
141
+ const styles = recipe({ variant, size });
61
142
  return (
62
- <CheckboxCard.Root
63
- {...rootProps}
64
- {...(onCheckedChange && {
65
- onCheckedChange: (details) => onCheckedChange(!!details.checked),
66
- })}
67
- >
68
- <CheckboxCard.Context>
69
- {({ checked }) => (
70
- <>
71
- <CheckboxCard.HiddenInput ref={ref} />
72
- <CheckboxCard.Control>
73
- <CheckboxCard.Content>
74
- <CheckboxCard.Label>
75
- {icon && (
76
- <Span width="24px">
77
- {checked ? icon.checked : icon.default}
78
- </Span>
79
- )}
80
-
81
- {rootProps.chipType !== "icon" && children}
82
-
83
- {rootProps.chipType === "filter" && checked && (
84
- <CloseOutline24Icon />
85
- )}
86
- </CheckboxCard.Label>
87
- </CheckboxCard.Content>
88
- </CheckboxCard.Control>
89
- </>
90
- )}
91
- </CheckboxCard.Context>
92
- </CheckboxCard.Root>
143
+ <ChoiceChipContext.Provider value={{ variant, size }}>
144
+ <ChakraRadioCard.Root
145
+ css={{ ...styles.root, ...css }}
146
+ ref={ref}
147
+ variant={variant}
148
+ {...rest}
149
+ >
150
+ {children}
151
+ </ChakraRadioCard.Root>
152
+ </ChoiceChipContext.Provider>
93
153
  );
94
154
  };
155
+
156
+ export const ChoiceChipLabel = ChakraRadioCard.Label;
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { CheckboxCard, CheckboxCardRootProps, Span } from "@chakra-ui/react";
3
+ import { CloseOutline24Icon } from "@vygruppen/spor-icon-react";
4
+ import React from "react";
5
+
6
+ type CheckBoxIcon = {
7
+ default: React.ReactNode;
8
+ checked: React.ReactNode;
9
+ };
10
+
11
+ export type FilterChipProps = Omit<
12
+ CheckboxCardRootProps,
13
+ "onCheckedChange" | "checked"
14
+ > & {
15
+ icon?: CheckBoxIcon;
16
+ onCheckedChange?: (checked: boolean) => void;
17
+ checked?: boolean;
18
+ };
19
+
20
+ /**
21
+ * Filter chips are checkboxes that look like selectable buttons.
22
+ *
23
+ * Filter chips are available in four different sizes - `xs`, `sm`, `md` and `lg`.
24
+ *
25
+ * ```tsx
26
+ * <Stack flexDirection="row">
27
+ * <FilterChip size="lg">Bus</FilterChip>
28
+ * <FilterChip size="lg">Train</FilterChip>
29
+ * </Stack>
30
+ * ```
31
+ *
32
+ * There are also three different variants - `core`, `accent` and `floating`.
33
+ *
34
+ * ```tsx
35
+ * <Stack flexDirection="row">
36
+ * <FilterChip variant="core">Bus</FilterChip>
37
+ * <FilterChip variant="accent">Boat</FilterChip>
38
+ * <FilterChip variant="floating">Train</FilterChip>
39
+ * </Stack>
40
+ * ```
41
+ *
42
+ * @see https://spor.vy.no/components/filter-chip
43
+ */
44
+
45
+ export const FilterChip = ({
46
+ ref,
47
+ children,
48
+ icon,
49
+ onCheckedChange,
50
+ ...rootProps
51
+ }: FilterChipProps & {
52
+ ref?: React.Ref<HTMLInputElement>;
53
+ }) => {
54
+ return (
55
+ <CheckboxCard.Root
56
+ {...rootProps}
57
+ {...(onCheckedChange && {
58
+ onCheckedChange: (details) => onCheckedChange(!!details.checked),
59
+ })}
60
+ >
61
+ <CheckboxCard.Context>
62
+ {({ checked }) => (
63
+ <>
64
+ <CheckboxCard.HiddenInput ref={ref} />
65
+ <CheckboxCard.Control>
66
+ <CheckboxCard.Content>
67
+ <CheckboxCard.Label>
68
+ {checked
69
+ ? icon?.checked && <Span>{icon.checked}</Span>
70
+ : icon?.default && <Span>{icon.default}</Span>}
71
+
72
+ {rootProps.chipType !== "icon" && children}
73
+
74
+ {rootProps.chipType === "filter" && checked && (
75
+ <CloseOutline24Icon />
76
+ )}
77
+ </CheckboxCard.Label>
78
+ </CheckboxCard.Content>
79
+ </CheckboxCard.Control>
80
+ </>
81
+ )}
82
+ </CheckboxCard.Context>
83
+ </CheckboxCard.Root>
84
+ );
85
+ };
@@ -69,7 +69,7 @@ export const PasswordInput = ({
69
69
  return (
70
70
  <Input
71
71
  ref={ref}
72
- startElement={startElement && startElement}
72
+ startElement={startElement}
73
73
  label={label}
74
74
  type={visible ? "text" : "password"}
75
75
  endElement={
@@ -7,6 +7,7 @@ export * from "./ChoiceChip";
7
7
  export * from "./Combobox";
8
8
  export * from "./Field";
9
9
  export * from "./Fieldset";
10
+ export * from "./FilterChip";
10
11
  export * from "./Input";
11
12
  export * from "./InputChip";
12
13
  export * from "./ListBox";
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import {
4
+ Box,
4
5
  Popover as ChakraPopover,
5
6
  Portal,
6
7
  usePopoverContext,
@@ -22,8 +23,13 @@ export const PopoverTrigger = ({
22
23
  const isStringChild = typeof children === "string";
23
24
 
24
25
  return (
25
- <ChakraPopover.Trigger {...props} ref={ref} asChild={!isStringChild}>
26
- {children}
26
+ <ChakraPopover.Trigger
27
+ ref={ref}
28
+ asChild={!isStringChild}
29
+ width={isStringChild ? undefined : "fit-content"}
30
+ {...props}
31
+ >
32
+ {isStringChild ? children : <Box>{children}</Box>}
27
33
  </ChakraPopover.Trigger>
28
34
  );
29
35
  };
@@ -58,6 +64,7 @@ export const PopoverContent = ({
58
64
  <ChakraPopover.Positioner>
59
65
  <ChakraPopover.Content ref={ref} {...props}>
60
66
  <ChakraPopover.Arrow />
67
+ <ChakraPopover.Body {...props}>{children}</ChakraPopover.Body>
61
68
  {showCloseButton && (
62
69
  <div>
63
70
  <ChakraPopover.CloseTrigger asChild>
@@ -68,7 +75,6 @@ export const PopoverContent = ({
68
75
  </ChakraPopover.CloseTrigger>
69
76
  </div>
70
77
  )}
71
- <ChakraPopover.Body {...props}>{children}</ChakraPopover.Body>
72
78
  </ChakraPopover.Content>
73
79
  </ChakraPopover.Positioner>
74
80
  </Portal>
@@ -158,6 +158,7 @@ export const radioCardAnatomy = createAnatomy("radio-card").parts(
158
158
  "itemText",
159
159
  "itemDescription",
160
160
  "itemContent",
161
+ "itemControl",
161
162
  );
162
163
 
163
164
  export const radioAnatomy = createAnatomy("radio").parts(
@@ -1,43 +1,33 @@
1
1
  import { defineSlotRecipe } from "@chakra-ui/react";
2
2
 
3
- import { checkboxCardAnatomy } from "./anatomy";
3
+ import { radioCardAnatomy } from "./anatomy";
4
4
 
5
5
  export const choiceChipSlotRecipe = defineSlotRecipe({
6
- slots: checkboxCardAnatomy.keys(),
7
- className: "chakra-checkbox-card",
6
+ className: "spor-choice-chip",
7
+ slots: radioCardAnatomy.keys(),
8
8
  base: {
9
9
  root: {
10
- display: "inline-flex",
11
- alignItems: "center",
12
- boxAlign: "center",
13
- cursor: "pointer",
10
+ display: "flex",
11
+ flexDirection: "row",
12
+ gap: "1",
13
+ width: "fit-content",
14
+ },
15
+ item: {
16
+ display: "flex-inline",
14
17
  transitionProperty: "all",
15
- borderRadius: "xl",
16
18
  transitionDuration: "fast",
17
- paddingInlineStart: "2",
18
- paddingInlineEnd: "2",
19
19
 
20
- outline: "1px solid",
21
- outlineColor: "outline.core",
22
20
  _checked: {
23
- backgroundColor: "surface.brand",
24
- borderRadius: "sm",
25
21
  outline: "none",
26
- color: "text.brand",
22
+ _focusVisible: {
23
+ outline: "2px solid",
24
+ outlineColor: "outline.focus",
25
+ outlineOffset: "1px",
26
+ },
27
27
  _hover: {
28
- backgroundColor: "surface.brand.hover",
29
- _active: {
30
- backgroundColor: "surface.brand.active",
31
- },
28
+ outline: "none",
32
29
  },
33
30
  },
34
-
35
- _focusVisible: {
36
- outline: "2px solid",
37
- outlineColor: "outline.focus",
38
- outlineOffset: "1px",
39
- },
40
-
41
31
  _disabled: {
42
32
  pointerEvents: "none",
43
33
  boxShadow: "none",
@@ -63,34 +53,44 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
63
53
  },
64
54
  },
65
55
  },
66
-
56
+ itemControl: {
57
+ display: "flex",
58
+ alignItems: "center",
59
+ justifyContent: "center",
60
+ },
67
61
  label: {
68
62
  display: "flex",
69
63
  alignItems: "center",
64
+ justifyContent: "center",
70
65
  gap: "1",
71
66
  },
72
67
  },
73
-
74
68
  variants: {
75
69
  size: {
76
70
  xs: {
77
- root: {
71
+ item: {
72
+ borderRadius: "xl",
78
73
  _checked: {
79
- borderRadius: "0.563rem",
74
+ borderRadius: "9px",
80
75
  },
76
+ },
77
+ itemControl: {
81
78
  height: 5,
82
79
  paddingX: 1.5,
83
80
  },
84
81
  label: {
85
82
  fontSize: { base: "mobile.sm", sm: "desktop.sm" },
86
- fontWeight: "medium",
83
+ fontWeight: "regular",
87
84
  },
88
85
  },
89
86
  sm: {
90
- root: {
87
+ item: {
88
+ borderRadius: "xl",
91
89
  _checked: {
92
90
  borderRadius: "sm",
93
91
  },
92
+ },
93
+ itemControl: {
94
94
  height: 6,
95
95
  paddingX: 2,
96
96
  },
@@ -100,10 +100,13 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
100
100
  },
101
101
  },
102
102
  md: {
103
- root: {
103
+ item: {
104
+ borderRadius: "xl",
104
105
  _checked: {
105
106
  borderRadius: "sm",
106
107
  },
108
+ },
109
+ itemControl: {
107
110
  height: 7,
108
111
  paddingX: 2,
109
112
  },
@@ -113,10 +116,13 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
113
116
  },
114
117
  },
115
118
  lg: {
116
- root: {
119
+ item: {
120
+ borderRadius: "xl",
117
121
  _checked: {
118
122
  borderRadius: "md",
119
123
  },
124
+ },
125
+ itemControl: {
120
126
  height: 8,
121
127
  paddingX: 3,
122
128
  },
@@ -126,13 +132,20 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
126
132
  },
127
133
  },
128
134
  },
129
-
130
135
  variant: {
131
136
  core: {
132
- root: {
133
- color: "text.core",
134
- outlineColor: "outline.core",
135
-
137
+ itemControl: {
138
+ _checked: {
139
+ backgroundColor: "surface.brand",
140
+ color: "text.brand",
141
+ outline: "none",
142
+ _hover: {
143
+ backgroundColor: "surface.brand.hover",
144
+ _active: {
145
+ backgroundColor: "surface.brand.active",
146
+ },
147
+ },
148
+ },
136
149
  _hover: {
137
150
  outline: "2px solid",
138
151
  outlineColor: "outline.core.hover",
@@ -146,11 +159,22 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
146
159
  },
147
160
  },
148
161
  accent: {
149
- root: {
162
+ itemControl: {
150
163
  backgroundColor: "surface.accent",
151
164
  color: "text.accent",
152
165
  outline: "none",
153
-
166
+ border: "none",
167
+ _checked: {
168
+ backgroundColor: "surface.brand",
169
+ color: "text.brand",
170
+ outline: "none",
171
+ _hover: {
172
+ backgroundColor: "surface.brand.hover",
173
+ _active: {
174
+ backgroundColor: "surface.brand.active",
175
+ },
176
+ },
177
+ },
154
178
  _hover: {
155
179
  backgroundColor: "surface.accent.hover",
156
180
 
@@ -161,13 +185,24 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
161
185
  },
162
186
  },
163
187
  floating: {
164
- root: {
188
+ itemControl: {
165
189
  backgroundColor: "surface.floating",
166
190
  outline: "1px solid",
167
191
  outlineColor: "outline.floating",
168
192
  color: "text.floating",
169
193
 
170
194
  boxShadow: "sm",
195
+ _checked: {
196
+ backgroundColor: "surface.brand",
197
+ color: "text.brand",
198
+ outline: "none",
199
+ _hover: {
200
+ backgroundColor: "surface.brand.hover",
201
+ _active: {
202
+ backgroundColor: "surface.brand.active",
203
+ },
204
+ },
205
+ },
171
206
  _hover: {
172
207
  backgroundColor: "surface.floating.hover",
173
208
  outline: "1px solid",
@@ -182,16 +217,9 @@ export const choiceChipSlotRecipe = defineSlotRecipe({
182
217
  },
183
218
  },
184
219
  },
185
- chipType: {
186
- icon: {},
187
- choice: {},
188
- filter: {},
189
- },
190
220
  },
191
-
192
221
  defaultVariants: {
193
222
  size: "sm",
194
223
  variant: "core",
195
- chipType: "choice",
196
224
  },
197
225
  });