@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.
- package/client/components/ui/Navigation.tsx +958 -0
- package/client/components/ui/SearchField.tsx +157 -0
- package/client/components/ui/StrategyTable.tsx +303 -0
- package/client/components/ui/Tag.tsx +127 -0
- package/client/components/ui/alert-dialog.tsx +1 -1
- package/client/components/ui/button.tsx +67 -127
- package/client/components/ui/calendar.tsx +2 -2
- package/client/components/ui/card.tsx +10 -13
- package/client/components/ui/carousel.tsx +56 -46
- package/client/components/ui/command.tsx +27 -16
- package/client/components/ui/dialog.tsx +113 -92
- package/client/components/ui/label.tsx +5 -3
- package/client/components/ui/menubar.tsx +1 -1
- package/client/components/ui/pagination.tsx +3 -3
- package/client/components/ui/sidebar.tsx +1 -1
- package/client/components/ui/tabs.tsx +350 -5
- package/client/components/ui/toggle.tsx +71 -19
- package/client/components/ui/tooltip.tsx +69 -18
- package/client/global.css +635 -58
- package/dist/ui/fonts/FT-Made/FTMade-Regular.otf +0 -0
- package/dist/ui/fonts/FT-Made/FTMade-Regular.ttf +0 -0
- package/dist/ui/fonts/FT-Made/FTMade-Regular.woff +0 -0
- package/dist/ui/fonts/FT-Made/FTMade-Regular.woff2 +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-black.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-blackitalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-bold.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-bolditalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extrabold.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extrabolditalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extralight.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-extralightitalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-italic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-light.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-lightitalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-medium.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-mediumitalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-regular.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-semibold.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-semibolditalic.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-thin.otf +0 -0
- package/dist/ui/fonts/Mazzard-M/mazzardsoftm-thinitalic.otf +0 -0
- package/dist/ui/index.cjs.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.es.js +401 -329
- 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";
|