@syscore/ui-library 1.1.8 → 1.1.10

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 (45) hide show
  1. package/client/components/ui/Navigation.tsx +958 -0
  2. package/client/components/ui/SearchField.tsx +157 -0
  3. package/client/components/ui/StrategyTable.tsx +303 -0
  4. package/client/components/ui/Tag.tsx +127 -0
  5. package/client/components/ui/alert-dialog.tsx +1 -1
  6. package/client/components/ui/button.tsx +67 -127
  7. package/client/components/ui/calendar.tsx +2 -2
  8. package/client/components/ui/card.tsx +10 -13
  9. package/client/components/ui/carousel.tsx +56 -46
  10. package/client/components/ui/command.tsx +27 -16
  11. package/client/components/ui/dialog.tsx +113 -92
  12. package/client/components/ui/label.tsx +5 -3
  13. package/client/components/ui/menubar.tsx +1 -1
  14. package/client/components/ui/pagination.tsx +3 -3
  15. package/client/components/ui/sidebar.tsx +1 -1
  16. package/client/components/ui/tabs.tsx +350 -5
  17. package/client/components/ui/toggle.tsx +71 -19
  18. package/client/components/ui/tooltip.tsx +69 -18
  19. package/client/global.css +635 -58
  20. package/dist/ui/fonts/FT-Made/FTMade-Regular.otf +0 -0
  21. package/dist/ui/fonts/FT-Made/FTMade-Regular.ttf +0 -0
  22. package/dist/ui/fonts/FT-Made/FTMade-Regular.woff +0 -0
  23. package/dist/ui/fonts/FT-Made/FTMade-Regular.woff2 +0 -0
  24. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-black.otf +0 -0
  25. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-blackitalic.otf +0 -0
  26. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-bold.otf +0 -0
  27. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-bolditalic.otf +0 -0
  28. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extrabold.otf +0 -0
  29. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extrabolditalic.otf +0 -0
  30. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extralight.otf +0 -0
  31. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extralightitalic.otf +0 -0
  32. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-italic.otf +0 -0
  33. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-light.otf +0 -0
  34. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-lightitalic.otf +0 -0
  35. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-medium.otf +0 -0
  36. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-mediumitalic.otf +0 -0
  37. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-regular.otf +0 -0
  38. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-semibold.otf +0 -0
  39. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-semibolditalic.otf +0 -0
  40. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-thin.otf +0 -0
  41. package/dist/ui/fonts/Mazzard-M/mazzardsoftm-thinitalic.otf +0 -0
  42. package/dist/ui/index.cjs.js +1 -1
  43. package/dist/ui/index.d.ts +1 -1
  44. package/dist/ui/index.es.js +401 -329
  45. package/package.json +3 -2
@@ -0,0 +1,157 @@
1
+ import {
2
+ Command,
3
+ CommandGroup,
4
+ CommandItem,
5
+ CommandList,
6
+ CommandInput,
7
+ CommandEmpty,
8
+ } from "@/components/ui/command";
9
+ import { Command as CommandPrimitive } from "cmdk";
10
+ import { useState, useRef, useCallback, type KeyboardEvent } from "react";
11
+
12
+ import { Check } from "lucide-react";
13
+ import { cn } from "@/lib/utils";
14
+
15
+ export type Option = Record<"value" | "label", string> & Record<string, string>;
16
+
17
+ type SearchFieldProps = {
18
+ options: Option[];
19
+ emptyMessage: string;
20
+ value?: Option;
21
+ onValueChange?: (value: Option) => void;
22
+ isLoading?: boolean;
23
+ disabled?: boolean;
24
+ placeholder?: string;
25
+ };
26
+
27
+ export const SearchField = ({
28
+ options,
29
+ placeholder,
30
+ emptyMessage,
31
+ value,
32
+ onValueChange,
33
+ disabled,
34
+ isLoading = false,
35
+ }: SearchFieldProps) => {
36
+ const inputRef = useRef<HTMLInputElement>(null);
37
+
38
+ const [isOpen, setOpen] = useState(false);
39
+ const [selected, setSelected] = useState<Option>(value as Option);
40
+ const [inputValue, setInputValue] = useState<string>(value?.label || "");
41
+
42
+ const handleKeyDown = useCallback(
43
+ (event: KeyboardEvent<HTMLDivElement>) => {
44
+ const input = inputRef.current;
45
+ if (!input) {
46
+ return;
47
+ }
48
+
49
+ // Keep the options displayed when the user is typing
50
+ if (!isOpen) {
51
+ setOpen(true);
52
+ }
53
+
54
+ // This is not a default behaviour of the <input /> field
55
+ if (event.key === "Enter" && input.value !== "") {
56
+ const optionToSelect = options.find(
57
+ (option) => option.label === input.value,
58
+ );
59
+ if (optionToSelect) {
60
+ setSelected(optionToSelect);
61
+ onValueChange?.(optionToSelect);
62
+ }
63
+ }
64
+
65
+ if (event.key === "Escape") {
66
+ input.blur();
67
+ }
68
+ },
69
+ [isOpen, options, onValueChange],
70
+ );
71
+
72
+ const handleBlur = useCallback(() => {
73
+ setOpen(false);
74
+ setInputValue(selected?.label);
75
+ }, [selected]);
76
+
77
+ const handleSelectOption = useCallback(
78
+ (selectedOption: Option) => {
79
+ setInputValue(selectedOption.label);
80
+
81
+ setSelected(selectedOption);
82
+ onValueChange?.(selectedOption);
83
+
84
+ // This is a hack to prevent the input from being focused after the user selects an option
85
+ // We can call this hack: "The next tick"
86
+ setTimeout(() => {
87
+ inputRef?.current?.blur();
88
+ }, 0);
89
+ },
90
+ [onValueChange],
91
+ );
92
+
93
+ return (
94
+ <CommandPrimitive onKeyDown={handleKeyDown}>
95
+ <div>
96
+ <CommandInput
97
+ ref={inputRef}
98
+ value={inputValue}
99
+ onValueChange={isLoading ? undefined : setInputValue}
100
+ onBlur={handleBlur}
101
+ onFocus={() => setOpen(true)}
102
+ placeholder={placeholder}
103
+ disabled={disabled}
104
+ className={cn(
105
+ "focus-within:border-cyan-300 focus:border-cyan-300",
106
+ isOpen && "rounded-b-none",
107
+ )}
108
+ />
109
+ </div>
110
+ <div className="relative">
111
+ <div
112
+ className={cn(
113
+ "animate-in fade-in-0 absolute top-0 z-10 w-full rounded-xl bg-white outline-none",
114
+ isOpen ? "block" : "hidden",
115
+ )}
116
+ >
117
+ <CommandList className="border-b border-x border-cyan-300">
118
+ {/* {isLoading ? (
119
+ <Skeleton>
120
+ <div className="p-1">
121
+ </div>
122
+ </Skeleton>
123
+ ) : null} */}
124
+
125
+ {options?.length > 0 && !isLoading ? (
126
+ <CommandGroup>
127
+ {options.map((option) => {
128
+ const isSelected = selected?.value === option.value;
129
+ return (
130
+ <CommandItem
131
+ key={option.value}
132
+ value={option.label}
133
+ onMouseDown={(event) => {
134
+ event.preventDefault();
135
+ event.stopPropagation();
136
+ }}
137
+ onSelect={() => handleSelectOption(option)}
138
+ >
139
+ {isSelected ? <Check className="w-4" /> : null}
140
+ {option.label}
141
+ </CommandItem>
142
+ );
143
+ })}
144
+ </CommandGroup>
145
+ ) : null}
146
+
147
+ {!isLoading ? (
148
+ <CommandEmpty className="select-none rounded-sm px-2 py-3 text-center text-sm text-gray-200">
149
+ {emptyMessage}
150
+ </CommandEmpty>
151
+ ) : null}
152
+ </CommandList>
153
+ </div>
154
+ </div>
155
+ </CommandPrimitive>
156
+ );
157
+ };
@@ -0,0 +1,303 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { UtilityChevronDown } from "../icons/UtilityChevronDown";
4
+
5
+ // Define the concept colors matching Figma design
6
+ export const conceptColors = {
7
+ mind: {
8
+ solid: "#0a5161",
9
+ light: "rgba(10,81,97,0.08)",
10
+ border: "rgba(10,81,97,0.16)",
11
+ prefix: "M",
12
+ },
13
+ community: {
14
+ solid: "#0f748a",
15
+ light: "rgba(15,116,138,0.12)",
16
+ border: "rgba(15,116,138,0.24)",
17
+ prefix: "C",
18
+ },
19
+ movement: {
20
+ solid: "#149ebd",
21
+ light: "rgba(20,158,189,0.12)",
22
+ border: "rgba(20,158,189,0.24)",
23
+ prefix: "V",
24
+ },
25
+ water: {
26
+ solid: "#39c9ea",
27
+ light: "rgba(57,201,234,0.12)",
28
+ border: "rgba(57,201,234,0.24)",
29
+ prefix: "W",
30
+ },
31
+ air: {
32
+ solid: "#87dff2",
33
+ light: "rgba(135,223,242,0.12)",
34
+ border: "rgba(135,223,242,0.24)",
35
+ prefix: "A",
36
+ },
37
+ light: {
38
+ solid: "#8aefdb",
39
+ light: "rgba(138,239,219,0.12)",
40
+ border: "rgba(138,239,219,0.24)",
41
+ prefix: "L",
42
+ },
43
+ thermalComfort: {
44
+ solid: "#3eddbf",
45
+ light: "rgba(62,221,191,0.12)",
46
+ border: "rgba(62,221,191,0.24)",
47
+ prefix: "T",
48
+ },
49
+ nourishment: {
50
+ solid: "#17aa8d",
51
+ light: "rgba(23,170,141,0.12)",
52
+ border: "rgba(23,170,141,0.24)",
53
+ prefix: "N",
54
+ },
55
+ sound: {
56
+ solid: "#0c705c",
57
+ light: "rgba(12,112,92,0.12)",
58
+ border: "rgba(12,112,92,0.24)",
59
+ prefix: "S",
60
+ },
61
+ materials: {
62
+ solid: "#0a4f41",
63
+ light: "rgba(10,79,65,0.08)",
64
+ border: "rgba(10,79,65,0.16)",
65
+ prefix: "X",
66
+ },
67
+ } as const;
68
+
69
+ export type ConceptType = keyof typeof conceptColors;
70
+
71
+ export interface StrategyItem {
72
+ id: string;
73
+ code: string;
74
+ name: string;
75
+ score?: string;
76
+ hasTarget?: boolean;
77
+ }
78
+
79
+ export interface ThemeItem {
80
+ id: string;
81
+ code: string;
82
+ name: string;
83
+ }
84
+
85
+ export interface ConceptItem {
86
+ id: string;
87
+ type: ConceptType;
88
+ name: string;
89
+ icon: React.ReactNode;
90
+ }
91
+
92
+ export interface StrategyTableData {
93
+ concept: ConceptItem;
94
+ theme: ThemeItem;
95
+ strategies: StrategyItem[];
96
+ }
97
+
98
+ interface StrategyTableRowProps {
99
+ data: StrategyTableData;
100
+ isFirst?: boolean;
101
+ isLast?: boolean;
102
+ showConceptHeader?: boolean;
103
+ isExpanded?: boolean;
104
+ }
105
+
106
+ const StrategyTableRow: React.FC<StrategyTableRowProps> = ({
107
+ data,
108
+ isFirst = false,
109
+ isLast = false,
110
+ showConceptHeader = true,
111
+ isExpanded = true,
112
+ }) => {
113
+ const { concept, theme, strategies } = data;
114
+ const colors = conceptColors[concept.type];
115
+
116
+ return (
117
+ <>
118
+ {/* Concept Row - always visible when showConceptHeader is true */}
119
+ {showConceptHeader && (
120
+ <div
121
+ className={cn(
122
+ "flex items-center gap-4 p-4 border border-[#DEDFE3] bg-[#FAFEFF]",
123
+ "border-b-0",
124
+ isFirst && "rounded-tl-[12px] rounded-tr-[12px]"
125
+ )}
126
+ >
127
+ {/* Icon */}
128
+ <div className="size-[48px] flex items-center justify-center shrink-0">
129
+ {concept.icon}
130
+ </div>
131
+
132
+ {/* Name */}
133
+ <div className="flex-1">
134
+ <span className="text-sm font-semibold text-[#282A31] uppercase tracking-[0.5px] leading-[14px]">
135
+ {concept.name}
136
+ </span>
137
+ </div>
138
+ </div>
139
+ )}
140
+
141
+ {/* Theme Row - only show if expanded */}
142
+ {isExpanded && (
143
+ <div className="flex items-center gap-4 p-4 border border-[#DEDFE3] bg-[#FAFEFF] border-b-0">
144
+ {/* Code Badge */}
145
+ <div
146
+ className="flex items-center justify-center h-8 w-12 rounded-[6px] shrink-0"
147
+ style={{ backgroundColor: colors.solid }}
148
+ >
149
+ <span className="text-sm font-semibold text-white leading-[19.6px]">
150
+ {theme.code}
151
+ </span>
152
+ </div>
153
+
154
+ {/* Name */}
155
+ <div className="flex-1">
156
+ <span className="text-base font-medium text-[#282A31] leading-[21px]">
157
+ {theme.name}
158
+ </span>
159
+ </div>
160
+ </div>
161
+ )}
162
+
163
+ {/* Strategy Rows - only show if expanded */}
164
+ {isExpanded && strategies.map((strategy, index) => {
165
+ const isLastStrategy = index === strategies.length - 1 && isLast;
166
+ const scoreParts = strategy.score?.split("–") || [];
167
+ const isRange = scoreParts.length > 1;
168
+
169
+ return (
170
+ <div
171
+ key={strategy.id}
172
+ className={cn(
173
+ "flex items-center gap-4 pl-8 pr-4 py-4 border border-[#DEDFE3] bg-white",
174
+ "border-b-0",
175
+ isLastStrategy && "rounded-bl-[12px] rounded-br-[12px] border-b-0"
176
+ )}
177
+ >
178
+ {/* Code Badge */}
179
+ <div
180
+ className="flex items-center justify-center h-8 w-12 rounded-[6px] border shrink-0"
181
+ style={{
182
+ backgroundColor: colors.light,
183
+ borderColor: colors.border,
184
+ }}
185
+ >
186
+ <span
187
+ className="text-sm font-semibold leading-[19.6px]"
188
+ style={{ color: colors.solid }}
189
+ >
190
+ {strategy.code}
191
+ </span>
192
+ </div>
193
+
194
+ {/* Name */}
195
+ <div className="flex-1">
196
+ <span className="text-base font-medium text-[#282A31] leading-[21px]">
197
+ {strategy.name}
198
+ </span>
199
+ </div>
200
+
201
+ {/* Actions */}
202
+ <div className="flex items-center gap-4 shrink-0">
203
+ {strategy.hasTarget && (
204
+ <div className="size-8 flex items-center justify-center">
205
+ <svg
206
+ width="14"
207
+ height="14"
208
+ viewBox="0 0 14 14"
209
+ fill="none"
210
+ className="text-gray-400"
211
+ >
212
+ <circle
213
+ cx="7"
214
+ cy="7"
215
+ r="6.25"
216
+ stroke="currentColor"
217
+ strokeWidth="1.5"
218
+ />
219
+ <circle
220
+ cx="7"
221
+ cy="7"
222
+ r="3.5"
223
+ stroke="currentColor"
224
+ strokeWidth="1.5"
225
+ />
226
+ <circle cx="7" cy="7" r="1" fill="currentColor" />
227
+ </svg>
228
+ </div>
229
+ )}
230
+
231
+ {strategy.score && (
232
+ <div className="flex items-end gap-[4px]">
233
+ <span className="text-lg font-semibold text-[#282A31] leading-[25.2px]">
234
+ {strategy.score}
235
+ </span>
236
+ <span className="text-xs font-semibold text-[#282A31] uppercase tracking-[0.5px] leading-[12px]">
237
+ {isRange ? "PTS" : "PT"}
238
+ {!isRange && <span className="text-transparent">S</span>}
239
+ </span>
240
+ </div>
241
+ )}
242
+ </div>
243
+ </div>
244
+ );
245
+ })}
246
+ </>
247
+ );
248
+ };
249
+
250
+ interface StrategyTableProps {
251
+ data: StrategyTableData[];
252
+ className?: string;
253
+ isExpanded?: boolean;
254
+ }
255
+
256
+ export const StrategyTable: React.FC<StrategyTableProps> = ({
257
+ data,
258
+ className,
259
+ isExpanded = true,
260
+ }) => {
261
+ // Group by concept to avoid duplicate concept headers
262
+ const groupedData = React.useMemo(() => {
263
+ const groups: Record<string, StrategyTableData[]> = {};
264
+ data.forEach((item) => {
265
+ const key = item.concept.id;
266
+ if (!groups[key]) {
267
+ groups[key] = [];
268
+ }
269
+ groups[key].push(item);
270
+ });
271
+ return Object.values(groups);
272
+ }, [data]);
273
+
274
+ return (
275
+ <div className={cn("flex flex-col", className)}>
276
+ {groupedData.map((group, groupIndex) => {
277
+ const isFirstGroup = groupIndex === 0;
278
+ const isLastGroup = groupIndex === groupedData.length - 1;
279
+
280
+ return group.map((item, itemIndex) => {
281
+ const isFirst = isFirstGroup && itemIndex === 0;
282
+ const isLastItemInGroup = itemIndex === group.length - 1;
283
+ // Check if this is the very last strategy row across all groups
284
+ const isLastStrategyRow = isLastGroup && isLastItemInGroup && item.strategies.length > 0;
285
+ const showConceptHeader = itemIndex === 0; // Only show concept header for first theme
286
+ const uniqueId = `${item.concept.id}-${item.theme.id}`;
287
+
288
+ return (
289
+ <StrategyTableRow
290
+ key={uniqueId}
291
+ data={item}
292
+ isFirst={isFirst}
293
+ isLast={isLastStrategyRow}
294
+ showConceptHeader={showConceptHeader}
295
+ isExpanded={isExpanded}
296
+ />
297
+ );
298
+ });
299
+ })}
300
+ </div>
301
+ );
302
+ };
303
+
@@ -0,0 +1,127 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export type TagStatus = "todo" | "low" | "medium" | "high" | "done";
5
+
6
+ export interface TagProps
7
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
8
+ children: React.ReactNode;
9
+ active?: boolean;
10
+ status?: TagStatus;
11
+ variant?: "light" | "dark";
12
+ onClick?: () => void;
13
+ }
14
+
15
+ const getStatusColors = (
16
+ status: TagStatus,
17
+ variant: "light" | "dark" = "light",
18
+ ) => {
19
+ const colors = {
20
+ light: {
21
+ todo: {
22
+ bg: "bg-gray-100",
23
+ text: "text-gray-600",
24
+ },
25
+ low: {
26
+ bg: "bg-cyan-100",
27
+ text: "text-cyan-600",
28
+ },
29
+ medium: {
30
+ bg: "bg-plum-100",
31
+ text: "text-plum-600",
32
+ },
33
+ high: {
34
+ bg: "bg-coral-100",
35
+ text: "text-coral-600",
36
+ },
37
+ done: {
38
+ bg: "bg-emerald-100",
39
+ text: "text-emerald-600",
40
+ },
41
+ },
42
+ dark: {
43
+ todo: {
44
+ bg: "bg-gray-600",
45
+ text: "text-gray-100",
46
+ },
47
+ low: {
48
+ bg: "bg-cyan-700",
49
+ text: "text-cyan-100",
50
+ },
51
+ medium: {
52
+ bg: "bg-plum-700",
53
+ text: "text-plum-100",
54
+ },
55
+ high: {
56
+ bg: "bg-coral-700",
57
+ text: "text-coral-100",
58
+ },
59
+ done: {
60
+ bg: "bg-emerald-700",
61
+ text: "text-emerald-100",
62
+ },
63
+ },
64
+ };
65
+
66
+ return colors[variant][status];
67
+ };
68
+
69
+ export const Tag = React.forwardRef<HTMLButtonElement, TagProps>(
70
+ (
71
+ {
72
+ children,
73
+ active = false,
74
+ status,
75
+ variant = "light",
76
+ className,
77
+ onClick,
78
+ ...props
79
+ },
80
+ ref,
81
+ ) => {
82
+ // Status tag styling
83
+ if (status) {
84
+ const statusColors = getStatusColors(status, variant);
85
+ return (
86
+ <button
87
+ ref={ref}
88
+ onClick={onClick}
89
+ className={cn(
90
+ "inline-flex items-center p-[8px] rounded-[6px] w-fit",
91
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
92
+ "disabled:opacity-50 disabled:cursor-not-allowed",
93
+ statusColors.bg,
94
+ statusColors.text,
95
+ className,
96
+ )}
97
+ {...props}
98
+ >
99
+ <span className="overline-medium">{children}</span>
100
+ </button>
101
+ );
102
+ }
103
+
104
+ // Dual-state general purpose tag styling
105
+ return (
106
+ <button
107
+ ref={ref}
108
+ onClick={onClick}
109
+ className={cn(
110
+ "inline-flex items-center h-[32px] px-[12px] py-0 rounded-[6px] w-fit",
111
+
112
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
113
+ "disabled:opacity-50 disabled:cursor-not-allowed",
114
+ active
115
+ ? "bg-white border border-cyan-300 text-gray-800 hover:border-cyan-400"
116
+ : "bg-blue-100 text-blue-700 hover:bg-blue-200",
117
+ className,
118
+ )}
119
+ {...props}
120
+ >
121
+ <span className="body-small font-medium">{children}</span>
122
+ </button>
123
+ );
124
+ },
125
+ );
126
+
127
+ Tag.displayName = "Tag";
@@ -115,7 +115,7 @@ const AlertDialogCancel = React.forwardRef<
115
115
  <AlertDialogPrimitive.Cancel
116
116
  ref={ref}
117
117
  className={cn(
118
- buttonVariants({ variant: "outline" }),
118
+ buttonVariants({ variant: "general-secondary" }),
119
119
  "mt-2 sm:mt-0",
120
120
  className,
121
121
  )}