@vygruppen/spor-react 13.4.6 → 13.6.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.4.6",
4
+ "version": "13.6.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -46,13 +46,14 @@
46
46
  "react-stately": "^3.31.1",
47
47
  "react-swipeable": "^7.0.1",
48
48
  "usehooks-ts": "^3.1.0",
49
- "@vygruppen/spor-design-tokens": "5.0.6",
49
+ "@vygruppen/spor-design-tokens": "5.1.0",
50
50
  "@vygruppen/spor-icon-react": "5.0.2",
51
51
  "@vygruppen/spor-loader": "0.7.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@react-types/datepicker": "^3.10.0",
55
55
  "@react-types/shared": "^3.27.0",
56
+ "@storybook/react": "^8",
56
57
  "@testing-library/jest-dom": "^6.9.1",
57
58
  "@testing-library/react": "^16.3.2",
58
59
  "@testing-library/user-event": "^14.6.1",
@@ -0,0 +1,51 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ Accordion,
4
+ Box,
5
+ Expandable,
6
+ ExpandableItem,
7
+ } from "@vygruppen/spor-react";
8
+
9
+ const meta = {
10
+ title: "Components/Accordion",
11
+ component: Accordion,
12
+ } satisfies Meta<typeof Accordion>;
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Grouped: Story = {
19
+ render: () => (
20
+ <Box>
21
+ <Accordion collapsible defaultValue={["refunds"]}>
22
+ <ExpandableItem value="refunds" title="Can I change my ticket?">
23
+ Yes, ticket changes depend on fare rules and route availability.
24
+ </ExpandableItem>
25
+ <ExpandableItem value="pets" title="Can I bring my pet?">
26
+ Small pets are usually allowed if they do not disturb other
27
+ passengers.
28
+ </ExpandableItem>
29
+ <ExpandableItem value="bike" title="Can I bring a bicycle?">
30
+ Bicycle availability varies between departures and must be reserved
31
+ when required.
32
+ </ExpandableItem>
33
+ </Accordion>
34
+ </Box>
35
+ ),
36
+ };
37
+
38
+ export const SingleExpandable: Story = {
39
+ render: () => (
40
+ <Box>
41
+ <Expandable
42
+ title="Read more about onboard services"
43
+ variant="floating"
44
+ defaultValue={["single-expandable"]}
45
+ >
46
+ Wi‑Fi, power outlets, and accessible seating may vary by train set and
47
+ route.
48
+ </Expandable>
49
+ </Box>
50
+ ),
51
+ };
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Alert, Flex } from "@vygruppen/spor-react";
3
+
4
+ const meta = {
5
+ title: "Components/Alert",
6
+ component: Alert,
7
+ args: {
8
+ title: "Traffic update",
9
+ children: "Track work may affect departures this afternoon.",
10
+ },
11
+ } satisfies Meta<typeof Alert>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ const variants = [
18
+ "info",
19
+ "important",
20
+ "alt",
21
+ "error",
22
+ "error-secondary",
23
+ "success",
24
+ ] as const;
25
+
26
+ export const Overview: Story = {
27
+ render: () => (
28
+ <Flex gap={2} direction="column">
29
+ {variants.map((variant) => (
30
+ <Alert key={variant} variant={variant} title={variant}>
31
+ Example of the {variant} alert variant.
32
+ </Alert>
33
+ ))}
34
+ </Flex>
35
+ ),
36
+ };
37
+
38
+ export const Closable: Story = {
39
+ args: {
40
+ variant: "info",
41
+ closable: true,
42
+ title: "Dismissible message",
43
+ children: "This alert can be closed by the user.",
44
+ },
45
+ };
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Box, Button, Flex } from "@vygruppen/spor-react";
3
+
4
+ const meta = {
5
+ title: "Components/Button",
6
+ component: Button,
7
+ args: {
8
+ children: "Book trip",
9
+ },
10
+ } satisfies Meta<typeof Button>;
11
+
12
+ export default meta;
13
+
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ const variants = [
17
+ "primary",
18
+ "secondary",
19
+ "tertiary",
20
+ "ghost",
21
+ "floating",
22
+ ] as const;
23
+ const sizes = ["lg", "md", "sm", "xs"] as const;
24
+
25
+ const icon = (symbol: string) => <span aria-hidden="true">{symbol}</span>;
26
+
27
+ export const Overview: Story = {
28
+ render: () => (
29
+ <Box display="grid" gap={4}>
30
+ {sizes.map((size) => (
31
+ <Flex key={size} alignItems="center" gap={2}>
32
+ {variants.map((variant) => (
33
+ <Button key={`${size}-${variant}`} size={size} variant={variant}>
34
+ {variant}
35
+ </Button>
36
+ ))}
37
+ </Flex>
38
+ ))}
39
+ </Box>
40
+ ),
41
+ };
42
+
43
+ export const WithIcons: Story = {
44
+ render: () => (
45
+ <Flex gap={4}>
46
+ <Button leftIcon={icon("←")}>Left icon</Button>
47
+ <Button rightIcon={icon("→")}>Right icon</Button>
48
+ <Button leftIcon={icon("★")} rightIcon={icon("→")} variant="secondary">
49
+ Both icons
50
+ </Button>
51
+ </Flex>
52
+ ),
53
+ };
54
+
55
+ export const States: Story = {
56
+ render: () => (
57
+ <Flex gap={4}>
58
+ <Button loading loadingText="Loading">
59
+ Loading
60
+ </Button>
61
+ <Button disabled variant="secondary">
62
+ Disabled
63
+ </Button>
64
+ <Button loading variant="ghost" leftIcon={icon("⟳")}>
65
+ Refreshing
66
+ </Button>
67
+ </Flex>
68
+ ),
69
+ };
@@ -56,6 +56,7 @@ type DatePickerProps = Omit<AriaDatePickerProps<DateValue>, "onChange"> &
56
56
  * A date picker component.
57
57
  *
58
58
  * There are three different variants –`core`, `floating` and `ghost`.
59
+ * There are two different sizes - `sm` and `md`
59
60
  *
60
61
  * ```tsx
61
62
  * <DatePicker label="Dato" variant="core" />
@@ -65,6 +66,7 @@ type DatePickerProps = Omit<AriaDatePickerProps<DateValue>, "onChange"> &
65
66
  export const DatePicker = ({
66
67
  ref: externalRef,
67
68
  variant,
69
+ size,
68
70
  errorText,
69
71
  minHeight,
70
72
  showYearNavigation,
@@ -144,12 +146,13 @@ export const DatePicker = ({
144
146
  invalid={invalid}
145
147
  helperText={helperText}
146
148
  required={props.required}
149
+ size={size}
147
150
  >
148
151
  <PopoverAnchor>
149
152
  <StyledField
150
153
  variant={variant}
154
+ size={size}
151
155
  onClick={onFieldClick}
152
- paddingX={3}
153
156
  minHeight={minHeight}
154
157
  isDisabled={props.isDisabled}
155
158
  isActive={props.isActive}
@@ -162,8 +165,7 @@ export const DatePicker = ({
162
165
  ) : (
163
166
  <ChakraPopover.Trigger asChild>
164
167
  <CalendarTriggerButton
165
- paddingLeft={1}
166
- paddingRight={1}
168
+ marginLeft={1}
167
169
  variant={variant}
168
170
  ref={ref}
169
171
  {...buttonProps}
@@ -52,12 +52,14 @@ type DateRangePickerProps = Omit<
52
52
  * A date range picker component.
53
53
  *
54
54
  * There are three variants to choose from – `core`, `floating` and `ghost`.
55
+ * There are two different sizes - `sm` and `md`
55
56
  *
56
57
  * ```tsx
57
58
  * <DateRangePicker startLabel="From" startName="from" endLabel="To" endName="to" variant="core" />
58
59
  * ```
59
60
  */ export function DateRangePicker({
60
61
  variant = "core",
62
+ size,
61
63
  minHeight,
62
64
  startName,
63
65
  endName,
@@ -135,6 +137,7 @@ type DateRangePickerProps = Omit<
135
137
  alignItems="center"
136
138
  paddingX={3}
137
139
  variant={variant}
140
+ size={size}
138
141
  onClick={onFieldClick}
139
142
  minHeight={minHeight}
140
143
  >
@@ -57,7 +57,6 @@ export const DateTimeSegment = ({
57
57
  textAlign="center"
58
58
  outline="none"
59
59
  borderRadius="xs"
60
- fontSize={["mobile.sm", "desktop.sm"]}
61
60
  css={styles.dateTimeSegment}
62
61
  aria-label={ariaLabel}
63
62
  aria-labelledby={ariaLabelledby}
@@ -29,6 +29,7 @@ export const StyledField = function StyledField({
29
29
  isDisabled,
30
30
  isActive,
31
31
  overrideBorderColor,
32
+ size,
32
33
  ...otherProps
33
34
  } = props;
34
35
  const { invalid } = useFieldContext() ?? {
@@ -38,7 +39,7 @@ export const StyledField = function StyledField({
38
39
  const recipe = useSlotRecipe({
39
40
  key: "datePicker",
40
41
  });
41
- const styles = recipe({ variant });
42
+ const styles = recipe({ variant, size });
42
43
 
43
44
  return (
44
45
  <Box
@@ -51,7 +52,6 @@ export const StyledField = function StyledField({
51
52
  ref={ref}
52
53
  aria-invalid={invalid}
53
54
  aria-disabled={isDisabled}
54
- fontSize={["mobile.md", "desktop.md"]}
55
55
  >
56
56
  {children}
57
57
  </Box>
@@ -36,8 +36,6 @@ export const TimeField = ({ state, ...props }: TimeFieldProps) => {
36
36
  color="text.subtle"
37
37
  top={0}
38
38
  cursor="text"
39
- left="50%"
40
- transform="translateX(-50%)"
41
39
  position="absolute"
42
40
  paddingTop="2px"
43
41
  whiteSpace="nowrap"
@@ -50,6 +50,11 @@ type TimePickerProps = Omit<BoxProps, "defaultValue" | "onChange"> &
50
50
  * Defaults to "core".
51
51
  */
52
52
  variant?: "core" | "floating" | "ghost";
53
+ /**
54
+ * The size of the time picker.
55
+ * Defaults to "md".
56
+ */
57
+ size?: "sm" | "md";
53
58
  };
54
59
  /** A time picker component.
55
60
  *
@@ -137,10 +142,8 @@ export const TimePicker = ({
137
142
  <Field as="time" {...boxProps}>
138
143
  <StyledField
139
144
  width="fit-content"
140
- paddingX={2}
141
145
  alignItems="center"
142
146
  justifyContent="space-between"
143
- gap={2}
144
147
  opacity={isDisabled ? 0.5 : 1}
145
148
  pointerEvents={isDisabled ? "none" : "auto"}
146
149
  aria-disabled={isDisabled}
@@ -2,4 +2,5 @@ import { ConditionalValue } from "@chakra-ui/react";
2
2
 
3
3
  export type CalendarVariants = {
4
4
  variant?: ConditionalValue<"core" | "floating" | "ghost">;
5
+ size?: ConditionalValue<"sm" | "md">;
5
6
  };
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Box, Flex, Input, SearchInput, Textarea } from "@vygruppen/spor-react";
3
+ import { useState } from "react";
4
+
5
+ const meta = {
6
+ title: "Components/Input",
7
+ component: Input,
8
+ } satisfies Meta<typeof Input>;
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Overview: Story = {
15
+ render: () => (
16
+ <Flex direction="column" gap={4}>
17
+ <Input label="Name" helperText="Enter your full name" />
18
+ <Input
19
+ label="Email"
20
+ variant="floating"
21
+ type="email"
22
+ helperText="Used for booking updates"
23
+ />
24
+ <Input label="Disabled field" disabled value="Unavailable" />
25
+ </Flex>
26
+ ),
27
+ };
28
+
29
+ export const Textareas: Story = {
30
+ render: () => (
31
+ <Flex direction="column" gap={4}>
32
+ <Textarea label="Message" helperText="Share additional travel details" />
33
+ <Textarea
34
+ label="Accessibility needs"
35
+ variant="floating"
36
+ helperText="Optional"
37
+ defaultValue="Wheelchair assistance at Oslo S."
38
+ />
39
+ </Flex>
40
+ ),
41
+ };
42
+
43
+ export const Search: Story = {
44
+ render: () => {
45
+ const SearchExample = () => {
46
+ const [value, setValue] = useState("Oslo");
47
+
48
+ return (
49
+ <Box>
50
+ <SearchInput
51
+ label="Search departures"
52
+ value={value}
53
+ onChange={(event) => setValue(event.target.value)}
54
+ onReset={() => setValue("")}
55
+ />
56
+ </Box>
57
+ );
58
+ };
59
+
60
+ return <SearchExample />;
61
+ },
62
+ };
@@ -36,10 +36,12 @@ type RadioCardItemProps = Exclude<
36
36
  RadioCardVariantProps & {
37
37
  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
38
38
  ariaLabel?: string;
39
+ showIndicator?: boolean;
39
40
  };
40
41
 
41
42
  export const RadioCard = ({
42
43
  ref,
44
+ showIndicator = false,
43
45
  ...props
44
46
  }: RadioCardItemProps & {
45
47
  ref?: React.Ref<HTMLInputElement>;
@@ -61,6 +63,7 @@ export const RadioCard = ({
61
63
  {...inputProps}
62
64
  />
63
65
  <ChakraRadioCard.ItemControl id={itemControlId} aria-hidden>
66
+ {showIndicator && <ChakraRadioCard.ItemIndicator />}
64
67
  {children}
65
68
  </ChakraRadioCard.ItemControl>
66
69
  </ChakraRadioCard.Item>
@@ -1,6 +1,11 @@
1
1
  export enum Brand {
2
2
  VyDigital = "VyDigital",
3
+ /**
4
+ * @deprecated The VyUtvikling theme is deprecated and will be removed in a
5
+ * future major version. Use {@link Brand.VyTeknologi} instead.
6
+ */
3
7
  VyUtvikling = "VyUtvikling",
8
+ VyTeknologi = "VyTeknologi",
4
9
  CargoNet = "CargoNet",
5
10
  }
6
11
 
@@ -48,6 +48,10 @@ export const themes = {
48
48
  defaultBaseConfig,
49
49
  generateTheme(Brand.VyUtvikling),
50
50
  ),
51
+ [Brand.VyTeknologi]: createSystem(
52
+ defaultBaseConfig,
53
+ generateTheme(Brand.VyTeknologi),
54
+ ),
51
55
  };
52
56
 
53
57
  export const system = themes[Brand.VyDigital];
@@ -3,7 +3,12 @@ import tokens from "@vygruppen/spor-design-tokens/raw-tokens";
3
3
 
4
4
  export enum Brand {
5
5
  VyDigital = "VyDigital",
6
+ /**
7
+ * @deprecated The VyUtvikling theme is deprecated and will be removed in a
8
+ * future major version. Use {@link Brand.VyTeknologi} instead.
9
+ */
6
10
  VyUtvikling = "VyUtvikling",
11
+ VyTeknologi = "VyTeknologi",
7
12
  CargoNet = "CargoNet",
8
13
  }
9
14
 
@@ -11,10 +16,18 @@ export const vyDigitalColors = defineSemanticTokens.colors({
11
16
  ...tokens.color["vy-digital"].color.vyDigital,
12
17
  });
13
18
 
19
+ /**
20
+ * @deprecated The VyUtvikling theme is deprecated and will be removed in a
21
+ * future major version. Use {@link vyTeknologiColors} instead.
22
+ */
14
23
  export const vyUtviklingColors = defineSemanticTokens.colors({
15
24
  ...tokens.color["vy-utvikling"].color.vyUtvikling,
16
25
  });
17
26
 
27
+ export const vyTeknologiColors = defineSemanticTokens.colors({
28
+ ...tokens.color["vy-teknologi"].color.vyTeknologi,
29
+ });
30
+
18
31
  export const cargonetColors = defineSemanticTokens.colors({
19
32
  ...tokens.color["cargonet"].color.cargonet,
20
33
  });
@@ -1,5 +1,10 @@
1
1
  import { Brand } from "../brand";
2
- import { cargonetColors, vyDigitalColors, vyUtviklingColors } from "./colors";
2
+ import {
3
+ cargonetColors,
4
+ vyDigitalColors,
5
+ vyTeknologiColors,
6
+ vyUtviklingColors,
7
+ } from "./colors";
3
8
  import { radii } from "./radii";
4
9
  import { shadows } from "./shadows";
5
10
 
@@ -13,6 +18,10 @@ export const semanticTokens = {
13
18
  ...baseSemanticTokens,
14
19
  colors: vyUtviklingColors,
15
20
  },
21
+ [Brand.VyTeknologi]: {
22
+ ...baseSemanticTokens,
23
+ colors: vyTeknologiColors,
24
+ },
16
25
  [Brand.CargoNet]: {
17
26
  ...baseSemanticTokens,
18
27
  colors: cargonetColors,
@@ -159,6 +159,7 @@ export const radioCardAnatomy = createAnatomy("radio-card").parts(
159
159
  "itemDescription",
160
160
  "itemContent",
161
161
  "itemControl",
162
+ "itemIndicator",
162
163
  );
163
164
 
164
165
  export const radioAnatomy = createAnatomy("radio").parts(
@@ -13,6 +13,8 @@ export const datePickerSlotRecipe = defineSlotRecipe({
13
13
  display: "flex",
14
14
  flex: 1,
15
15
  paddingY: 0.5,
16
+ paddingX: 2,
17
+ gap: 1.5,
16
18
  alignItems: "center",
17
19
  _hover: {
18
20
  zIndex: "docked",
@@ -237,8 +239,23 @@ export const datePickerSlotRecipe = defineSlotRecipe({
237
239
  },
238
240
  },
239
241
  },
242
+ size: {
243
+ sm: {
244
+ wrapper: {
245
+ fontSize: ["mobile.xs", "desktop.xs"],
246
+ paddingX: 1,
247
+ gap: 1,
248
+ },
249
+ },
250
+ md: {
251
+ wrapper: {
252
+ fontSize: ["mobile.sm", "desktop.sm"],
253
+ },
254
+ },
255
+ },
240
256
  },
241
257
  defaultVariants: {
242
258
  variant: "core",
259
+ size: "md",
243
260
  },
244
261
  });
@@ -37,6 +37,58 @@ export const radioCardSlotRecipe = defineSlotRecipe({
37
37
  fontWeight: "bold",
38
38
  fontSize: "inherit",
39
39
  },
40
+ itemControl: {
41
+ display: "flex",
42
+ alignItems: "flex-start",
43
+ justifyContent: "flex-start",
44
+ },
45
+
46
+ itemIndicator: {
47
+ display: "inline-flex",
48
+ flexShrink: 0,
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ borderWidth: "2px",
52
+ borderColor: "outline.core",
53
+ borderRadius: "xl",
54
+ width: 3,
55
+ height: 3,
56
+ marginRight: 2,
57
+ marginTop: 0.5,
58
+
59
+ _checked: {
60
+ borderColor: "surface.brand",
61
+ },
62
+ _hover: {
63
+ borderColor: "surface.brand.hover",
64
+ "& .dot": {
65
+ backgroundColor: "surface.brand.hover",
66
+ },
67
+ },
68
+
69
+ _disabled: {
70
+ pointerEvents: "none",
71
+ backgroundColor: "surface.disabled",
72
+ borderColor: "outline.disabled",
73
+ "& .dot": {
74
+ backgroundColor: "outline.disabled",
75
+ },
76
+ },
77
+ _focusVisible: {
78
+ outlineWidth: "2px",
79
+ outlineColor: "outline.focus",
80
+ outlineStyle: "solid",
81
+ outlineOffset: "1px",
82
+ },
83
+
84
+ "& .dot": {
85
+ height: "full",
86
+ width: "full",
87
+ borderRadius: "xl",
88
+ backgroundColor: "surface.brand",
89
+ scale: "0.5",
90
+ },
91
+ },
40
92
  },
41
93
  variants: {
42
94
  variant: {
@@ -10,6 +10,7 @@ import {
10
10
  } from "@chakra-ui/react";
11
11
 
12
12
  import { AlertIcon } from "@/alert/AlertIcon";
13
+ import { Button, CloseButton } from "@/button";
13
14
 
14
15
  const toaster = createToaster({
15
16
  placement: "bottom",
@@ -18,11 +19,18 @@ const toaster = createToaster({
18
19
 
19
20
  type Variant = "info" | "success" | "error";
20
21
 
22
+ type ToastAction = {
23
+ label: string;
24
+ onClick: () => void;
25
+ };
26
+
21
27
  type ToastProps = {
22
28
  duration?: number;
23
29
  text: string;
24
30
  variant: Variant;
25
31
  id?: string;
32
+ action?: ToastAction;
33
+ closable?: boolean;
26
34
  } & Pick<BoxProps, "width">;
27
35
 
28
36
  export const createToast = ({
@@ -31,13 +39,17 @@ export const createToast = ({
31
39
  id,
32
40
  duration = 6000,
33
41
  width = "sm",
42
+ action,
43
+ closable = false,
34
44
  }: ToastProps) =>
35
45
  toaster.create({
36
46
  description: text,
37
47
  type: variant,
38
48
  id: id ?? crypto.randomUUID(),
39
49
  duration,
50
+ action,
40
51
  meta: { width },
52
+ closable: closable,
41
53
  });
42
54
 
43
55
  export const Toaster = () => {
@@ -56,6 +68,18 @@ export const Toaster = () => {
56
68
  <Stack gap="1" flex="1" maxWidth="100%">
57
69
  <Toast.Description>{toast.description}</Toast.Description>
58
70
  </Stack>
71
+ {toast.action && (
72
+ <Toast.ActionTrigger onClick={toast.action.onClick}>
73
+ <Button variant="ghost" size="xs">
74
+ {toast.action.label}
75
+ </Button>
76
+ </Toast.ActionTrigger>
77
+ )}
78
+ {toast.closable && (
79
+ <Toast.CloseTrigger>
80
+ <CloseButton size="xs" />
81
+ </Toast.CloseTrigger>
82
+ )}
59
83
  </Toast.Root>
60
84
  )}
61
85
  </ChakraToaster>