@ttoss/geovis-workspace 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,674 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ import { GeoVisCanvas, GeoVisProvider } from "@ttoss/geovis";
3
+ import { Box, Flex, Heading, IconButton, Link, Text } from "@ttoss/ui";
4
+ import { defineMessages, useI18n } from "@ttoss/react-i18n";
5
+ import * as React from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+
8
+ //#region src/context/GeovisWorkspaceContext.ts
9
+ var GeovisWorkspaceContext = React.createContext(void 0);
10
+
11
+ //#endregion
12
+ //#region src/hooks/useGeovisWorkspace.ts
13
+ /**
14
+ * Consumes the GeovisWorkspace context.
15
+ * Must be used inside a GeovisWorkspaceProvider.
16
+ */
17
+ var useGeovisWorkspace = () => {
18
+ const context = React.useContext(GeovisWorkspaceContext);
19
+ if (!context) throw new Error("useGeovisWorkspace must be used within a GeovisWorkspaceProvider");
20
+ return context;
21
+ };
22
+
23
+ //#endregion
24
+ //#region src/messages.ts
25
+ var messages = defineMessages({
26
+ detailsTitle: {
27
+ id: "35Dt1p",
28
+ defaultMessage: [{
29
+ "type": 0,
30
+ "value": "Details"
31
+ }]
32
+ },
33
+ noSelection: {
34
+ id: "cculTE",
35
+ defaultMessage: [{
36
+ "type": 0,
37
+ "value": "Select an item to view details."
38
+ }]
39
+ },
40
+ openMenu: {
41
+ id: "ebJRt+",
42
+ defaultMessage: [{
43
+ "type": 0,
44
+ "value": "Open menu"
45
+ }]
46
+ },
47
+ closeMenu: {
48
+ id: "ncOMIu",
49
+ defaultMessage: [{
50
+ "type": 0,
51
+ "value": "Close menu"
52
+ }]
53
+ },
54
+ openDetails: {
55
+ id: "IU+fm9",
56
+ defaultMessage: [{
57
+ "type": 0,
58
+ "value": "Open details"
59
+ }]
60
+ },
61
+ closeDetails: {
62
+ id: "j3dtho",
63
+ defaultMessage: [{
64
+ "type": 0,
65
+ "value": "Close details"
66
+ }]
67
+ }
68
+ });
69
+
70
+ //#endregion
71
+ //#region src/components/MenuButton.tsx
72
+ /**
73
+ * Internal full-width menu item used inside the left sidebar groups.
74
+ * Highlights itself when `active` is true.
75
+ */
76
+ var MenuButton = ({
77
+ label,
78
+ active,
79
+ onClick
80
+ }) => {
81
+ return /* @__PURE__ */jsx(Box, {
82
+ as: "button",
83
+ type: "button",
84
+ "aria-pressed": active,
85
+ "data-active": active ? "" : void 0,
86
+ onClick,
87
+ sx: {
88
+ display: "block",
89
+ width: "100%",
90
+ marginBottom: "2px",
91
+ paddingBlock: "7px",
92
+ paddingInline: "10px",
93
+ border: "none",
94
+ borderRadius: "6px",
95
+ cursor: "pointer",
96
+ textAlign: "left",
97
+ fontFamily: "body",
98
+ fontSize: "14px",
99
+ lineHeight: "1.4",
100
+ letterSpacing: "0.01em",
101
+ fontWeight: active ? 600 : 500,
102
+ color: active ? "#4338ca" : "#374151",
103
+ backgroundColor: active ? "#eef2ff" : "transparent",
104
+ transition: "background-color 0.15s ease, color 0.15s ease",
105
+ "&:hover": {
106
+ backgroundColor: active ? "#e0e7ff" : "#f3f4f6",
107
+ color: "#4338ca"
108
+ },
109
+ "&:focus-visible": {
110
+ outline: "2px solid #6366f1",
111
+ outlineOffset: "1px"
112
+ }
113
+ },
114
+ children: label
115
+ });
116
+ };
117
+
118
+ //#endregion
119
+ //#region src/components/LeftSidebar.tsx
120
+ /**
121
+ * Internal left sidebar that renders the menu groups defined in the config.
122
+ * Reads and writes the per-group selection via GeovisWorkspaceContext.
123
+ */
124
+ var LeftSidebar = () => {
125
+ const {
126
+ intl: {
127
+ formatMessage
128
+ }
129
+ } = useI18n();
130
+ const {
131
+ config,
132
+ selection,
133
+ setSelection,
134
+ setLeftSidebarOpen
135
+ } = useGeovisWorkspace();
136
+ const menus = config.leftSidebar?.menus ?? [];
137
+ return /* @__PURE__ */jsxs(Flex, {
138
+ sx: {
139
+ position: "relative",
140
+ flexDirection: "column",
141
+ gap: "5",
142
+ width: "256px",
143
+ height: "100%",
144
+ flexShrink: 0,
145
+ paddingX: "4",
146
+ paddingTop: "5",
147
+ paddingBottom: "4",
148
+ backgroundColor: "#ffffff",
149
+ borderRight: "1px solid #e5e7eb",
150
+ overflowY: "auto"
151
+ },
152
+ children: [/* @__PURE__ */jsx(IconButton, {
153
+ icon: "lucide:chevron-left",
154
+ "aria-label": formatMessage(messages.closeMenu),
155
+ onClick: event => {
156
+ event.currentTarget.blur();
157
+ setLeftSidebarOpen({
158
+ open: false
159
+ });
160
+ },
161
+ sx: {
162
+ position: "absolute",
163
+ top: "3",
164
+ right: "3",
165
+ color: "#6b7280",
166
+ backgroundColor: "transparent",
167
+ borderRadius: "md",
168
+ "&:hover": {
169
+ color: "#4338ca"
170
+ }
171
+ }
172
+ }), menus.map(menu => {
173
+ return /* @__PURE__ */jsxs(Box, {
174
+ sx: {
175
+ display: "flex",
176
+ flexDirection: "column"
177
+ },
178
+ children: [/* @__PURE__ */jsx(Text, {
179
+ sx: {
180
+ fontSize: "xs",
181
+ fontWeight: "semibold",
182
+ color: "#6b7280",
183
+ textTransform: "uppercase",
184
+ letterSpacing: "0.08em",
185
+ marginBottom: "2"
186
+ },
187
+ children: menu.title
188
+ }), menu.items.map(item => {
189
+ return /* @__PURE__ */jsx(MenuButton, {
190
+ label: item.label,
191
+ active: selection[menu.id] === item.value,
192
+ onClick: () => {
193
+ setSelection({
194
+ menuId: menu.id,
195
+ value: item.value
196
+ });
197
+ }
198
+ }, item.value);
199
+ })]
200
+ }, menu.id);
201
+ })]
202
+ });
203
+ };
204
+
205
+ //#endregion
206
+ //#region src/components/RightSidebar.tsx
207
+ /** Renders one data-source entry, as an external link when `href` is set. */
208
+ var SourceItem = ({
209
+ label,
210
+ href
211
+ }) => {
212
+ return /* @__PURE__ */jsx(Box, {
213
+ as: "li",
214
+ sx: {
215
+ fontSize: "xs",
216
+ color: "#6b7280",
217
+ lineHeight: "base"
218
+ },
219
+ children: href ? /* @__PURE__ */jsx(Link, {
220
+ href,
221
+ target: "_blank",
222
+ rel: "noopener noreferrer",
223
+ sx: {
224
+ color: "#4338ca",
225
+ textDecoration: "underline"
226
+ },
227
+ children: label
228
+ }) : label
229
+ });
230
+ };
231
+ /**
232
+ * Color legend panel driven by `rightSidebar.legendWithColor`: an optional
233
+ * description, a swatch-per-class legend and a list of data sources. Each block
234
+ * renders only when present in the spec.
235
+ */
236
+ var LegendWithColorPanel = ({
237
+ description,
238
+ legend,
239
+ sources
240
+ }) => {
241
+ return /* @__PURE__ */jsxs(Flex, {
242
+ sx: {
243
+ flexDirection: "column",
244
+ gap: "4"
245
+ },
246
+ children: [description && /* @__PURE__ */jsx(Text, {
247
+ sx: {
248
+ fontSize: "sm",
249
+ color: "#374151",
250
+ lineHeight: "base"
251
+ },
252
+ children: description
253
+ }), legend && /* @__PURE__ */jsxs(Flex, {
254
+ sx: {
255
+ flexDirection: "column",
256
+ gap: "2"
257
+ },
258
+ children: [legend.title && /* @__PURE__ */jsx(Text, {
259
+ sx: {
260
+ fontSize: "xs",
261
+ fontWeight: "semibold",
262
+ textTransform: "uppercase",
263
+ letterSpacing: "0.08em",
264
+ color: "#6b7280"
265
+ },
266
+ children: legend.title
267
+ }), /* @__PURE__ */jsx(Flex, {
268
+ sx: {
269
+ flexDirection: "column",
270
+ gap: "1"
271
+ },
272
+ children: legend.items.map(item => {
273
+ return /* @__PURE__ */jsxs(Flex, {
274
+ sx: {
275
+ alignItems: "center",
276
+ gap: "2"
277
+ },
278
+ children: [/* @__PURE__ */jsx(Box, {
279
+ sx: {
280
+ width: "20px",
281
+ height: "14px",
282
+ borderRadius: "2px",
283
+ flexShrink: 0,
284
+ backgroundColor: item.color
285
+ }
286
+ }), /* @__PURE__ */jsx(Text, {
287
+ sx: {
288
+ fontSize: "xs",
289
+ color: "#374151"
290
+ },
291
+ children: item.label
292
+ })]
293
+ }, item.label);
294
+ })
295
+ })]
296
+ }), sources && /* @__PURE__ */jsxs(Box, {
297
+ children: [sources.title && /* @__PURE__ */jsx(Text, {
298
+ sx: {
299
+ fontSize: "sm",
300
+ fontWeight: "semibold",
301
+ color: "#6b7280"
302
+ },
303
+ children: sources.title
304
+ }), /* @__PURE__ */jsx(Box, {
305
+ as: "ul",
306
+ sx: {
307
+ paddingLeft: "4",
308
+ marginTop: "1",
309
+ display: "flex",
310
+ flexDirection: "column",
311
+ gap: "1"
312
+ },
313
+ children: sources.items.map(source => {
314
+ return /* @__PURE__ */jsx(SourceItem, {
315
+ ...source
316
+ }, source.label);
317
+ })
318
+ })]
319
+ })]
320
+ });
321
+ };
322
+ /**
323
+ * Internal right sidebar. Shows the config-defined title and an optional color
324
+ * legend panel. Rendered only when the config defines a rightSidebar.
325
+ */
326
+ var RightSidebar = () => {
327
+ const {
328
+ intl: {
329
+ formatMessage
330
+ }
331
+ } = useI18n();
332
+ const {
333
+ config,
334
+ setRightSidebarOpen
335
+ } = useGeovisWorkspace();
336
+ const legendWithColor = config.rightSidebar?.legendWithColor;
337
+ return /* @__PURE__ */jsxs(Flex, {
338
+ sx: {
339
+ position: "relative",
340
+ flexDirection: "column",
341
+ gap: "4",
342
+ width: "256px",
343
+ height: "100%",
344
+ flexShrink: 0,
345
+ paddingX: "4",
346
+ paddingTop: "5",
347
+ paddingBottom: "4",
348
+ backgroundColor: "#ffffff",
349
+ borderLeft: "1px solid #e5e7eb",
350
+ overflowY: "auto"
351
+ },
352
+ children: [/* @__PURE__ */jsx(IconButton, {
353
+ icon: "lucide:chevron-right",
354
+ "aria-label": formatMessage(messages.closeDetails),
355
+ onClick: event => {
356
+ event.currentTarget.blur();
357
+ setRightSidebarOpen({
358
+ open: false
359
+ });
360
+ },
361
+ sx: {
362
+ position: "absolute",
363
+ top: "3",
364
+ right: "3",
365
+ color: "#6b7280",
366
+ backgroundColor: "transparent",
367
+ borderRadius: "md",
368
+ "&:hover": {
369
+ color: "#4338ca"
370
+ }
371
+ }
372
+ }), /* @__PURE__ */jsx(Heading, {
373
+ as: "h3",
374
+ sx: {
375
+ margin: 0,
376
+ fontSize: "xs",
377
+ fontWeight: "semibold",
378
+ textTransform: "uppercase",
379
+ letterSpacing: "0.08em",
380
+ color: "#6b7280"
381
+ },
382
+ children: config.rightSidebar?.title ?? formatMessage(messages.detailsTitle)
383
+ }), legendWithColor && /* @__PURE__ */jsx(LegendWithColorPanel, {
384
+ ...legendWithColor
385
+ })]
386
+ });
387
+ };
388
+
389
+ //#endregion
390
+ //#region src/components/Layout.tsx
391
+ /**
392
+ * Slide-in overlay that hosts a sidebar on the given side. The sidebar fills
393
+ * the height and is positioned absolutely so it does not push the children.
394
+ */
395
+ var SidebarOverlay = ({
396
+ side,
397
+ open,
398
+ children
399
+ }) => {
400
+ const hiddenTransform = side === "left" ? "translateX(-100%)" : "translateX(100%)";
401
+ return /* @__PURE__ */jsx(Box, {
402
+ "aria-hidden": !open,
403
+ sx: {
404
+ position: "absolute",
405
+ top: 0,
406
+ bottom: 0,
407
+ [side]: 0,
408
+ zIndex: 2,
409
+ transform: open ? "translateX(0)" : hiddenTransform,
410
+ opacity: open ? 1 : 0,
411
+ visibility: open ? "visible" : "hidden",
412
+ boxShadow: open ? "lg" : "none",
413
+ transition: "transform 0.25s ease-in-out, opacity 0.2s ease-in-out, visibility 0.25s ease-in-out, box-shadow 0.25s ease-in-out"
414
+ },
415
+ children
416
+ });
417
+ };
418
+ /**
419
+ * Floating button that opens the left sidebar. Hidden while it is open.
420
+ */
421
+ var OpenLeftSidebarButton = () => {
422
+ const {
423
+ intl: {
424
+ formatMessage
425
+ }
426
+ } = useI18n();
427
+ const {
428
+ isLeftSidebarOpen,
429
+ setLeftSidebarOpen
430
+ } = useGeovisWorkspace();
431
+ return /* @__PURE__ */jsx(IconButton, {
432
+ icon: "lucide:sliders-horizontal",
433
+ "aria-label": formatMessage(messages.openMenu),
434
+ onClick: event => {
435
+ event.currentTarget.blur();
436
+ setLeftSidebarOpen({
437
+ open: true
438
+ });
439
+ },
440
+ "aria-hidden": isLeftSidebarOpen,
441
+ tabIndex: isLeftSidebarOpen ? -1 : 0,
442
+ sx: {
443
+ position: "absolute",
444
+ top: "4",
445
+ left: "4",
446
+ zIndex: 1,
447
+ color: "display.text.primary.default",
448
+ backgroundColor: "display.background.primary.default",
449
+ border: "sm",
450
+ borderColor: "display.border.muted.default",
451
+ boxShadow: "md",
452
+ opacity: isLeftSidebarOpen ? 0 : 1,
453
+ visibility: isLeftSidebarOpen ? "hidden" : "visible",
454
+ pointerEvents: isLeftSidebarOpen ? "none" : "auto",
455
+ transition: "opacity 0.2s ease-in-out, visibility 0.2s ease-in-out",
456
+ "&:hover": {
457
+ backgroundColor: "display.background.secondary.default"
458
+ }
459
+ }
460
+ });
461
+ };
462
+ /**
463
+ * Floating button that opens the right sidebar. Sits vertically centered on
464
+ * the right edge and is hidden while the sidebar is open.
465
+ */
466
+ var OpenRightSidebarButton = () => {
467
+ const {
468
+ intl: {
469
+ formatMessage
470
+ }
471
+ } = useI18n();
472
+ const {
473
+ isRightSidebarOpen,
474
+ setRightSidebarOpen
475
+ } = useGeovisWorkspace();
476
+ return /* @__PURE__ */jsx(IconButton, {
477
+ icon: "lucide:chevrons-left",
478
+ "aria-label": formatMessage(messages.openDetails),
479
+ onClick: event => {
480
+ event.currentTarget.blur();
481
+ setRightSidebarOpen({
482
+ open: true
483
+ });
484
+ },
485
+ "aria-hidden": isRightSidebarOpen,
486
+ tabIndex: isRightSidebarOpen ? -1 : 0,
487
+ sx: {
488
+ position: "absolute",
489
+ top: "50%",
490
+ right: 0,
491
+ transform: "translateY(-50%)",
492
+ zIndex: 1,
493
+ borderTopLeftRadius: "md",
494
+ borderBottomLeftRadius: "md",
495
+ borderTopRightRadius: 0,
496
+ borderBottomRightRadius: 0,
497
+ color: "display.text.primary.default",
498
+ backgroundColor: "display.background.primary.default",
499
+ border: "sm",
500
+ borderColor: "display.border.muted.default",
501
+ boxShadow: "md",
502
+ opacity: isRightSidebarOpen ? 0 : 1,
503
+ visibility: isRightSidebarOpen ? "hidden" : "visible",
504
+ pointerEvents: isRightSidebarOpen ? "none" : "auto",
505
+ transition: "opacity 0.2s ease-in-out, visibility 0.2s ease-in-out",
506
+ "&:hover": {
507
+ backgroundColor: "display.background.secondary.default"
508
+ }
509
+ }
510
+ });
511
+ };
512
+ /**
513
+ * Internal layout shell. The children fill the whole area, and each sidebar
514
+ * (and its floating reopen button) is rendered only when the corresponding
515
+ * section is defined in the spec.
516
+ */
517
+ var Layout = ({
518
+ children
519
+ }) => {
520
+ const {
521
+ config,
522
+ isLeftSidebarOpen,
523
+ isRightSidebarOpen
524
+ } = useGeovisWorkspace();
525
+ const hasLeftSidebar = config.leftSidebar !== void 0;
526
+ const hasRightSidebar = config.rightSidebar !== void 0;
527
+ return /* @__PURE__ */jsxs(Flex, {
528
+ sx: {
529
+ position: "relative",
530
+ overflow: "hidden",
531
+ minHeight: "440px",
532
+ border: "sm",
533
+ borderColor: "display.border.muted.default",
534
+ borderRadius: "lg",
535
+ backgroundColor: "display.background.primary.default"
536
+ },
537
+ children: [/* @__PURE__ */jsx(Flex, {
538
+ sx: {
539
+ flex: 1
540
+ },
541
+ children
542
+ }), hasLeftSidebar && /* @__PURE__ */jsx(SidebarOverlay, {
543
+ side: "left",
544
+ open: isLeftSidebarOpen,
545
+ children: /* @__PURE__ */jsx(LeftSidebar, {})
546
+ }), hasRightSidebar && /* @__PURE__ */jsx(SidebarOverlay, {
547
+ side: "right",
548
+ open: isRightSidebarOpen,
549
+ children: /* @__PURE__ */jsx(RightSidebar, {})
550
+ }), hasLeftSidebar && /* @__PURE__ */jsx(OpenLeftSidebarButton, {}), hasRightSidebar && /* @__PURE__ */jsx(OpenRightSidebarButton, {})]
551
+ });
552
+ };
553
+
554
+ //#endregion
555
+ //#region src/GeovisWorkspaceProvider.tsx
556
+ /**
557
+ * Builds the initial selection by reading the `defaultValue` of every menu
558
+ * group in the config. Use it to seed the parent's selection state when
559
+ * controlling the workspace.
560
+ */
561
+ var getInitialSelection = ({
562
+ config
563
+ }) => {
564
+ const selection = {};
565
+ const menus = config.leftSidebar?.menus ?? [];
566
+ for (const menu of menus) selection[menu.id] = menu.defaultValue;
567
+ return selection;
568
+ };
569
+ /**
570
+ * Provides shared state for GeovisWorkspace and all internal components.
571
+ * Manages the per-group selection (controlled or uncontrolled) and the sidebar
572
+ * open state, exposing them via useGeovisWorkspace.
573
+ */
574
+ var GeovisWorkspaceProvider = ({
575
+ children,
576
+ config,
577
+ selection,
578
+ onSelectionChange
579
+ }) => {
580
+ const isControlled = selection !== void 0;
581
+ const [internalSelection, setInternalSelection] = React.useState(() => {
582
+ return getInitialSelection({
583
+ config
584
+ });
585
+ });
586
+ const currentSelection = isControlled ? selection : internalSelection;
587
+ const [isLeftSidebarOpen, setIsLeftSidebarOpen] = React.useState(false);
588
+ const [isRightSidebarOpen, setIsRightSidebarOpen] = React.useState(false);
589
+ const setSelection = React.useCallback(({
590
+ menuId,
591
+ value
592
+ }) => {
593
+ const next = {
594
+ ...currentSelection,
595
+ [menuId]: value
596
+ };
597
+ if (!isControlled) setInternalSelection(next);
598
+ onSelectionChange?.(next);
599
+ }, [currentSelection, isControlled, onSelectionChange]);
600
+ const setLeftSidebarOpen = React.useCallback(({
601
+ open
602
+ }) => {
603
+ setIsLeftSidebarOpen(open);
604
+ }, []);
605
+ const setRightSidebarOpen = React.useCallback(({
606
+ open
607
+ }) => {
608
+ setIsRightSidebarOpen(open);
609
+ }, []);
610
+ const value = React.useMemo(() => {
611
+ return {
612
+ config,
613
+ selection: currentSelection,
614
+ setSelection,
615
+ isLeftSidebarOpen,
616
+ setLeftSidebarOpen,
617
+ isRightSidebarOpen,
618
+ setRightSidebarOpen
619
+ };
620
+ }, [config, currentSelection, setSelection, isLeftSidebarOpen, setLeftSidebarOpen, isRightSidebarOpen, setRightSidebarOpen]);
621
+ return /* @__PURE__ */jsx(GeovisWorkspaceContext.Provider, {
622
+ value,
623
+ children
624
+ });
625
+ };
626
+
627
+ //#endregion
628
+ //#region src/GeovisWorkspace.tsx
629
+ /**
630
+ * Renders the GeoVis map for the workspace inside the main content area. Kept
631
+ * as an internal component so the map fills the layout's main slot and mounts
632
+ * inside the provider tree.
633
+ */
634
+ var GeovisWorkspaceMap = ({
635
+ visualizationSpec
636
+ }) => {
637
+ return /* @__PURE__ */jsx(Box, {
638
+ sx: {
639
+ position: "relative",
640
+ flex: 1,
641
+ display: "flex",
642
+ minHeight: "440px"
643
+ },
644
+ children: /* @__PURE__ */jsx(GeoVisProvider, {
645
+ spec: visualizationSpec,
646
+ children: /* @__PURE__ */jsx(GeoVisCanvas, {
647
+ style: {
648
+ width: "100%",
649
+ height: "100%"
650
+ }
651
+ })
652
+ })
653
+ });
654
+ };
655
+ var GeovisWorkspace = ({
656
+ config,
657
+ visualizationSpec,
658
+ variables,
659
+ onVariableChange
660
+ }) => {
661
+ return /* @__PURE__ */jsx(GeovisWorkspaceProvider, {
662
+ config,
663
+ selection: variables,
664
+ onSelectionChange: onVariableChange,
665
+ children: /* @__PURE__ */jsx(Layout, {
666
+ children: /* @__PURE__ */jsx(GeovisWorkspaceMap, {
667
+ visualizationSpec
668
+ })
669
+ })
670
+ });
671
+ };
672
+
673
+ //#endregion
674
+ export { GeovisWorkspace, GeovisWorkspaceProvider, getInitialSelection, useGeovisWorkspace };