@syscore/ui-library 1.7.8 → 1.9.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.
@@ -3,26 +3,23 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "../../lib/utils";
4
4
  import { useSegmentedControl } from "../../hooks/use-segmented-control";
5
5
 
6
- const toggleVariants = cva(
7
- "toggle",
8
- {
9
- variants: {
10
- variant: {
11
- default: "toggle--default",
12
- outline: "toggle--outline",
13
- },
14
- size: {
15
- default: "toggle--size-default",
16
- sm: "toggle--size-sm",
17
- lg: "toggle--size-lg",
18
- },
6
+ const toggleVariants = cva("toggle", {
7
+ variants: {
8
+ variant: {
9
+ default: "toggle--default",
10
+ outline: "toggle--outline",
19
11
  },
20
- defaultVariants: {
21
- variant: "default",
22
- size: "default",
12
+ size: {
13
+ default: "toggle--size-default",
14
+ sm: "toggle--size-sm",
15
+ lg: "toggle--size-lg",
23
16
  },
24
17
  },
25
- );
18
+ defaultVariants: {
19
+ variant: "default",
20
+ size: "default",
21
+ },
22
+ });
26
23
 
27
24
  // Segmented Control Component
28
25
  export interface SegmentedControlProps<T extends string = string>
@@ -51,14 +48,13 @@ const SegmentedControlInner = React.forwardRef<
51
48
  return (
52
49
  <div ref={ref} className={cn("segmented-control", className)} {...props}>
53
50
  {options.map((option) => {
54
- const isReactElement = React.isValidElement(option.label);
55
51
  const isActive = selectedValue === option.value;
56
52
 
57
53
  return (
58
54
  <button
59
55
  key={option.value}
60
56
  className={cn(
61
- "segmented-control-button",
57
+ "segmented-control-button body-small",
62
58
  isActive
63
59
  ? "segmented-control-button--active"
64
60
  : "segmented-control-button--inactive",
@@ -67,11 +63,7 @@ const SegmentedControlInner = React.forwardRef<
67
63
  type="button"
68
64
  data-active={isActive}
69
65
  >
70
- {isReactElement ? (
71
- option.label
72
- ) : (
73
- <span className="body-small font-medium">{option.label}</span>
74
- )}
66
+ {option.label}
75
67
  </button>
76
68
  );
77
69
  })}
package/client/global.css CHANGED
@@ -1763,6 +1763,7 @@ body {
1763
1763
  .tag-general--active {
1764
1764
  background-color: var(--color-cyan-800, #0f748a);
1765
1765
  color: var(--color-white, #fff);
1766
+ font-weight: 600;
1766
1767
  }
1767
1768
 
1768
1769
  .tag-general--active:hover {
@@ -1772,11 +1773,39 @@ body {
1772
1773
  .tag-general--inactive {
1773
1774
  background-color: var(--color-blue-100, #eff5fb);
1774
1775
  color: var(--color-blue-700, #286495);
1776
+ font-weight: 500;
1775
1777
  }
1776
1778
 
1777
1779
  .tag-general--inactive:hover {
1778
- background-color: var(--color-cyan-800, #0f748a);
1779
- color: var(--color-white, #fff);
1780
+ background-color: var(--color-blue-200, #cbe0f1);
1781
+ }
1782
+
1783
+ /* Tag Code Variant - for code badges like "C1", "A3" */
1784
+ .tag-code {
1785
+ display: flex;
1786
+ align-items: center;
1787
+ justify-content: center;
1788
+ height: 2rem;
1789
+ width: 3rem;
1790
+ border-radius: calc(var(--radius-sm, 6px));
1791
+ flex-shrink: 0;
1792
+ border: 1.5px solid currentColor;
1793
+ padding-left: 1px;
1794
+ padding-right: 1px;
1795
+ background: transparent;
1796
+ cursor: pointer;
1797
+ }
1798
+
1799
+ .tag-code:focus-visible {
1800
+ outline: none;
1801
+ box-shadow:
1802
+ 0 0 0 2px hsl(var(--ring)),
1803
+ 0 0 0 4px var(--color-white, #fff);
1804
+ }
1805
+
1806
+ .tag-code:disabled {
1807
+ opacity: 0.5;
1808
+ cursor: not-allowed;
1780
1809
  }
1781
1810
 
1782
1811
  /* Toggle/Segmented Control Styles */
@@ -6625,7 +6654,6 @@ body {
6625
6654
  .body-large {
6626
6655
  font-size: 18px;
6627
6656
  font-style: normal;
6628
- font-weight: 400;
6629
6657
  line-height: 25.2px;
6630
6658
  }
6631
6659
 
@@ -6644,7 +6672,6 @@ body {
6644
6672
  .body-base {
6645
6673
  font-size: 16px;
6646
6674
  font-style: normal;
6647
- font-weight: 400;
6648
6675
  line-height: 22.4px;
6649
6676
  }
6650
6677
 
@@ -6663,7 +6690,6 @@ body {
6663
6690
  .body-small {
6664
6691
  font-size: 14px;
6665
6692
  font-style: normal;
6666
- font-weight: 400;
6667
6693
  line-height: 19.6px;
6668
6694
  }
6669
6695
 
@@ -4,3 +4,9 @@ import { twMerge } from "tailwind-merge";
4
4
  export function cn(...inputs: ClassValue[]) {
5
5
  return twMerge(clsx(inputs));
6
6
  }
7
+
8
+ /** Capitalize the first letter of a string */
9
+ export function capitalize(str: string): string {
10
+ if (!str) return str;
11
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
12
+ }
@@ -0,0 +1,430 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ Accordion,
4
+ AccordionItem,
5
+ AccordionContent,
6
+ AccordionContainer,
7
+ AccordionSectionHeader,
8
+ AccordionHeader,
9
+ AccordionTrigger,
10
+ AccordionListRow,
11
+ } from "@/components/ui/accordion";
12
+ import { Tag } from "@/components/ui/tag";
13
+ import { useState } from "react";
14
+ import { concepts } from "@/lib/concepts-mock-data";
15
+ import { getConceptColor } from "@/lib/concept-colors";
16
+ import { CONCEPT_ICONS } from "@/lib/concept-icons";
17
+ import { Text } from "@/components/ui/typography";
18
+
19
+ const meta = {
20
+ title: "Review/Accordion",
21
+ component: Accordion,
22
+ tags: ["autodocs"],
23
+ parameters: {
24
+ layout: "fullscreen",
25
+ },
26
+ } satisfies Meta<typeof Accordion>;
27
+
28
+ export default meta;
29
+
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ // Generic unstyled example
33
+ export const Default: Story = {
34
+ render: () => {
35
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
36
+
37
+ return (
38
+ <div className="p-8 max-w-2xl">
39
+ <Accordion
40
+ allowMultiple={false}
41
+ expandedValues={expanded}
42
+ onExpandedChange={setExpanded}
43
+ className="border rounded-lg overflow-hidden"
44
+ >
45
+ <AccordionItem value="item-1" className="border-b last:border-b-0">
46
+ <AccordionHeader className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
47
+ <span className="font-medium">Is it accessible?</span>
48
+ <AccordionTrigger />
49
+ </AccordionHeader>
50
+ <AccordionContent className="overflow-hidden">
51
+ <div className="p-4 pt-0">
52
+ Yes. It adheres to the WAI-ARIA design pattern and includes
53
+ smooth animations powered by Motion.
54
+ </div>
55
+ </AccordionContent>
56
+ </AccordionItem>
57
+
58
+ <AccordionItem value="item-2" className="border-b last:border-b-0">
59
+ <AccordionHeader className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
60
+ <span className="font-medium">Is it animated?</span>
61
+ <AccordionTrigger />
62
+ </AccordionHeader>
63
+ <AccordionContent className="overflow-hidden">
64
+ <div className="p-4 pt-0">
65
+ Yes. It uses Motion/React for smooth height and opacity
66
+ animations.
67
+ </div>
68
+ </AccordionContent>
69
+ </AccordionItem>
70
+
71
+ <AccordionItem value="item-3" className="border-b last:border-b-0">
72
+ <AccordionHeader className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
73
+ <span className="font-medium">Can multiple items be open?</span>
74
+ <AccordionTrigger />
75
+ </AccordionHeader>
76
+ <AccordionContent className="overflow-hidden">
77
+ <div className="p-4 pt-0">
78
+ Yes. Set <code>allowMultiple=true</code> to allow multiple items
79
+ to be expanded at once.
80
+ </div>
81
+ </AccordionContent>
82
+ </AccordionItem>
83
+ </Accordion>
84
+ </div>
85
+ );
86
+ },
87
+ };
88
+
89
+ // Multiple selection example
90
+ export const MultipleSelection: Story = {
91
+ render: () => {
92
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
93
+
94
+ return (
95
+ <div className="p-8 max-w-2xl">
96
+ <Accordion
97
+ allowMultiple={true}
98
+ expandedValues={expanded}
99
+ onExpandedChange={setExpanded}
100
+ className="border rounded-lg overflow-hidden"
101
+ >
102
+ <AccordionItem value="item-1" className="border-b last:border-b-0">
103
+ <AccordionHeader className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
104
+ <span className="font-medium">Section One</span>
105
+ <AccordionTrigger />
106
+ </AccordionHeader>
107
+ <AccordionContent className="overflow-hidden">
108
+ <div className="p-4 pt-0">
109
+ Content for section one. Multiple sections can be open
110
+ simultaneously.
111
+ </div>
112
+ </AccordionContent>
113
+ </AccordionItem>
114
+
115
+ <AccordionItem value="item-2" className="border-b last:border-b-0">
116
+ <AccordionHeader className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
117
+ <span className="font-medium">Section Two</span>
118
+ <AccordionTrigger />
119
+ </AccordionHeader>
120
+ <AccordionContent className="overflow-hidden">
121
+ <div className="p-4 pt-0">
122
+ Content for section two. Try opening multiple sections at once.
123
+ </div>
124
+ </AccordionContent>
125
+ </AccordionItem>
126
+
127
+ <AccordionItem value="item-3" className="border-b last:border-b-0">
128
+ <AccordionHeader className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
129
+ <span className="font-medium">Section Three</span>
130
+ <AccordionTrigger />
131
+ </AccordionHeader>
132
+ <AccordionContent className="overflow-hidden">
133
+ <div className="p-4 pt-0">
134
+ Content for section three. All three can be expanded together.
135
+ </div>
136
+ </AccordionContent>
137
+ </AccordionItem>
138
+ </Accordion>
139
+ </div>
140
+ );
141
+ },
142
+ };
143
+
144
+ // IWBI Standard Table styled variant
145
+ export const IWBIConceptsTable: Story = {
146
+ render: () => {
147
+ const [expandedConcepts, setExpandedConcepts] = useState<Set<string>>(
148
+ new Set(),
149
+ );
150
+
151
+ const hasAnyExpanded = expandedConcepts.size > 0;
152
+
153
+ const toggleAllConcepts = () => {
154
+ if (hasAnyExpanded) {
155
+ setExpandedConcepts(new Set());
156
+ } else {
157
+ setExpandedConcepts(new Set(concepts.map((c) => c.id)));
158
+ }
159
+ };
160
+
161
+ const navigateTo = (path: string) => {
162
+ console.log(path);
163
+ };
164
+
165
+ function getSlugFromName(name: string) {
166
+ return name.toLowerCase().replace(/\s+/g, "-");
167
+ }
168
+
169
+ return (
170
+ <AccordionContainer className="standard-table-container">
171
+ <AccordionSectionHeader
172
+ title="CONCEPTS"
173
+ hasExpanded={hasAnyExpanded}
174
+ onToggleAll={toggleAllConcepts}
175
+ className="standard-table-header"
176
+ />
177
+
178
+ <Accordion
179
+ allowMultiple={true}
180
+ expandedValues={expandedConcepts}
181
+ onExpandedChange={setExpandedConcepts}
182
+ className="border border-blue-200 rounded-xl overflow-hidden"
183
+ >
184
+ {[...concepts].map((concept) => {
185
+ const slug = getSlugFromName(concept.name);
186
+ const Icon = CONCEPT_ICONS[slug];
187
+ const color = getConceptColor(slug);
188
+
189
+ return (
190
+ <AccordionItem
191
+ key={concept.id}
192
+ value={concept.id}
193
+ className="standard-table-row"
194
+ >
195
+ <AccordionHeader
196
+ onClick={() => navigateTo("/")}
197
+ className="standard-table-row-header"
198
+ >
199
+ <Icon active={true} className="size-12 shrink-0" />
200
+ <span className="overline-large flex-1">{concept.name}</span>
201
+ <AccordionTrigger className="standard-table-trigger" />
202
+ </AccordionHeader>
203
+
204
+ <AccordionContent className="standard-table-content">
205
+ <div className="standard-table-content__inner">
206
+ {concept.themes.map((theme) => (
207
+ <AccordionListRow
208
+ key={theme.id}
209
+ badge={
210
+ <Tag
211
+ variant="code"
212
+ style={{
213
+ backgroundColor: color.contrast || color.solid,
214
+ borderColor: color.border,
215
+ color: "white",
216
+ }}
217
+ >
218
+ {theme.code}
219
+ </Tag>
220
+ }
221
+ title={theme.name}
222
+ titleClassName="standard-table-list-row__title body-large"
223
+ className="standard-table-list-row standard-table-list-row--nested"
224
+ onClick={() => navigateTo("/")}
225
+ />
226
+ ))}
227
+ </div>
228
+ </AccordionContent>
229
+ </AccordionItem>
230
+ );
231
+ })}
232
+ </Accordion>
233
+ </AccordionContainer>
234
+ );
235
+ },
236
+ };
237
+
238
+ export const IWBIThemesTable: Story = {
239
+ render: () => {
240
+ const conceptSlug = "community";
241
+
242
+ const [expandedThemes, setExpandedThemes] = useState<Set<string>>(
243
+ new Set(),
244
+ );
245
+
246
+ const hasAnyExpanded = expandedThemes.size > 0;
247
+
248
+ const toggleAllThemes = () => {
249
+ if (hasAnyExpanded) {
250
+ setExpandedThemes(new Set());
251
+ } else {
252
+ setExpandedThemes(new Set(conceptData?.themes.map((t) => t.id) || []));
253
+ }
254
+ };
255
+
256
+ const navigateTo = (path: string) => {
257
+ console.log(path);
258
+ };
259
+
260
+ const conceptData = concepts.find(
261
+ (c) => c.name.toLowerCase() === conceptSlug.toLowerCase(),
262
+ );
263
+
264
+ const conceptColor = getConceptColor(conceptSlug);
265
+
266
+ return (
267
+ <AccordionContainer className="standard-table-container">
268
+ <AccordionSectionHeader
269
+ title="THEMES"
270
+ hasExpanded={hasAnyExpanded}
271
+ onToggleAll={toggleAllThemes}
272
+ className="standard-table-header"
273
+ />
274
+
275
+ <Accordion
276
+ allowMultiple={true}
277
+ expandedValues={expandedThemes}
278
+ onExpandedChange={setExpandedThemes}
279
+ className="border border-blue-200 rounded-xl overflow-hidden"
280
+ >
281
+ {conceptData?.themes.map((theme) => {
282
+ return (
283
+ <AccordionItem
284
+ key={theme.id}
285
+ value={theme.id}
286
+ className="standard-table-row"
287
+ >
288
+ <AccordionHeader
289
+ onClick={() => navigateTo("/")}
290
+ className="standard-table-row-header"
291
+ >
292
+ <Tag
293
+ variant="code"
294
+ style={{
295
+ backgroundColor:
296
+ conceptColor.contrast || conceptColor.solid,
297
+ borderColor: conceptColor.border,
298
+ color: "white",
299
+ }}
300
+ >
301
+ {theme.code}
302
+ </Tag>
303
+ <Text as="p" variant="body-large" className="flex-1">
304
+ {theme.name}
305
+ </Text>
306
+ <AccordionTrigger className="standard-table-trigger" />
307
+ </AccordionHeader>
308
+
309
+ <AccordionContent className="standard-table-content">
310
+ <div className="standard-table-content__inner">
311
+ {theme.strategies.map((strategy) => (
312
+ <AccordionListRow
313
+ key={strategy.id}
314
+ badge={
315
+ <Tag
316
+ variant="code"
317
+ style={{
318
+ backgroundColor: conceptColor.light,
319
+ borderColor: conceptColor.border,
320
+ color:
321
+ conceptColor.contrast || conceptColor.solid,
322
+ }}
323
+ >
324
+ {strategy.code}
325
+ </Tag>
326
+ }
327
+ title={strategy.name}
328
+ titleClassName="standard-table-list-row__title body-large"
329
+ className="standard-table-list-row standard-table-list-row--nested"
330
+ onClick={() => navigateTo("/")}
331
+ rightContent={
332
+ <span className="flex justify-center items-center number-large font-semibold">
333
+ {strategy.points}{" "}
334
+ <span className="overline-medium ml-1">
335
+ PT
336
+ <span
337
+ className={
338
+ strategy.points === "1"
339
+ ? "opacity-0"
340
+ : "opacity-100"
341
+ }
342
+ >
343
+ S
344
+ </span>
345
+ </span>
346
+ </span>
347
+ }
348
+ />
349
+ ))}
350
+ </div>
351
+ </AccordionContent>
352
+ </AccordionItem>
353
+ );
354
+ })}
355
+ </Accordion>
356
+ </AccordionContainer>
357
+ );
358
+ },
359
+ };
360
+
361
+ export const IWBIStrategiesTable: Story = {
362
+ render: () => {
363
+ const conceptSlug = "community";
364
+ const themeCode = "C8";
365
+
366
+ const navigateTo = (path: string) => {
367
+ console.log(path);
368
+ };
369
+
370
+ const conceptData = concepts.find(
371
+ (c) => c.name.toLowerCase() === conceptSlug.toLowerCase(),
372
+ );
373
+ const themeData = conceptData?.themes.find((t) => t.code === themeCode);
374
+
375
+ const conceptColor = getConceptColor(conceptSlug);
376
+
377
+ return (
378
+ <AccordionContainer className="standard-table-container">
379
+ <AccordionSectionHeader
380
+ title="STRATEGIES"
381
+ className="standard-table-header"
382
+ />
383
+
384
+ <Accordion className="border border-blue-200 rounded-xl overflow-hidden">
385
+ {themeData?.strategies.map((strategy) => {
386
+ return (
387
+ <AccordionItem
388
+ key={strategy.id}
389
+ value={strategy.id}
390
+ className="standard-table-row"
391
+ >
392
+ <AccordionHeader
393
+ onClick={() => navigateTo("/")}
394
+ className="standard-table-row-header"
395
+ >
396
+ <Tag
397
+ variant="code"
398
+ style={{
399
+ backgroundColor: conceptColor.light,
400
+ borderColor: conceptColor.border,
401
+ color: conceptColor.contrast || conceptColor.solid,
402
+ }}
403
+ >
404
+ {strategy.code}
405
+ </Tag>
406
+ <Text as="p" variant="body-large" className="flex-1">
407
+ {strategy.name}
408
+ </Text>
409
+ <span className="flex justify-center items-center number-large font-semibold">
410
+ {strategy.points}{" "}
411
+ <span className="overline-medium ml-1">
412
+ PT
413
+ <span
414
+ className={
415
+ strategy.points === "1" ? "opacity-0" : "opacity-100"
416
+ }
417
+ >
418
+ S
419
+ </span>
420
+ </span>
421
+ </span>
422
+ </AccordionHeader>
423
+ </AccordionItem>
424
+ );
425
+ })}
426
+ </Accordion>
427
+ </AccordionContainer>
428
+ );
429
+ },
430
+ };
@@ -8,7 +8,7 @@ import {
8
8
  PageHeaderDescription,
9
9
  } from "../components/ui/page-header";
10
10
  import { BadgeCertificationBronze } from "../components/icons/AchievementBadges";
11
- import { CodeBadge } from "../components/ui/code-badge";
11
+ import { Tag } from "../components/ui/tag";
12
12
  import {
13
13
  Tooltip,
14
14
  TooltipTrigger,
@@ -96,14 +96,16 @@ export const ThemeHeader: Story = {
96
96
  <PageHeader>
97
97
  <PageHeaderTopSection className="mb-4">
98
98
  <PageHeaderLeftContent>
99
- <CodeBadge
100
- code={themeCode}
99
+ <Tag
100
+ variant="code"
101
101
  style={{
102
102
  backgroundColor: conceptColor.contrast || conceptColor.solid,
103
103
  borderColor: conceptColor.border,
104
104
  color: "white",
105
105
  }}
106
- />
106
+ >
107
+ {themeCode}
108
+ </Tag>
107
109
  <Tooltip trigger="click">
108
110
  <TooltipTrigger>
109
111
  <span className="body-base text-gray-600 underline-dotted cursor-pointer">