@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.
- package/client/components/ui/accordion.tsx +501 -45
- package/client/components/ui/label.tsx +1 -1
- package/client/components/ui/tag.tsx +41 -14
- package/client/components/ui/toggle.tsx +16 -24
- package/client/global.css +31 -5
- package/client/lib/utils.ts +6 -0
- package/client/ui/Accordion.stories.tsx +430 -0
- package/client/ui/PageHeader.stories.tsx +6 -4
- package/client/ui/Panel.stories.tsx +513 -436
- package/client/ui/Tag.stories.tsx +153 -46
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +30 -30
- package/dist/index.es.js +415 -466
- package/package.json +1 -1
- package/client/components/ui/code-badge.tsx +0 -22
- package/client/components/ui/standard-table.tsx +0 -554
- package/client/ui/Accordion/Accordion.stories.tsx +0 -74
- package/client/ui/CodeBadge.stories.tsx +0 -76
- package/client/ui/StandardTable.stories.tsx +0 -311
|
@@ -2,8 +2,7 @@ import { LayoutGroup } from "motion/react";
|
|
|
2
2
|
import { motion } from "motion/react";
|
|
3
3
|
import { AnimatePresence } from "motion/react";
|
|
4
4
|
import { getConceptColor } from "../lib/concept-colors";
|
|
5
|
-
import {
|
|
6
|
-
import { cn } from "../lib/utils";
|
|
5
|
+
import { capitalize, cn } from "../lib/utils";
|
|
7
6
|
import { Tag } from "../components/ui/tag";
|
|
8
7
|
import { CONCEPT_ICONS } from "../lib/concept-icons";
|
|
9
8
|
import { conceptColors } from "../lib/concept-colors";
|
|
@@ -12,326 +11,369 @@ import { useState } from "react";
|
|
|
12
11
|
import { Label } from "../components/ui/label";
|
|
13
12
|
import { Toggle } from "../components/ui/toggle";
|
|
14
13
|
|
|
15
|
-
|
|
16
14
|
// Order of concepts for display
|
|
17
15
|
const conceptOrder: ConceptSlug[] = [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
"air",
|
|
17
|
+
"water",
|
|
18
|
+
"nourishment",
|
|
19
|
+
"light",
|
|
20
|
+
"movement",
|
|
21
|
+
"thermal-comfort",
|
|
22
|
+
"sound",
|
|
23
|
+
"materials",
|
|
24
|
+
"community",
|
|
25
|
+
"mind",
|
|
26
|
+
"innovation",
|
|
29
27
|
];
|
|
30
28
|
|
|
31
|
-
|
|
32
29
|
// Wrapper component that manages state and accepts initial values from args
|
|
33
30
|
const ExploreSidePanelView = (args: any) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
backgroundColor: activeColor.contrast || activeColor.solid,
|
|
175
|
-
borderColor: activeColor.contrast || activeColor.solid,
|
|
176
|
-
color: "white",
|
|
177
|
-
borderWidth: "3px",
|
|
178
|
-
}
|
|
179
|
-
: isOutlined
|
|
180
|
-
? {
|
|
181
|
-
backgroundColor: "white",
|
|
182
|
-
borderColor: activeColor.contrast || activeColor.solid,
|
|
183
|
-
color: activeColor.contrast || activeColor.solid,
|
|
184
|
-
borderWidth: "3px",
|
|
185
|
-
}
|
|
186
|
-
: !isSelected
|
|
187
|
-
? {
|
|
188
|
-
backgroundColor: "var(--color-blue-100)",
|
|
189
|
-
color: "var(--color-blue-600)",
|
|
190
|
-
borderColor: "var(--color-blue-100)",
|
|
191
|
-
borderWidth: "3px",
|
|
192
|
-
}
|
|
193
|
-
: {
|
|
194
|
-
borderWidth: "3px",
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/>
|
|
198
|
-
</button>
|
|
199
|
-
);
|
|
200
|
-
})}
|
|
201
|
-
</div>
|
|
202
|
-
</motion.section>
|
|
31
|
+
const {
|
|
32
|
+
concepts,
|
|
33
|
+
themes,
|
|
34
|
+
strategies,
|
|
35
|
+
selectedConcept,
|
|
36
|
+
selectedTheme,
|
|
37
|
+
selectedStrategy,
|
|
38
|
+
onConceptClick,
|
|
39
|
+
onThemeClick,
|
|
40
|
+
onStrategyClick,
|
|
41
|
+
scope,
|
|
42
|
+
onScopeChange,
|
|
43
|
+
activePursuits,
|
|
44
|
+
onPursuitToggle,
|
|
45
|
+
} = args;
|
|
46
|
+
|
|
47
|
+
// Local UI state (hover)
|
|
48
|
+
const [hoveredConcept, setHoveredConcept] = useState<ConceptSlug | null>(
|
|
49
|
+
null,
|
|
50
|
+
);
|
|
51
|
+
const [hoveredTheme, setHoveredTheme] = useState<string | null>(null);
|
|
52
|
+
const [hoveredStrategy, setHoveredStrategy] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
// Derived data
|
|
55
|
+
const conceptsByKey = new Map(concepts.map((c) => [c.conceptKey, c]));
|
|
56
|
+
const activeColor = getConceptColor(selectedConcept || "community");
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<LayoutGroup>
|
|
60
|
+
<Card className="space-y-2 rounded-r-none pt-8 border-r-0">
|
|
61
|
+
{/* CONCEPT Section */}
|
|
62
|
+
<motion.section layout="position" className="flex flex-col gap-3">
|
|
63
|
+
<div className="flex items-center min-w-0">
|
|
64
|
+
<Label className="pr-1">CONCEPT</Label>
|
|
65
|
+
|
|
66
|
+
<div className="relative min-w-0 flex-1 self-stretch">
|
|
67
|
+
<AnimatePresence mode="wait">
|
|
68
|
+
{hoveredConcept && (
|
|
69
|
+
<motion.div
|
|
70
|
+
key={hoveredConcept}
|
|
71
|
+
initial={{ opacity: 0, x: -4 }}
|
|
72
|
+
animate={{ opacity: 1, x: 0 }}
|
|
73
|
+
exit={{ opacity: 0, x: 4 }}
|
|
74
|
+
transition={{ duration: 0.15 }}
|
|
75
|
+
className="absolute top-1/2 -translate-y-1/2 left-0 right-0 body-small"
|
|
76
|
+
style={{ willChange: "opacity, transform" }}
|
|
77
|
+
>
|
|
78
|
+
<span className="line-clamp-1">
|
|
79
|
+
·{" "}
|
|
80
|
+
{capitalize(
|
|
81
|
+
concepts.find((c) => c.conceptKey === hoveredConcept)
|
|
82
|
+
?.name ?? "",
|
|
83
|
+
)}
|
|
84
|
+
</span>
|
|
85
|
+
</motion.div>
|
|
86
|
+
)}
|
|
87
|
+
</AnimatePresence>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="grid grid-cols-3 lg:grid-cols-5 max-w-max gap-2">
|
|
92
|
+
{conceptOrder.map((slug) => {
|
|
93
|
+
const Icon = CONCEPT_ICONS[slug];
|
|
94
|
+
const isSelected = selectedConcept === slug;
|
|
95
|
+
const isHovered = hoveredConcept === slug;
|
|
96
|
+
const isFilled = (isSelected && !selectedTheme) || isHovered;
|
|
97
|
+
const isOutlined = isSelected && !!selectedTheme && !isHovered;
|
|
98
|
+
const color = getConceptColor(slug);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<button
|
|
102
|
+
key={slug}
|
|
103
|
+
onClick={() => onConceptClick(slug)}
|
|
104
|
+
onMouseEnter={() => setHoveredConcept(slug)}
|
|
105
|
+
onMouseLeave={() => setHoveredConcept(null)}
|
|
106
|
+
className="rounded-full transition-all duration-200 size-12 flex items-center justify-center cursor-pointer"
|
|
107
|
+
style={(() => {
|
|
108
|
+
if (isOutlined) {
|
|
109
|
+
return {
|
|
110
|
+
outline: `3px solid ${color.contrast || color.solid}`,
|
|
111
|
+
outlineOffset: "-3px",
|
|
112
|
+
backgroundColor: "white",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (isFilled) {
|
|
116
|
+
return {
|
|
117
|
+
backgroundColor: color.contrast || color.solid,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
})()}
|
|
122
|
+
aria-label={conceptsByKey.get(slug)?.name}
|
|
123
|
+
>
|
|
124
|
+
<Icon
|
|
125
|
+
active={isFilled}
|
|
126
|
+
outlined={isOutlined}
|
|
127
|
+
className="size-12"
|
|
128
|
+
/>
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
</motion.section>
|
|
134
|
+
|
|
135
|
+
{/* THEME Section - Animated reveal */}
|
|
136
|
+
<AnimatePresence mode="wait">
|
|
137
|
+
{themes.length > 0 && (
|
|
138
|
+
<motion.section
|
|
139
|
+
key="theme-section"
|
|
140
|
+
layout="position"
|
|
141
|
+
initial={{ opacity: 0 }}
|
|
142
|
+
animate={{ opacity: 1 }}
|
|
143
|
+
exit={{ opacity: 0 }}
|
|
144
|
+
transition={{ duration: 0.2 }}
|
|
145
|
+
className="flex flex-col gap-3"
|
|
146
|
+
style={{ willChange: "opacity" }}
|
|
147
|
+
>
|
|
148
|
+
<div className="flex items-center min-w-0">
|
|
149
|
+
<Label className="pr-1">THEME</Label>
|
|
150
|
+
|
|
151
|
+
<div className="relative min-w-0 flex-1 self-stretch">
|
|
152
|
+
<AnimatePresence mode="wait">
|
|
153
|
+
{hoveredTheme && (
|
|
154
|
+
<motion.div
|
|
155
|
+
key={hoveredTheme}
|
|
156
|
+
initial={{ opacity: 0, x: -4 }}
|
|
157
|
+
animate={{ opacity: 1, x: 0 }}
|
|
158
|
+
exit={{ opacity: 0, x: 4 }}
|
|
159
|
+
transition={{ duration: 0.15 }}
|
|
160
|
+
className="absolute top-1/2 -translate-y-1/2 left-0 right-0 body-small"
|
|
161
|
+
style={{ willChange: "opacity, transform" }}
|
|
162
|
+
>
|
|
163
|
+
<span className="line-clamp-1">
|
|
164
|
+
·{" "}
|
|
165
|
+
{capitalize(
|
|
166
|
+
themes.find((t) => t.code === hoveredTheme)?.name ??
|
|
167
|
+
"",
|
|
168
|
+
)}
|
|
169
|
+
</span>
|
|
170
|
+
</motion.div>
|
|
203
171
|
)}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
172
|
+
</AnimatePresence>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="grid grid-cols-3 lg:grid-cols-5 max-w-max gap-2">
|
|
176
|
+
{themes.map((theme) => {
|
|
177
|
+
const isSelected = selectedTheme === theme.code;
|
|
178
|
+
const isFilled = isSelected && !selectedStrategy;
|
|
179
|
+
const isOutlined = isSelected && !!selectedStrategy;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<button
|
|
183
|
+
key={theme.code}
|
|
184
|
+
onClick={() => onThemeClick(theme.code)}
|
|
185
|
+
onMouseEnter={() => setHoveredTheme(theme.code)}
|
|
186
|
+
onMouseLeave={() => setHoveredTheme(null)}
|
|
187
|
+
className="cursor-pointer"
|
|
188
|
+
>
|
|
189
|
+
<Tag
|
|
190
|
+
variant="code"
|
|
191
|
+
className={cn(!isSelected && "hover:border-blue-300")}
|
|
192
|
+
style={
|
|
193
|
+
isFilled
|
|
194
|
+
? {
|
|
195
|
+
backgroundColor:
|
|
196
|
+
activeColor.contrast || activeColor.solid,
|
|
197
|
+
borderColor:
|
|
198
|
+
activeColor.contrast || activeColor.solid,
|
|
199
|
+
color: "white",
|
|
200
|
+
borderWidth: "3px",
|
|
201
|
+
}
|
|
202
|
+
: isOutlined
|
|
203
|
+
? {
|
|
204
|
+
backgroundColor: "white",
|
|
205
|
+
borderColor:
|
|
206
|
+
activeColor.contrast || activeColor.solid,
|
|
207
|
+
color:
|
|
208
|
+
activeColor.contrast || activeColor.solid,
|
|
209
|
+
borderWidth: "3px",
|
|
210
|
+
}
|
|
211
|
+
: !isSelected
|
|
212
|
+
? {
|
|
213
|
+
backgroundColor: "var(--color-blue-100)",
|
|
214
|
+
color: "var(--color-blue-600)",
|
|
215
|
+
borderColor: "var(--color-blue-100)",
|
|
216
|
+
borderWidth: "3px",
|
|
217
|
+
}
|
|
218
|
+
: {
|
|
219
|
+
borderWidth: "3px",
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
>
|
|
223
|
+
{theme.code}
|
|
224
|
+
</Tag>
|
|
225
|
+
</button>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
</motion.section>
|
|
230
|
+
)}
|
|
231
|
+
</AnimatePresence>
|
|
232
|
+
|
|
233
|
+
{/* STRATEGY Section - Animated reveal */}
|
|
234
|
+
<AnimatePresence mode="wait">
|
|
235
|
+
{strategies.length > 0 && (
|
|
236
|
+
<motion.section
|
|
237
|
+
key="strategy-section"
|
|
238
|
+
layout="position"
|
|
239
|
+
initial={{ opacity: 0 }}
|
|
240
|
+
animate={{ opacity: 1 }}
|
|
241
|
+
exit={{ opacity: 0 }}
|
|
242
|
+
transition={{ duration: 0.2 }}
|
|
243
|
+
className="flex flex-col gap-3"
|
|
244
|
+
style={{ willChange: "opacity" }}
|
|
245
|
+
>
|
|
246
|
+
<div className="flex items-center min-w-0">
|
|
247
|
+
<Label className="pr-1">STRATEGY</Label>
|
|
248
|
+
|
|
249
|
+
<div className="relative min-w-0 flex-1 self-stretch mt-px">
|
|
250
|
+
<AnimatePresence mode="wait">
|
|
251
|
+
{hoveredStrategy && (
|
|
252
|
+
<motion.div
|
|
253
|
+
key={hoveredStrategy}
|
|
254
|
+
initial={{ opacity: 0, x: -4 }}
|
|
255
|
+
animate={{ opacity: 1, x: 0 }}
|
|
256
|
+
exit={{ opacity: 0, x: 4 }}
|
|
257
|
+
transition={{ duration: 0.15 }}
|
|
258
|
+
className="absolute top-1/2 -translate-y-1/2 left-0 right-0 body-small"
|
|
259
|
+
style={{ willChange: "opacity, transform" }}
|
|
260
|
+
>
|
|
261
|
+
<span className="line-clamp-1">
|
|
262
|
+
·{" "}
|
|
263
|
+
{capitalize(
|
|
264
|
+
strategies.find((s) => s.code === hoveredStrategy)
|
|
265
|
+
?.name ?? "",
|
|
266
|
+
)}
|
|
267
|
+
</span>
|
|
268
|
+
</motion.div>
|
|
276
269
|
)}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
</
|
|
323
|
-
|
|
324
|
-
|
|
270
|
+
</AnimatePresence>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div className="grid grid-cols-3 sm:grid-cols-5 max-w-max gap-2">
|
|
274
|
+
{strategies.map((strategy) => {
|
|
275
|
+
const isSelected = selectedStrategy === strategy.code;
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<button
|
|
279
|
+
key={strategy.code}
|
|
280
|
+
onClick={() => onStrategyClick(strategy.code)}
|
|
281
|
+
onMouseEnter={() => setHoveredStrategy(strategy.code)}
|
|
282
|
+
onMouseLeave={() => setHoveredStrategy(null)}
|
|
283
|
+
className="cursor-pointer"
|
|
284
|
+
>
|
|
285
|
+
<Tag
|
|
286
|
+
variant="code"
|
|
287
|
+
className={cn(
|
|
288
|
+
!isSelected && "hover:border-blue-300",
|
|
289
|
+
"border-[3px]",
|
|
290
|
+
)}
|
|
291
|
+
style={
|
|
292
|
+
isSelected
|
|
293
|
+
? {
|
|
294
|
+
backgroundColor:
|
|
295
|
+
activeColor.contrast || activeColor.solid,
|
|
296
|
+
borderColor:
|
|
297
|
+
activeColor.contrast || activeColor.solid,
|
|
298
|
+
color: "white",
|
|
299
|
+
borderWidth: "3px",
|
|
300
|
+
}
|
|
301
|
+
: {
|
|
302
|
+
backgroundColor: "var(--color-blue-100)",
|
|
303
|
+
color: "var(--color-blue-600)",
|
|
304
|
+
borderColor: "var(--color-blue-100)",
|
|
305
|
+
borderWidth: "3px",
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
>
|
|
309
|
+
{strategy.code}
|
|
310
|
+
</Tag>
|
|
311
|
+
</button>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</div>
|
|
315
|
+
</motion.section>
|
|
316
|
+
)}
|
|
317
|
+
</AnimatePresence>
|
|
318
|
+
|
|
319
|
+
{/* SCOPE Section */}
|
|
320
|
+
<motion.section
|
|
321
|
+
layout="position"
|
|
322
|
+
transition={{ duration: 0.2, ease: [0.25, 0.46, 0.45, 0.94] }}
|
|
323
|
+
className="flex flex-col gap-3 items-start max-w-max"
|
|
324
|
+
>
|
|
325
|
+
<Label>SCOPE</Label>
|
|
326
|
+
<div className="bg-white border border-gray-100 rounded-full inline-flex">
|
|
327
|
+
<Toggle
|
|
328
|
+
options={[
|
|
329
|
+
{ label: "Non-core", value: "non-core" },
|
|
330
|
+
{ label: "Core", value: "core" },
|
|
331
|
+
]}
|
|
332
|
+
value={scope}
|
|
333
|
+
onValueChange={(value) =>
|
|
334
|
+
onScopeChange(value as "non-core" | "core")
|
|
335
|
+
}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
</motion.section>
|
|
339
|
+
|
|
340
|
+
{/* PURSUIT Section */}
|
|
341
|
+
<motion.section
|
|
342
|
+
layout="position"
|
|
343
|
+
transition={{ duration: 0.2, ease: [0.25, 0.46, 0.45, 0.94] }}
|
|
344
|
+
className="flex flex-col gap-3 items-start max-w-max"
|
|
345
|
+
>
|
|
346
|
+
<Label>PURSUIT</Label>
|
|
347
|
+
<div className=" inline-flex gap-3">
|
|
348
|
+
<Tag
|
|
349
|
+
variant="text"
|
|
350
|
+
active={activePursuits.has("certification")}
|
|
351
|
+
onClick={() => onPursuitToggle("certification")}
|
|
352
|
+
>
|
|
353
|
+
Certification
|
|
354
|
+
</Tag>
|
|
355
|
+
<Tag
|
|
356
|
+
variant="text"
|
|
357
|
+
active={activePursuits.has("rating")}
|
|
358
|
+
onClick={() => onPursuitToggle("rating")}
|
|
359
|
+
>
|
|
360
|
+
Rating
|
|
361
|
+
</Tag>
|
|
362
|
+
</div>
|
|
363
|
+
</motion.section>
|
|
364
|
+
</Card>
|
|
365
|
+
</LayoutGroup>
|
|
366
|
+
);
|
|
325
367
|
};
|
|
326
368
|
|
|
327
369
|
export const ExploreSidePanel = {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
370
|
+
args: {
|
|
371
|
+
mockSearchParams: "concept=community&theme=C8",
|
|
372
|
+
},
|
|
373
|
+
parameters: {
|
|
374
|
+
docs: {
|
|
375
|
+
source: {
|
|
376
|
+
code: `<ExploreSidePanelView
|
|
335
377
|
concepts={concepts}
|
|
336
378
|
themes={themes}
|
|
337
379
|
strategies={strategies}
|
|
@@ -346,135 +388,170 @@ export const ExploreSidePanel = {
|
|
|
346
388
|
activePursuits={activePursuits}
|
|
347
389
|
onPursuitToggle={handlePursuitToggle}
|
|
348
390
|
/>`,
|
|
349
|
-
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
render: (args: any) => {
|
|
353
|
-
// Mock searchParams with state for Storybook //
|
|
354
|
-
const [urlState, setUrlState] = useState(args.mockSearchParams || "");
|
|
355
|
-
const searchParams = new URLSearchParams(urlState);
|
|
356
|
-
//In real app: const searchParams = useSearchParams();
|
|
357
|
-
|
|
358
|
-
// URL state
|
|
359
|
-
const selectedConcept = searchParams.get("concept") as ConceptSlug | null;
|
|
360
|
-
const selectedTheme = searchParams.get("theme");
|
|
361
|
-
const selectedStrategy = searchParams.get("strategy");
|
|
362
|
-
|
|
363
|
-
// Progressive data fetching //
|
|
364
|
-
// const { data: concepts = [] } = useConceptListQuery();
|
|
365
|
-
// const { data: themes = [] } = useThemesQuery(selectedConcept);
|
|
366
|
-
// const { data: strategies = [] } = useStrategiesQuery(selectedConcept, selectedTheme);
|
|
367
|
-
|
|
368
|
-
// Mock data for Storybook
|
|
369
|
-
const concepts = [{ "conceptKey": "mind", "name": "Mind" }, { "conceptKey": "community", "name": "Community" }, { "conceptKey": "movement", "name": "Movement" }, { "conceptKey": "water", "name": "Water" }, { "conceptKey": "air", "name": "Air" }, { "conceptKey": "light", "name": "Light" }, { "conceptKey": "thermal-comfort", "name": "Thermal Comfort" }, { "conceptKey": "nourishment", "name": "Nourishment" }, { "conceptKey": "sound", "name": "Sound" }, { "conceptKey": "materials", "name": "Materials" }, { "conceptKey": "innovation", "name": "Innovation" }];
|
|
370
|
-
|
|
371
|
-
// Themes only load when concept is selected
|
|
372
|
-
const themes = selectedConcept ? [{ "code": "C1", "name": "Community and occupant engagement" }, { "code": "C2", "name": "Emergency preparedness" }, { "code": "C3", "name": "Fair housing" }, { "code": "C4", "name": "Family and parental support" }, { "code": "C5", "name": "Health benefits and services" }, { "code": "C6", "name": "Health promotion" }, { "code": "C7", "name": "Inclusive design" }, { "code": "C8", "name": "Occupant experience" }, { "code": "C9", "name": "Organizational practices" }, { "code": "C10", "name": "Personal-professional development" }, { "code": "C11", "name": "Supportive construction practices" }] : [];
|
|
373
|
-
|
|
374
|
-
// Strategies only load when theme is selected
|
|
375
|
-
const strategies = selectedTheme ? [{ "code": "C8.1", "name": "Collect additional occupant research" }, { "code": "C8.2", "name": "Conduct qualitative research" }, { "code": "C8.3", "name": "Develop annual action plan" }, { "code": "C8.4", "name": "Perform annual occupant survey" }] : [];
|
|
376
|
-
|
|
377
|
-
// Local state
|
|
378
|
-
const [scope, setScope] = useState<"core" | "non-core">("core");
|
|
379
|
-
const [activePursuits, setActivePursuits] = useState<Set<"certification" | "rating">>(
|
|
380
|
-
new Set(["certification"])
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
// Navigation handlers
|
|
384
|
-
const handleConceptClick = (slug: ConceptSlug) => {
|
|
385
|
-
const params = new URLSearchParams(searchParams.toString());
|
|
386
|
-
|
|
387
|
-
if (selectedConcept === slug) {
|
|
388
|
-
params.delete("concept");
|
|
389
|
-
params.delete("theme");
|
|
390
|
-
params.delete("strategy");
|
|
391
|
-
} else {
|
|
392
|
-
params.set("concept", slug);
|
|
393
|
-
params.delete("theme");
|
|
394
|
-
params.delete("strategy");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
setUrlState(params.toString());
|
|
398
|
-
// In real app: router.push(`/explore?${params.toString()}`);
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const handleThemeClick = (themeCode: string) => {
|
|
402
|
-
const params = new URLSearchParams(searchParams.toString());
|
|
403
|
-
|
|
404
|
-
if (selectedTheme === themeCode) {
|
|
405
|
-
if (selectedStrategy) {
|
|
406
|
-
params.delete("strategy");
|
|
407
|
-
} else {
|
|
408
|
-
params.delete("theme");
|
|
409
|
-
params.delete("strategy");
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
params.set("theme", themeCode);
|
|
413
|
-
params.delete("strategy");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
setUrlState(params.toString());
|
|
417
|
-
// In real app: router.push(`/explore?${params.toString()}`);
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const handleStrategyClick = (strategyCode: string) => {
|
|
421
|
-
const params = new URLSearchParams(searchParams.toString());
|
|
422
|
-
|
|
423
|
-
if (selectedStrategy === strategyCode) {
|
|
424
|
-
params.delete("strategy");
|
|
425
|
-
} else {
|
|
426
|
-
params.set("strategy", strategyCode);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
setUrlState(params.toString());
|
|
430
|
-
// In real app: router.push(`/explore?${params.toString()}`);
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const handleScopeChange = (newScope: "core" | "non-core") => {
|
|
434
|
-
setScope(newScope);
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
const handlePursuitToggle = (pursuit: "certification" | "rating") => {
|
|
438
|
-
setActivePursuits((prev) => {
|
|
439
|
-
const next = new Set(prev);
|
|
440
|
-
if (next.has(pursuit)) {
|
|
441
|
-
next.delete(pursuit);
|
|
442
|
-
} else {
|
|
443
|
-
next.add(pursuit);
|
|
444
|
-
}
|
|
445
|
-
return next;
|
|
446
|
-
});
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
return (
|
|
450
|
-
<ExploreSidePanelView
|
|
451
|
-
concepts={concepts}
|
|
452
|
-
themes={themes}
|
|
453
|
-
strategies={strategies}
|
|
454
|
-
selectedConcept={selectedConcept}
|
|
455
|
-
selectedTheme={selectedTheme}
|
|
456
|
-
selectedStrategy={selectedStrategy}
|
|
457
|
-
onConceptClick={handleConceptClick}
|
|
458
|
-
onThemeClick={handleThemeClick}
|
|
459
|
-
onStrategyClick={handleStrategyClick}
|
|
460
|
-
scope={scope}
|
|
461
|
-
onScopeChange={handleScopeChange}
|
|
462
|
-
activePursuits={activePursuits}
|
|
463
|
-
onPursuitToggle={handlePursuitToggle}
|
|
464
|
-
/>
|
|
465
|
-
);
|
|
391
|
+
},
|
|
466
392
|
},
|
|
393
|
+
},
|
|
394
|
+
render: (args: any) => {
|
|
395
|
+
// Mock searchParams with state for Storybook //
|
|
396
|
+
const [urlState, setUrlState] = useState(args.mockSearchParams || "");
|
|
397
|
+
const searchParams = new URLSearchParams(urlState);
|
|
398
|
+
//In real app: const searchParams = useSearchParams();
|
|
399
|
+
|
|
400
|
+
// URL state
|
|
401
|
+
const selectedConcept = searchParams.get("concept") as ConceptSlug | null;
|
|
402
|
+
const selectedTheme = searchParams.get("theme");
|
|
403
|
+
const selectedStrategy = searchParams.get("strategy");
|
|
404
|
+
|
|
405
|
+
// Progressive data fetching //
|
|
406
|
+
// const { data: concepts = [] } = useConceptListQuery();
|
|
407
|
+
// const { data: themes = [] } = useThemesQuery(selectedConcept);
|
|
408
|
+
// const { data: strategies = [] } = useStrategiesQuery(selectedConcept, selectedTheme);
|
|
409
|
+
|
|
410
|
+
// Mock data for Storybook
|
|
411
|
+
const concepts = [
|
|
412
|
+
{ conceptKey: "mind", name: "Mind" },
|
|
413
|
+
{ conceptKey: "community", name: "Community" },
|
|
414
|
+
{ conceptKey: "movement", name: "Movement" },
|
|
415
|
+
{ conceptKey: "water", name: "Water" },
|
|
416
|
+
{ conceptKey: "air", name: "Air" },
|
|
417
|
+
{ conceptKey: "light", name: "Light" },
|
|
418
|
+
{ conceptKey: "thermal-comfort", name: "Thermal Comfort" },
|
|
419
|
+
{ conceptKey: "nourishment", name: "Nourishment" },
|
|
420
|
+
{ conceptKey: "sound", name: "Sound" },
|
|
421
|
+
{ conceptKey: "materials", name: "Materials" },
|
|
422
|
+
{ conceptKey: "innovation", name: "Innovation" },
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
// Themes only load when concept is selected
|
|
426
|
+
const themes = selectedConcept
|
|
427
|
+
? [
|
|
428
|
+
{ code: "C1", name: "Community and occupant engagement" },
|
|
429
|
+
{ code: "C2", name: "Emergency preparedness" },
|
|
430
|
+
{ code: "C3", name: "Fair housing" },
|
|
431
|
+
{ code: "C4", name: "Family and parental support" },
|
|
432
|
+
{ code: "C5", name: "Health benefits and services" },
|
|
433
|
+
{ code: "C6", name: "Health promotion" },
|
|
434
|
+
{ code: "C7", name: "Inclusive design" },
|
|
435
|
+
{ code: "C8", name: "Occupant experience" },
|
|
436
|
+
{ code: "C9", name: "Organizational practices" },
|
|
437
|
+
{ code: "C10", name: "Personal-professional development" },
|
|
438
|
+
{ code: "C11", name: "Supportive construction practices" },
|
|
439
|
+
]
|
|
440
|
+
: [];
|
|
441
|
+
|
|
442
|
+
// Strategies only load when theme is selected
|
|
443
|
+
const strategies = selectedTheme
|
|
444
|
+
? [
|
|
445
|
+
{ code: "C8.1", name: "Collect additional occupant research" },
|
|
446
|
+
{ code: "C8.2", name: "Conduct qualitative research" },
|
|
447
|
+
{ code: "C8.3", name: "Develop annual action plan" },
|
|
448
|
+
{ code: "C8.4", name: "Perform annual occupant survey" },
|
|
449
|
+
]
|
|
450
|
+
: [];
|
|
451
|
+
|
|
452
|
+
// Local state
|
|
453
|
+
const [scope, setScope] = useState<"core" | "non-core">("core");
|
|
454
|
+
const [activePursuits, setActivePursuits] = useState<
|
|
455
|
+
Set<"certification" | "rating">
|
|
456
|
+
>(new Set(["certification"]));
|
|
457
|
+
|
|
458
|
+
// Navigation handlers
|
|
459
|
+
const handleConceptClick = (slug: ConceptSlug) => {
|
|
460
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
461
|
+
|
|
462
|
+
if (selectedConcept === slug) {
|
|
463
|
+
params.delete("concept");
|
|
464
|
+
params.delete("theme");
|
|
465
|
+
params.delete("strategy");
|
|
466
|
+
} else {
|
|
467
|
+
params.set("concept", slug);
|
|
468
|
+
params.delete("theme");
|
|
469
|
+
params.delete("strategy");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
setUrlState(params.toString());
|
|
473
|
+
// In real app: router.push(`/explore?${params.toString()}`);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const handleThemeClick = (themeCode: string) => {
|
|
477
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
478
|
+
|
|
479
|
+
if (selectedTheme === themeCode) {
|
|
480
|
+
if (selectedStrategy) {
|
|
481
|
+
params.delete("strategy");
|
|
482
|
+
} else {
|
|
483
|
+
params.delete("theme");
|
|
484
|
+
params.delete("strategy");
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
params.set("theme", themeCode);
|
|
488
|
+
params.delete("strategy");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
setUrlState(params.toString());
|
|
492
|
+
// In real app: router.push(`/explore?${params.toString()}`);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const handleStrategyClick = (strategyCode: string) => {
|
|
496
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
497
|
+
|
|
498
|
+
if (selectedStrategy === strategyCode) {
|
|
499
|
+
params.delete("strategy");
|
|
500
|
+
} else {
|
|
501
|
+
params.set("strategy", strategyCode);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
setUrlState(params.toString());
|
|
505
|
+
// In real app: router.push(`/explore?${params.toString()}`);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const handleScopeChange = (newScope: "core" | "non-core") => {
|
|
509
|
+
setScope(newScope);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const handlePursuitToggle = (pursuit: "certification" | "rating") => {
|
|
513
|
+
setActivePursuits((prev) => {
|
|
514
|
+
const next = new Set(prev);
|
|
515
|
+
if (next.has(pursuit)) {
|
|
516
|
+
next.delete(pursuit);
|
|
517
|
+
} else {
|
|
518
|
+
next.add(pursuit);
|
|
519
|
+
}
|
|
520
|
+
return next;
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<div className="max-w-[322px]">
|
|
526
|
+
<ExploreSidePanelView
|
|
527
|
+
concepts={concepts}
|
|
528
|
+
themes={themes}
|
|
529
|
+
strategies={strategies}
|
|
530
|
+
selectedConcept={selectedConcept}
|
|
531
|
+
selectedTheme={selectedTheme}
|
|
532
|
+
selectedStrategy={selectedStrategy}
|
|
533
|
+
onConceptClick={handleConceptClick}
|
|
534
|
+
onThemeClick={handleThemeClick}
|
|
535
|
+
onStrategyClick={handleStrategyClick}
|
|
536
|
+
scope={scope}
|
|
537
|
+
onScopeChange={handleScopeChange}
|
|
538
|
+
activePursuits={activePursuits}
|
|
539
|
+
onPursuitToggle={handlePursuitToggle}
|
|
540
|
+
/>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
},
|
|
467
544
|
};
|
|
468
545
|
|
|
469
546
|
const meta = {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
547
|
+
title: "Review/Panel",
|
|
548
|
+
component: ExploreSidePanelView,
|
|
549
|
+
tags: ["autodocs"],
|
|
550
|
+
parameters: {
|
|
551
|
+
layout: "padded",
|
|
552
|
+
},
|
|
553
|
+
};
|
|
477
554
|
|
|
478
555
|
export default meta;
|
|
479
556
|
|
|
480
|
-
type ConceptSlug = keyof typeof conceptColors;
|
|
557
|
+
type ConceptSlug = keyof typeof conceptColors;
|