@vygruppen/spor-react 1.3.4 → 2.0.1

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.
Files changed (196) hide show
  1. package/.turbo/turbo-build.log +12 -10
  2. package/CHANGELOG.md +31 -0
  3. package/dist/index.d.ts +6720 -27
  4. package/dist/index.js +14143 -140
  5. package/dist/index.mjs +13822 -27
  6. package/package.json +19 -31
  7. package/src/accordion/Accordion.test.tsx +20 -0
  8. package/src/accordion/Accordion.tsx +62 -0
  9. package/src/accordion/AccordionContext.tsx +27 -0
  10. package/src/accordion/Expandable.tsx +157 -0
  11. package/src/accordion/index.tsx +2 -0
  12. package/src/alert/AlertIcon.tsx +75 -0
  13. package/src/alert/BaseAlert.test.tsx +37 -0
  14. package/src/alert/BaseAlert.tsx +21 -0
  15. package/src/alert/ClosableAlert.test.tsx +37 -0
  16. package/src/alert/ClosableAlert.tsx +75 -0
  17. package/src/alert/ExpandableAlert.test.tsx +84 -0
  18. package/src/alert/ExpandableAlert.tsx +84 -0
  19. package/src/alert/StaticAlert.tsx +25 -0
  20. package/src/alert/index.tsx +3 -0
  21. package/src/button/Button.test.tsx +23 -0
  22. package/src/button/Button.tsx +162 -0
  23. package/src/button/ButtonGroup.tsx +43 -0
  24. package/src/button/CloseButton.tsx +63 -0
  25. package/src/button/FloatingActionButton.tsx +113 -0
  26. package/src/button/IconButton.tsx +63 -0
  27. package/src/button/index.tsx +5 -0
  28. package/src/card/Card.tsx +59 -0
  29. package/src/card/index.tsx +1 -0
  30. package/src/datepicker/Calendar.tsx +32 -0
  31. package/src/datepicker/CalendarCell.tsx +74 -0
  32. package/src/datepicker/CalendarGrid.tsx +76 -0
  33. package/src/datepicker/CalendarHeader.tsx +153 -0
  34. package/src/datepicker/CalendarNavigationButton.tsx +26 -0
  35. package/src/datepicker/CalendarTriggerButton.tsx +36 -0
  36. package/src/datepicker/DateField.tsx +51 -0
  37. package/src/datepicker/DatePicker.tsx +153 -0
  38. package/src/datepicker/DateRangePicker.tsx +171 -0
  39. package/src/datepicker/DateTimeSegment.tsx +56 -0
  40. package/src/datepicker/RangeCalendar.tsx +35 -0
  41. package/src/datepicker/StyledField.tsx +31 -0
  42. package/src/datepicker/TimeField.tsx +46 -0
  43. package/src/datepicker/TimePicker.test.tsx +74 -0
  44. package/src/datepicker/TimePicker.tsx +196 -0
  45. package/src/datepicker/index.tsx +4 -0
  46. package/src/datepicker/utils.ts +33 -0
  47. package/src/i18n/index.tsx +38 -0
  48. package/src/image/index.tsx +2 -0
  49. package/src/index.tsx +25 -26
  50. package/src/input/CardSelect.tsx +165 -0
  51. package/src/input/Checkbox.tsx +24 -0
  52. package/src/input/CheckboxGroup.tsx +43 -0
  53. package/src/input/ChoiceChip.tsx +102 -0
  54. package/src/input/Dialog.tsx +29 -0
  55. package/src/input/FormControl.tsx +11 -0
  56. package/src/input/FormErrorMessage.tsx +91 -0
  57. package/src/input/FormLabel.tsx +11 -0
  58. package/src/input/InfoSelect.tsx +209 -0
  59. package/src/input/Input.tsx +59 -0
  60. package/src/input/InputElement.tsx +45 -0
  61. package/src/input/ListBox.tsx +123 -0
  62. package/src/input/NativeSelect.tsx +38 -0
  63. package/src/input/PasswordInput.tsx +70 -0
  64. package/src/input/Popover.tsx +70 -0
  65. package/src/input/Radio.tsx +34 -0
  66. package/src/input/RadioGroup.tsx +47 -0
  67. package/src/input/SearchInput.tsx +89 -0
  68. package/src/input/Switch.tsx +40 -0
  69. package/src/input/Textarea.tsx +98 -0
  70. package/src/input/index.tsx +20 -0
  71. package/src/layout/Divider.tsx +26 -0
  72. package/src/layout/Stack.tsx +42 -0
  73. package/src/layout/index.tsx +28 -0
  74. package/src/linjetag/InfoTag.tsx +54 -0
  75. package/src/linjetag/LineIcon.tsx +44 -0
  76. package/src/linjetag/TravelTag.tsx +121 -0
  77. package/src/linjetag/icons.tsx +80 -0
  78. package/src/linjetag/index.tsx +3 -0
  79. package/src/linjetag/types.d.ts +24 -0
  80. package/src/link/TextLink.tsx +45 -0
  81. package/src/link/index.tsx +1 -0
  82. package/src/loader/ClientOnly.tsx +29 -0
  83. package/src/loader/ColorInlineLoader.tsx +27 -0
  84. package/src/loader/ColorSpinner.tsx +44 -0
  85. package/src/loader/ContentLoader.tsx +27 -0
  86. package/src/loader/DarkFullScreenLoader.tsx +23 -0
  87. package/src/loader/DarkInlineLoader.tsx +25 -0
  88. package/src/loader/DarkSpinner.tsx +43 -0
  89. package/src/loader/LightFullScreenLoader.tsx +23 -0
  90. package/src/loader/LightInlineLoader.tsx +25 -0
  91. package/src/loader/LightSpinner.tsx +41 -0
  92. package/src/loader/Lottie.tsx +10 -0
  93. package/src/loader/ProgressBar.tsx +128 -0
  94. package/src/loader/ProgressLoader.tsx +140 -0
  95. package/src/loader/Skeleton.tsx +16 -0
  96. package/src/loader/SkeletonCircle.tsx +13 -0
  97. package/src/loader/SkeletonText.tsx +10 -0
  98. package/src/loader/index.tsx +14 -0
  99. package/src/loader/useHydrated.tsx +34 -0
  100. package/src/loader/useRotatingLabel.tsx +22 -0
  101. package/src/logo/VyLogo.tsx +101 -0
  102. package/src/logo/index.tsx +1 -0
  103. package/src/media-controller/JumpButton.tsx +69 -0
  104. package/src/media-controller/PlayPauseButton.tsx +67 -0
  105. package/src/media-controller/SkipButton.tsx +66 -0
  106. package/src/media-controller/icons.tsx +80 -0
  107. package/src/media-controller/index.test.tsx +59 -0
  108. package/src/media-controller/index.tsx +3 -0
  109. package/src/modal/Drawer.tsx +122 -0
  110. package/src/modal/Modal.tsx +15 -0
  111. package/src/modal/ModalHeader.tsx +31 -0
  112. package/src/modal/SimpleDrawer.tsx +44 -0
  113. package/src/modal/index.tsx +4 -0
  114. package/src/popover/PopoverWizardBody.tsx +91 -0
  115. package/src/popover/SimplePopover.tsx +75 -0
  116. package/src/popover/WizardPopover.tsx +61 -0
  117. package/src/popover/index.tsx +23 -0
  118. package/src/provider/SporProvider.tsx +67 -0
  119. package/src/provider/index.tsx +1 -0
  120. package/src/stepper/Stepper.tsx +115 -0
  121. package/src/stepper/StepperContext.tsx +55 -0
  122. package/src/stepper/StepperStep.tsx +48 -0
  123. package/src/stepper/index.tsx +2 -0
  124. package/src/tab/Tabs.tsx +20 -0
  125. package/src/tab/index.tsx +9 -0
  126. package/src/table/Table.tsx +58 -0
  127. package/src/table/index.tsx +19 -0
  128. package/src/theme/components/accordion.ts +143 -0
  129. package/src/theme/components/alert.ts +59 -0
  130. package/src/theme/components/badge.ts +109 -0
  131. package/src/theme/components/button.ts +217 -0
  132. package/src/theme/components/card-select.ts +158 -0
  133. package/src/theme/components/card.ts +174 -0
  134. package/src/theme/components/checkbox.ts +90 -0
  135. package/src/theme/components/choice-chip.ts +79 -0
  136. package/src/theme/components/close-button.ts +56 -0
  137. package/src/theme/components/code.ts +17 -0
  138. package/src/theme/components/datepicker.ts +194 -0
  139. package/src/theme/components/drawer.ts +92 -0
  140. package/src/theme/components/fab.ts +111 -0
  141. package/src/theme/components/form-label.ts +17 -0
  142. package/src/theme/components/form.ts +27 -0
  143. package/src/theme/components/index.ts +34 -0
  144. package/src/theme/components/info-select.ts +91 -0
  145. package/src/theme/components/info-tag.ts +49 -0
  146. package/src/theme/components/input.ts +97 -0
  147. package/src/theme/components/line-icon.ts +121 -0
  148. package/src/theme/components/link.ts +155 -0
  149. package/src/theme/components/listbox.ts +52 -0
  150. package/src/theme/components/media-controller-button.ts +134 -0
  151. package/src/theme/components/modal.ts +93 -0
  152. package/src/theme/components/popover.ts +63 -0
  153. package/src/theme/components/radio.ts +64 -0
  154. package/src/theme/components/select.ts +52 -0
  155. package/src/theme/components/skeleton.ts +40 -0
  156. package/src/theme/components/stepper.ts +230 -0
  157. package/src/theme/components/switch.ts +227 -0
  158. package/src/theme/components/table.ts +163 -0
  159. package/src/theme/components/tabs.ts +282 -0
  160. package/src/theme/components/textarea.ts +14 -0
  161. package/src/theme/components/toast.ts +28 -0
  162. package/src/theme/components/travel-tag.ts +267 -0
  163. package/src/theme/font-faces.ts +66 -0
  164. package/src/theme/foundations/borders.ts +11 -0
  165. package/src/theme/foundations/breakpoints.ts +9 -0
  166. package/src/theme/foundations/colors.ts +10 -0
  167. package/src/theme/foundations/config.ts +5 -0
  168. package/src/theme/foundations/fontSizes.ts +29 -0
  169. package/src/theme/foundations/fontWeights.ts +5 -0
  170. package/src/theme/foundations/fonts.ts +7 -0
  171. package/src/theme/foundations/index.ts +14 -0
  172. package/src/theme/foundations/lineHeights.ts +5 -0
  173. package/src/theme/foundations/radii.ts +12 -0
  174. package/src/theme/foundations/shadows.ts +8 -0
  175. package/src/theme/foundations/sizes.ts +34 -0
  176. package/src/theme/foundations/spacing.ts +30 -0
  177. package/src/theme/foundations/textStyles.ts +60 -0
  178. package/src/theme/foundations/zIndices.ts +17 -0
  179. package/src/theme/index.ts +14 -0
  180. package/src/theme/utils/box-shadow-utils.ts +44 -0
  181. package/src/theme/utils/focus-utils.ts +16 -0
  182. package/src/toast/ActionToast.test.tsx +22 -0
  183. package/src/toast/ActionToast.tsx +28 -0
  184. package/src/toast/BaseToast.test.tsx +27 -0
  185. package/src/toast/BaseToast.tsx +75 -0
  186. package/src/toast/ClosableToast.test.tsx +17 -0
  187. package/src/toast/ClosableToast.tsx +40 -0
  188. package/src/toast/index.tsx +1 -0
  189. package/src/toast/useToast.tsx +99 -0
  190. package/src/typography/Badge.tsx +68 -0
  191. package/src/typography/Code.tsx +32 -0
  192. package/src/typography/Heading.tsx +32 -0
  193. package/src/typography/Text.tsx +26 -0
  194. package/src/typography/index.tsx +4 -0
  195. package/src/util/externals.tsx +23 -0
  196. package/src/util/index.tsx +1 -0
@@ -0,0 +1,84 @@
1
+ import {
2
+ Accordion,
3
+ AccordionButton,
4
+ AccordionIcon,
5
+ AccordionItem,
6
+ AccordionPanel,
7
+ Box,
8
+ Flex,
9
+ } from "@chakra-ui/react";
10
+ import React from "react";
11
+ import { AlertIcon } from "./AlertIcon";
12
+ import { BaseAlert, BaseAlertProps } from "./BaseAlert";
13
+
14
+ type ExpandableAlertProps = BaseAlertProps & {
15
+ /** The title string */
16
+ title: string;
17
+ /** Callback for when the expandable panel is opened or closed */
18
+ onToggle?: (isOpen: boolean) => void;
19
+ /** Whether or not the default state of the expandable alert is open */
20
+ defaultOpen?: boolean;
21
+ /**
22
+ * The HTML element used for the `title` prop.
23
+ *
24
+ * Defaults to h3 */
25
+ headingLevel?: "h2" | "h3" | "h4" | "h5" | "h6";
26
+ };
27
+ /**
28
+ * An expandable alert component.
29
+ *
30
+ * A regular alert with an expandable body. The expandable body can be used to provide more information about the alert.
31
+ *
32
+ * ```tsx
33
+ * <ExpandableAlert variant="alt-transport" title="Replacement bus service">
34
+ * The replacement bus service will be running from 10:00 to 16:00.
35
+ * </ExpandableAlert>
36
+ * ```
37
+ */
38
+ export const ExpandableAlert = ({
39
+ variant,
40
+ children,
41
+ title,
42
+ headingLevel = "h3",
43
+ defaultOpen = false,
44
+ onToggle = () => {},
45
+ }: ExpandableAlertProps) => {
46
+ return (
47
+ <BaseAlert variant={variant} paddingX={0} paddingY={0} padding={0}>
48
+ <Accordion
49
+ onChange={(expandedIndex) => onToggle(expandedIndex === 0)}
50
+ defaultIndex={defaultOpen ? 0 : -1}
51
+ allowToggle
52
+ flexGrow="1"
53
+ >
54
+ <AccordionItem>
55
+ <AccordionButton paddingX={3} paddingY={2}>
56
+ <Flex
57
+ justifyContent="space-between"
58
+ alignItems="center"
59
+ flexGrow="1"
60
+ >
61
+ <Flex as={headingLevel}>
62
+ <AlertIcon variant={variant} />
63
+ <Box
64
+ as="span"
65
+ sx={{
66
+ // Truncate the title to one line
67
+ display: "-webkit-box",
68
+ overflow: "hidden",
69
+ "-webkit-line-clamp": "1",
70
+ "-webkit-box-orient": "vertical",
71
+ }}
72
+ >
73
+ {title}
74
+ </Box>
75
+ </Flex>
76
+ <AccordionIcon />
77
+ </Flex>
78
+ </AccordionButton>
79
+ <AccordionPanel>{children}</AccordionPanel>
80
+ </AccordionItem>
81
+ </Accordion>
82
+ </BaseAlert>
83
+ );
84
+ };
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { AlertIcon } from "./AlertIcon";
3
+ import { BaseAlert, BaseAlertProps } from "./BaseAlert";
4
+
5
+ type StaticAlertProps = BaseAlertProps;
6
+
7
+ /**
8
+ * A static alert component.
9
+ *
10
+ * This alert component cannot be closed, nor dismissed.
11
+ *
12
+ * ```tsx
13
+ * <StaticAlert variant="info" title="Nice to know">
14
+ * Thomas the Train was originally only a wooden toy made for the creator’s son.
15
+ * </StaticAlert>
16
+ * ```
17
+ */
18
+ export const StaticAlert = ({ children, ...props }: StaticAlertProps) => {
19
+ return (
20
+ <BaseAlert {...props}>
21
+ <AlertIcon variant={props.variant} />
22
+ {children}
23
+ </BaseAlert>
24
+ );
25
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./ClosableAlert";
2
+ export * from "./ExpandableAlert";
3
+ export * from "./StaticAlert";
@@ -0,0 +1,23 @@
1
+ import { render } from "@testing-library/react";
2
+ import React from "react";
3
+ import { vi } from "vitest";
4
+ import { axe } from "vitest-axe";
5
+ import { Button } from ".";
6
+
7
+ describe("<Button />", () => {
8
+ it("works like a button", async () => {
9
+ const handleClick = vi.fn();
10
+ const { getByRole } = render(
11
+ <Button variant="primary" onClick={handleClick}>
12
+ Click me
13
+ </Button>
14
+ );
15
+ getByRole("button").click();
16
+ expect(handleClick).toHaveBeenCalled();
17
+ });
18
+
19
+ it("is accessible", async () => {
20
+ const { container } = render(<Button variant="primary">Click me</Button>);
21
+ expect(await axe(container)).toHaveNoViolations();
22
+ });
23
+ });
@@ -0,0 +1,162 @@
1
+ import {
2
+ Box,
3
+ Center,
4
+ Button as ChakraButton,
5
+ ButtonProps as ChakraButtonProps,
6
+ forwardRef,
7
+ useButtonGroup,
8
+ } from "@chakra-ui/react";
9
+ import React from "react";
10
+ import { createTexts, useTranslation } from "../i18n";
11
+ import { ColorInlineLoader } from "../loader";
12
+
13
+ export type ButtonProps = Exclude<
14
+ ChakraButtonProps,
15
+ "colorScheme" | "loadingText" | "size" | "variant"
16
+ > & {
17
+ /**
18
+ * The size of the button.
19
+ *
20
+ * Defaults to "md"
21
+ * */
22
+ size?: "xs" | "sm" | "md" | "lg";
23
+ /** The different variants of a button
24
+ *
25
+ * Defaults to "primary"
26
+ */
27
+ variant?:
28
+ | "control"
29
+ | "primary"
30
+ | "secondary"
31
+ | "tertiary"
32
+ | "additional"
33
+ | "ghost"
34
+ | "floating";
35
+ };
36
+ /**
37
+ * Buttons are used to trigger actions.
38
+ *
39
+ * There are several button variants. You can specify which one you want with the `variant` prop. The available variants are:
40
+ *
41
+ * - `control`: This button is used for ticket controls only.
42
+ * - `primary`: This is our main button. It's used for the main actions in a view, like a call to action. There should only be a single primary button in each view.
43
+ * - `secondary`: Used for secondary actions in a view, and when you need to make several actions available at the same time.
44
+ * - `tertiary`: Used for non-essential actions, as well as in combination with the primary button.
45
+ * - `additional`: Used for additional choices, like a less important tertiary action.
46
+ * - `ghost`: Used inside other interactive elements, like date pickers and input fields.
47
+ * - `floating`: Used for floating actions, like a menu button in a menu.
48
+ *
49
+ * ```tsx
50
+ * <Button variant="primary" onClick={confirmOrder}>
51
+ * Buy trip
52
+ * </Button>
53
+ * ```
54
+ *
55
+ * There are also different sizes. You can specify which one you want with the `size` prop. The available sizes are "lg", "md", "sm" and "xs".
56
+ *
57
+ * ```tsx
58
+ * <Button variant="tertiary" size="sm" onClick={cancelOrder}>
59
+ * Cancel trip
60
+ * </Button>
61
+ * ```
62
+ *
63
+ * @see https://spor.vy.no/komponenter/button
64
+ */
65
+ export const Button = forwardRef<ButtonProps, "button">((props, ref) => {
66
+ const {
67
+ size,
68
+ variant,
69
+ children,
70
+ isLoading,
71
+ isDisabled,
72
+ leftIcon,
73
+ rightIcon,
74
+ } = props;
75
+ const ariaLabel = useCorrectAriaLabel(props);
76
+ const buttonGroup = useButtonGroup();
77
+ const finalVariant = (variant ??
78
+ buttonGroup?.variant ??
79
+ "primary") as Required<ButtonProps["variant"]>;
80
+ const finalSize = (size ?? buttonGroup?.size ?? "md") as Required<
81
+ ButtonProps["size"]
82
+ >;
83
+
84
+ return (
85
+ <ChakraButton
86
+ size={finalSize}
87
+ variant={finalVariant}
88
+ {...props}
89
+ ref={ref}
90
+ aria-label={ariaLabel}
91
+ aria-busy={isLoading}
92
+ isDisabled={isDisabled || isLoading}
93
+ leftIcon={
94
+ isLoading && leftIcon ? (
95
+ <Box visibility={isLoading ? "hidden" : "visible"} aria-hidden="true">
96
+ {leftIcon}
97
+ </Box>
98
+ ) : (
99
+ leftIcon
100
+ )
101
+ }
102
+ rightIcon={
103
+ isLoading && rightIcon ? (
104
+ <Box visibility={isLoading ? "hidden" : "visible"} aria-hidden="true">
105
+ {rightIcon}
106
+ </Box>
107
+ ) : (
108
+ rightIcon
109
+ )
110
+ }
111
+ position="relative"
112
+ >
113
+ {isLoading && (
114
+ <Center
115
+ position="absolute"
116
+ right="0"
117
+ paddingBottom={1}
118
+ left="0"
119
+ paddingTop={2}
120
+ >
121
+ <ColorInlineLoader
122
+ maxWidth={getLoaderWidth(finalSize)}
123
+ width="100%"
124
+ mx={2}
125
+ />
126
+ </Center>
127
+ )}
128
+ <Box visibility={isLoading ? "hidden" : "visible"}>{children}</Box>
129
+ </ChakraButton>
130
+ );
131
+ });
132
+
133
+ function getLoaderWidth(size: any) {
134
+ switch (size) {
135
+ case "xs":
136
+ return "4rem";
137
+ case "sm":
138
+ return "4rem";
139
+ case "md":
140
+ return "5rem";
141
+ case "lg":
142
+ default:
143
+ return "6rem";
144
+ }
145
+ }
146
+
147
+ function useCorrectAriaLabel(props: ButtonProps): string {
148
+ const { t } = useTranslation();
149
+ if (props.isLoading) {
150
+ return String(props.loadingText) ?? t(texts.loadingText);
151
+ }
152
+ return props["aria-label"] as string;
153
+ }
154
+
155
+ const texts = createTexts({
156
+ loadingText: {
157
+ nb: "Laster…",
158
+ nn: "Lastar…",
159
+ en: "Loading…",
160
+ sv: "Laddar…",
161
+ },
162
+ });
@@ -0,0 +1,43 @@
1
+ import {
2
+ ButtonGroup as ChakraButtonGroup,
3
+ ButtonGroupProps as ChakraButtonGroupProps,
4
+ forwardRef,
5
+ } from "@chakra-ui/react";
6
+ import React from "react";
7
+
8
+ export type ButtonGroupProps = ChakraButtonGroupProps;
9
+ /**
10
+ * Group buttons together with a `ButtonGroup`!
11
+ *
12
+ * If you have more than one button next to eachother, you might want to add a `ButtonGroup` to group them.
13
+ *
14
+ * ```tsx
15
+ * <ButtonGroup>
16
+ * <Button variant="tertiary">Cancel</Button>
17
+ * <Button variant="primary">Save</Button>
18
+ * </ButtonGroup>
19
+ * ```
20
+ *
21
+ * You can specify the size of all buttons in a group with the `size` prop. You can also set the same variant across all buttons with the `variant` prop.
22
+ *
23
+ * ```tsx
24
+ * <ButtonGroup variant="secondary" size="md">
25
+ * <Button>Open</Button>
26
+ * <Button>Save</Button>
27
+ * </ButtonGroup>
28
+ * ```
29
+ *
30
+ * Finally, you can join several buttons together with the `isAttached` prop.
31
+ *
32
+ * ```tsx
33
+ * <ButtonGroup variant="secondary" size="md" isAttached>
34
+ * <Button>Open</Button>
35
+ * <IconButton>
36
+ * <SaveIcon aria-label="Save"/>
37
+ * </IconButton>
38
+ * </ButtonGroup>
39
+ * ```
40
+ */
41
+ export const ButtonGroup = forwardRef<ButtonGroupProps, "div">((props, ref) => (
42
+ <ChakraButtonGroup {...props} ref={ref} />
43
+ ));
@@ -0,0 +1,63 @@
1
+ import { forwardRef } from "@chakra-ui/react";
2
+ import {
3
+ CloseFill18Icon,
4
+ CloseFill24Icon,
5
+ CloseFill30Icon,
6
+ } from "@vygruppen/spor-icon-react";
7
+ import React from "react";
8
+ import { createTexts, useTranslation } from "../i18n";
9
+ import { IconButton, IconButtonProps } from "./IconButton";
10
+
11
+ export type CloseButtonProps = Omit<
12
+ IconButtonProps,
13
+ "variant" | "aria-label"
14
+ > & {
15
+ /** Defaults to a localized version of "close" */
16
+ "aria-label"?: string;
17
+ };
18
+
19
+ /**
20
+ * A close button component.
21
+ *
22
+ * This button closes stuff, like modals and dialogs.
23
+ *
24
+ * ```tsx
25
+ * <CloseButton onClick={closeModal} />
26
+ * ```
27
+ */
28
+ export const CloseButton = forwardRef<CloseButtonProps, "button">(
29
+ ({ size = "sm", ...props }, ref) => {
30
+ const { t } = useTranslation();
31
+ return (
32
+ <IconButton
33
+ ref={ref}
34
+ variant="ghost"
35
+ icon={getIcon(size)}
36
+ size={size}
37
+ aria-label={props["aria-label"] || t(texts.close)}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+ );
43
+
44
+ const getIcon = (size: CloseButtonProps["size"]) => {
45
+ switch (size) {
46
+ case "xs":
47
+ case "sm":
48
+ return <CloseFill18Icon />;
49
+ case "md":
50
+ return <CloseFill24Icon />;
51
+ case "lg":
52
+ return <CloseFill30Icon />;
53
+ }
54
+ };
55
+
56
+ const texts = createTexts({
57
+ close: {
58
+ en: "Close",
59
+ nb: "Lukk",
60
+ nn: "Lukk",
61
+ sv: "Stäng",
62
+ },
63
+ });
@@ -0,0 +1,113 @@
1
+ import { Box, BoxProps, useMultiStyleConfig } from "@chakra-ui/react";
2
+ import { motion } from "framer-motion";
3
+ import React from "react";
4
+
5
+ const MotionBox = motion(Box);
6
+
7
+ type FloatingActionButtonProps = BoxProps & {
8
+ variant?: "green" | "light" | "dark";
9
+ placement?: "bottom right" | "bottom left" | "top right" | "top left";
10
+ icon: React.ReactNode;
11
+ children: React.ReactNode;
12
+ isTextVisible?: boolean;
13
+ };
14
+
15
+ /**
16
+ * Creates a floating action button.
17
+ *
18
+ * By default it will be placed at the bottom right of the screen. You can override this with specifying the `placement` prop.
19
+ *
20
+ * ```tsx
21
+ * <FloatingActionButton
22
+ * variant="green"
23
+ * icon={<TicketControlFill30Icon />}
24
+ * placement="bottom right"
25
+ * />
26
+ */
27
+ export const FloatingActionButton = ({
28
+ children,
29
+ icon,
30
+ variant,
31
+ isTextVisible: externalIsTextVisible,
32
+ placement = "bottom right",
33
+ ...props
34
+ }: FloatingActionButtonProps) => {
35
+ const [isTextVisible, setIsTextVisible] = React.useState(
36
+ externalIsTextVisible !== undefined ? externalIsTextVisible : true
37
+ );
38
+ const scrollDirection = useScrollDirection();
39
+ React.useEffect(() => {
40
+ if (externalIsTextVisible !== undefined) {
41
+ return;
42
+ }
43
+ const id = window.setTimeout(
44
+ () => setIsTextVisible(scrollDirection !== "down"),
45
+ 1000
46
+ );
47
+ return () => window.clearTimeout(id);
48
+ }, [scrollDirection, externalIsTextVisible]);
49
+
50
+ React.useEffect(() => {
51
+ setIsTextVisible(!!externalIsTextVisible);
52
+ }, [externalIsTextVisible]);
53
+
54
+ const style = useMultiStyleConfig("FloatingActionButton", {
55
+ variant,
56
+ isTextVisible,
57
+ placement,
58
+ });
59
+ return (
60
+ <MotionBox
61
+ __css={style.container}
62
+ as="button"
63
+ aria-label={children}
64
+ {...props}
65
+ >
66
+ <Box __css={style.icon}>
67
+ {icon}
68
+ </Box>
69
+ <MotionBox
70
+ animate={isTextVisible ? "show" : "hide"}
71
+ initial="show"
72
+ variants={{
73
+ show: {
74
+ opacity: 1,
75
+ width: "auto",
76
+ visibility: "visible",
77
+ },
78
+ hide: {
79
+ opacity: 0,
80
+ width: 0,
81
+ visibility: "hidden",
82
+ },
83
+ }}
84
+ __css={style.text}
85
+ >
86
+ {children}
87
+ </MotionBox>
88
+ </MotionBox>
89
+ );
90
+ };
91
+
92
+ type ScrollDirection = "up" | "down" | null;
93
+ const useScrollDirection = () => {
94
+ const [scrollDirection, setScrollDirection] =
95
+ React.useState<ScrollDirection>(null);
96
+ const lastScrollPosition = React.useRef(window.scrollY);
97
+ React.useEffect(() => {
98
+ const onScroll = () => {
99
+ const delta = window.scrollY - lastScrollPosition.current;
100
+ if (delta === 0) {
101
+ return;
102
+ }
103
+
104
+ lastScrollPosition.current = window.scrollY;
105
+ setScrollDirection(delta > 0 ? "down" : "up");
106
+ };
107
+ window.addEventListener("scroll", onScroll);
108
+ return () => {
109
+ window.removeEventListener("scroll", onScroll);
110
+ };
111
+ }, [scrollDirection]);
112
+ return scrollDirection;
113
+ };
@@ -0,0 +1,63 @@
1
+ import {
2
+ As,
3
+ IconButton as ChakraIconButton,
4
+ IconButtonProps as ChakraIconButtonProps,
5
+ forwardRef,
6
+ } from "@chakra-ui/react";
7
+ import React from "react";
8
+ import { ColorSpinner } from "..";
9
+
10
+ export type IconButtonProps = Omit<ChakraIconButtonProps, "variant"> & {
11
+ variant:
12
+ | "control"
13
+ | "primary"
14
+ | "secondary"
15
+ | "tertiary"
16
+ | "additional"
17
+ | "ghost";
18
+ };
19
+ /**
20
+ * An icon-only button.
21
+ *
22
+ * Remember to specify the `aria-label` prop for screen readers.
23
+ *
24
+ * There are several icon button variants. You can specify which one you want with the `variant` prop. The available variants are:
25
+ *
26
+ * - `control`: This button is used for ticket controls only.
27
+ * - `primary`: This is our main button. It's used for the main actions in a view, like a call to action. There should only be a single primary button in each view.
28
+ * - `secondary`: Used for secondary actions in a view, and when you need to make several actions available at the same time.
29
+ * - `tertiary`: Used for non-essential actions, as well as in combination with the primary button.
30
+ * - `additional`: Used for additional choices, like a less important tertiary action.
31
+ * - `ghost`: Used inside other interactive elements, like date pickers and input fields.
32
+ *
33
+ * ```tsx
34
+ * <IconButton
35
+ * aria-label="Buy trip"
36
+ * icon={<ShoppingCartIcon />}
37
+ * variant="primary"
38
+ * onClick={confirmOrder}
39
+ * />
40
+ * ```
41
+ *
42
+ * There are also different sizes. You can specify which one you want with the `size` prop. The available sizes are "lg", "md", "sm" and "xs".
43
+ *
44
+ * ```tsx
45
+ * <IconButton
46
+ * aria-label="Cancel trip"
47
+ * icon={<CancelIcon />}
48
+ * variant="ghost"
49
+ * size="sm"
50
+ * onClick={cancelOrder}
51
+ * />
52
+ * ```
53
+ */
54
+ export const IconButton = forwardRef<IconButtonProps, As>(
55
+ ({ ...props }, ref) => (
56
+ <ChakraIconButton
57
+ title={props["aria-label"]}
58
+ {...props}
59
+ spinner={<ColorSpinner m={1} />}
60
+ ref={ref}
61
+ />
62
+ )
63
+ );
@@ -0,0 +1,5 @@
1
+ export * from "./Button";
2
+ export * from "./ButtonGroup";
3
+ export * from "./CloseButton";
4
+ export * from "./FloatingActionButton";
5
+ export * from "./IconButton";
@@ -0,0 +1,59 @@
1
+ import { As, forwardRef, useStyleConfig } from "@chakra-ui/react";
2
+ import React from "react";
3
+ import { Box, BoxProps } from "../layout";
4
+
5
+ export type CardProps = Exclude<BoxProps, "size"> & {
6
+ size?: "sm" | "lg";
7
+ children: React.ReactNode;
8
+ colorScheme:
9
+ | "white"
10
+ | "grey"
11
+ | "blue"
12
+ | "green"
13
+ | "teal"
14
+ | "yellow"
15
+ | "orange";
16
+ };
17
+ /**
18
+ * Renders a card.
19
+ *
20
+ * The most basic version looks like this:
21
+ *
22
+ * ```tsx
23
+ * <Card>
24
+ * Content
25
+ * </Card>
26
+ * ```
27
+ *
28
+ * There are lots of color schemes available. You can also set the size as either `sm` or `lg`. The default is `lg`.
29
+ *
30
+ * ```tsx
31
+ * <Card colorScheme="orange" size="sm">
32
+ * A smaller card
33
+ * </Card>
34
+ * ```
35
+ *
36
+ * Cards are not interactive by default. If you specify the `as` prop to be a link or a button, you can make them work as links or buttons respectively. This will also give it a drop shadow.
37
+ *
38
+ * ```tsx
39
+ * <Card colorScheme="blue" as="button" onClick={handleClick}>
40
+ * Click for profit
41
+ * </Card>
42
+ * <Card colorScheme="green" as="a" href="https://vy.no">
43
+ * Go to start
44
+ * </Card>
45
+ * ```
46
+ */
47
+ export const Card = forwardRef<CardProps, As>(
48
+ ({ size = "lg", colorScheme = "white", children, ...props }, ref) => {
49
+ const styles = useStyleConfig("Card", {
50
+ colorScheme,
51
+ size,
52
+ });
53
+ return (
54
+ <Box __css={styles} {...props} ref={ref}>
55
+ {children}
56
+ </Box>
57
+ );
58
+ }
59
+ );
@@ -0,0 +1 @@
1
+ export * from "./Card";
@@ -0,0 +1,32 @@
1
+ import { Box } from "@chakra-ui/react";
2
+ import { createCalendar, DateValue } from "@internationalized/date";
3
+ import { useCalendarState } from "@react-stately/calendar";
4
+ import React from "react";
5
+ import {
6
+ CalendarProps as ReactAriaCalendarProps,
7
+ useCalendar,
8
+ } from "react-aria";
9
+ import { CalendarGrid } from "./CalendarGrid";
10
+ import { CalendarHeader } from "./CalendarHeader";
11
+ import { useCurrentLocale } from "./utils";
12
+
13
+ type CalendarProps = ReactAriaCalendarProps<DateValue> & {
14
+ showYearNavigation?: boolean;
15
+ };
16
+ export function Calendar({ showYearNavigation, ...props }: CalendarProps) {
17
+ const locale = useCurrentLocale();
18
+ const state = useCalendarState({
19
+ ...props,
20
+ locale,
21
+ createCalendar,
22
+ });
23
+
24
+ const { calendarProps } = useCalendar(props, state);
25
+
26
+ return (
27
+ <Box {...calendarProps}>
28
+ <CalendarHeader state={state} showYearNavigation={showYearNavigation} />
29
+ <CalendarGrid state={state} />
30
+ </Box>
31
+ );
32
+ }