@altruist03/ui 0.1.0 → 0.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.
Files changed (207) hide show
  1. package/assets/images/noData.webp +0 -0
  2. package/dist/index.js +15237 -650
  3. package/dist/index.js.map +1 -1
  4. package/dist/noData-XPRHKMFR.webp +0 -0
  5. package/package.json +21 -7
  6. package/src/ThemeProvider/ThemeContext.tsx +75 -0
  7. package/src/ThemeProvider/index.ts +7 -0
  8. package/src/assets/images/noData.webp +0 -0
  9. package/src/assets.d.ts +10 -0
  10. package/src/components/Accordion/Accordion.stories.tsx +64 -0
  11. package/src/components/Accordion/Accordion.tsx +219 -0
  12. package/src/components/Accordion/Accordion.types.ts +16 -0
  13. package/src/components/Avatar/Avatar.stories.tsx +304 -0
  14. package/src/components/Avatar/Avatar.tsx +211 -0
  15. package/src/components/Avatar/Avatar.types.ts +33 -0
  16. package/src/components/Badge/Badge.stories.tsx +143 -0
  17. package/src/components/Badge/Badge.tsx +138 -0
  18. package/src/components/Badge/Badge.types.ts +24 -0
  19. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
  20. package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
  21. package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
  22. package/src/components/Button/Button.stories.tsx +457 -0
  23. package/src/components/Button/Button.tsx +202 -0
  24. package/src/components/Button/Button.types.ts +18 -0
  25. package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
  26. package/src/components/Checkbox/Checkbox.tsx +352 -0
  27. package/src/components/Checkbox/Checkbox.types.ts +123 -0
  28. package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
  29. package/src/components/CloseButton/CloseButton.tsx +117 -0
  30. package/src/components/CloseButton/CloseButton.types.ts +15 -0
  31. package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
  32. package/src/components/DataTable/DataTable.stories.tsx +233 -0
  33. package/src/components/DataTable/DataTable.tsx +968 -0
  34. package/src/components/DataTable/DataTable.types.ts +49 -0
  35. package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
  36. package/src/components/DatePicker/DatePicker.tsx +669 -0
  37. package/src/components/DatePicker/DatePicker.types.ts +105 -0
  38. package/src/components/DatePicker/DateTimePicker.tsx +35 -0
  39. package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
  40. package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
  41. package/src/components/Drawer/Drawer.stories.tsx +243 -0
  42. package/src/components/Drawer/Drawer.tsx +196 -0
  43. package/src/components/Drawer/Drawer.types.ts +60 -0
  44. package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
  45. package/src/components/FileUpload/FileUpload.tsx +216 -0
  46. package/src/components/FileUpload/FileUpload.types.ts +80 -0
  47. package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
  48. package/src/components/FilterPopover/FilterPopover.tsx +310 -0
  49. package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
  50. package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
  51. package/src/components/ImageViewer/useImagePanRotate.js +102 -0
  52. package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
  53. package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
  54. package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
  55. package/src/components/Loader/Loader.stories.tsx +95 -0
  56. package/src/components/Loader/Loader.tsx +132 -0
  57. package/src/components/Loader/Loader.types.ts +6 -0
  58. package/src/components/Menu/Menu.tsx +143 -0
  59. package/src/components/Menu/Menu.types.ts +28 -0
  60. package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
  61. package/src/components/MenuButton/MenuButton.tsx +45 -0
  62. package/src/components/MenuButton/MenuButton.types.ts +21 -0
  63. package/src/components/Modal/Modal.stories.tsx +266 -0
  64. package/src/components/Modal/Modal.tsx +399 -0
  65. package/src/components/Modal/index.ts +2 -0
  66. package/src/components/Modal/types.ts +126 -0
  67. package/src/components/Pagination/Pagination.stories.tsx +167 -0
  68. package/src/components/Pagination/Pagination.tsx +266 -0
  69. package/src/components/Pagination/Pagination.types.ts +20 -0
  70. package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
  71. package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
  72. package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
  73. package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
  74. package/src/components/PhoneField/PhoneField.tsx +284 -0
  75. package/src/components/PhoneField/PhoneField.types.ts +89 -0
  76. package/src/components/PhoneField/countryCodes.ts +236 -0
  77. package/src/components/Progress/Progress.stories.tsx +83 -0
  78. package/src/components/Progress/Progress.tsx +91 -0
  79. package/src/components/Progress/Progress.types.ts +22 -0
  80. package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
  81. package/src/components/RadioButton/RadioButton.tsx +427 -0
  82. package/src/components/RadioButton/RadioButton.types.ts +117 -0
  83. package/src/components/ReportCard/ReportCard.tsx +183 -0
  84. package/src/components/ReportCard/ReportCard.types.ts +17 -0
  85. package/src/components/SectionIcon/SectionIcon.tsx +87 -0
  86. package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
  87. package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
  88. package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
  89. package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
  90. package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
  91. package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
  92. package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
  93. package/src/components/Selector/Selector.stories.tsx +382 -0
  94. package/src/components/Selector/Selector.tsx +1310 -0
  95. package/src/components/Selector/Selector.types.ts +71 -0
  96. package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
  97. package/src/components/Skeleton/Skeleton.tsx +20 -0
  98. package/src/components/Skeleton/Skeleton.types.ts +3 -0
  99. package/src/components/Spinner/Spinner.stories.tsx +84 -0
  100. package/src/components/Spinner/Spinner.tsx +79 -0
  101. package/src/components/Spinner/Spinner.types.ts +8 -0
  102. package/src/components/StatCard/StatCard.stories.tsx +125 -0
  103. package/src/components/StatCard/StatCard.tsx +167 -0
  104. package/src/components/StatCard/StatCard.types.ts +20 -0
  105. package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
  106. package/src/components/StatusBadge/StatusBadge.tsx +74 -0
  107. package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
  108. package/src/components/Stepper/Stepper.stories.tsx +85 -0
  109. package/src/components/Stepper/Stepper.tsx +138 -0
  110. package/src/components/Stepper/Stepper.types.ts +16 -0
  111. package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
  112. package/src/components/SubTabs/SubTabs.tsx +279 -0
  113. package/src/components/SubTabs/SubTabs.types.ts +44 -0
  114. package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
  115. package/src/components/SummaryCard/SummaryCard.tsx +141 -0
  116. package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
  117. package/src/components/Switch/Switch.stories.tsx +59 -0
  118. package/src/components/Switch/Switch.tsx +217 -0
  119. package/src/components/Switch/Switch.types.ts +22 -0
  120. package/src/components/Tabs/Tabs.stories.tsx +178 -0
  121. package/src/components/Tabs/Tabs.tsx +197 -0
  122. package/src/components/Tabs/Tabs.types.ts +34 -0
  123. package/src/components/TextField/TextField.stories.tsx +249 -0
  124. package/src/components/TextField/TextField.tsx +462 -0
  125. package/src/components/TextField/TextField.types.ts +141 -0
  126. package/src/components/Toast/Toast.stories.tsx +237 -0
  127. package/src/components/Toast/Toast.tsx +246 -0
  128. package/src/components/Toast/Toast.types.ts +31 -0
  129. package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
  130. package/src/components/ToggleButton/ToggleButton.tsx +267 -0
  131. package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
  132. package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
  133. package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
  134. package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
  135. package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
  136. package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
  137. package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
  138. package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
  139. package/src/components/index.ts +157 -0
  140. package/src/icons/ReportDollarIcon.tsx +71 -0
  141. package/src/icons/index.ts +27 -0
  142. package/src/index.ts +214 -0
  143. package/src/storybook/preview.tsx +64 -0
  144. package/src/theme/index.ts +4 -0
  145. package/src/theme/types.d.ts +1 -0
  146. package/src/themes/cpc.ts +4 -0
  147. package/src/themes/firebird.ts +4 -0
  148. package/src/tokens/brands/cpc/themes/dark.json +82 -0
  149. package/src/tokens/brands/cpc/themes/light.json +1 -0
  150. package/src/tokens/brands/default/brand.json +20 -0
  151. package/src/tokens/brands/firebird/themes/dark.json +28 -0
  152. package/src/tokens/brands/firebird/themes/light.json +20 -0
  153. package/src/tokens/button.ts +24 -0
  154. package/src/tokens/components/accordion.json +52 -0
  155. package/src/tokens/components/avatar.json +40 -0
  156. package/src/tokens/components/badge.json +41 -0
  157. package/src/tokens/components/breadcrumb.json +13 -0
  158. package/src/tokens/components/button.json +67 -0
  159. package/src/tokens/components/card.json +39 -0
  160. package/src/tokens/components/checkbox.json +64 -0
  161. package/src/tokens/components/closebutton.json +55 -0
  162. package/src/tokens/components/datatable.json +40 -0
  163. package/src/tokens/components/datepicker.json +99 -0
  164. package/src/tokens/components/drawer.json +23 -0
  165. package/src/tokens/components/field.json +29 -0
  166. package/src/tokens/components/fileupload.json +57 -0
  167. package/src/tokens/components/filterpopover.json +29 -0
  168. package/src/tokens/components/input.json +31 -0
  169. package/src/tokens/components/ipwhitelistfield.json +16 -0
  170. package/src/tokens/components/loader.json +15 -0
  171. package/src/tokens/components/menu.json +21 -0
  172. package/src/tokens/components/modal.json +25 -0
  173. package/src/tokens/components/pagination.json +45 -0
  174. package/src/tokens/components/phonefield.json +41 -0
  175. package/src/tokens/components/progress.json +12 -0
  176. package/src/tokens/components/radio.json +87 -0
  177. package/src/tokens/components/reportcard.json +17 -0
  178. package/src/tokens/components/selector.json +87 -0
  179. package/src/tokens/components/selectscroll.json +65 -0
  180. package/src/tokens/components/skeleton.json +6 -0
  181. package/src/tokens/components/stepper.json +38 -0
  182. package/src/tokens/components/subtabs.json +45 -0
  183. package/src/tokens/components/switch.json +55 -0
  184. package/src/tokens/components/tabs.json +38 -0
  185. package/src/tokens/components/textfield.json +59 -0
  186. package/src/tokens/components/togglebutton.json +48 -0
  187. package/src/tokens/components/uploaddropzone.json +61 -0
  188. package/src/tokens/drawer.ts +37 -0
  189. package/src/tokens/foundation/breakpoints.json +10 -0
  190. package/src/tokens/foundation/colors.json +122 -0
  191. package/src/tokens/foundation/opacity.json +16 -0
  192. package/src/tokens/foundation/radius.json +14 -0
  193. package/src/tokens/foundation/size.json +38 -0
  194. package/src/tokens/foundation/spacing.json +19 -0
  195. package/src/tokens/foundation/typography.json +43 -0
  196. package/src/tokens/index.ts +3665 -0
  197. package/src/tokens/modal.ts +42 -0
  198. package/src/tokens/semantic/color.json +148 -0
  199. package/src/tokens/semantic/field.json +28 -0
  200. package/src/tokens/semantic/opacity.json +5 -0
  201. package/src/tokens/semantic/size.json +722 -0
  202. package/src/tokens/semantic/spacing.json +27 -0
  203. package/src/tokens/semantic/typography.json +87 -0
  204. package/src/utils/scrollLock.ts +44 -0
  205. package/dist/index.css +0 -514
  206. package/dist/index.css.map +0 -1
  207. package/dist/index.d.ts +0 -289
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+
3
+ export type SelectorOption = string | { label?: string; [key: string]: unknown };
4
+
5
+ // ----- Unified props: multiple=false => value T|null, multiple=true => value T[] -----
6
+ export interface SelectorProps<T extends SelectorOption> {
7
+ /** When true, multi-select with checkboxes and chips; when false, single select. */
8
+ multiple?: boolean;
9
+ options: T[];
10
+ /** Single: T | null. Multiple: T[]. */
11
+ value: T | null | T[];
12
+ /** Single: (value: T | null) => void. Multiple: (value: T[]) => void. */
13
+ onChange?: (value: T | null | T[]) => void;
14
+ label?: string;
15
+ placeholder?: string;
16
+ required?: boolean;
17
+ error?: boolean | string;
18
+ helperText?: string;
19
+ disabled?: boolean;
20
+ size?: 'xs' | 'sm' | 'md' | 'lg';
21
+ /** Background color variant */
22
+ color?: 'grey' | 'white';
23
+ noOptionsText?: string;
24
+ loading?: boolean;
25
+ loadingText?: string;
26
+ isOptionDisabled?: (option: T) => boolean;
27
+ getOptionLabel?: (option: T) => string;
28
+ renderOption?: (option: T, state: { index: number }) => React.ReactNode;
29
+ /**
30
+ * Group options by a string key (similar to MUI Autocomplete `groupBy`).
31
+ * When provided, the listbox will render grouped sections.
32
+ */
33
+ groupBy?: (option: T) => string;
34
+ /**
35
+ * Custom group renderer (similar to MUI Autocomplete `renderGroup`).
36
+ * Called only when `groupBy` is provided.
37
+ * The heading div with background is always rendered by Selector; return only the list content (e.g. ul with children).
38
+ */
39
+ renderGroup?: (params: {
40
+ key: string;
41
+ group: string;
42
+ children: React.ReactNode;
43
+ }) => React.ReactNode;
44
+ id?: string;
45
+ /** Only when multiple: max chips shown before "+N". -1 = no limit. */
46
+ limitTags?: number;
47
+ /** Show clear button on hover/select. Default true. */
48
+ clearable?: boolean;
49
+ filterOptions?: (
50
+ options: T[],
51
+ state: { inputValue: string; getOptionLabel: (option: T) => string }
52
+ ) => T[];
53
+ /** Handler for scroll events on the listbox */
54
+ onListboxScroll?: (event: React.UIEvent<HTMLUListElement>) => void;
55
+ /** Called when the dropdown opens or closes */
56
+ onOpenChange?: (open: boolean) => void;
57
+ /** Optional external trigger for opening/closing the dropdown. */
58
+ open?: boolean;
59
+ /** Loading state - shows skeleton instead of component */
60
+ skeletonLoading?: boolean;
61
+ onInput?: React.FormEventHandler<HTMLInputElement>;
62
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
63
+ /** When true, render country flag (from option.iso) before each option label. */
64
+ showFlag?: boolean;
65
+ /** Browser autocomplete hint for the underlying input. */
66
+ autoComplete?: string;
67
+ /** Minimum width (px) for the dropdown listbox. */
68
+ listboxMinWidth?: number;
69
+ /** Custom equality check between an option and a selected value. Defaults to reference equality. */
70
+ isOptionEqualToValue?: (option: T, value: T) => boolean;
71
+ }
@@ -0,0 +1,133 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Skeleton } from "./Skeleton";
3
+
4
+ const meta: Meta<typeof Skeleton> = {
5
+ title: "Design System/Skeleton",
6
+ component: Skeleton,
7
+ tags: ["autodocs"],
8
+ decorators: [
9
+ (Story) => (
10
+ <div style={{ padding: "24px", width: "520px", maxWidth: "100%" }}>
11
+ <Story />
12
+ </div>
13
+ ),
14
+ ],
15
+ args: {
16
+ height: 20,
17
+ width: "100%",
18
+ borderRadius: 8,
19
+ enableAnimation: true,
20
+ },
21
+ argTypes: {
22
+ count: {
23
+ control: { type: "number", min: 1 },
24
+ description: "Number of skeleton placeholders to render",
25
+ table: { type: { summary: "number" } },
26
+ },
27
+ height: {
28
+ control: { type: "number" },
29
+ description: "Height (px when number)",
30
+ table: { type: { summary: "string | number" } },
31
+ },
32
+ width: {
33
+ control: { type: "text" },
34
+ description: "Width (number or CSS string)",
35
+ table: { type: { summary: "string | number" } },
36
+ },
37
+ borderRadius: {
38
+ control: { type: "text" },
39
+ description: "Border radius (number or CSS string)",
40
+ table: { type: { summary: "string | number" } },
41
+ },
42
+ circle: {
43
+ control: { type: "boolean" },
44
+ description: "Render as a circle (useful for avatars)",
45
+ table: { type: { summary: "boolean" } },
46
+ },
47
+ enableAnimation: {
48
+ control: { type: "boolean" },
49
+ description: "Enable/disable shimmer animation",
50
+ table: { type: { summary: "boolean" } },
51
+ },
52
+ duration: {
53
+ control: { type: "number", min: 0 },
54
+ description: "Animation duration (seconds)",
55
+ table: { type: { summary: "number" } },
56
+ },
57
+ direction: {
58
+ control: { type: "radio" },
59
+ options: ["ltr", "rtl"],
60
+ description: "Animation direction",
61
+ table: { type: { summary: '"ltr" | "rtl"' } },
62
+ },
63
+ inline: {
64
+ control: { type: "boolean" },
65
+ description: "Render inline (useful alongside text)",
66
+ table: { type: { summary: "boolean" } },
67
+ },
68
+ },
69
+ };
70
+
71
+ export default meta;
72
+
73
+ type Story = StoryObj<typeof Skeleton>;
74
+
75
+ export const Playground: Story = {
76
+ args: {},
77
+ };
78
+
79
+ const ShowcaseComponent = () => {
80
+ return (
81
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
82
+ {/* Card-sized block (matches dashboard usage) */}
83
+ <div>
84
+ <div style={{ fontSize: "12px", marginBottom: "8px" }}>
85
+ Card placeholder
86
+ </div>
87
+ <Skeleton height={108} borderRadius={16} />
88
+ </div>
89
+
90
+ {/* Text lines */}
91
+ <div>
92
+ <div style={{ fontSize: "12px", marginBottom: "8px" }}>Text lines</div>
93
+ <Skeleton height={14} width="100%" style={{ marginBottom: 8 }} />
94
+ <Skeleton height={14} width="92%" style={{ marginBottom: 8 }} />
95
+ <Skeleton height={14} width="76%" />
96
+ </div>
97
+
98
+ {/* Avatar + text row */}
99
+ <div>
100
+ <div style={{ fontSize: "12px", marginBottom: "8px" }}>
101
+ Avatar + text
102
+ </div>
103
+ <div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
104
+ <Skeleton circle width={44} height={44} />
105
+ <div style={{ flex: 1 }}>
106
+ <Skeleton height={14} width="60%" style={{ marginBottom: 8 }} />
107
+ <Skeleton height={12} width="40%" />
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ {/* Multiple items */}
113
+ <div>
114
+ <div style={{ fontSize: "12px", marginBottom: "8px" }}>
115
+ Multiple items
116
+ </div>
117
+ <Skeleton count={4} height={12} style={{ marginBottom: 8 }} />
118
+ </div>
119
+
120
+ {/* No animation */}
121
+ <div>
122
+ <div style={{ fontSize: "12px", marginBottom: "8px" }}>
123
+ No animation
124
+ </div>
125
+ <Skeleton height={40} enableAnimation={false} />
126
+ </div>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ export const Showcase: Story = {
132
+ render: () => <ShowcaseComponent />,
133
+ };
@@ -0,0 +1,20 @@
1
+ import PluginSkeleton from 'react-loading-skeleton';
2
+ import 'react-loading-skeleton/dist/skeleton.css';
3
+
4
+ import { useTokens } from '../../ThemeProvider/ThemeContext';
5
+ import { getSkeletonTokensFromResolved } from '../../tokens';
6
+ import type { SkeletonProps } from './Skeleton.types';
7
+
8
+ /**
9
+ * Design-system Skeleton component. Wraps react-loading-skeleton and applies
10
+ * design tokens for baseColor and highlightColor by default. All props are
11
+ * passed through to the plugin; component props override token defaults.
12
+ */
13
+ export const Skeleton = (props: SkeletonProps) => {
14
+ const resolvedTokens = useTokens();
15
+ const tokens = getSkeletonTokensFromResolved(resolvedTokens);
16
+
17
+ const { baseColor = tokens.baseColor, highlightColor = tokens.highlightColor, ...rest } = props;
18
+
19
+ return <PluginSkeleton baseColor={baseColor} highlightColor={highlightColor} {...rest} />;
20
+ };
@@ -0,0 +1,3 @@
1
+ export type SkeletonProps = import("react-loading-skeleton").SkeletonProps;
2
+ export type SkeletonStyleProps =
3
+ import("react-loading-skeleton").SkeletonStyleProps;
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Spinner } from "./Spinner";
4
+ import type { SpinnerProps } from "./Spinner.types";
5
+
6
+ const meta: Meta<typeof Spinner> = {
7
+ title: "Design System/Spinner",
8
+ component: Spinner,
9
+ tags: ["autodocs"],
10
+ decorators: [
11
+ (Story) => (
12
+ <div style={{ padding: "24px" }}>
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ args: {
18
+ size: undefined,
19
+ color: "currentColor",
20
+ } as SpinnerProps,
21
+ argTypes: {
22
+ size: {
23
+ control: "text",
24
+ description: "Size of the spinner (e.g., '24px', '2rem', 24)",
25
+ },
26
+ color: {
27
+ control: "color",
28
+ description: "Color of the spinner. Defaults to 'currentColor'.",
29
+ },
30
+ },
31
+ };
32
+
33
+ export default meta;
34
+
35
+ type Story = StoryObj<typeof Spinner>;
36
+
37
+ export const Playground: Story = {
38
+ args: {
39
+ size: 24,
40
+ color: "#ff5722",
41
+ },
42
+ render: (args) => <Spinner {...args} />,
43
+ };
44
+
45
+ const ShowcaseComponent = () => {
46
+ return (
47
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px", color: "#333" }}>
48
+ <div>
49
+ <h4 style={{ margin: "0 0 8px 0", fontSize: "14px", fontWeight: 600 }}>Default</h4>
50
+ <Spinner />
51
+ </div>
52
+
53
+ <div>
54
+ <h4 style={{ margin: "0 0 8px 0", fontSize: "14px", fontWeight: 600 }}>Custom Size</h4>
55
+ <div style={{ display: "flex", gap: "16px", alignItems: "center" }}>
56
+ <Spinner size={16} />
57
+ <Spinner size={32} />
58
+ <Spinner size={48} />
59
+ </div>
60
+ </div>
61
+
62
+ <div>
63
+ <h4 style={{ margin: "0 0 8px 0", fontSize: "14px", fontWeight: 600 }}>Custom Color</h4>
64
+ <div style={{ display: "flex", gap: "16px", alignItems: "center" }}>
65
+ <Spinner color="#f44336" />
66
+ <Spinner color="#4caf50" />
67
+ <Spinner color="#2196f3" />
68
+ </div>
69
+ </div>
70
+
71
+ <div>
72
+ <h4 style={{ margin: "0 0 8px 0", fontSize: "14px", fontWeight: 600 }}>Inherited Color</h4>
73
+ <div style={{ color: "#9c27b0", display: "flex", alignItems: "center", gap: "8px" }}>
74
+ <span>Parent text color is purple</span>
75
+ <Spinner />
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export const Showcase: Story = {
83
+ render: () => <ShowcaseComponent />,
84
+ };
@@ -0,0 +1,79 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useTokens } from "../../ThemeProvider/ThemeContext";
3
+ import { getButtonTokensFromResolved } from "../../tokens";
4
+ import type { SpinnerProps } from "./Spinner.types";
5
+
6
+ const KEYFRAMES_ID = "ds-spinner-keyframes";
7
+
8
+ function ensureKeyframes() {
9
+ if (typeof document === "undefined") return;
10
+ if (document.getElementById(KEYFRAMES_ID)) return;
11
+
12
+ const css = `
13
+ @keyframes dsSpinnerSpin {
14
+ 0% { transform: rotate(0deg); }
15
+ 100% { transform: rotate(360deg); }
16
+ }`;
17
+
18
+ const style = document.createElement("style");
19
+ style.id = KEYFRAMES_ID;
20
+ style.textContent = css;
21
+ document.head.appendChild(style);
22
+ }
23
+
24
+ export const Spinner = ({
25
+ size,
26
+ color = "currentColor",
27
+ style,
28
+ }: SpinnerProps) => {
29
+ const resolvedTokens = useTokens();
30
+ const { sizes } = getButtonTokensFromResolved(resolvedTokens);
31
+
32
+ // Default to md button icon size
33
+ const defaultSize = sizes.md.fontSize;
34
+ const finalSize = size ?? defaultSize;
35
+
36
+ const injectedRef = useRef(false);
37
+
38
+ useEffect(() => {
39
+ if (!injectedRef.current) {
40
+ ensureKeyframes();
41
+ injectedRef.current = true;
42
+ }
43
+ }, []);
44
+
45
+ return (
46
+ <svg
47
+ width={finalSize}
48
+ height={finalSize}
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ style={{
53
+ animation: "dsSpinnerSpin 1s linear infinite",
54
+ display: "block",
55
+ transformOrigin: "center",
56
+ ...style,
57
+ fill: "transparent",
58
+ }}
59
+ >
60
+ <circle
61
+ cx="12"
62
+ cy="12"
63
+ r="10"
64
+ stroke={color}
65
+ strokeOpacity="0.25"
66
+ strokeWidth="3"
67
+ />
68
+ <circle
69
+ cx="12"
70
+ cy="12"
71
+ r="10"
72
+ stroke={color}
73
+ strokeWidth="3"
74
+ strokeLinecap="round"
75
+ strokeDasharray="20 60"
76
+ />
77
+ </svg>
78
+ );
79
+ };
@@ -0,0 +1,8 @@
1
+ export interface SpinnerProps {
2
+ /** Size of the spinner, typically inheriting the font size */
3
+ size?: number | string;
4
+ /** Color of the spinner, inherits current color by default */
5
+ color?: string;
6
+ /** Additional styles for the SVG element */
7
+ style?: React.CSSProperties;
8
+ }
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { StatCard } from './StatCard';
4
+ import type { StatCardProps } from './StatCard.types';
5
+
6
+ function CheckIcon() {
7
+ return (
8
+ <svg
9
+ width={14}
10
+ height={14}
11
+ viewBox="0 0 14 14"
12
+ fill="none"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ aria-hidden
15
+ >
16
+ <path
17
+ d="M2.5 7L5.5 10L11.5 4"
18
+ stroke="currentColor"
19
+ strokeWidth={2}
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ />
23
+ </svg>
24
+ );
25
+ }
26
+
27
+ function AlertIcon() {
28
+ return (
29
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none" aria-hidden>
30
+ <path d="M7 3.5V8" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
31
+ <circle cx="7" cy="10.5" r="1" fill="currentColor" />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ const meta: Meta<typeof StatCard> = {
37
+ title: 'Design System/StatCard',
38
+ component: StatCard,
39
+ tags: [],
40
+ decorators: [
41
+ (Story) => (
42
+ <div style={{ padding: 24, maxWidth: 420, backgroundColor: '#E5E9EB' }}>
43
+ <Story />
44
+ </div>
45
+ ),
46
+ ],
47
+ args: {
48
+ label: 'Active',
49
+ infoNote: 'Rider is currently active and available for assignments',
50
+ value: '972',
51
+ accentColor: '#22C55E',
52
+ iconBackgroundColor: '#DDF3E3',
53
+ icon: <CheckIcon />,
54
+ } satisfies StatCardProps,
55
+ argTypes: {
56
+ label: { control: { type: 'text' } },
57
+ infoNote: { control: { type: 'text' } },
58
+ value: { control: { type: 'text' } },
59
+ accentColor: { control: { type: 'color' } },
60
+ iconBackgroundColor: { control: { type: 'color' } },
61
+ ariaLabel: { control: { type: 'text' } },
62
+ },
63
+ };
64
+
65
+ export default meta;
66
+
67
+ type Story = StoryObj<typeof StatCard>;
68
+
69
+ export const Playground: Story = {
70
+ args: {
71
+ label: 'Active',
72
+ infoNote: 'Rider is currently active and available for assignments',
73
+ value: '972',
74
+ accentColor: '#22C55E',
75
+ iconBackgroundColor: '#DDF3E3',
76
+ icon: <CheckIcon />,
77
+ },
78
+ };
79
+
80
+ export const Showcase: Story = {
81
+ render: () => (
82
+ <div
83
+ style={{
84
+ display: 'flex',
85
+ flexDirection: 'column',
86
+ gap: 16,
87
+ maxWidth: 400,
88
+ }}
89
+ >
90
+ <StatCard
91
+ label="Active"
92
+ infoNote="Rider is currently active and available for assignments"
93
+ value="972"
94
+ accentColor="#22C55E"
95
+ iconBackgroundColor="#DDF3E3"
96
+ icon={<CheckIcon />}
97
+ />
98
+ <StatCard
99
+ label="Pending"
100
+ infoNote="Riders waiting for the next approval step"
101
+ value="14"
102
+ accentColor="#F97316"
103
+ iconBackgroundColor="#FFE7D6"
104
+ icon={<AlertIcon />}
105
+ />
106
+ <StatCard
107
+ label="Completed deliveries"
108
+ infoNote="Total number of successfully completed delivery orders"
109
+ value={12840}
110
+ accentColor="#16A34A"
111
+ iconBackgroundColor="#DDF3E3"
112
+ icon={<CheckIcon />}
113
+ ariaLabel="Completed deliveries: 12840"
114
+ />
115
+ <StatCard
116
+ label="At risk"
117
+ infoNote="Riders requiring immediate review or intervention"
118
+ value="3"
119
+ accentColor="#DC2626"
120
+ iconBackgroundColor="#FBDCDC"
121
+ icon={<AlertIcon />}
122
+ />
123
+ </div>
124
+ ),
125
+ };
@@ -0,0 +1,167 @@
1
+ import type { CSSProperties } from 'react';
2
+ import { useTokens } from '../../ThemeProvider/ThemeContext';
3
+ import { getStatCardTokensFromResolved } from '../../tokens';
4
+ import type { StatCardProps } from './StatCard.types';
5
+
6
+ function InfoIcon() {
7
+ return (
8
+ <svg
9
+ width="12"
10
+ height="12"
11
+ viewBox="0 0 12 12"
12
+ fill="none"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ aria-hidden
15
+ >
16
+ <circle cx="6" cy="6" r="5.25" stroke="currentColor" strokeWidth="1.5" />
17
+ <path d="M6 5.2V8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
18
+ <circle cx="6" cy="3.55" r="0.75" fill="currentColor" />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ function defaultAriaLabel(
24
+ label: StatCardProps['label'],
25
+ value: StatCardProps['value'],
26
+ infoNote?: string
27
+ ) {
28
+ if (typeof label === 'string' || typeof label === 'number') {
29
+ if (typeof value === 'string' || typeof value === 'number') {
30
+ return infoNote ? `${label}: ${value}. ${infoNote}` : `${label}: ${value}`;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export const StatCard = ({
37
+ label,
38
+ infoNote,
39
+ value,
40
+ accentColor,
41
+ iconBackgroundColor,
42
+ icon,
43
+ ariaLabel,
44
+ className,
45
+ style,
46
+ }: StatCardProps) => {
47
+ const resolvedTokens = useTokens();
48
+ const t = getStatCardTokensFromResolved(resolvedTokens);
49
+
50
+ const mixPercent = `${t.iconWellAccentMixPercent}%`;
51
+ const iconWellStyle = {
52
+ ['--stat-accent' as string]: accentColor,
53
+ ['--stat-well-base' as string]: t.surfaceBackground,
54
+ width: t.iconWellSize,
55
+ height: t.iconWellSize,
56
+ borderRadius: t.iconWellBorderRadius,
57
+ flexShrink: 0,
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ marginLeft: '-2px',
62
+ background:
63
+ iconBackgroundColor ??
64
+ `color-mix(in srgb, var(--stat-accent) ${mixPercent}, var(--stat-well-base))`,
65
+ } as CSSProperties;
66
+
67
+ const computedAria = ariaLabel ?? defaultAriaLabel(label, value, infoNote);
68
+
69
+ return (
70
+ <div
71
+ className={className}
72
+ role="group"
73
+ aria-label={computedAria}
74
+ style={{
75
+ boxSizing: 'border-box',
76
+ display: 'flex',
77
+ flexDirection: 'row',
78
+ alignItems: 'center',
79
+ gap: t.gap,
80
+ padding: t.padding,
81
+ backgroundColor: t.surfaceBackground,
82
+ border: t.border,
83
+ borderRadius: t.borderRadius,
84
+ ...style,
85
+ }}
86
+ >
87
+ <div style={iconWellStyle}>
88
+ <div
89
+ style={{
90
+ display: 'flex',
91
+ alignItems: 'center',
92
+ justifyContent: 'center',
93
+ color: accentColor,
94
+ lineHeight: 0,
95
+ }}
96
+ >
97
+ {icon}
98
+ </div>
99
+ </div>
100
+ <div
101
+ style={{
102
+ display: 'flex',
103
+ flexDirection: 'column',
104
+ flex: 1,
105
+ alignItems: 'flex-start',
106
+ justifyContent: 'center',
107
+ minWidth: 0,
108
+ fontFamily: resolvedTokens.semantic?.typography?.body?.medium?.fontFamily,
109
+ }}
110
+ >
111
+ <div
112
+ style={{
113
+ display: 'flex',
114
+ alignItems: 'center',
115
+ gap: '4px',
116
+ width: '100%',
117
+ minWidth: 0,
118
+ }}
119
+ >
120
+ <span
121
+ style={{
122
+ flex: 1,
123
+ fontSize: t.labelFontSize,
124
+ fontWeight: t.labelFontWeight,
125
+ color: t.labelColor,
126
+ lineHeight: 1.2,
127
+ whiteSpace: 'nowrap',
128
+ overflow: 'hidden',
129
+ textOverflow: 'ellipsis',
130
+ }}
131
+ >
132
+ {label}
133
+ </span>
134
+ {infoNote ?
135
+ <span
136
+ title={infoNote}
137
+ aria-label={infoNote}
138
+ tabIndex={0}
139
+ style={{
140
+ display: 'inline-flex',
141
+ alignItems: 'center',
142
+ justifyContent: 'center',
143
+ color: t.labelColor,
144
+ opacity: 0.72,
145
+ cursor: 'pointer',
146
+ flexShrink: 0,
147
+ lineHeight: 0,
148
+ }}
149
+ >
150
+ <InfoIcon />
151
+ </span>
152
+ : null}
153
+ </div>
154
+ <span
155
+ style={{
156
+ fontSize: t.valueFontSize,
157
+ fontWeight: t.valueFontWeight,
158
+ color: t.valueColor,
159
+ lineHeight: 1.2,
160
+ }}
161
+ >
162
+ {value}
163
+ </span>
164
+ </div>
165
+ </div>
166
+ );
167
+ };