@mich8060/chg-design-system 0.1.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 (260) hide show
  1. package/.github/workflows/figma-sync.yml +30 -0
  2. package/ARCHITECTURE_FIX.md +241 -0
  3. package/LICENSE +21 -0
  4. package/README.lib.md +103 -0
  5. package/README.md +177 -0
  6. package/figma.config.json +9 -0
  7. package/package.json +67 -0
  8. package/package.lib.json +49 -0
  9. package/public/data/figma-variables.json +40026 -0
  10. package/public/favicon.ico +0 -0
  11. package/public/index.html +46 -0
  12. package/public/logo192.png +0 -0
  13. package/public/logo512.png +0 -0
  14. package/public/manifest.json +25 -0
  15. package/public/robots.txt +3 -0
  16. package/public/styles/tokens.css +1994 -0
  17. package/scripts/index.js +896 -0
  18. package/scripts/publish-lib.js +150 -0
  19. package/scripts/validate.js +94 -0
  20. package/src/App.css +457 -0
  21. package/src/App.css.map +1 -0
  22. package/src/App.js +161 -0
  23. package/src/App.scss +548 -0
  24. package/src/App.test.js +8 -0
  25. package/src/assets/images/.gitkeep +0 -0
  26. package/src/assets/images/doctors/Avatar-1.png +0 -0
  27. package/src/assets/images/doctors/Avatar-10.png +0 -0
  28. package/src/assets/images/doctors/Avatar-11.png +0 -0
  29. package/src/assets/images/doctors/Avatar-12.png +0 -0
  30. package/src/assets/images/doctors/Avatar-13.png +0 -0
  31. package/src/assets/images/doctors/Avatar-14.png +0 -0
  32. package/src/assets/images/doctors/Avatar-15.png +0 -0
  33. package/src/assets/images/doctors/Avatar-16.png +0 -0
  34. package/src/assets/images/doctors/Avatar-17.png +0 -0
  35. package/src/assets/images/doctors/Avatar-18.png +0 -0
  36. package/src/assets/images/doctors/Avatar-19.png +0 -0
  37. package/src/assets/images/doctors/Avatar-2.png +0 -0
  38. package/src/assets/images/doctors/Avatar-20.png +0 -0
  39. package/src/assets/images/doctors/Avatar-21.png +0 -0
  40. package/src/assets/images/doctors/Avatar-3.png +0 -0
  41. package/src/assets/images/doctors/Avatar-4.png +0 -0
  42. package/src/assets/images/doctors/Avatar-5.png +0 -0
  43. package/src/assets/images/doctors/Avatar-6.png +0 -0
  44. package/src/assets/images/doctors/Avatar-7.png +0 -0
  45. package/src/assets/images/doctors/Avatar-8.png +0 -0
  46. package/src/assets/images/doctors/Avatar-9.png +0 -0
  47. package/src/assets/images/doctors/Avatar.png +0 -0
  48. package/src/assets/images/doctors/index.js +141 -0
  49. package/src/data/figma-variables.json +90305 -0
  50. package/src/index.js +20 -0
  51. package/src/index.scss +10 -0
  52. package/src/pages/AccordionDemo.jsx +206 -0
  53. package/src/pages/AccordionDemo.scss +34 -0
  54. package/src/pages/ActionMenuDemo.jsx +957 -0
  55. package/src/pages/ActionMenuDemo.scss +34 -0
  56. package/src/pages/AvatarDemo.jsx +328 -0
  57. package/src/pages/AvatarDemo.scss +40 -0
  58. package/src/pages/BadgeDemo.jsx +254 -0
  59. package/src/pages/BadgeDemo.scss +40 -0
  60. package/src/pages/BorderRadiusDemo.jsx +112 -0
  61. package/src/pages/BorderRadiusDemo.scss +50 -0
  62. package/src/pages/BrandingDemo.jsx +117 -0
  63. package/src/pages/BreadcrumbDemo.jsx +172 -0
  64. package/src/pages/ButtonDemo.jsx +708 -0
  65. package/src/pages/ButtonDemo.scss +34 -0
  66. package/src/pages/CheckboxDemo.jsx +194 -0
  67. package/src/pages/ChipDemo.jsx +359 -0
  68. package/src/pages/ChipDemo.scss +40 -0
  69. package/src/pages/ColorsDemo.jsx +566 -0
  70. package/src/pages/ColorsDemo.scss +243 -0
  71. package/src/pages/ComponentsUsage.jsx +401 -0
  72. package/src/pages/DatepickerDemo.jsx +223 -0
  73. package/src/pages/DividerDemo.jsx +337 -0
  74. package/src/pages/DotStatusDemo.jsx +223 -0
  75. package/src/pages/DropdownDemo.jsx +229 -0
  76. package/src/pages/FieldDemo.jsx +253 -0
  77. package/src/pages/FigmaVariablesDemo.jsx +426 -0
  78. package/src/pages/FigmaVariablesDemo.scss +316 -0
  79. package/src/pages/FileUploadDemo.jsx +186 -0
  80. package/src/pages/FlexDemo.jsx +144 -0
  81. package/src/pages/FlexDemo.scss +119 -0
  82. package/src/pages/FontInstallation.jsx +252 -0
  83. package/src/pages/FontInstallation.scss +40 -0
  84. package/src/pages/Home.jsx +3156 -0
  85. package/src/pages/IconDemo.jsx +1680 -0
  86. package/src/pages/ImageAspectDemo.jsx +152 -0
  87. package/src/pages/InputDemo.jsx +245 -0
  88. package/src/pages/Installation.jsx +257 -0
  89. package/src/pages/Installation.scss +40 -0
  90. package/src/pages/KeyDemo.jsx +184 -0
  91. package/src/pages/MenuDemo.jsx +139 -0
  92. package/src/pages/MicroCalendarDemo.jsx +165 -0
  93. package/src/pages/PaginationDemo.jsx +176 -0
  94. package/src/pages/PillToggleDemo.jsx +212 -0
  95. package/src/pages/ProgressCircleDemo.jsx +206 -0
  96. package/src/pages/ProgressIndicatorDemo.jsx +227 -0
  97. package/src/pages/RadioDemo.jsx +282 -0
  98. package/src/pages/ShadowsDemo.jsx +118 -0
  99. package/src/pages/ShadowsDemo.scss +93 -0
  100. package/src/pages/SliderDemo.jsx +226 -0
  101. package/src/pages/SpacingDemo.jsx +160 -0
  102. package/src/pages/SpacingDemo.scss +107 -0
  103. package/src/pages/StatusDemo.jsx +196 -0
  104. package/src/pages/StepsDemo.jsx +308 -0
  105. package/src/pages/TableDemo.jsx +376 -0
  106. package/src/pages/TabsDemo.jsx +221 -0
  107. package/src/pages/ToastDemo.jsx +195 -0
  108. package/src/pages/ToggleDemo.jsx +187 -0
  109. package/src/pages/TokensDemo.jsx +637 -0
  110. package/src/pages/TokensDemo.scss +270 -0
  111. package/src/pages/TokensUsage.jsx +220 -0
  112. package/src/pages/TooltipDemo.jsx +170 -0
  113. package/src/pages/TypographyDemo.jsx +229 -0
  114. package/src/pages/TypographyDemo.scss +105 -0
  115. package/src/pages/UtilitiesDemo.jsx +381 -0
  116. package/src/pages/UtilitiesDemo.scss +214 -0
  117. package/src/reportWebVitals.js +13 -0
  118. package/src/setupTests.js +5 -0
  119. package/src/styles/_typography.scss +932 -0
  120. package/src/styles/_utilities.scss +3635 -0
  121. package/src/styles/_variables.scss +887 -0
  122. package/src/styles/prism-custom.css +206 -0
  123. package/src/styles/prism-custom.css.map +1 -0
  124. package/src/styles/prism-custom.scss +205 -0
  125. package/src/styles/tokens.css +4416 -0
  126. package/src/styles/tokens.css.map +1 -0
  127. package/src/styles/tokens.scss +1456 -0
  128. package/src/ui/Accordion/Accordion.jsx +70 -0
  129. package/src/ui/Accordion/Accordion.scss +82 -0
  130. package/src/ui/Accordion/index.js +1 -0
  131. package/src/ui/ActionMenu/ActionMenu.jsx +383 -0
  132. package/src/ui/ActionMenu/ActionMenu.scss +198 -0
  133. package/src/ui/ActionMenu/index.js +1 -0
  134. package/src/ui/Avatar/Avatar.jsx +49 -0
  135. package/src/ui/Avatar/Avatar.scss +82 -0
  136. package/src/ui/Avatar/index.js +1 -0
  137. package/src/ui/Badge/Badge.jsx +64 -0
  138. package/src/ui/Badge/Badge.scss +84 -0
  139. package/src/ui/Badge/index.js +1 -0
  140. package/src/ui/Branding/Branding.jsx +65 -0
  141. package/src/ui/Branding/Branding.scss +116 -0
  142. package/src/ui/Branding/index.js +1 -0
  143. package/src/ui/Breadcrumb/Breadcrumb.jsx +162 -0
  144. package/src/ui/Breadcrumb/Breadcrumb.scss +46 -0
  145. package/src/ui/Breadcrumb/index.js +2 -0
  146. package/src/ui/Button/Button.figma.tsx +49 -0
  147. package/src/ui/Button/Button.jsx +135 -0
  148. package/src/ui/Button/Button.scss +188 -0
  149. package/src/ui/Button/index.js +1 -0
  150. package/src/ui/Card/Card.jsx +25 -0
  151. package/src/ui/Card/Card.scss +47 -0
  152. package/src/ui/Card/index.js +1 -0
  153. package/src/ui/Checkbox/Checkbox.jsx +70 -0
  154. package/src/ui/Checkbox/Checkbox.scss +96 -0
  155. package/src/ui/Checkbox/index.js +1 -0
  156. package/src/ui/Chip/Chip.jsx +104 -0
  157. package/src/ui/Chip/Chip.scss +118 -0
  158. package/src/ui/Chip/index.js +1 -0
  159. package/src/ui/CopyButton/CopyButton.jsx +102 -0
  160. package/src/ui/CopyButton/CopyButton.scss +56 -0
  161. package/src/ui/CopyButton/index.js +1 -0
  162. package/src/ui/Datepicker/Datepicker.jsx +326 -0
  163. package/src/ui/Datepicker/Datepicker.scss +187 -0
  164. package/src/ui/Datepicker/index.js +2 -0
  165. package/src/ui/Divider/Divider.jsx +89 -0
  166. package/src/ui/Divider/Divider.scss +112 -0
  167. package/src/ui/Divider/index.js +1 -0
  168. package/src/ui/DotStatus/DotStatus.jsx +64 -0
  169. package/src/ui/DotStatus/DotStatus.scss +87 -0
  170. package/src/ui/DotStatus/index.js +1 -0
  171. package/src/ui/Dropdown/Dropdown.jsx +200 -0
  172. package/src/ui/Dropdown/Dropdown.scss +156 -0
  173. package/src/ui/Dropdown/index.js +1 -0
  174. package/src/ui/Field/Field.jsx +89 -0
  175. package/src/ui/Field/Field.scss +119 -0
  176. package/src/ui/Field/index.js +1 -0
  177. package/src/ui/FileUpload/FileUpload.figma.tsx +28 -0
  178. package/src/ui/FileUpload/FileUpload.jsx +153 -0
  179. package/src/ui/FileUpload/FileUpload.scss +78 -0
  180. package/src/ui/FileUpload/index.js +2 -0
  181. package/src/ui/Flex/Flex.jsx +42 -0
  182. package/src/ui/Flex/Flex.scss +119 -0
  183. package/src/ui/Flex/index.js +1 -0
  184. package/src/ui/Icon/Icon.figma.tsx +22 -0
  185. package/src/ui/Icon/Icon.jsx +47 -0
  186. package/src/ui/Icon/index.js +1 -0
  187. package/src/ui/ImageAspect/ImageAspect.jsx +56 -0
  188. package/src/ui/ImageAspect/ImageAspect.scss +62 -0
  189. package/src/ui/ImageAspect/index.js +1 -0
  190. package/src/ui/Input/Input.figma.tsx +35 -0
  191. package/src/ui/Input/Input.jsx +68 -0
  192. package/src/ui/Input/Input.scss +64 -0
  193. package/src/ui/Input/index.js +2 -0
  194. package/src/ui/Key/Key.jsx +37 -0
  195. package/src/ui/Key/Key.scss +34 -0
  196. package/src/ui/Key/index.js +1 -0
  197. package/src/ui/Menu/Menu.jsx +389 -0
  198. package/src/ui/Menu/Menu.scss +382 -0
  199. package/src/ui/Menu/index.js +1 -0
  200. package/src/ui/MicroCalendar/MicroCalendar.jsx +392 -0
  201. package/src/ui/MicroCalendar/MicroCalendar.scss +277 -0
  202. package/src/ui/MicroCalendar/index.js +1 -0
  203. package/src/ui/Pagination/Pagination.jsx +237 -0
  204. package/src/ui/Pagination/Pagination.scss +182 -0
  205. package/src/ui/Pagination/index.js +1 -0
  206. package/src/ui/PillToggle/PillToggle.jsx +56 -0
  207. package/src/ui/PillToggle/PillToggle.scss +84 -0
  208. package/src/ui/PillToggle/index.js +1 -0
  209. package/src/ui/Playground/Playground.jsx +524 -0
  210. package/src/ui/Playground/Playground.scss +310 -0
  211. package/src/ui/Playground/index.js +2 -0
  212. package/src/ui/ProgressCircle/ProgressCircle.jsx +147 -0
  213. package/src/ui/ProgressCircle/ProgressCircle.scss +143 -0
  214. package/src/ui/ProgressCircle/index.js +1 -0
  215. package/src/ui/ProgressIndicator/ProgressIndicator.jsx +92 -0
  216. package/src/ui/ProgressIndicator/ProgressIndicator.scss +133 -0
  217. package/src/ui/ProgressIndicator/index.js +1 -0
  218. package/src/ui/Radio/Radio.jsx +57 -0
  219. package/src/ui/Radio/Radio.scss +84 -0
  220. package/src/ui/Radio/index.js +1 -0
  221. package/src/ui/Slider/Slider.jsx +283 -0
  222. package/src/ui/Slider/Slider.scss +156 -0
  223. package/src/ui/Slider/index.js +1 -0
  224. package/src/ui/Status/Status.jsx +66 -0
  225. package/src/ui/Status/Status.scss +90 -0
  226. package/src/ui/Status/index.js +1 -0
  227. package/src/ui/Steps/Steps.jsx +201 -0
  228. package/src/ui/Steps/Steps.scss +240 -0
  229. package/src/ui/Steps/index.js +1 -0
  230. package/src/ui/Table/Table.jsx +143 -0
  231. package/src/ui/Table/Table.scss +90 -0
  232. package/src/ui/Table/index.js +1 -0
  233. package/src/ui/Tabs/TabItem.jsx +86 -0
  234. package/src/ui/Tabs/Tabs.figma.tsx +30 -0
  235. package/src/ui/Tabs/Tabs.jsx +318 -0
  236. package/src/ui/Tabs/Tabs.scss +164 -0
  237. package/src/ui/Tabs/Untitled +1 -0
  238. package/src/ui/Tabs/index.js +3 -0
  239. package/src/ui/Tag/Tag.figma.tsx +29 -0
  240. package/src/ui/Tag/Tag.jsx +93 -0
  241. package/src/ui/Tag/Tag.scss +229 -0
  242. package/src/ui/Tag/index.js +2 -0
  243. package/src/ui/Textarea/Textarea.figma.tsx +35 -0
  244. package/src/ui/Textarea/Textarea.jsx +68 -0
  245. package/src/ui/Textarea/Textarea.scss +69 -0
  246. package/src/ui/Textarea/index.js +2 -0
  247. package/src/ui/Toast/Toast.jsx +75 -0
  248. package/src/ui/Toast/Toast.scss +132 -0
  249. package/src/ui/Toast/index.js +2 -0
  250. package/src/ui/Toggle/Toggle.jsx +73 -0
  251. package/src/ui/Toggle/Toggle.scss +139 -0
  252. package/src/ui/Toggle/index.js +1 -0
  253. package/src/ui/Tooltip/Tooltip.figma.tsx +24 -0
  254. package/src/ui/Tooltip/Tooltip.jsx +123 -0
  255. package/src/ui/Tooltip/Tooltip.scss +80 -0
  256. package/src/ui/Tooltip/index.js +2 -0
  257. package/src/ui/index.js +63 -0
  258. package/src/utils/formatDate.js +27 -0
  259. package/src/utils/headerVariants.js +69 -0
  260. package/vite.config.lib.js +55 -0
@@ -0,0 +1,253 @@
1
+ import React, { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import Field from "../ui/Field/Field";
4
+ import Flex from "../ui/Flex/Flex";
5
+ import Breadcrumb from "../ui/Breadcrumb/Breadcrumb";
6
+ import Divider from "../ui/Divider/Divider";
7
+ import { formatLastUpdated } from "../utils/formatDate";
8
+
9
+ /**
10
+ * Field Component Demo & Documentation
11
+ *
12
+ * This page demonstrates the Field component and its various configurations.
13
+ *
14
+ * ## Field Component Props:
15
+ *
16
+ * ### Optional Props:
17
+ * - `label` (string): Label text for the field
18
+ * - `required` (boolean): Whether the field is required (adds asterisk to label)
19
+ * - `helperMessage` (string): Helper text displayed below the input
20
+ * - `infoIcon` (string): Icon name for info icon (e.g., "Info")
21
+ * - `onInfoClick` (function): Callback when info icon is clicked
22
+ * - `maxLength` (number): Maximum character length (enables character count)
23
+ * - `value` (number|string): Current value (for character count calculation)
24
+ * - `id` (string): Unique identifier for the field
25
+ * - `children` (React.ReactNode): The input element to wrap
26
+ *
27
+ * ## Usage Examples:
28
+ *
29
+ * Basic field:
30
+ * ```jsx
31
+ * <Field label="Name">
32
+ * <input type="text" />
33
+ * </Field>
34
+ * ```
35
+ *
36
+ * Field with helper message:
37
+ * ```jsx
38
+ * <Field label="Email" helperMessage="We'll never share your email">
39
+ * <input type="email" />
40
+ * </Field>
41
+ * ```
42
+ *
43
+ * Field with character count:
44
+ * ```jsx
45
+ * <Field label="Description" maxLength={100} value={description}>
46
+ * <textarea />
47
+ * </Field>
48
+ * ```
49
+ */
50
+
51
+ export default function FieldDemo() {
52
+ const [basicValue, setBasicValue] = useState("");
53
+ const [requiredValue, setRequiredValue] = useState("");
54
+ const [helperValue, setHelperValue] = useState("");
55
+ const [countValue, setCountValue] = useState("");
56
+ const [allFeaturesValue, setAllFeaturesValue] = useState("");
57
+ const [requiredAllValue, setRequiredAllValue] = useState("");
58
+
59
+ return (
60
+ <section className="page">
61
+ <header className="page__header">
62
+ <div className="page__header-container">
63
+ <Breadcrumb />
64
+ <div className="page__header-info">
65
+ <div className="page__header-content">
66
+ <h1 className="page__header-title">Field</h1>
67
+ <p className="page__header-description">
68
+ The Field component provides a consistent wrapper for form inputs with
69
+ labels, helper messages, character counts, and optional info icons. It
70
+ can wrap any input type (text, email, textarea, etc.) and provides a
71
+ unified styling and behavior pattern.
72
+ </p>
73
+ </div>
74
+ <div className="page__header-metadata">
75
+ <div className="page__metadata-row">
76
+ <p className="page__metadata-label">Author</p>
77
+ <a
78
+ href="https://chgit.slack.com/team/U06V9C0K06S"
79
+ className="page__metadata-link"
80
+ target="_blank"
81
+ rel="noopener noreferrer"
82
+ >
83
+ @Michael-Stevens
84
+ </a>
85
+ </div>
86
+ <div className="page__metadata-row">
87
+ <p className="page__metadata-label">Last updated</p>
88
+ <p className="page__metadata-value">{formatLastUpdated()}</p>
89
+ </div>
90
+ <div className="page__metadata-row">
91
+ <p className="page__metadata-label">Version</p>
92
+ <Flex direction="row" gap="8" alignItems="center">
93
+ <p className="page__metadata-value">1.0.0</p>
94
+ <span className="page__version-badge">BETA</span>
95
+ </Flex>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </header>
101
+ <main className="page__content">
102
+ <div className="page__examples-section">
103
+ <div className="demo-group">
104
+ <h2 className="demo-group__heading">Basic Usage</h2>
105
+ <p className="demo-group__description">
106
+ A simple field wrapper with a label. The Field component wraps any input element and provides consistent styling and structure.
107
+ </p>
108
+ <div className="demo-content">
109
+ <Field label="Name">
110
+ <input
111
+ type="text"
112
+ value={basicValue}
113
+ onChange={(e) => setBasicValue(e.target.value)}
114
+ placeholder="Enter your name"
115
+ />
116
+ </Field>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="demo-group">
121
+ <h2 className="demo-group__heading">Required Field</h2>
122
+ <p className="demo-group__description">
123
+ Required fields display an asterisk (*) next to the label to indicate that the field must be filled out.
124
+ </p>
125
+ <div className="demo-content">
126
+ <Field label="Email" required>
127
+ <input
128
+ type="email"
129
+ value={requiredValue}
130
+ onChange={(e) => setRequiredValue(e.target.value)}
131
+ placeholder="Enter your email"
132
+ />
133
+ </Field>
134
+ </div>
135
+ </div>
136
+
137
+ <div className="demo-group">
138
+ <h2 className="demo-group__heading">With Helper Message</h2>
139
+ <p className="demo-group__description">
140
+ Helper messages provide additional context or instructions to help users understand what to enter in the field.
141
+ </p>
142
+ <div className="demo-content">
143
+ <Field
144
+ label="Password"
145
+ helperMessage="Must be at least 8 characters long"
146
+ required
147
+ >
148
+ <input
149
+ type="password"
150
+ value={helperValue}
151
+ onChange={(e) => setHelperValue(e.target.value)}
152
+ placeholder="Enter your password"
153
+ />
154
+ </Field>
155
+ </div>
156
+ </div>
157
+
158
+ <div className="demo-group">
159
+ <h2 className="demo-group__heading">With Character Count</h2>
160
+ <p className="demo-group__description">
161
+ Fields can display a character count when a maxLength is specified. This is useful for textareas or inputs with character limits.
162
+ </p>
163
+ <div className="demo-content">
164
+ <Field
165
+ label="Description"
166
+ maxLength={100}
167
+ value={countValue}
168
+ helperMessage="Brief description of your item"
169
+ >
170
+ <textarea
171
+ value={countValue}
172
+ onChange={(e) => setCountValue(e.target.value)}
173
+ placeholder="Enter a description"
174
+ rows={4}
175
+ />
176
+ </Field>
177
+ </div>
178
+ </div>
179
+
180
+ <div className="demo-group">
181
+ <h2 className="demo-group__heading">With Info Icon</h2>
182
+ <p className="demo-group__description">
183
+ Fields can include an info icon that can trigger additional information or tooltips when clicked.
184
+ </p>
185
+ <div className="demo-content">
186
+ <Field
187
+ label="Account Number"
188
+ infoIcon="Info"
189
+ onInfoClick={() => alert("Account numbers are 8-12 digits long")}
190
+ helperMessage="Your unique account identifier"
191
+ >
192
+ <input
193
+ type="text"
194
+ value={allFeaturesValue}
195
+ onChange={(e) => setAllFeaturesValue(e.target.value)}
196
+ placeholder="Enter account number"
197
+ />
198
+ </Field>
199
+ </div>
200
+ </div>
201
+
202
+ <div className="demo-group">
203
+ <h2 className="demo-group__heading">All Features Combined</h2>
204
+ <p className="demo-group__description">
205
+ A field with all available features: label, required indicator, helper message, info icon, and character count.
206
+ </p>
207
+ <div className="demo-content">
208
+ <Field
209
+ label="Product Description"
210
+ required
211
+ helperMessage="Provide a detailed description of your product"
212
+ infoIcon="Info"
213
+ onInfoClick={() => alert("Include key features and benefits")}
214
+ maxLength={200}
215
+ value={requiredAllValue}
216
+ >
217
+ <textarea
218
+ value={requiredAllValue}
219
+ onChange={(e) => setRequiredAllValue(e.target.value)}
220
+ placeholder="Enter product description"
221
+ rows={5}
222
+ />
223
+ </Field>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <Divider variant="solid" />
229
+
230
+ <div className="page__tabs-content-container">
231
+ <div className="demo-group">
232
+ <div className="page__navigation">
233
+ <Link
234
+ to="/dropdown"
235
+ className="page__nav-link page__nav-link--prev"
236
+ >
237
+ <span className="page__nav-label">Previous</span>
238
+ <span className="page__nav-title">Dropdown</span>
239
+ </Link>
240
+ <Link
241
+ to="/file-upload"
242
+ className="page__nav-link page__nav-link--next"
243
+ >
244
+ <span className="page__nav-label">Next</span>
245
+ <span className="page__nav-title">File Upload</span>
246
+ </Link>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </main>
251
+ </section>
252
+ );
253
+ }
@@ -0,0 +1,426 @@
1
+ import React, { useState, useEffect, useMemo } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import Divider from "../ui/Divider/Divider";
4
+ import Breadcrumb from "../ui/Breadcrumb/Breadcrumb";
5
+ import { formatLastUpdated } from "../utils/formatDate";
6
+ import Flex from "../ui/Flex/Flex";
7
+ import "./FigmaVariablesDemo.scss";
8
+
9
+ function FigmaVariablesDemo() {
10
+ const [data, setData] = useState(null);
11
+ const [loading, setLoading] = useState(true);
12
+ const [error, setError] = useState(null);
13
+ const [selectedCollection, setSelectedCollection] = useState("all");
14
+ const [searchTerm, setSearchTerm] = useState("");
15
+
16
+
17
+ useEffect(() => {
18
+ const loadData = async () => {
19
+ try {
20
+ // Try to load from the data file
21
+ const response = await fetch("/data/figma-variables.json");
22
+
23
+ // Check if response is actually JSON
24
+ const contentType = response.headers.get("content-type");
25
+ if (!contentType || !contentType.includes("application/json")) {
26
+ throw new Error(
27
+ "Data file not found. Please run: npm run fetch:variables"
28
+ );
29
+ }
30
+
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to load data: ${response.status}`);
33
+ }
34
+
35
+ const jsonData = await response.json();
36
+ setData(jsonData);
37
+ setLoading(false);
38
+ } catch (err) {
39
+ console.error("Error loading Figma variables:", err);
40
+ // Check if it's a JSON parse error (likely HTML 404 page)
41
+ if (err.message.includes("JSON") || err.message.includes("<!DOCTYPE")) {
42
+ setError("Data file not found. Please run the fetch script first.");
43
+ } else {
44
+ setError(err.message);
45
+ }
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ loadData();
51
+ }, []);
52
+
53
+ const filteredVariables = useMemo(() => {
54
+ if (!data || !data.variables) return [];
55
+
56
+ // Only include variables that start with "uds/"
57
+ let filtered = data.variables.filter((v) => {
58
+ const name = v.name || "";
59
+ return name.toLowerCase().startsWith("uds/");
60
+ });
61
+
62
+ // Filter by collection
63
+ if (selectedCollection !== "all") {
64
+ // Try matching by collectionId first
65
+ let collectionMatch = filtered.filter(
66
+ (v) => v.collectionId === selectedCollection,
67
+ );
68
+
69
+ // If no matches by ID, try matching by collection name (for inferred collections)
70
+ if (collectionMatch.length === 0) {
71
+ const selectedCollectionObj = data.collections.find(
72
+ (c) => c.id === selectedCollection
73
+ );
74
+ if (selectedCollectionObj) {
75
+ const collectionName = selectedCollectionObj.name;
76
+ // Match variables by prefix (e.g., "uds/icon/primary" matches collection "uds")
77
+ collectionMatch = filtered.filter((v) => {
78
+ const name = v.name || "";
79
+ const parts = name.split("/");
80
+ return parts.length > 0 && parts[0] === collectionName;
81
+ });
82
+ }
83
+ }
84
+
85
+ filtered = collectionMatch;
86
+ }
87
+
88
+ // Filter by search term
89
+ if (searchTerm) {
90
+ const term = searchTerm.toLowerCase();
91
+ filtered = filtered.filter(
92
+ (v) =>
93
+ v.name.toLowerCase().includes(term) ||
94
+ v.type.toLowerCase().includes(term) ||
95
+ (v.description && v.description.toLowerCase().includes(term)),
96
+ );
97
+ }
98
+
99
+ return filtered;
100
+ }, [data, selectedCollection, searchTerm]);
101
+
102
+ // Group variables by their path prefix (e.g., "uds/icon", "uds/color/primary")
103
+ const groupedVariables = useMemo(() => {
104
+ if (!filteredVariables || filteredVariables.length === 0) return {};
105
+
106
+ const groups = {};
107
+
108
+ filteredVariables.forEach((variable) => {
109
+ const name = variable.name || "";
110
+ const parts = name.split("/").filter(Boolean);
111
+
112
+ if (parts.length === 0) {
113
+ // Variables without a path go to "root"
114
+ if (!groups["root"]) {
115
+ groups["root"] = [];
116
+ }
117
+ groups["root"].push(variable);
118
+ } else if (parts.length === 1) {
119
+ // Single segment - group by that segment
120
+ const groupKey = parts[0];
121
+ if (!groups[groupKey]) {
122
+ groups[groupKey] = [];
123
+ }
124
+ groups[groupKey].push(variable);
125
+ } else {
126
+ // Multiple segments - group by all but the last segment
127
+ // e.g., "uds/icon/primary" -> group "uds/icon"
128
+ // e.g., "uds/color/primary/500" -> group "uds/color/primary"
129
+ const groupKey = parts.slice(0, -1).join("/");
130
+ if (!groups[groupKey]) {
131
+ groups[groupKey] = [];
132
+ }
133
+ groups[groupKey].push(variable);
134
+ }
135
+ });
136
+
137
+ // Sort groups by name for consistent display
138
+ const sortedGroups = {};
139
+ Object.keys(groups)
140
+ .sort((a, b) => {
141
+ // Sort "root" to the end
142
+ if (a === "root") return 1;
143
+ if (b === "root") return -1;
144
+ return a.localeCompare(b);
145
+ })
146
+ .forEach((key) => {
147
+ sortedGroups[key] = groups[key];
148
+ });
149
+
150
+ return sortedGroups;
151
+ }, [filteredVariables]);
152
+
153
+ const isColorValue = (value) => {
154
+ if (typeof value !== "string") return false;
155
+ return (
156
+ /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/i.test(value.trim()) ||
157
+ /^rgba?\(/.test(value.trim())
158
+ );
159
+ };
160
+
161
+ const getColorValue = (value) => {
162
+ if (typeof value !== "string") return null;
163
+ if (isColorValue(value)) {
164
+ return value.trim();
165
+ }
166
+ return null;
167
+ };
168
+
169
+ const VariableValue = ({ value, type, variableName }) => {
170
+ const colorValue = getColorValue(value);
171
+
172
+ if (colorValue) {
173
+ return (
174
+ <div className="variable-value variable-value--color">
175
+ <span
176
+ className="variable-value__swatch"
177
+ style={{ backgroundColor: colorValue }}
178
+ />
179
+ <span className="variable-value__text">{value}</span>
180
+ </div>
181
+ );
182
+ }
183
+
184
+ if (type === "BOOLEAN") {
185
+ return (
186
+ <span className="variable-value variable-value--boolean">
187
+ {value ? "true" : "false"}
188
+ </span>
189
+ );
190
+ }
191
+
192
+ if (type === "NUMBER" || type === "FLOAT") {
193
+ // Add px units for spacing, sizing, blur, border-width, gap, and radius variables
194
+ let displayValue = value;
195
+ if (variableName && typeof value === "number") {
196
+ const varLower = variableName.toLowerCase();
197
+ // Exclude typography-related variables
198
+ const isTypography = ['font', 'line', 'letter', 'typography', 'lhu', 'paragraph', 'heading', 'body', 'display', 'label'].some(term => varLower.includes(term));
199
+ // Check for border-width (can be border/width or border-width)
200
+ const hasBorderWidth = (varLower.includes('border') && varLower.includes('width')) || varLower.includes('border-width');
201
+ // Include variables that need px units
202
+ const needsPx = ['spacing', 'sizing', 'blur', 'gap', 'radius'].some(term => varLower.includes(term)) || hasBorderWidth;
203
+
204
+ if (!isTypography && needsPx) {
205
+ displayValue = `${value}px`;
206
+ }
207
+ }
208
+ return (
209
+ <span className="variable-value variable-value--number">{displayValue}</span>
210
+ );
211
+ }
212
+
213
+ return <span className="variable-value variable-value--text">{value}</span>;
214
+ };
215
+
216
+ if (loading) {
217
+ return (
218
+ <div className="figma-variables-demo">
219
+ <div className="figma-variables-demo__loading">Loading variables...</div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ if (error || !data) {
225
+ return (
226
+ <div>
227
+ <header className="page__header">
228
+ <div className="page__header-container">
229
+ <Breadcrumb />
230
+ <div className="page__header-info">
231
+ <div className="page__header-content">
232
+ <h1 className="page__header-title">Figma Variables</h1>
233
+ <p className="page__header-description">
234
+ Published design variables from Figma, fetched via API
235
+ </p>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </header>
240
+ <main className="page__content">
241
+ <div className="figma-variables-demo">
242
+ <div className="figma-variables-demo__error">
243
+ <h2>No Data Available</h2>
244
+ <p>{error || "Failed to load variables."}</p>
245
+ <br />
246
+ <div className="figma-variables-demo__instructions">
247
+ <h3>To fetch variables from Figma:</h3>
248
+ <ol>
249
+ <li>
250
+ Make sure you have a <code>FIGMA_ACCESS_TOKEN</code> in your <code>.env</code> file
251
+ </li>
252
+ <li>
253
+ Run the fetch script:
254
+ <pre>
255
+ <code>npm run fetch:variables</code>
256
+ </pre>
257
+ Or:
258
+ <pre>
259
+ <code>node scripts/fetch-figma-variables.js</code>
260
+ </pre>
261
+ </li>
262
+ <li>Refresh this page to see the variables</li>
263
+ </ol>
264
+ <p>
265
+ <strong>Note:</strong> The script will fetch variables from the Figma file and save them to{" "}
266
+ <code>public/data/figma-variables.json</code>
267
+ </p>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </main>
272
+ </div>
273
+ );
274
+ }
275
+
276
+ return (
277
+ <div>
278
+ <header className="page__header">
279
+ <div className="page__header-container">
280
+ <Breadcrumb />
281
+ <div className="page__header-info">
282
+ <div className="page__header-content">
283
+ <h1 className="page__header-title">Figma Variables</h1>
284
+ <p className="page__header-description">
285
+ Published design variables from Figma, fetched via API
286
+ </p>
287
+ </div>
288
+ <div className="page__header-metadata">
289
+ <div className="page__metadata-row">
290
+ <p className="page__metadata-label">File Key</p>
291
+ <p className="page__metadata-value">{data.fileKey}</p>
292
+ </div>
293
+ <div className="page__metadata-row">
294
+ <p className="page__metadata-label">Last fetched</p>
295
+ <p className="page__metadata-value">
296
+ {new Date(data.fetchedAt).toLocaleString()}
297
+ </p>
298
+ </div>
299
+ <div className="page__metadata-row">
300
+ <p className="page__metadata-label">Total Variables</p>
301
+ <p className="page__metadata-value">{data.stats.total}</p>
302
+ </div>
303
+ <div className="page__metadata-row">
304
+ <p className="page__metadata-label">Collections</p>
305
+ <p className="page__metadata-value">{data.stats.collections}</p>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </header>
311
+ <main className="page__content">
312
+ <div className="page__tabs-content-container">
313
+ <div className="figma-variables-demo__filters">
314
+ <div className="figma-variables-demo__filter-group">
315
+ <label htmlFor="collection-filter" className="figma-variables-demo__filter-label">
316
+ Collection:
317
+ </label>
318
+ <select
319
+ id="collection-filter"
320
+ className="figma-variables-demo__filter-select"
321
+ value={selectedCollection}
322
+ onChange={(e) => setSelectedCollection(e.target.value)}
323
+ >
324
+ <option value="all">All Collections</option>
325
+ {data.collections && data.collections.length > 0 && data.collections.map((collection) => (
326
+ <option key={collection.id} value={collection.id}>
327
+ {collection.name} ({collection.variableCount || 0})
328
+ </option>
329
+ ))}
330
+ </select>
331
+ </div>
332
+ <div className="figma-variables-demo__filter-group">
333
+ <label htmlFor="search-filter" className="figma-variables-demo__filter-label">
334
+ Search:
335
+ </label>
336
+ <input
337
+ id="search-filter"
338
+ type="text"
339
+ className="figma-variables-demo__filter-input"
340
+ placeholder="Search by name, type, or description..."
341
+ value={searchTerm}
342
+ onChange={(e) => setSearchTerm(e.target.value)}
343
+ />
344
+ </div>
345
+ </div>
346
+
347
+ <div className="figma-variables-demo__stats">
348
+ <p>
349
+ Showing {filteredVariables.length} of {data.stats.total} variables
350
+ </p>
351
+ </div>
352
+
353
+ {Object.keys(groupedVariables).length === 0 ? (
354
+ <div className="figma-variables-demo__table-wrapper">
355
+ <div className="figma-variables-table__empty">
356
+ No variables found matching your filters.
357
+ </div>
358
+ </div>
359
+ ) : (
360
+ Object.entries(groupedVariables).map(([groupName, variables]) => {
361
+ const displayName = groupName === "root" ? "Root Variables" : groupName;
362
+
363
+ return (
364
+ <div key={groupName} className="figma-variables-group">
365
+ <h3 className="figma-variables-group__title">
366
+ {displayName} <span className="figma-variables-group__count">({variables.length})</span>
367
+ </h3>
368
+ <div className="figma-variables-demo__table-wrapper">
369
+ <table className="figma-variables-table">
370
+ <thead>
371
+ <tr>
372
+ <th className="figma-variables-table__header figma-variables-table__header--name">
373
+ Name
374
+ </th>
375
+ <th className="figma-variables-table__header figma-variables-table__header--mode">
376
+ Light
377
+ </th>
378
+ <th className="figma-variables-table__header figma-variables-table__header--mode">
379
+ Dark
380
+ </th>
381
+ </tr>
382
+ </thead>
383
+ <tbody>
384
+ {variables.map((variable) => {
385
+ const lightMode = variable.modes?.find(
386
+ (m) => m.name.toLowerCase() === "light"
387
+ );
388
+ const darkMode = variable.modes?.find(
389
+ (m) => m.name.toLowerCase() === "dark"
390
+ );
391
+
392
+ const lightValue = variable.valuesByModeName?.light ||
393
+ (lightMode ? variable.valuesByMode?.[lightMode.modeId] : null) ||
394
+ variable.value;
395
+ const darkValue = variable.valuesByModeName?.dark ||
396
+ (darkMode ? variable.valuesByMode?.[darkMode.modeId] : null) ||
397
+ variable.value;
398
+
399
+ return (
400
+ <tr key={variable.id} className="figma-variables-table__row">
401
+ <td className="figma-variables-table__cell figma-variables-table__cell--name">
402
+ <code>{variable.name}</code>
403
+ </td>
404
+ <td className="figma-variables-table__cell figma-variables-table__cell--mode">
405
+ <VariableValue value={lightValue} type={variable.type} variableName={variable.name} />
406
+ </td>
407
+ <td className="figma-variables-table__cell figma-variables-table__cell--mode">
408
+ <VariableValue value={darkValue} type={variable.type} variableName={variable.name} />
409
+ </td>
410
+ </tr>
411
+ );
412
+ })}
413
+ </tbody>
414
+ </table>
415
+ </div>
416
+ </div>
417
+ );
418
+ })
419
+ )}
420
+ </div>
421
+ </main>
422
+ </div>
423
+ );
424
+ }
425
+
426
+ export default FigmaVariablesDemo;