@atmos.build/ui 0.1.3 → 0.1.5

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 (43) hide show
  1. package/README.md +6 -6
  2. package/dist/components/calendar.js +6 -5
  3. package/dist/components/charts/bar-chart.js +4 -2
  4. package/dist/components/charts/colors.js +1 -1
  5. package/dist/components/charts/index.d.ts +2 -2
  6. package/dist/components/charts/index.js +1 -1
  7. package/dist/components/charts/layout.d.ts +2 -1
  8. package/dist/components/charts/layout.js +15 -8
  9. package/dist/components/charts/pie-chart.d.ts +10 -1
  10. package/dist/components/charts/pie-chart.js +34 -9
  11. package/dist/components/code-editor-theme.d.ts +1 -1
  12. package/dist/components/code-editor-theme.js +1 -1
  13. package/dist/components/date-time-picker.js +13 -4
  14. package/dist/components/inline-alert.d.ts +6 -1
  15. package/dist/components/inline-alert.js +20 -3
  16. package/dist/components/metric-grid.d.ts +5 -1
  17. package/dist/components/metric-grid.js +19 -8
  18. package/dist/components/property-selector.js +9 -29
  19. package/dist/components/shimmer-text.js +1 -1
  20. package/dist/components/time-column.js +3 -1
  21. package/dist/components/usage-bar.d.ts +3 -1
  22. package/dist/components/usage-bar.js +5 -3
  23. package/dist/flow/editor/adapter.d.ts +1 -1
  24. package/dist/flow/editor/flow-editor.d.ts +2 -2
  25. package/dist/flow/editor/flow-editor.js +4 -4
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +2 -1
  28. package/dist/lib/scroll-lock-escape.d.ts +9 -0
  29. package/dist/lib/scroll-lock-escape.js +22 -0
  30. package/dist/mcp-app/checkout-version.d.ts +14 -0
  31. package/dist/mcp-app/checkout-version.js +19 -0
  32. package/dist/mcp-app/index.d.ts +4 -0
  33. package/dist/mcp-app/index.js +3 -0
  34. package/dist/mcp-app/resource-protocol.d.ts +5 -0
  35. package/dist/mcp-app/resource-protocol.js +13 -0
  36. package/dist/mcp-app/typed-schema.d.ts +28 -0
  37. package/dist/mcp-app/typed-schema.js +1 -0
  38. package/dist/mcp-app/use-mcp-resource.d.ts +11 -0
  39. package/dist/mcp-app/use-mcp-resource.js +206 -0
  40. package/dist/mcp-app/use-mcp-tool.d.ts +9 -0
  41. package/dist/mcp-app/use-mcp-tool.js +73 -0
  42. package/dist/mcp-app/vite.js +1 -1
  43. package/package.json +2 -1
package/README.md CHANGED
@@ -13,30 +13,30 @@ import { Button, Command, Dialog, DialogContent, Table } from '@atmos/ui'
13
13
  MCP-specific host integration is available from a subpath of the same package:
14
14
 
15
15
  ```tsx
16
- import { useAtmosMcpApp } from '@atmos/ui/mcp-app'
16
+ import { useAtmosMcpApp } from '@atmos.build/ui/mcp-app'
17
17
  ```
18
18
 
19
19
  The build configuration and stylesheet use their own subpaths:
20
20
 
21
21
  ```ts
22
- import { atmosMcpAppViteConfig } from '@atmos/ui/mcp-app/vite'
22
+ import { atmosMcpAppViteConfig } from '@atmos.build/ui/mcp-app/vite'
23
23
  ```
24
24
 
25
25
  ```css
26
- @import '@atmos/ui/mcp-app/styles.css';
26
+ @import '@atmos.build/ui/mcp-app/styles.css';
27
27
 
28
28
  @source './**/*.{ts,tsx}';
29
29
  ```
30
30
 
31
- The stylesheet scans the complete component catalog, so any component exported by `@atmos/ui` receives its normal Tailwind styles without maintaining a second MCP-specific allowlist.
31
+ The stylesheet scans the complete component catalog, so any component exported by `@atmos.build/ui` receives its normal Tailwind styles without maintaining a second MCP-specific allowlist.
32
32
 
33
33
  Inside this repository, extension UI packages resolve `@atmos/ui` through the pnpm workspace. External extensions install an exact public npm version during authoring:
34
34
 
35
35
  ```sh
36
- npm install --save-exact @atmos/ui react@19 react-dom@19
36
+ npm install --save-exact @atmos.build/ui react@19 react-dom@19
37
37
  ```
38
38
 
39
- The compiled `dist/app.js` and `dist/app.css` are committed with the extension and inlined into its MCP App resource. Attaching or opening an extension therefore never installs packages or executes a package manager.
39
+ The compiled `app-build/app.js` and `app-build/app.css` are committed with the extension and inlined into its MCP App resource. Attaching or opening an extension therefore never installs packages or executes a package manager.
40
40
 
41
41
  Flow surfaces import their stylesheet explicitly so the JavaScript entry remains server-rendering compatible:
42
42
 
@@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react';
5
5
  import { LayoutGroup, motion, useReducedMotion } from 'motion/react';
6
6
  import { Button } from './button.js';
7
7
  import { cn } from '../lib/cn.js';
8
- import { parseLooseDate } from "../lib/parse-loose.js";
8
+ import { parseLooseDate } from '../lib/parse-loose.js';
9
9
  import { resolveDateTimeFormat } from '../lib/date-time-format.js';
10
10
  const DEFAULT_LABELS = {
11
11
  aria: 'Date picker',
@@ -136,13 +136,14 @@ export function Calendar({ value, onSelect, selectionMode = 'day', locale, forma
136
136
  (selectionMode === 'week' ? sameWeek(d, previewDate) : sameDay(d, previewDate));
137
137
  const hostsSelection = isSelected && (selectionMode === 'week' ? isWeekStart : true);
138
138
  const isDisabled = isBeforeMin(d);
139
- return (_jsxs("button", { type: "button", onClick: () => handlePick(d), disabled: isDisabled, "aria-disabled": isDisabled || undefined, "aria-pressed": isSelected || undefined, "aria-label": d.toDateString(), className: cn('relative h-7 rounded-md text-[12px] tabular-nums transition-colors', 'hover:bg-accent hover:text-accent-foreground cursor-pointer', 'focus-visible:ring-ring/40 focus-visible:ring-2 focus-visible:outline-none', isToday && 'rounded-lg ring-1 ring-foreground/40', inMonth ? 'text-foreground/90' : 'text-muted-foreground/45', isPreview && 'bg-primary/10', isSelected &&
139
+ return (_jsxs("button", { type: "button", onClick: () => handlePick(d), disabled: isDisabled, "aria-disabled": isDisabled || undefined, "aria-pressed": isSelected || undefined, "aria-label": d.toDateString(), className: cn('relative h-7 rounded-md text-[12px] tabular-nums transition-colors', 'hover:bg-accent hover:text-accent-foreground cursor-pointer', 'focus-visible:ring-ring/40 focus-visible:ring-2 focus-visible:outline-none', isToday && 'ring-foreground/40 rounded-lg ring-1', inMonth ? 'text-foreground/90' : 'text-muted-foreground/45', isPreview && 'bg-primary/10', isSelected &&
140
140
  (selectionMode === 'week'
141
- ? 'text-foreground hover:bg-transparent hover:text-foreground'
142
- : 'text-primary-foreground hover:text-primary-foreground'), selectionMode === 'week' && (isSelected || isPreview) && 'rounded-none', (isSelected || isPreview) && isWeekStart && 'rounded-l-md', (isSelected || isPreview) && isWeekEnd && 'rounded-r-md', isDisabled && 'pointer-events-none cursor-default opacity-30 hover:bg-transparent'), children: [hostsSelection ? (_jsx(motion.div, { layoutId: "calendar-selection", "aria-hidden": true, "data-testid": "calendar-selection", transition: selectionTransition, className: cn('pointer-events-none absolute rounded-md', selectionMode === 'week'
141
+ ? 'text-foreground hover:text-foreground hover:bg-transparent'
142
+ : 'text-primary-foreground hover:text-primary-foreground'), selectionMode === 'week' && (isSelected || isPreview) && 'rounded-none', (isSelected || isPreview) && isWeekStart && 'rounded-l-md', (isSelected || isPreview) && isWeekEnd && 'rounded-r-md', isDisabled &&
143
+ 'pointer-events-none cursor-default opacity-30 hover:bg-transparent'), children: [hostsSelection ? (_jsx(motion.div, { layoutId: "calendar-selection", "aria-hidden": true, "data-testid": "calendar-selection", transition: selectionTransition, className: cn('pointer-events-none absolute rounded-md', selectionMode === 'week'
143
144
  ? 'bg-primary/15 inset-y-0 left-0 w-[700%]'
144
145
  : 'bg-primary inset-0') })) : null, _jsx("span", { className: "relative z-10", children: d.getDate() })] }, d.toISOString()));
145
- }) }) }), quickActions ? (_jsxs("div", { className: "mt-2 flex items-center gap-1 px-0.5", children: [_jsx(Button, { type: "button", size: "xs", variant: "ghost", onClick: () => onSelect(ymd(today)), children: t.today }), _jsx(Button, { type: "button", size: "xs", variant: "ghost", onClick: () => {
146
+ }) }) }), quickActions ? (_jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-1 px-0.5", children: [_jsx(Button, { type: "button", size: "xs", variant: "ghost", onClick: () => onSelect(ymd(today)), children: t.today }), _jsx(Button, { type: "button", size: "xs", variant: "ghost", onClick: () => {
146
147
  const tomorrow = new Date(today);
147
148
  tomorrow.setDate(today.getDate() + 1);
148
149
  onSelect(ymd(tomorrow));
@@ -11,7 +11,7 @@ export function BarChart(props) {
11
11
  const barW = stacked
12
12
  ? Math.max(7, Math.min(36, groupW * 0.54))
13
13
  : Math.max(5, Math.min(28, groupW / Math.max(1, yKeys.length) - 5));
14
- const cornerRadius = Math.min(8, barW / 2);
14
+ const cornerRadius = Math.min(4, barW / 2);
15
15
  const { state: hoverState, move, clear } = useCartesianHover(metrics, data.length, true);
16
16
  const hoveredRow = hoverState ? data[hoverState.index] : undefined;
17
17
  return (_jsxs("div", { className: "relative h-full w-full", children: [_jsxs("svg", { viewBox: `0 0 ${metrics.w} ${metrics.h}`, className: cn('h-full w-full overflow-visible', className), role: "img", "aria-label": props.ariaLabel, onPointerMove: move, onPointerLeave: clear, children: [_jsx(CartesianGrid, { ...metrics, data: data, xKey: xKey, banded: true }), hoverState ? (_jsx("rect", { x: metrics.pad.l + hoverState.index * groupW, y: metrics.pad.t, width: groupW, height: metrics.innerH, fill: "currentColor", opacity: 0.05, "aria-hidden": true })) : null, data.map((row, rowIndex) => {
@@ -33,7 +33,9 @@ export function BarChart(props) {
33
33
  if (stacked && keyIndex === topStackIndex && height > 0) {
34
34
  return (_jsx("path", { d: topRoundedRectPath(barX, barY, barW, height, cornerRadius), fill: color, opacity: opacity, className: "animate-in fade-in slide-in-from-bottom-1 duration-300 hover:opacity-90" }, `${rowIndex}:${key}`));
35
35
  }
36
- return (_jsx("rect", { x: barX, y: barY, width: barW, height: height, rx: stacked ? 0 : cornerRadius, fill: color, opacity: opacity, className: "animate-in fade-in slide-in-from-bottom-1 duration-300 hover:opacity-90" }, `${rowIndex}:${key}`));
36
+ return (_jsx("path", { d: stacked
37
+ ? `M ${barX} ${barY} h ${barW} v ${height} h ${-barW} Z`
38
+ : topRoundedRectPath(barX, barY, barW, height, cornerRadius), fill: color, opacity: opacity, className: "animate-in fade-in slide-in-from-bottom-1 duration-300 hover:opacity-90" }, `${rowIndex}:${key}`));
37
39
  });
38
40
  })] }), hoverState && hoveredRow ? (_jsx(ChartTooltip, { state: hoverState, row: hoveredRow, xKey: xKey, yKeys: yKeys, colors: colors, format: format })) : null] }));
39
41
  }
@@ -18,7 +18,7 @@ export function chartColor(colors, index) {
18
18
  const SAFE_CSS_COLOR_FUNCTION = /^(?:rgb|rgba|hsl|hsla|oklab|oklch)\([\d\s.,%/+-]+\)$/i;
19
19
  const SAFE_HEX_COLOR = /^#[\da-f]{3,8}$/i;
20
20
  const SAFE_COLOR_KEYWORD = /^(?:black|white|transparent|currentColor)$/i;
21
- const SAFE_INTERNAL_COLOR_TOKEN = /^var\(--(?:primary|foreground|muted-foreground|border|ring|destructive|background|category-[1-6]|(?:success|warning|danger|info)-(?:3|9|11))\)$/i;
21
+ const SAFE_INTERNAL_COLOR_TOKEN = /^var\(--(?:primary|foreground|muted-foreground|border|ring|destructive|background|category-[1-6]|usage|usage-strong|(?:success|warning|danger|info)-(?:3|9|11))\)$/i;
22
22
  function safeChartColor(color) {
23
23
  const value = color?.trim();
24
24
  if (!value || value.length > 96 || hasControlCharacter(value))
@@ -2,9 +2,9 @@ export { AreaChart } from './area-chart.js';
2
2
  export { BarChart } from './bar-chart.js';
3
3
  export { ChartLegend } from './legend.js';
4
4
  export { LineChart } from './line-chart.js';
5
- export { PieChart } from './pie-chart.js';
5
+ export { PieChart, type PieChartProps } from './pie-chart.js';
6
6
  export { RadarChart } from './radar-chart.js';
7
7
  export { RadialChart } from './radial-chart.js';
8
8
  export { ScatterChart } from './scatter-chart.js';
9
- export { chartColor } from './colors.js';
9
+ export { CHART_COLORS, chartColor } from './colors.js';
10
10
  export type { BarChartProps, CartesianChartProps, CategoricalChartProps, ChartAxisOptions, ChartDatum, ChartLegendItem, ScatterChartProps, } from './types.js';
@@ -6,4 +6,4 @@ export { PieChart } from './pie-chart.js';
6
6
  export { RadarChart } from './radar-chart.js';
7
7
  export { RadialChart } from './radial-chart.js';
8
8
  export { ScatterChart } from './scatter-chart.js';
9
- export { chartColor } from './colors.js';
9
+ export { CHART_COLORS, chartColor } from './colors.js';
@@ -24,12 +24,13 @@ export declare function CartesianGrid({ data, xKey, w, h, pad, innerW, innerH, m
24
24
  xKey: string;
25
25
  banded?: boolean;
26
26
  }): import("react").JSX.Element;
27
- export declare function categoricalMetrics(height: number): {
27
+ export declare function categoricalMetrics(height: number, layout?: 'wide' | 'square'): {
28
28
  w: number;
29
29
  h: number;
30
30
  cx: number;
31
31
  cy: number;
32
32
  r: number;
33
33
  };
34
+ export declare function donutPath(cx: number, cy: number, rOuter: number, rInner: number, start: number, end: number): string;
34
35
  export declare function piePath(cx: number, cy: number, r: number, start: number, end: number): string;
35
36
  export declare function ringArcPath(cx: number, cy: number, r: number, start: number, end: number): string;
@@ -1,15 +1,16 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  export function cartesianMetrics(props, stacked) {
3
3
  const w = 720;
4
- const h = 220;
4
+ const h = Math.max(64, Math.min(props.height ?? 220, 720));
5
5
  const axis = props.axis;
6
6
  const showXAxis = axis?.showXAxis ?? true;
7
7
  const showYAxis = axis?.showYAxis ?? true;
8
+ const bare = !showXAxis && !showYAxis;
8
9
  const pad = {
9
- l: showYAxis ? (axis?.yLabel ? 62 : 44) : 20,
10
- r: 16,
11
- t: 14,
12
- b: showXAxis ? (axis?.xLabel ? 46 : 32) : 16,
10
+ l: showYAxis ? (axis?.yLabel ? 62 : 44) : bare ? 2 : 20,
11
+ r: bare ? 2 : 16,
12
+ t: bare ? 4 : 14,
13
+ b: showXAxis ? (axis?.xLabel ? 46 : 32) : bare ? 2 : 16,
13
14
  };
14
15
  const innerW = w - pad.l - pad.r;
15
16
  const innerH = h - pad.t - pad.b;
@@ -72,17 +73,23 @@ export function CartesianGrid({ data, xKey, w, h, pad, innerW, innerH, max, axis
72
73
  return (_jsx("text", { x: xPosition(index), y: h - (axis?.xLabel ? 24 : 10), textAnchor: "middle", fill: "currentColor", opacity: 0.58, fontSize: "9", children: String(row[xKey] ?? index + 1) }, `x-${index}`));
73
74
  }) })) : null, showXAxis && axis?.xLabel ? (_jsx("text", { x: pad.l + innerW / 2, y: h - 6, textAnchor: "middle", fill: "currentColor", opacity: 0.58, fontSize: "10", children: axis.xLabel })) : null, showYAxis && axis?.yLabel ? (_jsx("text", { x: 16, y: pad.t + innerH / 2, textAnchor: "middle", transform: `rotate(-90 16 ${pad.t + innerH / 2})`, fill: "currentColor", opacity: 0.58, fontSize: "10", children: axis.yLabel })) : null] }));
74
75
  }
75
- export function categoricalMetrics(height) {
76
- const w = 720;
76
+ export function categoricalMetrics(height, layout = 'wide') {
77
77
  const h = Math.max(170, Math.min(height, 420));
78
+ const w = layout === 'square' ? h : 720;
78
79
  return {
79
80
  w,
80
81
  h,
81
82
  cx: w / 2,
82
83
  cy: h / 2,
83
- r: Math.min(82, h / 2 - 22),
84
+ r: layout === 'square' ? h / 2 - 4 : Math.min(82, h / 2 - 22),
84
85
  };
85
86
  }
87
+ export function donutPath(cx, cy, rOuter, rInner, start, end) {
88
+ const large = end - start > Math.PI ? 1 : 0;
89
+ const point = (r, angle) => `${(cx + Math.cos(angle) * r).toFixed(2)} ${(cy + Math.sin(angle) * r).toFixed(2)}`;
90
+ return (`M ${point(rOuter, start)} A ${rOuter} ${rOuter} 0 ${large} 1 ${point(rOuter, end)} ` +
91
+ `L ${point(rInner, end)} A ${rInner} ${rInner} 0 ${large} 0 ${point(rInner, start)} Z`);
92
+ }
86
93
  export function piePath(cx, cy, r, start, end) {
87
94
  const large = end - start > Math.PI ? 1 : 0;
88
95
  const x1 = cx + Math.cos(start) * r;
@@ -1,2 +1,11 @@
1
+ import * as React from 'react';
1
2
  import type { CategoricalChartProps } from './types.js';
2
- export declare function PieChart({ data, categoryKey, valueKey, colors, height, ariaLabel, valueFormatter, className, }: CategoricalChartProps): import("react").JSX.Element;
3
+ export interface PieChartProps extends CategoricalChartProps {
4
+ innerRadius?: number;
5
+ center?: React.ReactNode;
6
+ activeKey?: string | null;
7
+ onActiveChange?: (key: string | null) => void;
8
+ layout?: 'wide' | 'square';
9
+ categoryFormatter?: (category: string) => string;
10
+ }
11
+ export declare function PieChart({ data, categoryKey, valueKey, colors, height, ariaLabel, valueFormatter, className, innerRadius, center, activeKey, onActiveChange, layout, categoryFormatter, }: PieChartProps): React.JSX.Element;
@@ -1,9 +1,12 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from 'react';
2
4
  import { cn } from '../../lib/cn.js';
3
5
  import { chartColor } from './colors.js';
4
- import { categoricalMetrics, piePath } from './layout.js';
5
- export function PieChart({ data, categoryKey = 'label', valueKey = 'value', colors, height = 220, ariaLabel, valueFormatter, className, }) {
6
- const { w, h, cx, cy, r } = categoricalMetrics(height);
6
+ import { categoricalMetrics, donutPath, piePath } from './layout.js';
7
+ export function PieChart({ data, categoryKey = 'label', valueKey = 'value', colors, height = 220, ariaLabel, valueFormatter, className, innerRadius = 0.42, center, activeKey = null, onActiveChange, layout = 'wide', categoryFormatter, }) {
8
+ const { w, h, cx, cy, r } = categoricalMetrics(height, layout);
9
+ const hole = Math.max(0, Math.min(0.9, innerRadius)) * r;
7
10
  const total = data.reduce((sum, row) => sum + Math.max(0, Number(row[valueKey] ?? 0)), 0) || 1;
8
11
  const format = valueFormatter ?? ((value) => String(value));
9
12
  const slices = data.reduce((acc, row, index) => {
@@ -13,10 +16,32 @@ export function PieChart({ data, categoryKey = 'label', valueKey = 'value', colo
13
16
  acc.push({ row, value, start, end, index });
14
17
  return acc;
15
18
  }, []);
16
- return (_jsxs("svg", { viewBox: `0 0 ${w} ${h}`, className: cn('h-full w-full overflow-visible', className), role: "img", "aria-label": ariaLabel, children: [slices.map(({ row, value, start, end, index }) => {
17
- const label = String(row[categoryKey] ?? index + 1);
18
- const angle = (value / total) * Math.PI * 2;
19
- const d = angle >= Math.PI * 2 - 0.0001 ? undefined : piePath(cx, cy, r, start, end);
20
- return d ? (_jsx("path", { d: d, fill: chartColor(colors, index), stroke: "var(--background)", strokeWidth: 1.5, className: "animate-in fade-in zoom-in-95 duration-300 hover:opacity-90", children: _jsx("title", { children: `${label}: ${format(value)}` }) }, label)) : (_jsx("circle", { cx: cx, cy: cy, r: r, fill: chartColor(colors, index), stroke: "var(--background)", strokeWidth: 1.5, transform: `rotate(${start} ${cx} ${cy})`, className: "animate-in fade-in zoom-in-95 duration-300 hover:opacity-90", children: _jsx("title", { children: `${label}: ${format(value)}` }) }, label));
21
- }), _jsx("circle", { cx: cx, cy: cy, r: r * 0.42, fill: "var(--background)", opacity: 0.94 })] }));
19
+ const dimmed = activeKey !== null;
20
+ const chart = (_jsx("svg", { viewBox: `0 0 ${w} ${h}`, className: cn('h-full w-full overflow-visible', center ? undefined : className), role: "img", "aria-label": ariaLabel, children: slices.map(({ row, value, start, end, index }) => {
21
+ const category = String(row[categoryKey] ?? index + 1);
22
+ const label = categoryFormatter ? categoryFormatter(category) : category;
23
+ const angle = (value / total) * Math.PI * 2;
24
+ const full = angle >= Math.PI * 2 - 0.0001;
25
+ const active = activeKey === category;
26
+ const d = full
27
+ ? hole > 0
28
+ ? `${donutPath(cx, cy, r, hole, -Math.PI / 2, Math.PI / 2)} ${donutPath(cx, cy, r, hole, Math.PI / 2, Math.PI * 1.5)}`
29
+ : undefined
30
+ : hole > 0
31
+ ? donutPath(cx, cy, r, hole, start, end)
32
+ : piePath(cx, cy, r, start, end);
33
+ const shared = {
34
+ fill: chartColor(colors, index),
35
+ stroke: 'var(--background)',
36
+ strokeWidth: 1.5,
37
+ className: cn('animate-in fade-in zoom-in-95 duration-300 transition-opacity', dimmed && !active && 'opacity-30'),
38
+ onMouseEnter: () => onActiveChange?.(category),
39
+ onMouseLeave: () => onActiveChange?.(null),
40
+ children: _jsx("title", { children: `${label}: ${format(value)}` }),
41
+ };
42
+ return d ? (_jsx("path", { d: d, ...shared }, category)) : (_jsx("circle", { cx: cx, cy: cy, r: r, ...shared }, category));
43
+ }) }));
44
+ if (!center || hole <= 0)
45
+ return chart;
46
+ return (_jsxs("div", { className: cn('relative', className), children: [chart, _jsx("div", { className: "pointer-events-none absolute inset-0 grid place-content-center text-center", children: center })] }));
22
47
  }
@@ -4,7 +4,7 @@ import type { Extension } from '@codemirror/state';
4
4
  * How every CodeMirror surface in atmOS looks.
5
5
  *
6
6
  * The colours are the app's own tokens rather than a packaged editor theme, so an editor
7
- * follows the workspace into dark mode and stays the same instrument whether it is holding
7
+ * follows the org into dark mode and stays the same instrument whether it is holding
8
8
  * a SQL statement, a formula, or a file off a machine. A packaged theme would be a second
9
9
  * palette to keep in sync with the first, and it would be wrong the moment the app's is.
10
10
  */
@@ -6,7 +6,7 @@ import { tags as t } from '@lezer/highlight';
6
6
  * How every CodeMirror surface in atmOS looks.
7
7
  *
8
8
  * The colours are the app's own tokens rather than a packaged editor theme, so an editor
9
- * follows the workspace into dark mode and stays the same instrument whether it is holding
9
+ * follows the org into dark mode and stays the same instrument whether it is holding
10
10
  * a SQL statement, a formula, or a file off a machine. A packaged theme would be a second
11
11
  * palette to keep in sync with the first, and it would be wrong the moment the app's is.
12
12
  */
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useId, useMemo, useState, } from 'react';
3
+ import { useId, useMemo, useState } from 'react';
4
4
  import { CalendarClock, Clock } from 'lucide-react';
5
5
  import { Calendar } from './calendar.js';
6
6
  import { PickerField } from './picker-field.js';
@@ -14,6 +14,11 @@ const DEFAULT_LABELS = {
14
14
  preview: (when) => `Preview: ${when}`,
15
15
  timeAria: 'Choose a time',
16
16
  };
17
+ function nextSlotAfterNow(step, now) {
18
+ const stride = Math.max(1, Math.round(step));
19
+ const minutes = now.getHours() * 60 + now.getMinutes();
20
+ return Math.min(Math.ceil((minutes + 1) / stride) * stride, 24 * 60 - 1);
21
+ }
17
22
  function ymd(d) {
18
23
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
19
24
  }
@@ -94,7 +99,11 @@ export function DateTimePicker({ value, onChange, mode = 'datetime', step = 15,
94
99
  const previewLabel = preview ? fmt(preview) : null;
95
100
  const gridValue = preview ? ymd(preview) : value ? ymd(value) : null;
96
101
  const columnMinutes = preview ? minutesOf(preview) : value ? minutesOf(value) : null;
97
- const slots = useMemo(() => buildTimeSlots(step, 0, 24 * 60 - 1), [step]);
102
+ const now = civilDateForTimeZone(new Date(), parseTimeZone);
103
+ const todayYmd = ymd(now);
104
+ const earliestMinutesOn = (day) => minDate && minDate <= todayYmd && day === todayYmd ? nextSlotAfterNow(step, now) : 0;
105
+ const earliestMinutes = earliestMinutesOn(gridValue ?? todayYmd);
106
+ const slots = useMemo(() => buildTimeSlots(step, earliestMinutes, 24 * 60 - 1), [step, earliestMinutes]);
98
107
  const { active, setActive, move } = useTimeNavigation({
99
108
  slots,
100
109
  selectedMinutes: value ? minutesOf(value) : null,
@@ -113,7 +122,7 @@ export function DateTimePicker({ value, onChange, mode = 'datetime', step = 15,
113
122
  const d = fromYmd(day);
114
123
  if (!d)
115
124
  return;
116
- commit(compose(d, columnMinutes ?? 9 * 60));
125
+ commit(compose(d, Math.max(columnMinutes ?? 9 * 60, earliestMinutesOn(day))));
117
126
  };
118
127
  const pickTime = (minutes) => {
119
128
  commit(compose(value ?? carrier(), minutes));
@@ -149,5 +158,5 @@ export function DateTimePicker({ value, onChange, mode = 'datetime', step = 15,
149
158
  setTyped(null);
150
159
  setActive(null);
151
160
  }
152
- }, display: display, typed: typed, onTypedChange: setTyped, onInputKeyDown: handleKeyDown, placeholder: placeholder ?? labels.placeholder, parseState: typed ? (preview ? 'ok' : 'unparseable') : 'empty', icon: mode === 'time' ? (_jsx(Clock, { className: "size-3.5", strokeWidth: 1.8 })) : (_jsx(CalendarClock, { className: "size-3.5", strokeWidth: 1.8 })), disabled: disabled, size: size, align: align, "aria-label": ariaLabel, className: cn(mode === 'time' ? 'w-[116px]' : 'w-[212px]', className), listboxId: listboxId, activeDescendantId: active !== null ? optionId(active) : undefined, children: [previewLabel ? (_jsx("p", { className: "text-muted-foreground border-border/60 border-b px-3 py-1.5 text-[11px] tabular-nums", children: labels.preview(previewLabel) })) : null, _jsxs("div", { className: "flex items-stretch", children: [mode !== 'time' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-2.5", children: _jsx(Calendar, { value: gridValue, locale: resolvedLocale, format: format, typeable: false, minDate: minDate, onSelect: pickDay, className: "w-[244px]" }, gridValue ? gridValue.slice(0, 7) : 'none') }), _jsx("div", { className: "bg-border/60 w-px self-stretch" })] })), _jsx(TimeColumn, { id: listboxId, optionId: optionId, slots: slots, selectedMinutes: columnMinutes, activeMinutes: active, onPick: pickTime, locale: resolvedLocale, format: format, "aria-label": labels.timeAria })] })] }));
161
+ }, display: display, typed: typed, onTypedChange: setTyped, onInputKeyDown: handleKeyDown, placeholder: placeholder ?? labels.placeholder, parseState: typed ? (preview ? 'ok' : 'unparseable') : 'empty', icon: mode === 'time' ? (_jsx(Clock, { className: "size-3.5", strokeWidth: 1.8 })) : (_jsx(CalendarClock, { className: "size-3.5", strokeWidth: 1.8 })), disabled: disabled, size: size, align: align, "aria-label": ariaLabel, className: cn(mode === 'time' ? 'w-[116px]' : 'w-[212px]', className), listboxId: listboxId, activeDescendantId: active !== null ? optionId(active) : undefined, children: [previewLabel ? (_jsx("p", { className: "text-muted-foreground border-border/60 border-b px-3 py-1.5 text-[11px] tabular-nums", children: labels.preview(previewLabel) })) : null, _jsxs("div", { className: "flex items-stretch", children: [mode !== 'time' && (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-2.5", children: _jsx(Calendar, { value: gridValue, locale: resolvedLocale, format: format, typeable: false, minDate: minDate, onSelect: pickDay }, gridValue ? gridValue.slice(0, 7) : 'none') }), _jsx("div", { className: "bg-border/60 w-px self-stretch" })] })), _jsx(TimeColumn, { id: listboxId, optionId: optionId, slots: slots, selectedMinutes: columnMinutes, activeMinutes: active, onPick: pickTime, locale: resolvedLocale, format: format, "aria-label": labels.timeAria })] })] }));
153
162
  }
@@ -1,11 +1,16 @@
1
1
  import * as React from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
  export type InlineAlertTone = 'error' | 'warning' | 'info';
4
+ export type InlineAlertVariant = 'filled' | 'quiet';
4
5
  export interface InlineAlertProps extends Omit<React.ComponentProps<'div'>, 'children'> {
5
6
  tone?: InlineAlertTone;
7
+ variant?: InlineAlertVariant;
6
8
  children: ReactNode;
7
9
  onDismiss?: () => void;
8
10
  dismissLabel?: string;
11
+ detail?: string;
12
+ detailLabel?: string;
13
+ copyDetailLabel?: string;
9
14
  }
10
- export declare function InlineAlert({ tone, children, onDismiss, dismissLabel, className, ...props }: InlineAlertProps): React.JSX.Element;
15
+ export declare function InlineAlert({ tone, variant, children, onDismiss, dismissLabel, detail, detailLabel, copyDetailLabel, className, ...props }: InlineAlertProps): React.JSX.Element;
11
16
  export declare function InlineNotice({ tone, children, className, }: Pick<InlineAlertProps, 'tone' | 'children' | 'className'>): React.JSX.Element;
@@ -1,17 +1,34 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
- import { AlertTriangle, Info, X } from 'lucide-react';
4
+ import { AlertTriangle, ChevronRight, Info, X } from 'lucide-react';
5
5
  import { cn } from '../lib/cn.js';
6
6
  import { Button } from './button.js';
7
+ import { CopyButton } from './copy-button.js';
7
8
  const toneClass = {
8
9
  error: 'border-destructive/30 bg-destructive/10 text-destructive',
9
10
  warning: 'border-warning/30 bg-warning/15 text-warning',
10
11
  info: 'border-info/30 bg-info/15 text-info',
11
12
  };
12
- export function InlineAlert({ tone = 'error', children, onDismiss, dismissLabel, className, ...props }) {
13
+ const quietRailClass = {
14
+ error: 'border-l-destructive',
15
+ warning: 'border-l-warning',
16
+ info: 'border-l-info',
17
+ };
18
+ const iconToneClass = {
19
+ error: 'text-destructive',
20
+ warning: 'text-warning',
21
+ info: 'text-info',
22
+ };
23
+ const quietClass = 'border-border/70 bg-muted/20 text-foreground border-l-2';
24
+ export function InlineAlert({ tone = 'error', variant = 'filled', children, onDismiss, dismissLabel, detail, detailLabel, copyDetailLabel, className, ...props }) {
13
25
  const Icon = tone === 'info' ? Info : AlertTriangle;
14
- return (_jsxs("div", { role: "alert", "data-slot": "inline-alert", "data-tone": tone, className: cn('flex items-start gap-2 rounded-md border px-3 py-2 text-xs', toneClass[tone], className), ...props, children: [_jsx(Icon, { className: "mt-0.5 size-3.5 shrink-0", strokeWidth: 1.5, "aria-hidden": true }), _jsx("span", { className: "flex-1 leading-relaxed", children: children }), onDismiss ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", onClick: onDismiss, "aria-label": dismissLabel, className: "-mr-1 size-4 rounded text-current opacity-80 hover:bg-transparent hover:opacity-100", children: _jsx(X, { className: "size-3", strokeWidth: 1.5 }) })) : null] }));
26
+ const quiet = variant === 'quiet';
27
+ const [detailOpen, setDetailOpen] = React.useState(false);
28
+ const detailId = React.useId();
29
+ return (_jsxs("div", { role: "alert", "data-slot": "inline-alert", "data-tone": tone, "data-variant": variant, className: cn('flex items-start gap-2 rounded-md border px-3 py-2 text-xs', quiet ? cn(quietClass, quietRailClass[tone]) : toneClass[tone], className), ...props, children: [_jsx(Icon, { className: cn('mt-0.5 size-3.5 shrink-0', quiet && iconToneClass[tone]), strokeWidth: 1.5, "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1 leading-relaxed", children: [children, detail ? (_jsxs("div", { className: "mt-1", children: [_jsxs(Button, { type: "button", variant: "ghost", size: "xs", onClick: () => setDetailOpen((open) => !open), "aria-expanded": detailOpen, "aria-controls": detailOpen ? detailId : undefined, "data-slot": "inline-alert-detail-toggle", className: cn('-ml-2 h-5 opacity-80 hover:bg-transparent hover:opacity-100', quiet
30
+ ? 'text-muted-foreground hover:text-foreground'
31
+ : 'text-current hover:text-current'), children: [_jsx(ChevronRight, { className: cn('transition-transform duration-125 ease-[var(--ease-out-expo)] motion-reduce:transition-none', detailOpen && 'rotate-90'), strokeWidth: 1.5, "aria-hidden": true }), detailLabel] }), detailOpen ? (_jsxs("div", { className: "relative mt-1", children: [_jsx("pre", { id: detailId, "data-slot": "inline-alert-detail", className: "border-border/50 bg-background/70 text-foreground max-h-56 overflow-auto rounded-md border py-1.5 pr-9 pl-2 font-mono text-[11px] leading-4 whitespace-pre-wrap", children: detail }), copyDetailLabel ? (_jsx(CopyButton, { value: detail, label: copyDetailLabel, "data-slot": "inline-alert-detail-copy", className: "absolute top-1 right-1" })) : null] })) : null] })) : null] }), onDismiss ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", onClick: onDismiss, "aria-label": dismissLabel, className: "-mr-1 size-4 rounded text-current opacity-80 hover:bg-transparent hover:opacity-100", children: _jsx(X, { className: "size-3", strokeWidth: 1.5 }) })) : null] }));
15
32
  }
16
33
  export function InlineNotice({ tone = 'info', children, className, }) {
17
34
  return (_jsx(InlineAlert, { tone: tone, className: className, children: children }));
@@ -33,11 +33,15 @@ export interface MetricBreakdownItem {
33
33
  tone?: MetricTone;
34
34
  title?: string;
35
35
  onSelect?: () => void;
36
+ color?: string;
36
37
  }
37
38
  export interface MetricBreakdownProps extends Omit<React.ComponentProps<'div'>, 'title'> {
38
39
  title?: React.ReactNode;
39
40
  items: MetricBreakdownItem[];
40
41
  emptyLabel?: React.ReactNode;
41
42
  showBars?: boolean;
43
+ barPlacement?: 'below' | 'inline';
44
+ activeKey?: string | null;
45
+ onActiveChange?: (key: string | null) => void;
42
46
  }
43
- export declare function MetricBreakdown({ title, items, emptyLabel, showBars, className, ...props }: MetricBreakdownProps): React.JSX.Element;
47
+ export declare function MetricBreakdown({ title, items, emptyLabel, showBars, barPlacement, activeKey, onActiveChange, className, ...props }: MetricBreakdownProps): React.JSX.Element;
@@ -10,7 +10,7 @@ const TONE_TEXT = {
10
10
  danger: 'text-danger-9',
11
11
  };
12
12
  export function MetricSectionHeading({ title, hint, action, className, ...props }) {
13
- return (_jsxs("div", { "data-slot": "metric-section-heading", className: cn('mb-2.5 flex flex-wrap items-start justify-between gap-2', className), ...props, children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "text-[13px] font-medium", children: title }), hint ? (_jsx("p", { className: "text-muted-foreground mt-0.5 text-[11.5px] leading-snug", children: hint })) : null] }), action] }));
13
+ return (_jsxs("div", { "data-slot": "metric-section-heading", className: cn('mb-2.5 flex flex-wrap items-start justify-between gap-2', className), ...props, children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h2", { className: "text-[13px] font-medium", children: title }), hint ? (_jsx("p", { className: "text-muted-foreground mt-0.5 text-[11.5px] leading-snug", children: hint })) : null] }), action ? _jsx("div", { className: "shrink-0", children: action }) : null] }));
14
14
  }
15
15
  export function MetricGrid({ className, ...props }) {
16
16
  return (_jsx("div", { "data-slot": "metric-grid", className: cn('bg-background grid gap-px overflow-hidden rounded-xl', className), ...props }));
@@ -28,13 +28,24 @@ const TONE_BAR = {
28
28
  warning: 'bg-warning-9/60',
29
29
  danger: 'bg-danger-9/55',
30
30
  };
31
- export function MetricBreakdown({ title, items, emptyLabel, showBars = false, className, ...props }) {
32
- return (_jsxs("div", { "data-slot": "metric-breakdown", className: cn('min-w-0', className), ...props, children: [title ? _jsx("h3", { className: "mb-1 text-xs font-medium", children: title }) : null, items.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: emptyLabel })) : (_jsx("ul", { className: cn(showBars && 'grid gap-2'), children: items.map((item) => (_jsx("li", { children: _jsx(MetricBreakdownRow, { item: item, showBar: showBars }) }, item.key))) }))] }));
31
+ export function MetricBreakdown({ title, items, emptyLabel, showBars = false, barPlacement = 'below', activeKey = null, onActiveChange, className, ...props }) {
32
+ return (_jsxs("div", { "data-slot": "metric-breakdown", className: cn('min-w-0', className), ...props, children: [title ? _jsx("h3", { className: "mb-1 text-xs font-medium", children: title }) : null, items.length === 0 ? (_jsx("p", { className: "text-muted-foreground text-xs", children: emptyLabel })) : (_jsx("ul", { className: cn(showBars && barPlacement === 'below' && 'grid gap-2'), children: items.map((item) => (_jsx("li", { onMouseEnter: onActiveChange ? () => onActiveChange(item.key) : undefined, onMouseLeave: onActiveChange ? () => onActiveChange(null) : undefined, onFocus: onActiveChange ? () => onActiveChange(item.key) : undefined, onBlur: onActiveChange ? () => onActiveChange(null) : undefined, children: _jsx(MetricBreakdownRow, { item: item, showBar: showBars, placement: barPlacement, active: activeKey === item.key, dimmed: activeKey !== null && activeKey !== item.key }) }, item.key))) }))] }));
33
33
  }
34
- function MetricBreakdownRow({ item, showBar }) {
34
+ function MetricBreakdownRow({ item, showBar, placement, active, dimmed, }) {
35
35
  const tone = item.tone ?? 'neutral';
36
- const row = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex items-center justify-between gap-3 text-xs", children: [_jsx("span", { className: cn('min-w-0 truncate', tone === 'neutral' ? 'text-muted-foreground' : TONE_TEXT[tone]), title: item.title, children: item.label }), _jsx("span", { className: "shrink-0 font-mono tabular-nums", children: item.value })] }), showBar ? (_jsx("span", { className: "bg-background mt-1.5 block h-1 overflow-hidden rounded-full", children: _jsx("span", { className: cn('block h-full rounded-full', TONE_BAR[tone]), style: { width: `${Math.round(Math.max(0, Math.min(1, item.fraction ?? 0)) * 100)}%` } }) })) : null] }));
37
- if (!item.onSelect)
38
- return _jsx("div", { className: "py-0.5", children: row });
39
- return (_jsx("button", { type: "button", onClick: item.onSelect, className: "hover:bg-background/60 focus-visible:ring-ring -mx-1.5 block w-[calc(100%+0.75rem)] rounded-sm px-1.5 py-0.5 text-left focus-visible:ring-2 focus-visible:outline-none", children: row }));
36
+ const inline = showBar && placement === 'inline';
37
+ const width = `${Math.round(Math.max(0, Math.min(1, item.fraction ?? 0)) * 100)}%`;
38
+ const fill = (_jsx("span", { className: cn('block h-full rounded-full', !item.color && TONE_BAR[tone]), style: { width, backgroundColor: item.color } }));
39
+ const row = (_jsxs(_Fragment, { children: [_jsxs("span", { className: cn('text-xs', inline
40
+ ? 'grid grid-cols-[auto_7rem_minmax(0,1fr)] items-center gap-2.5 sm:grid-cols-[auto_12rem_minmax(0,1fr)]'
41
+ : 'flex items-center gap-2.5'), children: [item.color ? (_jsx("span", { "aria-hidden": true, className: "size-2 shrink-0 rounded-[2px]", style: { backgroundColor: item.color } })) : inline ? (_jsx("span", { "aria-hidden": true, className: "size-2 shrink-0" })) : null, _jsx("span", { className: cn('min-w-0 truncate', !inline && 'flex-1', tone === 'neutral' ? 'text-muted-foreground' : TONE_TEXT[tone]), title: item.title, children: item.label }), inline ? (_jsxs("span", { className: "relative flex h-4 items-center", children: [_jsx("span", { className: "bg-background block h-1 w-full overflow-hidden rounded-full", children: fill }), _jsx("span", { "data-slot": "metric-row-value", className: cn('bg-muted absolute inset-y-0 right-0 flex items-center rounded-sm pl-1.5 font-mono tabular-nums transition-opacity', active ? 'opacity-100' : 'opacity-100 [@media(hover:hover)]:opacity-0'), children: item.value })] })) : (_jsx("span", { className: "shrink-0 font-mono tabular-nums", children: item.value }))] }), showBar && placement === 'below' ? (_jsx("span", { className: cn('bg-background mt-1.5 block h-1 overflow-hidden rounded-full', item.color && 'ml-4'), children: fill })) : null] }));
42
+ const dim = cn('transition-opacity', dimmed && 'opacity-40');
43
+ if (!item.onSelect) {
44
+ return (_jsx("div", { className: cn('py-0.5', showBar &&
45
+ placement === 'inline' &&
46
+ 'focus-visible:ring-ring -mx-1.5 rounded-sm px-1.5 focus-visible:ring-2 focus-visible:outline-none', showBar &&
47
+ placement === 'inline' &&
48
+ '[&:focus-visible_[data-slot=metric-row-value]]:opacity-100', dim), tabIndex: showBar && placement === 'inline' ? 0 : undefined, children: row }));
49
+ }
50
+ return (_jsx("button", { type: "button", onClick: item.onSelect, className: cn('hover:bg-background/60 focus-visible:ring-ring -mx-1.5 block w-[calc(100%+0.75rem)] rounded-sm px-1.5 py-0.5 text-left focus-visible:ring-2 focus-visible:outline-none', '[&:focus-visible_[data-slot=metric-row-value]]:opacity-100', dim), children: row }));
40
51
  }
@@ -3,10 +3,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useId, useMemo, useRef, useState, } from 'react';
4
4
  import { Check, Plus } from 'lucide-react';
5
5
  import { Slot } from 'radix-ui';
6
- import { StackedPopover, StackedPopoverContent, StackedPopoverTrigger, } from './popover-stack.js';
6
+ import { StackedPopover, StackedPopoverContent, StackedPopoverTrigger } from './popover-stack.js';
7
7
  import { Kbd } from './kbd.js';
8
8
  import { EmptyState } from './empty-state.js';
9
9
  import { cn } from '../lib/cn.js';
10
+ import { useScrollLockEscape } from '../lib/scroll-lock-escape.js';
10
11
  const DEFAULT_LABELS = {
11
12
  noMatches: 'No matches',
12
13
  create: 'Create',
@@ -18,23 +19,8 @@ export function PropertySelector({ open, onOpenChange, children, items, onSelect
18
19
  const [activeOverride, setActiveOverride] = useState(null);
19
20
  const inputRef = useRef(null);
20
21
  const triggerRef = useRef(null);
21
- const wheelCleanupRef = useRef(null);
22
22
  const listId = useId();
23
- // The list is portalled to <body>, outside the scroll-lock (react-remove-scroll) of any
24
- // enclosing dialog. That lock's document-level, bubble-phase `wheel` handler cancels
25
- // mouse-wheel scrolling on portalled content (trackpad deltas slip its overscroll heuristic,
26
- // discrete wheel ticks don't). Stopping the wheel event at the list keeps it from reaching that
27
- // handler, so the list scrolls natively while the dialog's background stays locked. A callback
28
- // ref (re)attaches the listener every time the list mounts, since it only exists while open.
29
- const listRefCallback = useCallback((node) => {
30
- wheelCleanupRef.current?.();
31
- wheelCleanupRef.current = null;
32
- if (!node)
33
- return;
34
- const stop = (event) => event.stopPropagation();
35
- node.addEventListener('wheel', stop, { passive: true });
36
- wheelCleanupRef.current = () => node.removeEventListener('wheel', stop);
37
- }, []);
23
+ const listRefCallback = useScrollLockEscape();
38
24
  const filtered = useMemo(() => {
39
25
  const q = query.trim().toLowerCase();
40
26
  if (!q)
@@ -57,9 +43,7 @@ export function PropertySelector({ open, onOpenChange, children, items, onSelect
57
43
  ids.push(CREATE_ROW);
58
44
  return ids;
59
45
  }, [filtered, showCreate]);
60
- const activeId = activeOverride && rowIds.includes(activeOverride)
61
- ? activeOverride
62
- : (rowIds[0] ?? null);
46
+ const activeId = activeOverride && rowIds.includes(activeOverride) ? activeOverride : (rowIds[0] ?? null);
63
47
  const handleOpenChange = useCallback((next) => {
64
48
  if (next) {
65
49
  setQuery('');
@@ -106,11 +90,7 @@ export function PropertySelector({ open, onOpenChange, children, items, onSelect
106
90
  handleOpenChange(false);
107
91
  return;
108
92
  }
109
- if (e.key.length === 1 &&
110
- !e.metaKey &&
111
- !e.ctrlKey &&
112
- !e.altKey &&
113
- query.length === 0) {
93
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey && query.length === 0) {
114
94
  const key = e.key.toLowerCase();
115
95
  const hit = items.find((i) => i.shortcut?.toLowerCase() === key);
116
96
  if (hit) {
@@ -120,7 +100,7 @@ export function PropertySelector({ open, onOpenChange, children, items, onSelect
120
100
  }
121
101
  }, [rowIds, activeId, filtered, items, query.length, handleSelect, handleCreate, handleOpenChange]);
122
102
  const activeDomId = activeId ? `${listId}-row-${activeId}` : undefined;
123
- return (_jsxs(StackedPopover, { open: open, onOpenChange: handleOpenChange, children: [_jsx(StackedPopoverTrigger, { asChild: true, children: triggerAsChild ? (_jsx(Slot.Root, { ref: triggerRef, "aria-haspopup": "listbox", "aria-expanded": open, children: children })) : (_jsx("div", { ref: triggerRef, className: "inline-flex w-full", "aria-haspopup": "listbox", "aria-expanded": open, children: children })) }), _jsxs(StackedPopoverContent, { className: cn('w-64 p-0 gap-0 overflow-hidden', contentClassName), align: "start", "aria-label": ariaLabel, onCloseAutoFocus: (e) => {
103
+ return (_jsxs(StackedPopover, { open: open, onOpenChange: handleOpenChange, children: [_jsx(StackedPopoverTrigger, { asChild: true, children: triggerAsChild ? (_jsx(Slot.Root, { ref: triggerRef, "aria-haspopup": "listbox", "aria-expanded": open, children: children })) : (_jsx("div", { ref: triggerRef, className: "inline-flex w-full", "aria-haspopup": "listbox", "aria-expanded": open, children: children })) }), _jsxs(StackedPopoverContent, { className: cn('w-64 gap-0 overflow-hidden p-0', contentClassName), align: "start", "aria-label": ariaLabel, onCloseAutoFocus: (e) => {
124
104
  e.preventDefault();
125
105
  const el = triggerRef.current;
126
106
  const focusable = el?.matches('button, [tabindex]:not([tabindex="-1"])')
@@ -130,12 +110,12 @@ export function PropertySelector({ open, onOpenChange, children, items, onSelect
130
110
  }, onOpenAutoFocus: (e) => {
131
111
  e.preventDefault();
132
112
  inputRef.current?.focus();
133
- }, children: [_jsxs("div", { className: "flex items-center gap-2 px-2.5 pt-2.5 pb-1.5 border-b border-border/60", children: [_jsx("input", { ref: inputRef, type: "text", role: "combobox", "aria-autocomplete": "list", "aria-controls": listId, "aria-expanded": true, "aria-activedescendant": activeDomId, value: query, placeholder: searchPlaceholder, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, className: "flex-1 min-w-0 bg-transparent outline-none text-[13px] placeholder:text-muted-foreground/70" }), triggerKey ? (_jsx(Kbd, { className: "text-[10.5px] shrink-0", children: triggerKey })) : null] }), _jsxs("ul", { ref: listRefCallback, id: listId, role: "listbox", "aria-label": ariaLabel, className: "max-h-[280px] overflow-y-auto overscroll-contain py-1", children: [filtered.length === 0 && !showCreate ? (_jsx("li", { children: _jsx(EmptyState, { title: labels.noMatches, className: "py-6" }) })) : (filtered.map((item) => {
113
+ }, children: [_jsxs("div", { className: "border-border/60 flex items-center gap-2 border-b px-2.5 pt-2.5 pb-1.5", children: [_jsx("input", { ref: inputRef, type: "text", role: "combobox", "aria-autocomplete": "list", "aria-controls": listId, "aria-expanded": true, "aria-activedescendant": activeDomId, value: query, placeholder: searchPlaceholder, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, className: "placeholder:text-muted-foreground/70 min-w-0 flex-1 bg-transparent text-[13px] outline-none" }), triggerKey ? _jsx(Kbd, { className: "shrink-0 text-[10.5px]", children: triggerKey }) : null] }), _jsxs("ul", { ref: listRefCallback, id: listId, role: "listbox", "aria-label": ariaLabel, className: "max-h-[280px] overflow-y-auto overscroll-contain py-1", children: [filtered.length === 0 && !showCreate ? (_jsx("li", { children: _jsx(EmptyState, { title: labels.noMatches, className: "py-6" }) })) : (filtered.map((item) => {
134
114
  const active = activeId === item.id;
135
115
  return (
136
116
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- keyboard handled by the parent input via aria-activedescendant
137
- _jsxs("li", { id: `${listId}-row-${item.id}`, role: "option", "aria-selected": item.selected ?? false, "data-active": active || undefined, onMouseEnter: () => setActiveOverride(item.id), onClick: () => handleSelect(item), className: cn('mx-1 px-2 py-1.5 rounded-md cursor-pointer text-[13px] text-foreground', 'flex gap-2', item.description ? 'items-start' : 'items-center', active && 'bg-muted'), children: [item.icon ? (_jsx("span", { className: cn('shrink-0 inline-flex items-center justify-center', item.description && 'pt-0.5'), children: item.icon })) : null, _jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate", children: item.label }), item.description ? (_jsx("span", { className: "block truncate text-[11px] leading-snug text-muted-foreground", children: item.description })) : null] }), _jsxs("span", { className: cn('shrink-0 inline-flex items-center gap-1 text-muted-foreground', item.description && 'pt-0.5'), children: [item.selected ? _jsx(Check, { className: "size-3.5", "aria-hidden": true }) : null, item.shortcut ? (_jsx(Kbd, { className: "text-[10.5px]", children: item.shortcut })) : null] })] }, item.id));
117
+ _jsxs("li", { id: `${listId}-row-${item.id}`, role: "option", "aria-selected": item.selected ?? false, "data-active": active || undefined, onMouseEnter: () => setActiveOverride(item.id), onClick: () => handleSelect(item), className: cn('text-foreground mx-1 cursor-pointer rounded-md px-2 py-1.5 text-[13px]', 'flex gap-2', item.description ? 'items-start' : 'items-center', active && 'bg-muted'), children: [item.icon ? (_jsx("span", { className: cn('inline-flex shrink-0 items-center justify-center', item.description && 'pt-0.5'), children: item.icon })) : null, _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.label }), item.description ? (_jsx("span", { className: "text-muted-foreground block truncate text-[11px] leading-snug", children: item.description })) : null] }), _jsxs("span", { className: cn('text-muted-foreground inline-flex shrink-0 items-center gap-1', item.description && 'pt-0.5'), children: [item.selected ? _jsx(Check, { className: "size-3.5", "aria-hidden": true }) : null, item.shortcut ? _jsx(Kbd, { className: "text-[10.5px]", children: item.shortcut }) : null] })] }, item.id));
138
118
  })), showCreate ? (
139
119
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- keyboard handled by the parent input via aria-activedescendant
140
- _jsxs("li", { id: `${listId}-row-${CREATE_ROW}`, role: "option", "aria-selected": false, "data-active": activeId === CREATE_ROW || undefined, onMouseEnter: () => setActiveOverride(CREATE_ROW), onClick: handleCreate, className: cn('mx-1 mt-1 px-2 py-1.5 rounded-md cursor-pointer text-[13px]', 'flex items-center gap-2', filtered.length > 0 && 'border-t border-border/60 pt-2', activeId === CREATE_ROW && 'bg-muted'), children: [_jsx(Plus, { className: "size-3.5 text-muted-foreground shrink-0", strokeWidth: 1.8, "aria-hidden": true }), _jsxs("span", { className: "text-foreground truncate", children: [createLabel ?? labels.create, ":", ' ', _jsxs("span", { className: "text-foreground/90", children: ["\"", trimmedQuery, "\""] })] })] })) : null] })] })] }));
120
+ _jsxs("li", { id: `${listId}-row-${CREATE_ROW}`, role: "option", "aria-selected": false, "data-active": activeId === CREATE_ROW || undefined, onMouseEnter: () => setActiveOverride(CREATE_ROW), onClick: handleCreate, className: cn('mx-1 mt-1 cursor-pointer rounded-md px-2 py-1.5 text-[13px]', 'flex items-center gap-2', filtered.length > 0 && 'border-border/60 border-t pt-2', activeId === CREATE_ROW && 'bg-muted'), children: [_jsx(Plus, { className: "text-muted-foreground size-3.5 shrink-0", strokeWidth: 1.8, "aria-hidden": true }), _jsxs("span", { className: "text-foreground truncate", children: [createLabel ?? labels.create, ":", ' ', _jsxs("span", { className: "text-foreground/90", children: ["\"", trimmedQuery, "\""] })] })] })) : null] })] })] }));
141
121
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
4
  import { useMediaQuery } from '../hooks/use-media-query.js';
5
5
  import { cn } from '../lib/cn.js';
6
- const SHIMMER_FRAME_MS = 1000 / 8;
6
+ const SHIMMER_FRAME_MS = 1000 / 30;
7
7
  const shimmerEntries = new Set();
8
8
  let shimmerTimer = 0;
9
9
  function paintShimmers() {
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { cn } from '../lib/cn.js';
5
5
  import { resolveDateTimeFormat } from '../lib/date-time-format.js';
6
+ import { useScrollLockEscape } from '../lib/scroll-lock-escape.js';
6
7
  export function buildTimeSlots(step, minMinutes, maxMinutes) {
7
8
  const stride = Math.max(1, Math.round(step));
8
9
  const out = [];
@@ -47,10 +48,11 @@ export function TimeColumn({ slots, selectedMinutes, activeMinutes = null, onPic
47
48
  }, [format, locale, hour12]);
48
49
  const scrollTarget = activeMinutes ?? selectedMinutes;
49
50
  const currentRef = useRef(null);
51
+ const listRefCallback = useScrollLockEscape();
50
52
  useEffect(() => {
51
53
  currentRef.current?.scrollIntoView({ block: 'center' });
52
54
  }, [scrollTarget]);
53
- return (_jsx("div", { id: id, role: "listbox", "aria-label": ariaLabel, className: cn('max-h-[316px] w-[92px] space-y-0.5 overflow-y-auto px-1.5 py-2', className), children: slots.map((minutes) => {
55
+ return (_jsx("div", { id: id, ref: listRefCallback, role: "listbox", "aria-label": ariaLabel, className: cn('max-h-[316px] w-[92px] space-y-0.5 overflow-y-auto px-1.5 py-2', className), children: slots.map((minutes) => {
54
56
  const selected = selectedMinutes === minutes;
55
57
  const active = activeMinutes === minutes;
56
58
  return (_jsx("button", { ref: minutes === scrollTarget ? currentRef : undefined, id: optionId?.(minutes), type: "button", role: "option", "aria-selected": selected, tabIndex: -1, onClick: () => onPick(minutes), className: cn('w-full cursor-pointer rounded-md px-2 py-1 text-left text-[12px] tabular-nums transition-colors', selected