@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.
@@ -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 { CodeBadge } from "../components/ui/code-badge";
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
- "air",
19
- "water",
20
- "nourishment",
21
- "light",
22
- "movement",
23
- "thermal-comfort",
24
- "sound",
25
- "materials",
26
- "community",
27
- "mind",
28
- "innovation",
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
- const { concepts,
35
- themes,
36
- strategies,
37
- selectedConcept,
38
- selectedTheme,
39
- selectedStrategy,
40
- onConceptClick,
41
- onThemeClick,
42
- onStrategyClick,
43
- scope,
44
- onScopeChange,
45
- activePursuits,
46
- onPursuitToggle, } = args;
47
-
48
- // Local UI state (hover)
49
- const [hoveredConcept, setHoveredConcept] = useState<ConceptSlug | null>(null);
50
- const [hoveredTheme, setHoveredTheme] = useState<string | null>(null);
51
- const [hoveredStrategy, setHoveredStrategy] = useState<string | null>(null);
52
-
53
- // Derived data
54
- const conceptsByKey = new Map(concepts.map((c) => [c.conceptKey, c]));
55
- const activeColor = getConceptColor(selectedConcept || "community");
56
-
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
- <Label className="px-2 text-gray-600 flex items-center min-w-0">
64
- <span className="shrink-0 pr-1">CONCEPT</span>
65
- <AnimatePresence mode="wait">
66
- {hoveredConcept && (
67
- <motion.span
68
- key={hoveredConcept}
69
- initial={{ opacity: 0, x: -4 }}
70
- animate={{ opacity: 1, x: 0 }}
71
- exit={{ opacity: 0, x: 4 }}
72
- transition={{ duration: 0.15 }}
73
- className="truncate"
74
- style={{ willChange: "opacity, transform" }}
75
- >
76
- · {conceptsByKey.get(hoveredConcept)?.name.toUpperCase()}
77
- </motion.span>
78
- )}
79
- </AnimatePresence>
80
- </Label>
81
-
82
- <div className="grid grid-cols-3 lg:grid-cols-5 max-w-max gap-2">
83
- {conceptOrder.map((slug) => {
84
- const Icon = CONCEPT_ICONS[slug];
85
- const isSelected = selectedConcept === slug;
86
- const isHovered = hoveredConcept === slug;
87
- const isFilled = (isSelected && !selectedTheme) || isHovered;
88
- const isOutlined = isSelected && !!selectedTheme && !isHovered;
89
- const color = getConceptColor(slug);
90
-
91
- return (
92
- <button
93
- key={slug}
94
- onClick={() => onConceptClick(slug)}
95
- onMouseEnter={() => setHoveredConcept(slug)}
96
- onMouseLeave={() => setHoveredConcept(null)}
97
- className="rounded-full transition-all duration-200 size-12 flex items-center justify-center cursor-pointer"
98
- style={(() => {
99
- if (isOutlined) {
100
- return {
101
- border: `3px solid ${color.contrast || color.solid}`,
102
- backgroundColor: "white",
103
- };
104
- }
105
- if (isFilled) {
106
- return {
107
- backgroundColor: color.contrast || color.solid,
108
- };
109
- }
110
- return undefined;
111
- })()}
112
- aria-label={conceptsByKey.get(slug)?.name}
113
- >
114
- <Icon active={isFilled} outlined={isOutlined} className="size-12" />
115
- </button>
116
- );
117
- })}
118
- </div>
119
- </motion.section>
120
-
121
- {/* THEME Section - Animated reveal */}
122
- <AnimatePresence mode="wait">
123
- {themes.length > 0 && (
124
- <motion.section
125
- key="theme-section"
126
- layout="position"
127
- initial={{ opacity: 0 }}
128
- animate={{ opacity: 1 }}
129
- exit={{ opacity: 0 }}
130
- transition={{ duration: 0.2 }}
131
- className="flex flex-col gap-3"
132
- style={{ willChange: "opacity" }}
133
- >
134
- <Label className="px-2 text-gray-600 flex items-center min-w-0">
135
- <span className="shrink-0 pr-1">THEME</span>
136
- <AnimatePresence mode="wait">
137
- {hoveredTheme && (
138
- <motion.span
139
- key={hoveredTheme}
140
- initial={{ opacity: 0, x: -4 }}
141
- animate={{ opacity: 1, x: 0 }}
142
- exit={{ opacity: 0, x: 4 }}
143
- transition={{ duration: 0.15 }}
144
- className="truncate"
145
- style={{ willChange: "opacity, transform" }}
146
- >
147
- · {themes.find((t) => t.code === hoveredTheme)?.name.toUpperCase()}
148
- </motion.span>
149
- )}
150
- </AnimatePresence>
151
- </Label>
152
- <div className="grid grid-cols-3 lg:grid-cols-5 max-w-max gap-2">
153
- {themes.map((theme) => {
154
- const isSelected = selectedTheme === theme.code;
155
- const isFilled = isSelected && !selectedStrategy;
156
- const isOutlined = isSelected && !!selectedStrategy;
157
-
158
- return (
159
- <button
160
- key={theme.code}
161
- onClick={() => onThemeClick(theme.code)}
162
- onMouseEnter={() => setHoveredTheme(theme.code)}
163
- onMouseLeave={() => setHoveredTheme(null)}
164
- className="cursor-pointer"
165
- >
166
- <CodeBadge
167
- code={theme.code}
168
- className={cn(
169
- !isSelected && "hover:border-blue-300"
170
- )}
171
- style={
172
- isFilled
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
- </AnimatePresence>
205
-
206
- {/* STRATEGY Section - Animated reveal */}
207
- <AnimatePresence mode="wait">
208
- {strategies.length > 0 && (
209
- <motion.section
210
- key="strategy-section"
211
- layout="position"
212
- initial={{ opacity: 0 }}
213
- animate={{ opacity: 1 }}
214
- exit={{ opacity: 0 }}
215
- transition={{ duration: 0.2 }}
216
- className="flex flex-col gap-3"
217
- style={{ willChange: "opacity" }}
218
- >
219
- <Label className="px-2 text-gray-600 flex items-center min-w-0">
220
- <span className="shrink-0 pr-1">STRATEGY</span>
221
- <AnimatePresence mode="wait">
222
- {hoveredStrategy && (
223
- <motion.span
224
- key={hoveredStrategy}
225
- initial={{ opacity: 0, x: -4 }}
226
- animate={{ opacity: 1, x: 0 }}
227
- exit={{ opacity: 0, x: 4 }}
228
- transition={{ duration: 0.15 }}
229
- className="truncate"
230
- style={{ willChange: "opacity, transform" }}
231
- >
232
- · {strategies.find((s) => s.code === hoveredStrategy)?.name.toUpperCase()}
233
- </motion.span>
234
- )}
235
- </AnimatePresence>
236
- </Label>
237
- <div className="grid grid-cols-3 sm:grid-cols-5 max-w-max gap-2">
238
- {strategies.map((strategy) => {
239
- const isSelected = selectedStrategy === strategy.code;
240
-
241
- return (
242
- <button
243
- key={strategy.code}
244
- onClick={() => onStrategyClick(strategy.code)}
245
- onMouseEnter={() => setHoveredStrategy(strategy.code)}
246
- onMouseLeave={() => setHoveredStrategy(null)}
247
- className="cursor-pointer"
248
- >
249
- <CodeBadge
250
- code={strategy.code}
251
- className={cn(
252
- !isSelected && "hover:border-blue-300",
253
- "border-[3px]"
254
- )}
255
- style={
256
- isSelected
257
- ? {
258
- backgroundColor: activeColor.contrast || activeColor.solid,
259
- borderColor: activeColor.contrast || activeColor.solid,
260
- color: "white",
261
- borderWidth: "3px",
262
- }
263
- : {
264
- backgroundColor: "var(--color-blue-100)",
265
- color: "var(--color-blue-600)",
266
- borderColor: "var(--color-blue-100)",
267
- borderWidth: "3px",
268
- }
269
- }
270
- />
271
- </button>
272
- );
273
- })}
274
- </div>
275
- </motion.section>
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
- </AnimatePresence>
278
-
279
- {/* SCOPE Section */}
280
- <motion.section
281
- layout="position"
282
- transition={{ duration: 0.2, ease: [0.25, 0.46, 0.45, 0.94] }}
283
- className="flex flex-col gap-3 items-start max-w-max"
284
- >
285
- <Label className="px-2 text-gray-600">SCOPE</Label>
286
- <div className="bg-white border border-gray-100 rounded-full inline-flex">
287
- <Toggle
288
- options={[
289
- { label: "Non-core", value: "non-core" },
290
- { label: "Core", value: "core" },
291
- ]}
292
- value={scope}
293
- onValueChange={(value) => onScopeChange(value as "non-core" | "core")}
294
- />
295
- </div>
296
- </motion.section>
297
-
298
- {/* PURSUIT Section */}
299
- <motion.section
300
- layout="position"
301
- transition={{ duration: 0.2, ease: [0.25, 0.46, 0.45, 0.94] }}
302
- className="flex flex-col gap-3 items-start max-w-max"
303
- >
304
- <Label className="px-2 text-gray-600">PURSUIT</Label>
305
- <div className=" inline-flex gap-3">
306
- <Tag
307
- variant="light"
308
- active={activePursuits.has("certification")}
309
- onClick={() => onPursuitToggle("certification")}
310
- >
311
- Certification
312
- </Tag>
313
- <Tag
314
- variant="light"
315
- active={activePursuits.has("rating")}
316
- onClick={() => onPursuitToggle("rating")}
317
- >
318
- Rating
319
- </Tag>
320
- </div>
321
- </motion.section>
322
- </Card>
323
- </LayoutGroup>
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
- args: {
329
- mockSearchParams: "concept=community&theme=C8",
330
- },
331
- parameters: {
332
- docs: {
333
- source: {
334
- code: `<ExploreSidePanelView
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
- title: "Review/Panel",
471
- component: ExploreSidePanelView,
472
- tags: ["autodocs"],
473
- parameters: {
474
- layout: "padded",
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;