@sybilion/uilib 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/esm/components/ui/Chart/lightweight/LightweightForecastChart.js +25 -10
  2. package/dist/esm/components/ui/ChartAreaInteractive/ChartLines.js +6 -1
  3. package/dist/esm/components/ui/ChartAreaInteractive/overlays/useForecastColor.js +2 -4
  4. package/dist/esm/components/ui/Confirm/Confirm.js +30 -0
  5. package/dist/esm/components/ui/Dialog/Dialog.js +1 -1
  6. package/dist/esm/components/ui/Dialog/Dialog.styl.js +2 -2
  7. package/dist/esm/index.js +1 -0
  8. package/dist/esm/types/src/components/ui/Chart/chartForecastVisualization.types.d.ts +2 -0
  9. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartLines.d.ts +2 -0
  10. package/dist/esm/types/src/components/ui/Confirm/Confirm.d.ts +2 -0
  11. package/dist/esm/types/src/components/ui/Confirm/Confirm.types.d.ts +15 -0
  12. package/dist/esm/types/src/components/ui/Confirm/index.d.ts +2 -0
  13. package/dist/esm/types/src/docs/pages/ConfirmPage.d.ts +1 -0
  14. package/dist/esm/types/src/index.d.ts +1 -0
  15. package/package.json +2 -2
  16. package/src/components/ui/Chart/chartForecastVisualization.types.ts +2 -0
  17. package/src/components/ui/Chart/lightweight/LightweightForecastChart.tsx +31 -10
  18. package/src/components/ui/ChartAreaInteractive/ChartLines.tsx +10 -0
  19. package/src/components/ui/ChartAreaInteractive/overlays/useForecastColor.ts +5 -5
  20. package/src/components/ui/Confirm/Confirm.tsx +78 -0
  21. package/src/components/ui/Confirm/Confirm.types.ts +17 -0
  22. package/src/components/ui/Confirm/index.ts +3 -0
  23. package/src/components/ui/Dialog/Dialog.styl +3 -0
  24. package/src/components/ui/Dialog/Dialog.styl.d.ts +1 -0
  25. package/src/components/ui/Dialog/Dialog.tsx +5 -1
  26. package/src/docs/config/webpack.config.js +28 -17
  27. package/src/docs/pages/ConfirmPage.tsx +63 -0
  28. package/src/docs/pages/LightweightChartPage.tsx +0 -2
  29. package/src/docs/registry.ts +6 -0
  30. package/src/index.ts +1 -0
@@ -6,7 +6,7 @@ import { ChartStyle } from '../components/ChartContainer.js';
6
6
  import { ChartTooltipContent } from '../components/ChartTooltipContent.js';
7
7
  import { CustomChartLegend } from '../components/CustomChartLegend/CustomChartLegend.js';
8
8
  import { formatDate } from '../tools/formatters.js';
9
- import { getForecastColor, getForecastQuantileBandColor } from '../../ChartAreaInteractive/ChartLines.js';
9
+ import { getForecastColor, resolveQuantileBandFillForForecastLine } from '../../ChartAreaInteractive/ChartLines.js';
10
10
  import { Skeleton } from '../../Skeleton/Skeleton.js';
11
11
  import { ensureChartForecastBridge } from '../../../../utils/chartConnectionPoint.js';
12
12
  import { createChart, LineSeries, LineType, LineStyle } from 'lightweight-charts';
@@ -14,6 +14,21 @@ import S from './LightweightForecastChart.styl.js';
14
14
  import { buildLightweightChartOptions, buildHistoricalLineData, buildForecastLineData, buildQuantileBandCustomData, findNearestChartRow } from './lightweightForecastChart.helpers.js';
15
15
  import { QuantileBandPaneView } from './quantileBandCustomSeries.js';
16
16
 
17
+ function forecastIndexForQuantileBand(band, forecastData) {
18
+ if (!forecastData.length)
19
+ return 0;
20
+ if (band.forecastSeriesId != null) {
21
+ const idx = forecastData.findIndex(f => f.id === band.forecastSeriesId);
22
+ if (idx !== -1)
23
+ return idx;
24
+ }
25
+ return 0;
26
+ }
27
+ function quantileBandFillForConfig(band, forecastData) {
28
+ const fi = forecastIndexForQuantileBand(band, forecastData);
29
+ const lineColor = forecastData[fi]?.color?.toString() ?? getForecastColor(fi);
30
+ return resolveQuantileBandFillForForecastLine(lineColor, fi);
31
+ }
17
32
  function clampTooltipTranslate(args) {
18
33
  const { coordinate, viewW, viewH, tooltipWidth: tw, tooltipHeight: th, offset, edgeMargin, } = args;
19
34
  const minX = edgeMargin;
@@ -93,10 +108,10 @@ function LightweightForecastChart(props) {
93
108
  color: f.color?.toString() ?? getForecastColor(index),
94
109
  };
95
110
  });
96
- quantileBands?.forEach((band, index) => {
111
+ quantileBands?.forEach(band => {
97
112
  base[band.key] = {
98
113
  label: band.name,
99
- color: band.color ?? getForecastQuantileBandColor(index),
114
+ color: quantileBandFillForConfig(band, forecastData),
100
115
  };
101
116
  });
102
117
  return base;
@@ -200,8 +215,8 @@ function LightweightForecastChart(props) {
200
215
  }),
201
216
  });
202
217
  const bands = new Map();
203
- quantileBands?.forEach((band, index) => {
204
- const fill = band.color ?? getForecastQuantileBandColor(index);
218
+ quantileBands?.forEach(band => {
219
+ const fill = quantileBandFillForConfig(band, forecastData);
205
220
  const view = new QuantileBandPaneView({
206
221
  fill,
207
222
  stroke: band.strokeWidth ? fill : undefined,
@@ -297,7 +312,7 @@ function LightweightForecastChart(props) {
297
312
  payload: row,
298
313
  });
299
314
  });
300
- bandsCfg?.forEach((band, index) => {
315
+ bandsCfg?.forEach(band => {
301
316
  if (hid.has(band.key))
302
317
  return;
303
318
  const tuple = row[band.key];
@@ -305,7 +320,7 @@ function LightweightForecastChart(props) {
305
320
  tuple.length === 2 &&
306
321
  typeof tuple[0] === 'number' &&
307
322
  typeof tuple[1] === 'number') {
308
- const color = band.color ?? getForecastQuantileBandColor(index);
323
+ const color = quantileBandFillForConfig(band, forecastsList);
309
324
  payload.push({
310
325
  type: 'line',
311
326
  name: band.name,
@@ -366,11 +381,11 @@ function LightweightForecastChart(props) {
366
381
  for (const [key, api] of model.forecasts.entries()) {
367
382
  api.setData(buildForecastLineData(bridgedChartData, key));
368
383
  }
369
- quantileBands?.forEach((band, index) => {
384
+ quantileBands?.forEach(band => {
370
385
  const entry = model.bands.get(band.key);
371
386
  if (!entry)
372
387
  return;
373
- const fill = band.color ?? getForecastQuantileBandColor(index);
388
+ const fill = quantileBandFillForConfig(band, forecastData);
374
389
  entry.view.updateStyle({
375
390
  fill,
376
391
  stroke: band.strokeWidth ? fill : undefined,
@@ -382,7 +397,7 @@ function LightweightForecastChart(props) {
382
397
  entry.api.setData(buildQuantileBandCustomData(bridgedChartData, band.key));
383
398
  });
384
399
  scheduleFitTimeScale(model.chart);
385
- }, [bridgedChartData, quantileBands]);
400
+ }, [bridgedChartData, quantileBands, forecastData]);
386
401
  // Visibility toggles
387
402
  useEffect(() => {
388
403
  const model = modelRef.current;
@@ -17,6 +17,11 @@ const FORECAST_COLORS_MAP = {
17
17
  const FORECAST_LINE_COLORS = Object.keys(FORECAST_COLORS_MAP);
18
18
  const getForecastColor = (index) => FORECAST_LINE_COLORS[index % FORECAST_LINE_COLORS.length];
19
19
  const getForecastQuantileBandColor = (index) => FORECAST_COLORS_MAP[FORECAST_LINE_COLORS[index % FORECAST_LINE_COLORS.length]];
20
+ /** Same tint logic as intervals/threshold overlays: band fill follows forecast line hex when known. */
21
+ function resolveQuantileBandFillForForecastLine(lineColor, forecastIndex) {
22
+ const mapped = FORECAST_COLORS_MAP[lineColor];
23
+ return mapped ?? getForecastQuantileBandColor(forecastIndex);
24
+ }
20
25
  // Memoized component for chart lines - only re-renders when data/analyses/hiddenSeries change
21
26
  const ChartLines = memo(({ chartData, forecastData, hiddenSeries, isDarkTheme, shouldAnimate, historicalLineColor = isDarkTheme ? '#ffffff' : '#000000', showLegend = true, forecastLineStyle = 'dashed', disableHistoricalAnimation = false, }) => {
22
27
  const dotStroke = isDarkTheme ? '#000000' : '#FFFFFF';
@@ -78,4 +83,4 @@ const ChartLines = memo(({ chartData, forecastData, hiddenSeries, isDarkTheme, s
78
83
  });
79
84
  ChartLines.displayName = 'ChartLines';
80
85
 
81
- export { ChartLines, FORECAST_COLORS_MAP, FORECAST_LINE_COLORS, getForecastColor, getForecastQuantileBandColor };
86
+ export { ChartLines, FORECAST_COLORS_MAP, FORECAST_LINE_COLORS, getForecastColor, getForecastQuantileBandColor, resolveQuantileBandFillForForecastLine };
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import { DEFAULT_QUANTILE_BAND_COLOR } from '../../Chart/components/QuantileBands.js';
3
- import { FORECAST_LINE_COLORS, FORECAST_COLORS_MAP, getForecastQuantileBandColor } from '../ChartLines.js';
3
+ import { FORECAST_LINE_COLORS, resolveQuantileBandFillForForecastLine } from '../ChartLines.js';
4
4
 
5
5
  const DEFAULT_FORECAST_COLORS = {
6
6
  lineColor: DEFAULT_QUANTILE_BAND_COLOR,
@@ -20,9 +20,7 @@ function useForecastColor(selectedForecast, forecastData) {
20
20
  const forecastItem = forecastData[forecastIndex];
21
21
  const lineColor = forecastItem.color ||
22
22
  FORECAST_LINE_COLORS[forecastIndex % FORECAST_LINE_COLORS.length];
23
- const quantileBandColor = forecastItem.color
24
- ? FORECAST_COLORS_MAP[forecastItem.color]
25
- : getForecastQuantileBandColor(forecastIndex);
23
+ const quantileBandColor = resolveQuantileBandFillForForecastLine(lineColor, forecastIndex);
26
24
  return {
27
25
  lineColor,
28
26
  quantileBandColor,
@@ -0,0 +1,30 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useRef, useEffect, useCallback } from 'react';
3
+ import { Button } from '../Button/Button.js';
4
+ import { Dialog } from '../Dialog/Dialog.js';
5
+
6
+ function Confirm({ children, open, onOpenChange, onConfirm, onCancel, title = 'Confirm', confirmLabel = 'Confirm', cancelLabel = 'Cancel', trigger, disabled, confirmVariant = 'default', }) {
7
+ const closedByConfirmRef = useRef(false);
8
+ useEffect(() => {
9
+ if (open) {
10
+ closedByConfirmRef.current = false;
11
+ }
12
+ }, [open]);
13
+ const handleOpenChange = useCallback((next) => {
14
+ if (!next) {
15
+ if (!closedByConfirmRef.current) {
16
+ onCancel?.();
17
+ }
18
+ closedByConfirmRef.current = false;
19
+ }
20
+ onOpenChange(next);
21
+ }, [onCancel, onOpenChange]);
22
+ const handleConfirm = () => {
23
+ closedByConfirmRef.current = true;
24
+ onConfirm();
25
+ handleOpenChange(false);
26
+ };
27
+ return (jsx(Dialog, { open: open, onOpenChange: handleOpenChange, disabled: disabled, trigger: trigger, title: title, width: "min(fit-content, calc(100vw - var(--p-4)))", content: children, footerAlignment: "between", footer: jsxs(Fragment, { children: [jsx(Button, { type: "button", variant: "outline", onClick: () => handleOpenChange(false), children: cancelLabel }), jsx(Button, { type: "button", variant: confirmVariant, onClick: handleConfirm, children: confirmLabel })] }) }));
28
+ }
29
+
30
+ export { Confirm };
@@ -118,7 +118,7 @@ function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disabl
118
118
  maxHeight: maxHeight
119
119
  ? `min(${maxHeight}, calc(100% - var(--p-4)))`
120
120
  : null,
121
- }, children: [(title || subtitle || icon) && (jsx(CardHeader, { icon: icon, title: title, description: subtitle })), !disableCloseButton && (jsx("button", { className: S.dialogClose, onClick: handleCloseClick, "aria-label": "Close dialog", type: "button", children: jsx(XIcon, { size: 16 }) })), content && (jsx(CardContent, { className: contentClassName, noScroll: noScroll, autoScrollBottom: autoScrollBottom, children: content })), footer && (jsx(CardFooter, { className: cn(S[`align-${footerAlignment}`], footerClassName), children: footer }))] })] }));
121
+ }, children: [(title || subtitle || icon) && (jsx(CardHeader, { icon: icon, title: title, description: subtitle })), !disableCloseButton && (jsx("button", { className: S.dialogClose, onClick: handleCloseClick, "aria-label": "Close dialog", type: "button", children: jsx(XIcon, { size: 16 }) })), content && (jsx(CardContent, { className: contentClassName, noScroll: noScroll, autoScrollBottom: autoScrollBottom, children: content })), footer && (jsx(CardFooter, { className: cn(S.footer, S[`align-${footerAlignment}`], footerClassName), children: footer }))] })] }));
122
122
  const onTriggerClick = !disabled ? () => onOpenChange(true) : undefined;
123
123
  return (jsxs(Fragment, { children: [trigger && jsx("div", { onClick: onTriggerClick, children: trigger }), open && !disabled && createPortal(dialogContent, document.body)] }));
124
124
  }
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Dialog_dialogOverlay__DE-Lj{backdrop-filter:blur(4px);background-color:var(--background-alpha-700);inset:0;opacity:0;position:fixed;transition:opacity .2s ease-out;z-index:50}.Dialog_dialogOverlay__DE-Lj.Dialog_isOpen__KU869{animation-duration:.2s;animation-fill-mode:forwards;animation-name:Dialog_fadeIn__bMYrw;animation-timing-function:ease-out;opacity:1}.Dialog_dialogContent__9aiOO{border-radius:20px;box-shadow:0 0 0 1px var(--border),0 10px 15px -3px var(--border),0 4px 6px -2px var(--border);display:flex;left:50%;max-height:calc(100% - var(--p-4));opacity:0;position:fixed;top:50%;transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%) scale(.95);transition:all .2s ease-out;width:-moz-fit-content;width:fit-content;z-index:50}.dark .Dialog_dialogContent__9aiOO{box-shadow:0 0 0 1px var(--border),0 10px 15px -3px oklch(0 0 0),0 4px 6px -2px oklch(0 0 0)}.Dialog_dialogContent__9aiOO.Dialog_isOpen__KU869{animation-duration:.2s;animation-fill-mode:forwards;animation-name:Dialog_dialogIn__pAese;animation-timing-function:ease-out;opacity:1;transform:translateX(-50%) translateY(-50%) scale(1)}.Dialog_dialogContent__9aiOO.Dialog_isClosed__iUpMZ{animation-duration:.2s;animation-fill-mode:forwards;animation-name:Dialog_dialogOut__U2hfZ;animation-timing-function:ease-in;opacity:0;pointer-events:none;transform:translateX(-50%) translateY(-50%) scale(.95)}@media (max-width:768px){.Dialog_dialogContent__9aiOO{max-width:calc(100% - var(--p-4));width:calc(100% - var(--p-4))}}.Dialog_size-wide__TgCOo{max-width:calc(100% - var(--p-4))}@media (min-width:768px){.Dialog_size-wide__TgCOo{max-width:80rem}}.Dialog_dialogDescription__vcdnx{color:var(--muted-foreground);font-size:.875rem}.Dialog_dialogClose__v3o4O{align-items:center;background-color:transparent;border:none;border-radius:.125rem;cursor:pointer;display:flex;height:2rem;justify-content:center;opacity:.7;outline:none;position:absolute;right:var(--p-2);top:var(--p-2);transform-origin:center;transition:opacity .2s ease,transform .2s ease;width:2rem;z-index:10}.Dialog_dialogClose__v3o4O:hover{opacity:1;transform:scale(1.1)}.Dialog_dialogClose__v3o4O:focus-visible{box-shadow:0 0 0 2px var(--ring)}.Dialog_dialogClose__v3o4O:disabled{pointer-events:none}.Dialog_dialogContent__9aiOO [data-slot=card-footer]{width:100%}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-left__ml-lG{justify-content:flex-start}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-center__3bzlB{justify-content:center}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-right__uV3uE{justify-content:flex-end}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-between__AFcCs{justify-content:space-between}@keyframes Dialog_fadeIn__bMYrw{0%{opacity:0}to{opacity:1}}@keyframes Dialog_fadeOut__gOjrp{0%{opacity:1}to{opacity:0}}@keyframes Dialog_dialogIn__pAese{0%{opacity:0;transform:translateX(-50%) translateY(-50%) scale(.95)}to{opacity:1;transform:translateX(-50%) translateY(-50%) scale(1)}}@keyframes Dialog_dialogOut__U2hfZ{0%{opacity:1;transform:translateX(-50%) translateY(-50%) scale(1)}to{opacity:0;transform:translateX(-50%) translateY(-50%) scale(.95)}}";
4
- var S = {"dialogOverlay":"Dialog_dialogOverlay__DE-Lj","isOpen":"Dialog_isOpen__KU869","fadeIn":"Dialog_fadeIn__bMYrw","dialogContent":"Dialog_dialogContent__9aiOO","dialogIn":"Dialog_dialogIn__pAese","isClosed":"Dialog_isClosed__iUpMZ","dialogOut":"Dialog_dialogOut__U2hfZ","size-wide":"Dialog_size-wide__TgCOo","dialogDescription":"Dialog_dialogDescription__vcdnx","dialogClose":"Dialog_dialogClose__v3o4O","align-left":"Dialog_align-left__ml-lG","align-center":"Dialog_align-center__3bzlB","align-right":"Dialog_align-right__uV3uE","align-between":"Dialog_align-between__AFcCs","fadeOut":"Dialog_fadeOut__gOjrp"};
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.Dialog_dialogOverlay__DE-Lj{backdrop-filter:blur(4px);background-color:var(--background-alpha-700);inset:0;opacity:0;position:fixed;transition:opacity .2s ease-out;z-index:50}.Dialog_dialogOverlay__DE-Lj.Dialog_isOpen__KU869{animation-duration:.2s;animation-fill-mode:forwards;animation-name:Dialog_fadeIn__bMYrw;animation-timing-function:ease-out;opacity:1}.Dialog_dialogContent__9aiOO{border-radius:20px;box-shadow:0 0 0 1px var(--border),0 10px 15px -3px var(--border),0 4px 6px -2px var(--border);display:flex;left:50%;max-height:calc(100% - var(--p-4));opacity:0;position:fixed;top:50%;transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%) scale(.95);transition:all .2s ease-out;width:-moz-fit-content;width:fit-content;z-index:50}.dark .Dialog_dialogContent__9aiOO{box-shadow:0 0 0 1px var(--border),0 10px 15px -3px oklch(0 0 0),0 4px 6px -2px oklch(0 0 0)}.Dialog_dialogContent__9aiOO.Dialog_isOpen__KU869{animation-duration:.2s;animation-fill-mode:forwards;animation-name:Dialog_dialogIn__pAese;animation-timing-function:ease-out;opacity:1;transform:translateX(-50%) translateY(-50%) scale(1)}.Dialog_dialogContent__9aiOO.Dialog_isClosed__iUpMZ{animation-duration:.2s;animation-fill-mode:forwards;animation-name:Dialog_dialogOut__U2hfZ;animation-timing-function:ease-in;opacity:0;pointer-events:none;transform:translateX(-50%) translateY(-50%) scale(.95)}@media (max-width:768px){.Dialog_dialogContent__9aiOO{max-width:calc(100% - var(--p-4));width:calc(100% - var(--p-4))}}.Dialog_size-wide__TgCOo{max-width:calc(100% - var(--p-4))}@media (min-width:768px){.Dialog_size-wide__TgCOo{max-width:80rem}}.Dialog_dialogDescription__vcdnx{color:var(--muted-foreground);font-size:.875rem}.Dialog_dialogClose__v3o4O{align-items:center;background-color:transparent;border:none;border-radius:.125rem;cursor:pointer;display:flex;height:2rem;justify-content:center;opacity:.7;outline:none;position:absolute;right:var(--p-2);top:var(--p-2);transform-origin:center;transition:opacity .2s ease,transform .2s ease;width:2rem;z-index:10}.Dialog_dialogClose__v3o4O:hover{opacity:1;transform:scale(1.1)}.Dialog_dialogClose__v3o4O:focus-visible{box-shadow:0 0 0 2px var(--ring)}.Dialog_dialogClose__v3o4O:disabled{pointer-events:none}.Dialog_dialogContent__9aiOO [data-slot=card-footer]{width:100%}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-left__ml-lG{justify-content:flex-start}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-center__3bzlB{justify-content:center}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-right__uV3uE{justify-content:flex-end}.Dialog_dialogContent__9aiOO [data-slot=card-footer].Dialog_align-between__AFcCs{justify-content:space-between}.Dialog_footer__7U5vn button:not(:last-child){margin-right:var(--p-2)}@keyframes Dialog_fadeIn__bMYrw{0%{opacity:0}to{opacity:1}}@keyframes Dialog_fadeOut__gOjrp{0%{opacity:1}to{opacity:0}}@keyframes Dialog_dialogIn__pAese{0%{opacity:0;transform:translateX(-50%) translateY(-50%) scale(.95)}to{opacity:1;transform:translateX(-50%) translateY(-50%) scale(1)}}@keyframes Dialog_dialogOut__U2hfZ{0%{opacity:1;transform:translateX(-50%) translateY(-50%) scale(1)}to{opacity:0;transform:translateX(-50%) translateY(-50%) scale(.95)}}";
4
+ var S = {"dialogOverlay":"Dialog_dialogOverlay__DE-Lj","isOpen":"Dialog_isOpen__KU869","fadeIn":"Dialog_fadeIn__bMYrw","dialogContent":"Dialog_dialogContent__9aiOO","dialogIn":"Dialog_dialogIn__pAese","isClosed":"Dialog_isClosed__iUpMZ","dialogOut":"Dialog_dialogOut__U2hfZ","size-wide":"Dialog_size-wide__TgCOo","dialogDescription":"Dialog_dialogDescription__vcdnx","dialogClose":"Dialog_dialogClose__v3o4O","align-left":"Dialog_align-left__ml-lG","align-center":"Dialog_align-center__3bzlB","align-right":"Dialog_align-right__uV3uE","align-between":"Dialog_align-between__AFcCs","footer":"Dialog_footer__7U5vn","fadeOut":"Dialog_fadeOut__gOjrp"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
package/dist/esm/index.js CHANGED
@@ -28,6 +28,7 @@ export { ChatPresets } from './components/ui/Chat/ChatPresets/ChatPresets.js';
28
28
  export { MessageRole } from './components/ui/Chat/Chat.types.js';
29
29
  export { CsvIcon } from './components/icons/CsvIcon/CsvIcon.js';
30
30
  export { Checkbox } from './components/ui/Checkbox/Checkbox.js';
31
+ export { Confirm } from './components/ui/Confirm/Confirm.js';
31
32
  export { Dialog } from './components/ui/Dialog/Dialog.js';
32
33
  export { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from './components/ui/Drawer/Drawer.js';
33
34
  export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from './components/ui/DropdownMenu/DropdownMenu.js';
@@ -72,6 +72,8 @@ export interface ForecastVisualizationRow {
72
72
  }
73
73
  export interface QuantileBandConfig {
74
74
  key: string;
75
+ /** When set, band fill follows this forecast series line color (LW chart). */
76
+ forecastSeriesId?: number;
75
77
  quantiles: [string, string];
76
78
  opacity: number;
77
79
  color?: string;
@@ -32,6 +32,8 @@ export declare const FORECAST_COLORS_MAP: {
32
32
  export declare const FORECAST_LINE_COLORS: string[];
33
33
  export declare const getForecastColor: (index: number) => string;
34
34
  export declare const getForecastQuantileBandColor: (index: number) => any;
35
+ /** Same tint logic as intervals/threshold overlays: band fill follows forecast line hex when known. */
36
+ export declare function resolveQuantileBandFillForForecastLine(lineColor: string, forecastIndex: number): string;
35
37
  export declare const ChartLines: import("react").MemoExoticComponent<({ chartData, forecastData, hiddenSeries, isDarkTheme, shouldAnimate, historicalLineColor, showLegend, forecastLineStyle, disableHistoricalAnimation, }: {
36
38
  chartData: ChartDataPoint[];
37
39
  forecastData?: ForecastItemData[];
@@ -0,0 +1,2 @@
1
+ import type { ConfirmProps } from './Confirm.types';
2
+ export declare function Confirm({ children, open, onOpenChange, onConfirm, onCancel, title, confirmLabel, cancelLabel, trigger, disabled, confirmVariant, }: ConfirmProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ButtonVariant } from '#uilib/components/ui/Button/Button.types';
3
+ export interface ConfirmProps {
4
+ children: ReactNode;
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ onConfirm: () => void;
8
+ onCancel?: () => void;
9
+ title?: ReactNode;
10
+ confirmLabel?: string;
11
+ cancelLabel?: string;
12
+ trigger?: ReactNode;
13
+ disabled?: boolean;
14
+ confirmVariant?: Extract<ButtonVariant, 'default' | 'destructive'>;
15
+ }
@@ -0,0 +1,2 @@
1
+ export { Confirm } from './Confirm';
2
+ export type { ConfirmProps } from './Confirm.types';
@@ -0,0 +1 @@
1
+ export default function ConfirmPage(): import("react/jsx-runtime").JSX.Element;
@@ -17,6 +17,7 @@ export * from './components/ui/Chart';
17
17
  export * from './components/ui/ChartAreaInteractive';
18
18
  export * from './components/ui/Chat';
19
19
  export * from './components/ui/Checkbox';
20
+ export * from './components/ui/Confirm';
20
21
  export * from './components/ui/Dialog';
21
22
  export * from './components/ui/Drawer';
22
23
  export * from './components/ui/DropdownMenu';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -131,7 +131,6 @@
131
131
  },
132
132
  "devDependencies": {
133
133
  "@auth0/auth0-react": "^2.3.1",
134
- "@sybilion/platform-sdk": "file:../platform-sdk",
135
134
  "@babel/core": "^7.20.12",
136
135
  "@babel/preset-typescript": "^7.21.0",
137
136
  "@homecode/ui": "^4.30.6",
@@ -144,6 +143,7 @@
144
143
  "@rollup/plugin-url": "^8.0.2",
145
144
  "@svgr/rollup": "^6.5.1",
146
145
  "@svgr/webpack": "^8.1.0",
146
+ "@sybilion/platform-sdk": "^0.0.4",
147
147
  "@testing-library/dom": "^10.4.1",
148
148
  "@testing-library/jest-dom": "^6.5.0",
149
149
  "@testing-library/react": "^16.0.1",
@@ -78,6 +78,8 @@ export interface ForecastVisualizationRow {
78
78
 
79
79
  export interface QuantileBandConfig {
80
80
  key: string;
81
+ /** When set, band fill follows this forecast series line color (LW chart). */
82
+ forecastSeriesId?: number;
81
83
  quantiles: [string, string];
82
84
  opacity: number;
83
85
  color?: string;
@@ -19,7 +19,7 @@ import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/C
19
19
  import {
20
20
  ForecastItemData,
21
21
  getForecastColor,
22
- getForecastQuantileBandColor,
22
+ resolveQuantileBandFillForForecastLine,
23
23
  } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
24
24
  import { Skeleton } from '#uilib/components/ui/Skeleton';
25
25
  import { ensureChartForecastBridge } from '#uilib/utils/chartConnectionPoint';
@@ -55,6 +55,27 @@ type TooltipRow = {
55
55
  dataKey?: string;
56
56
  };
57
57
 
58
+ function forecastIndexForQuantileBand(
59
+ band: QuantileBandConfig,
60
+ forecastData: ForecastItemData[],
61
+ ): number {
62
+ if (!forecastData.length) return 0;
63
+ if (band.forecastSeriesId != null) {
64
+ const idx = forecastData.findIndex(f => f.id === band.forecastSeriesId);
65
+ if (idx !== -1) return idx;
66
+ }
67
+ return 0;
68
+ }
69
+
70
+ function quantileBandFillForConfig(
71
+ band: QuantileBandConfig,
72
+ forecastData: ForecastItemData[],
73
+ ): string {
74
+ const fi = forecastIndexForQuantileBand(band, forecastData);
75
+ const lineColor = forecastData[fi]?.color?.toString() ?? getForecastColor(fi);
76
+ return resolveQuantileBandFillForForecastLine(lineColor, fi);
77
+ }
78
+
58
79
  function clampTooltipTranslate(args: {
59
80
  coordinate: { x: number; y: number };
60
81
  viewW: number;
@@ -218,10 +239,10 @@ export function LightweightForecastChart(props: LightweightForecastChartProps) {
218
239
  };
219
240
  });
220
241
 
221
- quantileBands?.forEach((band, index) => {
242
+ quantileBands?.forEach(band => {
222
243
  base[band.key] = {
223
244
  label: band.name,
224
- color: band.color ?? getForecastQuantileBandColor(index),
245
+ color: quantileBandFillForConfig(band, forecastData),
225
246
  };
226
247
  });
227
248
 
@@ -353,8 +374,8 @@ export function LightweightForecastChart(props: LightweightForecastChartProps) {
353
374
  { api: ISeriesApi<'Custom'>; view: QuantileBandPaneView }
354
375
  >();
355
376
 
356
- quantileBands?.forEach((band, index) => {
357
- const fill = band.color ?? getForecastQuantileBandColor(index);
377
+ quantileBands?.forEach(band => {
378
+ const fill = quantileBandFillForConfig(band, forecastData);
358
379
  const view = new QuantileBandPaneView({
359
380
  fill,
360
381
  stroke: band.strokeWidth ? fill : undefined,
@@ -460,7 +481,7 @@ export function LightweightForecastChart(props: LightweightForecastChartProps) {
460
481
  });
461
482
  });
462
483
 
463
- bandsCfg?.forEach((band, index) => {
484
+ bandsCfg?.forEach(band => {
464
485
  if (hid.has(band.key)) return;
465
486
  const tuple = row[band.key];
466
487
  if (
@@ -469,7 +490,7 @@ export function LightweightForecastChart(props: LightweightForecastChartProps) {
469
490
  typeof tuple[0] === 'number' &&
470
491
  typeof tuple[1] === 'number'
471
492
  ) {
472
- const color = band.color ?? getForecastQuantileBandColor(index);
493
+ const color = quantileBandFillForConfig(band, forecastsList);
473
494
  payload.push({
474
495
  type: 'line',
475
496
  name: band.name,
@@ -539,10 +560,10 @@ export function LightweightForecastChart(props: LightweightForecastChartProps) {
539
560
  api.setData(buildForecastLineData(bridgedChartData, key));
540
561
  }
541
562
 
542
- quantileBands?.forEach((band, index) => {
563
+ quantileBands?.forEach(band => {
543
564
  const entry = model.bands.get(band.key);
544
565
  if (!entry) return;
545
- const fill = band.color ?? getForecastQuantileBandColor(index);
566
+ const fill = quantileBandFillForConfig(band, forecastData);
546
567
  entry.view.updateStyle({
547
568
  fill,
548
569
  stroke: band.strokeWidth ? fill : undefined,
@@ -557,7 +578,7 @@ export function LightweightForecastChart(props: LightweightForecastChartProps) {
557
578
  });
558
579
 
559
580
  scheduleFitTimeScale(model.chart);
560
- }, [bridgedChartData, quantileBands]);
581
+ }, [bridgedChartData, quantileBands, forecastData]);
561
582
 
562
583
  // Visibility toggles
563
584
  useEffect(() => {
@@ -52,6 +52,16 @@ export const getForecastQuantileBandColor = (index: number) =>
52
52
  FORECAST_LINE_COLORS[index % FORECAST_LINE_COLORS.length]
53
53
  ];
54
54
 
55
+ /** Same tint logic as intervals/threshold overlays: band fill follows forecast line hex when known. */
56
+ export function resolveQuantileBandFillForForecastLine(
57
+ lineColor: string,
58
+ forecastIndex: number,
59
+ ): string {
60
+ const mapped =
61
+ FORECAST_COLORS_MAP[lineColor as keyof typeof FORECAST_COLORS_MAP];
62
+ return mapped ?? getForecastQuantileBandColor(forecastIndex);
63
+ }
64
+
55
65
  // Memoized component for chart lines - only re-renders when data/analyses/hiddenSeries change
56
66
  export const ChartLines = memo(
57
67
  ({
@@ -4,10 +4,9 @@ import { DEFAULT_QUANTILE_BAND_COLOR } from '#uilib/components/ui/Chart/componen
4
4
 
5
5
  import { Analysis } from '../ChartAreaInteractive.types';
6
6
  import {
7
- FORECAST_COLORS_MAP,
8
7
  FORECAST_LINE_COLORS,
9
8
  ForecastItemData,
10
- getForecastQuantileBandColor,
9
+ resolveQuantileBandFillForForecastLine,
11
10
  } from '../ChartLines';
12
11
 
13
12
  const DEFAULT_FORECAST_COLORS = {
@@ -37,9 +36,10 @@ export function useForecastColor(
37
36
  forecastItem.color ||
38
37
  FORECAST_LINE_COLORS[forecastIndex % FORECAST_LINE_COLORS.length];
39
38
 
40
- const quantileBandColor = forecastItem.color
41
- ? FORECAST_COLORS_MAP[forecastItem.color]
42
- : getForecastQuantileBandColor(forecastIndex);
39
+ const quantileBandColor = resolveQuantileBandFillForForecastLine(
40
+ lineColor,
41
+ forecastIndex,
42
+ );
43
43
 
44
44
  return {
45
45
  lineColor,
@@ -0,0 +1,78 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+
3
+ import { Button } from '#uilib/components/ui/Button';
4
+ import { Dialog } from '#uilib/components/ui/Dialog';
5
+
6
+ import type { ConfirmProps } from './Confirm.types';
7
+
8
+ export function Confirm({
9
+ children,
10
+ open,
11
+ onOpenChange,
12
+ onConfirm,
13
+ onCancel,
14
+ title = 'Confirm',
15
+ confirmLabel = 'Confirm',
16
+ cancelLabel = 'Cancel',
17
+ trigger,
18
+ disabled,
19
+ confirmVariant = 'default',
20
+ }: ConfirmProps) {
21
+ const closedByConfirmRef = useRef(false);
22
+
23
+ useEffect(() => {
24
+ if (open) {
25
+ closedByConfirmRef.current = false;
26
+ }
27
+ }, [open]);
28
+
29
+ const handleOpenChange = useCallback(
30
+ (next: boolean) => {
31
+ if (!next) {
32
+ if (!closedByConfirmRef.current) {
33
+ onCancel?.();
34
+ }
35
+ closedByConfirmRef.current = false;
36
+ }
37
+ onOpenChange(next);
38
+ },
39
+ [onCancel, onOpenChange],
40
+ );
41
+
42
+ const handleConfirm = () => {
43
+ closedByConfirmRef.current = true;
44
+ onConfirm();
45
+ handleOpenChange(false);
46
+ };
47
+
48
+ return (
49
+ <Dialog
50
+ open={open}
51
+ onOpenChange={handleOpenChange}
52
+ disabled={disabled}
53
+ trigger={trigger}
54
+ title={title}
55
+ width="min(fit-content, calc(100vw - var(--p-4)))"
56
+ content={children}
57
+ footerAlignment="between"
58
+ footer={
59
+ <>
60
+ <Button
61
+ type="button"
62
+ variant="outline"
63
+ onClick={() => handleOpenChange(false)}
64
+ >
65
+ {cancelLabel}
66
+ </Button>
67
+ <Button
68
+ type="button"
69
+ variant={confirmVariant}
70
+ onClick={handleConfirm}
71
+ >
72
+ {confirmLabel}
73
+ </Button>
74
+ </>
75
+ }
76
+ />
77
+ );
78
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type { ButtonVariant } from '#uilib/components/ui/Button/Button.types';
4
+
5
+ export interface ConfirmProps {
6
+ children: ReactNode;
7
+ open: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ onConfirm: () => void;
10
+ onCancel?: () => void;
11
+ title?: ReactNode;
12
+ confirmLabel?: string;
13
+ cancelLabel?: string;
14
+ trigger?: ReactNode;
15
+ disabled?: boolean;
16
+ confirmVariant?: Extract<ButtonVariant, 'default' | 'destructive'>;
17
+ }
@@ -0,0 +1,3 @@
1
+ export { Confirm } from './Confirm';
2
+
3
+ export type { ConfirmProps } from './Confirm.types';
@@ -143,3 +143,6 @@
143
143
  justify-content flex-end
144
144
  &.align-between
145
145
  justify-content space-between
146
+
147
+ .footer button:not(:last-child)
148
+ margin-right var(--p-2)
@@ -13,6 +13,7 @@ interface CssExports {
13
13
  'dialogOverlay': string;
14
14
  'fadeIn': string;
15
15
  'fadeOut': string;
16
+ 'footer': string;
16
17
  'isClosed': string;
17
18
  'isOpen': string;
18
19
  'size-wide': string;
@@ -210,7 +210,11 @@ export function Dialog({
210
210
 
211
211
  {footer && (
212
212
  <CardFooter
213
- className={cn(S[`align-${footerAlignment}`], footerClassName)}
213
+ className={cn(
214
+ S.footer,
215
+ S[`align-${footerAlignment}`],
216
+ footerClassName,
217
+ )}
214
218
  >
215
219
  {footer}
216
220
  </CardFooter>
@@ -52,6 +52,28 @@ export default (env, argv) => {
52
52
  alias.theme = themeStyl;
53
53
  }
54
54
 
55
+ const docsHtmlMinify = isDev
56
+ ? false
57
+ : {
58
+ removeComments: true,
59
+ collapseWhitespace: true,
60
+ removeRedundantAttributes: true,
61
+ useShortDoctype: true,
62
+ removeEmptyAttributes: true,
63
+ removeStyleLinkTypeAttributes: true,
64
+ keepClosingSlash: true,
65
+ minifyJS: true,
66
+ minifyCSS: true,
67
+ minifyURLs: true,
68
+ };
69
+
70
+ const docsHtmlPluginBase = {
71
+ lang: 'en',
72
+ baseUrl: publicPath,
73
+ template: `${paths.assets}/index.html`,
74
+ minify: docsHtmlMinify,
75
+ };
76
+
55
77
  const config = {
56
78
  entry: [paths.docs],
57
79
  output: {
@@ -193,24 +215,13 @@ export default (env, argv) => {
193
215
  }),
194
216
 
195
217
  new HtmlWebpackPlugin({
196
- lang: 'en',
197
- baseUrl: publicPath,
218
+ ...docsHtmlPluginBase,
198
219
  filename: 'index.html',
199
- template: `${paths.assets}/index.html`,
200
- minify: isDev
201
- ? false
202
- : {
203
- removeComments: true,
204
- collapseWhitespace: true,
205
- removeRedundantAttributes: true,
206
- useShortDoctype: true,
207
- removeEmptyAttributes: true,
208
- removeStyleLinkTypeAttributes: true,
209
- keepClosingSlash: true,
210
- minifyJS: true,
211
- minifyCSS: true,
212
- minifyURLs: true,
213
- },
220
+ }),
221
+ /** GitHub Pages serves this for missing paths; same shell as index.html so SPA routes work. */
222
+ new HtmlWebpackPlugin({
223
+ ...docsHtmlPluginBase,
224
+ filename: '404.html',
214
225
  }),
215
226
  existsSync(logoSvgPath) &&
216
227
  new FaviconWebpackPlugin({
@@ -0,0 +1,63 @@
1
+ import { useState } from 'react';
2
+
3
+ import { Button } from '#uilib/components/ui/Button';
4
+ import { Confirm } from '#uilib/components/ui/Confirm';
5
+ import { PageContentSection } from '#uilib/components/ui/Page';
6
+
7
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
8
+ import { DocsHeaderActions } from '../docsHeaderActions';
9
+
10
+ export default function ConfirmPage() {
11
+ const [openDefault, setOpenDefault] = useState(false);
12
+ const [openDestructive, setOpenDestructive] = useState(false);
13
+
14
+ return (
15
+ <>
16
+ <AppPageHeader
17
+ breadcrumbs={[{ label: 'Confirm' }]}
18
+ title="Confirm"
19
+ subheader="Confirmation dialog built on Dialog with primary and cancel actions."
20
+ actions={<DocsHeaderActions />}
21
+ />
22
+ <PageContentSection>
23
+ <div
24
+ style={{
25
+ display: 'flex',
26
+ flexWrap: 'wrap',
27
+ gap: 'var(--p-4)',
28
+ alignItems: 'center',
29
+ }}
30
+ >
31
+ <Confirm
32
+ open={openDefault}
33
+ onOpenChange={setOpenDefault}
34
+ trigger={<Button type="button">Open confirm</Button>}
35
+ title="Discard changes?"
36
+ confirmLabel="Discard"
37
+ cancelLabel="Keep editing"
38
+ onConfirm={() => setOpenDefault(false)}
39
+ >
40
+ You have unsaved changes. They will be lost if you continue.
41
+ </Confirm>
42
+
43
+ <Confirm
44
+ open={openDestructive}
45
+ onOpenChange={setOpenDestructive}
46
+ trigger={
47
+ <Button type="button" variant="destructive">
48
+ Delete item
49
+ </Button>
50
+ }
51
+ title="Delete item?"
52
+ confirmLabel="Delete"
53
+ cancelLabel="Cancel"
54
+ confirmVariant="destructive"
55
+ onConfirm={() => setOpenDestructive(false)}
56
+ >
57
+ This action cannot be undone.
58
+ </Confirm>
59
+ </div>
60
+ </PageContentSection>
61
+ </>
62
+ );
63
+ }
@@ -85,7 +85,6 @@ const DEMO_QUANTILE_BANDS: QuantileBandConfig[] = [
85
85
  opacity: 0.35,
86
86
  name: 'P10–P90',
87
87
  type: 'monotone',
88
- color: 'rgba(100,190,220,0.35)',
89
88
  strokeWidth: 0,
90
89
  },
91
90
  {
@@ -94,7 +93,6 @@ const DEMO_QUANTILE_BANDS: QuantileBandConfig[] = [
94
93
  opacity: 0.45,
95
94
  name: 'P25–P75',
96
95
  type: 'monotone',
97
- color: 'rgba(65,165,238,0.45)',
98
96
  strokeWidth: 0,
99
97
  },
100
98
  ];
@@ -121,6 +121,12 @@ export const DOC_REGISTRY: DocEntry[] = [
121
121
  section: 'Overlays',
122
122
  load: () => import('./pages/DialogPage'),
123
123
  },
124
+ {
125
+ slug: 'confirm',
126
+ title: 'Confirm',
127
+ section: 'Overlays',
128
+ load: () => import('./pages/ConfirmPage'),
129
+ },
124
130
  {
125
131
  slug: 'driver-map',
126
132
  title: 'DriverMap & DriverCard',
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export * from './components/ui/Chart';
17
17
  export * from './components/ui/ChartAreaInteractive';
18
18
  export * from './components/ui/Chat';
19
19
  export * from './components/ui/Checkbox';
20
+ export * from './components/ui/Confirm';
20
21
  export * from './components/ui/Dialog';
21
22
  export * from './components/ui/Drawer';
22
23
  export * from './components/ui/DropdownMenu';