@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.
- package/README.md +6 -6
- package/dist/components/calendar.js +6 -5
- package/dist/components/charts/bar-chart.js +4 -2
- package/dist/components/charts/colors.js +1 -1
- package/dist/components/charts/index.d.ts +2 -2
- package/dist/components/charts/index.js +1 -1
- package/dist/components/charts/layout.d.ts +2 -1
- package/dist/components/charts/layout.js +15 -8
- package/dist/components/charts/pie-chart.d.ts +10 -1
- package/dist/components/charts/pie-chart.js +34 -9
- package/dist/components/code-editor-theme.d.ts +1 -1
- package/dist/components/code-editor-theme.js +1 -1
- package/dist/components/date-time-picker.js +13 -4
- package/dist/components/inline-alert.d.ts +6 -1
- package/dist/components/inline-alert.js +20 -3
- package/dist/components/metric-grid.d.ts +5 -1
- package/dist/components/metric-grid.js +19 -8
- package/dist/components/property-selector.js +9 -29
- package/dist/components/shimmer-text.js +1 -1
- package/dist/components/time-column.js +3 -1
- package/dist/components/usage-bar.d.ts +3 -1
- package/dist/components/usage-bar.js +5 -3
- package/dist/flow/editor/adapter.d.ts +1 -1
- package/dist/flow/editor/flow-editor.d.ts +2 -2
- package/dist/flow/editor/flow-editor.js +4 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/scroll-lock-escape.d.ts +9 -0
- package/dist/lib/scroll-lock-escape.js +22 -0
- package/dist/mcp-app/checkout-version.d.ts +14 -0
- package/dist/mcp-app/checkout-version.js +19 -0
- package/dist/mcp-app/index.d.ts +4 -0
- package/dist/mcp-app/index.js +3 -0
- package/dist/mcp-app/resource-protocol.d.ts +5 -0
- package/dist/mcp-app/resource-protocol.js +13 -0
- package/dist/mcp-app/typed-schema.d.ts +28 -0
- package/dist/mcp-app/typed-schema.js +1 -0
- package/dist/mcp-app/use-mcp-resource.d.ts +11 -0
- package/dist/mcp-app/use-mcp-resource.js +206 -0
- package/dist/mcp-app/use-mcp-tool.d.ts +9 -0
- package/dist/mcp-app/use-mcp-tool.js +73 -0
- package/dist/mcp-app/vite.js +1 -1
- 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 `
|
|
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
|
|
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
|
|
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:
|
|
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 &&
|
|
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(
|
|
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("
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 /
|
|
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
|