aplance-ui 1.0.1
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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/api.d.ts +175 -0
- package/dist/api.js +1954 -0
- package/dist/api.js.map +1 -0
- package/dist/frameworks/astro/astro.config.snippet.md +23 -0
- package/dist/frameworks/astro/postcss.config.mjs.template +20 -0
- package/dist/frameworks/astro/vite-inline.snippet.md +22 -0
- package/dist/frameworks/electron-vite/vite.config.snippet.md +44 -0
- package/dist/frameworks/next/layout-import.snippet.md +19 -0
- package/dist/frameworks/next/next.config.snippet.md +15 -0
- package/dist/frameworks/next/postcss.config.js.template +16 -0
- package/dist/frameworks/tanstack-start/postcss.config.ts.template +17 -0
- package/dist/frameworks/tanstack-start/vite-inline.snippet.md +22 -0
- package/dist/frameworks/vite-react/postcss.config.ts.template +16 -0
- package/dist/frameworks/vite-react/vite-inline.snippet.md +20 -0
- package/dist/frameworks/vite-react/vite.config.ts.template +48 -0
- package/dist/index.js +2047 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/registry/components/primitives/icon/icon.module.css +63 -0
- package/registry/components/primitives/icon/icon.tsx +53 -0
- package/registry/components/primitives/modal/modal-context.ts +19 -0
- package/registry/components/primitives/modal/modal.module.css +415 -0
- package/registry/components/primitives/modal/modal.tsx +550 -0
- package/registry/components/primitives/popover/popover-context.ts +28 -0
- package/registry/components/primitives/popover/popover.module.css +28 -0
- package/registry/components/primitives/popover/popover.tsx +465 -0
- package/registry/components/ui/accordion/accordion.module.css +208 -0
- package/registry/components/ui/accordion/accordion.tsx +327 -0
- package/registry/components/ui/avatar/avatar.module.css +32 -0
- package/registry/components/ui/avatar/avatar.tsx +37 -0
- package/registry/components/ui/badge/badge.module.css +103 -0
- package/registry/components/ui/badge/badge.tsx +29 -0
- package/registry/components/ui/button/button.module.css +216 -0
- package/registry/components/ui/button/button.tsx +133 -0
- package/registry/components/ui/chart/chart.module.css +168 -0
- package/registry/components/ui/chart/chart.tsx +1293 -0
- package/registry/components/ui/chart/types.ts +210 -0
- package/registry/components/ui/chart/utils.ts +59 -0
- package/registry/components/ui/copy-button/copy-button.module.css +21 -0
- package/registry/components/ui/copy-button/copy-button.tsx +123 -0
- package/registry/components/ui/countdown/countdown.module.css +5 -0
- package/registry/components/ui/countdown/countdown.tsx +152 -0
- package/registry/components/ui/data-table/checkbox-column.tsx +59 -0
- package/registry/components/ui/data-table/data-table.module.css +594 -0
- package/registry/components/ui/data-table/data-table.tsx +1109 -0
- package/registry/components/ui/data-table/filter-bar.tsx +33 -0
- package/registry/components/ui/data-table/pagination-bar.tsx +125 -0
- package/registry/components/ui/data-table/types.ts +594 -0
- package/registry/components/ui/data-table/useDataTable.tsx +573 -0
- package/registry/components/ui/data-table/utils.ts +445 -0
- package/registry/components/ui/empty-state/empty-state.module.css +26 -0
- package/registry/components/ui/empty-state/empty-state.tsx +32 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.module.css +102 -0
- package/registry/components/ui/input-fields/color-picker/color-palette.tsx +92 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.module.css +29 -0
- package/registry/components/ui/input-fields/color-picker/color-picker.tsx +168 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.module.css +28 -0
- package/registry/components/ui/input-fields/common/field-message/field-message.tsx +39 -0
- package/registry/components/ui/input-fields/common/input-field-common.module.css +318 -0
- package/registry/components/ui/input-fields/common/input-field-native.module.css +267 -0
- package/registry/components/ui/input-fields/common/picker-popover-common.module.css +40 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.module.css +9 -0
- package/registry/components/ui/input-fields/common/select-button/select-button.tsx +171 -0
- package/registry/components/ui/input-fields/common/types.ts +38 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.module.css +56 -0
- package/registry/components/ui/input-fields/custom-select/custom-select.tsx +403 -0
- package/registry/components/ui/input-fields/date-picker/calendar.module.css +311 -0
- package/registry/components/ui/input-fields/date-picker/calendar.tsx +186 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.module.css +57 -0
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +945 -0
- package/registry/components/ui/input-fields/date-picker/types.ts +102 -0
- package/registry/components/ui/input-fields/date-picker/utils.ts +236 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.module.css +90 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +168 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.module.css +8 -0
- package/registry/components/ui/input-fields/emoji-picker/emoji-picker.tsx +102 -0
- package/registry/components/ui/input-fields/emoji-picker/utils.ts +505 -0
- package/registry/components/ui/input-fields/field-group/field-group.module.css +86 -0
- package/registry/components/ui/input-fields/field-group/field-group.tsx +86 -0
- package/registry/components/ui/input-fields/input/input.tsx +319 -0
- package/registry/components/ui/input-fields/media-input/kind-icons.tsx +62 -0
- package/registry/components/ui/input-fields/media-input/media-input.module.css +294 -0
- package/registry/components/ui/input-fields/media-input/media-input.tsx +327 -0
- package/registry/components/ui/input-fields/media-input/media-preview.tsx +101 -0
- package/registry/components/ui/input-fields/media-input/types.ts +129 -0
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +302 -0
- package/registry/components/ui/input-fields/media-input/utils.ts +325 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.module.css +24 -0
- package/registry/components/ui/input-fields/otp-input/otp-input.tsx +291 -0
- package/registry/components/ui/input-fields/select/select.tsx +118 -0
- package/registry/components/ui/input-fields/switch/switch.module.css +70 -0
- package/registry/components/ui/input-fields/switch/switch.tsx +59 -0
- package/registry/components/ui/input-fields/textarea/textarea.tsx +133 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.module.css +57 -0
- package/registry/components/ui/input-fields/time-picker/time-picker.tsx +437 -0
- package/registry/components/ui/loader/loader.module.css +156 -0
- package/registry/components/ui/loader/loader.tsx +102 -0
- package/registry/components/ui/stepper/stepper.module.css +294 -0
- package/registry/components/ui/stepper/stepper.tsx +197 -0
- package/registry/components/ui/tabs/tabs.module.css +150 -0
- package/registry/components/ui/tabs/tabs.tsx +134 -0
- package/registry/components/ui/toast/toast.module.css +159 -0
- package/registry/components/ui/toast/toast.tsx +104 -0
- package/registry/components/ui/tooltip/tooltip.module.css +82 -0
- package/registry/components/ui/tooltip/tooltip.tsx +72 -0
- package/registry/global.d.ts +4 -0
- package/registry/hooks/useFocusOutside.tsx +33 -0
- package/registry/hooks/useResizeObserver.tsx +32 -0
- package/registry/lib/dom.ts +25 -0
- package/registry/lib/string.ts +23 -0
- package/registry/registry.json +719 -0
- package/registry/styles/app.css +240 -0
- package/registry/styles/components.css +78 -0
- package/registry/styles/fonts/geist-regular.woff2 +0 -0
- package/registry/styles/globals.css +7 -0
- package/registry/styles/media.css +25 -0
- package/registry/styles/utilities.css +90 -0
- package/registry/themes/colors/blue.css +37 -0
- package/registry/themes/colors/green.css +37 -0
- package/registry/themes/colors/rose.css +37 -0
- package/registry/themes/colors/slate.css +37 -0
- package/registry/themes/colors/stone.css +37 -0
- package/registry/themes/colors/violet.css +37 -0
- package/registry/themes/colors/zinc.css +37 -0
- package/registry/themes/radius/pill.css +8 -0
- package/registry/themes/radius/rounded.css +8 -0
- package/registry/themes/radius/sharp.css +8 -0
- package/registry/themes/spacing/compact.css +17 -0
- package/registry/themes/spacing/spacious.css +17 -0
- package/registry/tsconfig.json +25 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type ChartDatum = Record<string, string | number | null | undefined>;
|
|
4
|
+
|
|
5
|
+
export type ChartColorMap = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
export type ChartSeriesType = "bar" | "line" | "area";
|
|
8
|
+
|
|
9
|
+
export type AxisFormatter = (value: string | number) => string;
|
|
10
|
+
|
|
11
|
+
export type TooltipValueFormatter = (
|
|
12
|
+
value: string | number,
|
|
13
|
+
meta: { name?: string | number; dataKey?: string | number },
|
|
14
|
+
) => string;
|
|
15
|
+
|
|
16
|
+
export type TooltipLabelFormatter = (label: string | number) => string;
|
|
17
|
+
|
|
18
|
+
export type ReferenceLineConfig = {
|
|
19
|
+
value: number;
|
|
20
|
+
/** Which axis to draw the line on. Default: "y". */
|
|
21
|
+
axis?: "x" | "y";
|
|
22
|
+
label?: string;
|
|
23
|
+
/** CSS token (e.g. "--color-chart-secondary") or raw CSS color. */
|
|
24
|
+
color?: string;
|
|
25
|
+
strokeDasharray?: string;
|
|
26
|
+
/** Which Y axis to attach to when axis is "y". Default: "left". */
|
|
27
|
+
yAxisId?: "left" | "right";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ChartSeries = {
|
|
31
|
+
key: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
/** CSS token or raw CSS color. */
|
|
34
|
+
color?: string;
|
|
35
|
+
type?: ChartSeriesType;
|
|
36
|
+
stackId?: string;
|
|
37
|
+
yAxisId?: "left" | "right";
|
|
38
|
+
strokeWidth?: number;
|
|
39
|
+
fillOpacity?: number;
|
|
40
|
+
/** Show dots on data points. Applies to line and area series. Default: false. */
|
|
41
|
+
dot?: boolean;
|
|
42
|
+
/** Connect across null/undefined data gaps. Applies to line and area series. Default: false. */
|
|
43
|
+
connectNulls?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ScatterSeries = {
|
|
47
|
+
key: string;
|
|
48
|
+
label?: string;
|
|
49
|
+
/** CSS token or raw CSS color. */
|
|
50
|
+
color?: string;
|
|
51
|
+
data: ChartDatum[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const CHART_COLOR_TOKENS = {
|
|
55
|
+
primary: "--color-chart-primary",
|
|
56
|
+
secondary: "--color-chart-secondary",
|
|
57
|
+
tertiary: "--color-chart-tertiary",
|
|
58
|
+
quaternary: "--color-chart-quaternary",
|
|
59
|
+
quinary: "--color-chart-quinary",
|
|
60
|
+
muted: "--color-chart-muted",
|
|
61
|
+
grid: "--color-chart-grid",
|
|
62
|
+
cursor: "--color-chart-cursor",
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export type ChartColorTokenName = keyof typeof CHART_COLOR_TOKENS;
|
|
66
|
+
|
|
67
|
+
// ─── Base prop shapes ─────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
type ChartBaseProps = {
|
|
70
|
+
title: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
height?: number;
|
|
73
|
+
className?: string;
|
|
74
|
+
emptyMessage?: string;
|
|
75
|
+
/** Show a loading indicator in place of the chart. */
|
|
76
|
+
isLoading?: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type CartesianChartBaseProps = ChartBaseProps & {
|
|
80
|
+
data: ChartDatum[];
|
|
81
|
+
categoryKey: string;
|
|
82
|
+
showGrid?: boolean;
|
|
83
|
+
showXAxis?: boolean;
|
|
84
|
+
showYAxis?: boolean;
|
|
85
|
+
showTooltip?: boolean;
|
|
86
|
+
/** Show a color legend below the chart. Default: false. */
|
|
87
|
+
showLegend?: boolean;
|
|
88
|
+
allowDecimals?: boolean;
|
|
89
|
+
xTickFormatter?: AxisFormatter;
|
|
90
|
+
yTickFormatter?: AxisFormatter;
|
|
91
|
+
tooltipValueFormatter?: TooltipValueFormatter;
|
|
92
|
+
tooltipLabelFormatter?: TooltipLabelFormatter;
|
|
93
|
+
/** Horizontal or vertical threshold / target lines drawn across the chart. */
|
|
94
|
+
referenceLines?: ReferenceLineConfig[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ─── Chart variant props ──────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export type BarChartProps = CartesianChartBaseProps & {
|
|
100
|
+
type: "bar";
|
|
101
|
+
dataKey?: string;
|
|
102
|
+
series?: ChartSeries[];
|
|
103
|
+
maxBarSize?: number;
|
|
104
|
+
/** Map of normalized category label → token/color (e.g. "qualified": "--color-chart-secondary"). */
|
|
105
|
+
colors?: ChartColorMap;
|
|
106
|
+
/**
|
|
107
|
+
* Bar orientation. "vertical" = bars grow up (default).
|
|
108
|
+
* "horizontal" = bars grow right (category axis on Y).
|
|
109
|
+
*/
|
|
110
|
+
orientation?: "vertical" | "horizontal";
|
|
111
|
+
/** Gap between bar groups as a percent string ("20%") or pixel number. */
|
|
112
|
+
barCategoryGap?: number | string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type AreaChartProps = CartesianChartBaseProps & {
|
|
116
|
+
type: "area";
|
|
117
|
+
dataKey?: string;
|
|
118
|
+
series?: ChartSeries[];
|
|
119
|
+
/** Token or CSS color for single-series shorthand. */
|
|
120
|
+
color?: string;
|
|
121
|
+
seriesLabel?: string;
|
|
122
|
+
/** Show dots on data points. Default: false. */
|
|
123
|
+
showDots?: boolean;
|
|
124
|
+
/** Connect across null/undefined data gaps. Default: false. */
|
|
125
|
+
connectNulls?: boolean;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type LineChartProps = CartesianChartBaseProps & {
|
|
129
|
+
type: "line";
|
|
130
|
+
dataKey?: string;
|
|
131
|
+
series?: ChartSeries[];
|
|
132
|
+
/** Token or CSS color for single-series shorthand. */
|
|
133
|
+
color?: string;
|
|
134
|
+
seriesLabel?: string;
|
|
135
|
+
curveType?: "linear" | "monotone" | "step";
|
|
136
|
+
/** Show dots on data points. Default: false. */
|
|
137
|
+
showDots?: boolean;
|
|
138
|
+
/** Connect across null/undefined data gaps. Default: false. */
|
|
139
|
+
connectNulls?: boolean;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type ComposedChartProps = CartesianChartBaseProps & {
|
|
143
|
+
type: "composed";
|
|
144
|
+
series: ChartSeries[];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type DonutChartProps = ChartBaseProps & {
|
|
148
|
+
type: "donut";
|
|
149
|
+
data: ChartDatum[];
|
|
150
|
+
dataKey: string;
|
|
151
|
+
nameKey: string;
|
|
152
|
+
/** Map of normalized segment label → token/color. */
|
|
153
|
+
colors?: ChartColorMap;
|
|
154
|
+
centerContent?: ReactNode;
|
|
155
|
+
/** Show color legend below the chart. Default: true. */
|
|
156
|
+
showLegend?: boolean;
|
|
157
|
+
innerRadius?: number;
|
|
158
|
+
outerRadius?: number;
|
|
159
|
+
paddingAngle?: number;
|
|
160
|
+
tooltipValueFormatter?: TooltipValueFormatter;
|
|
161
|
+
tooltipLabelFormatter?: TooltipLabelFormatter;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export type RadialBarChartProps = ChartBaseProps & {
|
|
165
|
+
type: "radialbar";
|
|
166
|
+
data: ChartDatum[];
|
|
167
|
+
dataKey: string;
|
|
168
|
+
nameKey: string;
|
|
169
|
+
/** Map of normalized item label → token/color. */
|
|
170
|
+
colors?: ChartColorMap;
|
|
171
|
+
/** Show color legend below the chart. Default: true. */
|
|
172
|
+
showLegend?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Domain maximum - all bars scale relative to this value.
|
|
175
|
+
* Defaults to the maximum value found in the dataset.
|
|
176
|
+
*/
|
|
177
|
+
maxValue?: number;
|
|
178
|
+
tooltipValueFormatter?: TooltipValueFormatter;
|
|
179
|
+
tooltipLabelFormatter?: TooltipLabelFormatter;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type ScatterChartProps = ChartBaseProps & {
|
|
183
|
+
type: "scatter";
|
|
184
|
+
series: ScatterSeries[];
|
|
185
|
+
/** Data key used for the X axis across all series. */
|
|
186
|
+
xKey: string;
|
|
187
|
+
/** Data key used for the Y axis across all series. */
|
|
188
|
+
yKey: string;
|
|
189
|
+
/** Optional data key for bubble size (Z axis). */
|
|
190
|
+
zKey?: string;
|
|
191
|
+
showGrid?: boolean;
|
|
192
|
+
showXAxis?: boolean;
|
|
193
|
+
showYAxis?: boolean;
|
|
194
|
+
showTooltip?: boolean;
|
|
195
|
+
/** Show color legend below the chart when there are multiple series. Default: false. */
|
|
196
|
+
showLegend?: boolean;
|
|
197
|
+
allowDecimals?: boolean;
|
|
198
|
+
xTickFormatter?: AxisFormatter;
|
|
199
|
+
yTickFormatter?: AxisFormatter;
|
|
200
|
+
tooltipValueFormatter?: TooltipValueFormatter;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export type ChartProps =
|
|
204
|
+
| BarChartProps
|
|
205
|
+
| AreaChartProps
|
|
206
|
+
| LineChartProps
|
|
207
|
+
| ComposedChartProps
|
|
208
|
+
| DonutChartProps
|
|
209
|
+
| RadialBarChartProps
|
|
210
|
+
| ScatterChartProps;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CHART_COLOR_TOKENS } from "./types";
|
|
2
|
+
|
|
3
|
+
const SERIES_COLOR_ORDER = [
|
|
4
|
+
"primary",
|
|
5
|
+
"secondary",
|
|
6
|
+
"tertiary",
|
|
7
|
+
"quaternary",
|
|
8
|
+
"quinary",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
const FALLBACK_COLORS = {
|
|
12
|
+
primary: "#3b82f6",
|
|
13
|
+
secondary: "#5f97f7",
|
|
14
|
+
tertiary: "#82adf9",
|
|
15
|
+
quaternary: "#a4c2fb",
|
|
16
|
+
quinary: "#6d79e6",
|
|
17
|
+
muted: "#6b7280",
|
|
18
|
+
grid: "#e5e7eb",
|
|
19
|
+
cursor: "rgba(148, 163, 184, 0.20)",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Accepts either a CSS variable token (e.g. "--color-chart-primary") or a raw CSS color.
|
|
24
|
+
* Falls back safely when tokens are missing.
|
|
25
|
+
*/
|
|
26
|
+
export const resolveColorToken = (tokenOrColor: string | undefined, fallback: string): string => {
|
|
27
|
+
if (!tokenOrColor) return fallback;
|
|
28
|
+
|
|
29
|
+
const value = tokenOrColor.trim();
|
|
30
|
+
if (!value) return fallback;
|
|
31
|
+
|
|
32
|
+
if (!value.startsWith("--")) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Use CSS variable references directly so theme changes update chart colors
|
|
37
|
+
// even when Storybook toggles theme tokens without triggering a React rerender.
|
|
38
|
+
return `var(${value}, ${fallback})`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const createChartPalette = () => ({
|
|
42
|
+
primary: resolveColorToken(CHART_COLOR_TOKENS.primary, FALLBACK_COLORS.primary),
|
|
43
|
+
secondary: resolveColorToken(CHART_COLOR_TOKENS.secondary, FALLBACK_COLORS.secondary),
|
|
44
|
+
tertiary: resolveColorToken(CHART_COLOR_TOKENS.tertiary, FALLBACK_COLORS.tertiary),
|
|
45
|
+
quaternary: resolveColorToken(CHART_COLOR_TOKENS.quaternary, FALLBACK_COLORS.quaternary),
|
|
46
|
+
quinary: resolveColorToken(CHART_COLOR_TOKENS.quinary, FALLBACK_COLORS.quinary),
|
|
47
|
+
muted: resolveColorToken(CHART_COLOR_TOKENS.muted, FALLBACK_COLORS.muted),
|
|
48
|
+
grid: resolveColorToken(CHART_COLOR_TOKENS.grid, FALLBACK_COLORS.grid),
|
|
49
|
+
cursor: resolveColorToken(CHART_COLOR_TOKENS.cursor, FALLBACK_COLORS.cursor),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type ChartPalette = ReturnType<typeof createChartPalette>;
|
|
53
|
+
|
|
54
|
+
export const pickSeriesColor = (palette: ChartPalette, index: number): string => {
|
|
55
|
+
const tokenName = SERIES_COLOR_ORDER[index % SERIES_COLOR_ORDER.length];
|
|
56
|
+
return palette[tokenName];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const toLookupKey = (value: unknown): string => String(value ?? "").trim().toLowerCase();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.base {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.icon {
|
|
7
|
+
flex-shrink: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.preview {
|
|
11
|
+
max-width: 20ch;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
text-overflow: ellipsis;
|
|
14
|
+
white-space: nowrap;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.token_preview {
|
|
18
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas,
|
|
19
|
+
"Liberation Mono", "Courier New", monospace;
|
|
20
|
+
letter-spacing: 0.01em;
|
|
21
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Button } from "../button/button";
|
|
4
|
+
import { IconCopy, IconCheck } from "@tabler/icons-react";
|
|
5
|
+
import type { ButtonLayout, ButtonSize, ButtonVariant } from "../button/button";
|
|
6
|
+
import s from "./copy-button.module.css";
|
|
7
|
+
|
|
8
|
+
export type CopyPreview = "none" | "truncate" | "mask-token";
|
|
9
|
+
|
|
10
|
+
export type CopyButtonProps = {
|
|
11
|
+
value: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
buttonVariant?: ButtonVariant;
|
|
14
|
+
buttonSize?: ButtonSize;
|
|
15
|
+
buttonLayout?: ButtonLayout;
|
|
16
|
+
variant?: ButtonVariant;
|
|
17
|
+
size?: ButtonSize;
|
|
18
|
+
layout?: ButtonLayout;
|
|
19
|
+
preview?: CopyPreview;
|
|
20
|
+
previewMaxLength?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const maskToken = (raw: string): string => {
|
|
24
|
+
const value = raw.trim();
|
|
25
|
+
if (value.length <= 8) return "*".repeat(Math.max(value.length, 4));
|
|
26
|
+
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const previewValue = (value: string, preview: CopyPreview, maxLength: number): string | null => {
|
|
30
|
+
if (preview === "none") return null;
|
|
31
|
+
if (preview === "mask-token") return maskToken(value);
|
|
32
|
+
if (value.length <= maxLength) return value;
|
|
33
|
+
return `${value.slice(0, maxLength)}...`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function CopyButton({
|
|
37
|
+
value,
|
|
38
|
+
className,
|
|
39
|
+
buttonVariant,
|
|
40
|
+
buttonSize,
|
|
41
|
+
buttonLayout,
|
|
42
|
+
variant,
|
|
43
|
+
size,
|
|
44
|
+
layout,
|
|
45
|
+
preview = "none",
|
|
46
|
+
previewMaxLength = 24,
|
|
47
|
+
}: CopyButtonProps) {
|
|
48
|
+
const [copied, setCopied] = useState(false);
|
|
49
|
+
const timeoutRef = useRef<number | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return () => {
|
|
53
|
+
if (timeoutRef.current !== null) {
|
|
54
|
+
window.clearTimeout(timeoutRef.current);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const copyWithFallback = async (text: string): Promise<boolean> => {
|
|
60
|
+
try {
|
|
61
|
+
if (navigator.clipboard?.writeText) {
|
|
62
|
+
await navigator.clipboard.writeText(text);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// fallback below
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const area = document.createElement("textarea");
|
|
70
|
+
area.value = text;
|
|
71
|
+
area.setAttribute("readonly", "");
|
|
72
|
+
area.style.position = "fixed";
|
|
73
|
+
area.style.opacity = "0";
|
|
74
|
+
document.body.appendChild(area);
|
|
75
|
+
area.select();
|
|
76
|
+
const success = document.execCommand("copy");
|
|
77
|
+
document.body.removeChild(area);
|
|
78
|
+
return success;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleCopy = async () => {
|
|
82
|
+
const success = await copyWithFallback(value);
|
|
83
|
+
if (!success) return;
|
|
84
|
+
|
|
85
|
+
setCopied(true);
|
|
86
|
+
if (timeoutRef.current !== null) {
|
|
87
|
+
window.clearTimeout(timeoutRef.current);
|
|
88
|
+
}
|
|
89
|
+
timeoutRef.current = window.setTimeout(() => setCopied(false), 2000);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const previewText = previewValue(value, preview, previewMaxLength);
|
|
93
|
+
const hasPreview = previewText !== null;
|
|
94
|
+
const resolvedVariant = buttonVariant ?? variant ?? "bordered";
|
|
95
|
+
const resolvedSize = buttonSize ?? size ?? "default";
|
|
96
|
+
const resolvedLayout = buttonLayout ?? layout ?? (hasPreview ? "default" : "box");
|
|
97
|
+
const showPreview = hasPreview && resolvedLayout !== "box";
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Button
|
|
101
|
+
variant={resolvedVariant}
|
|
102
|
+
size={resolvedSize}
|
|
103
|
+
layout={resolvedLayout}
|
|
104
|
+
onClick={handleCopy}
|
|
105
|
+
type="button"
|
|
106
|
+
className={clsx(s.base, className)}
|
|
107
|
+
title={copied ? "Copied!" : "Copy to clipboard"}
|
|
108
|
+
aria-label={copied ? "Copied value" : "Copy value"}
|
|
109
|
+
>
|
|
110
|
+
{copied ? <IconCheck size={16} className={s.icon} /> : <IconCopy size={16} className={s.icon} />}
|
|
111
|
+
{showPreview && (
|
|
112
|
+
<span
|
|
113
|
+
className={clsx(
|
|
114
|
+
s.preview,
|
|
115
|
+
preview === "mask-token" && s.token_preview,
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{copied ? "Copied" : previewText}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
</Button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type Ref,
|
|
9
|
+
} from "react";
|
|
10
|
+
import clsx from "clsx";
|
|
11
|
+
import s from "./countdown.module.css";
|
|
12
|
+
|
|
13
|
+
export type CountdownFormat =
|
|
14
|
+
| "mm:ss"
|
|
15
|
+
| "ss"
|
|
16
|
+
| "hh:mm:ss"
|
|
17
|
+
| ((remaining: number) => ReactNode);
|
|
18
|
+
|
|
19
|
+
export type CountdownState = {
|
|
20
|
+
remaining: number;
|
|
21
|
+
isRunning: boolean;
|
|
22
|
+
isComplete: boolean;
|
|
23
|
+
formatted: ReactNode;
|
|
24
|
+
start: () => void;
|
|
25
|
+
pause: () => void;
|
|
26
|
+
reset: (newDuration?: number) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CountdownHandle = Pick<CountdownState, "start" | "pause" | "reset">;
|
|
30
|
+
|
|
31
|
+
export type CountdownProps = {
|
|
32
|
+
/** Initial duration in seconds. */
|
|
33
|
+
duration: number;
|
|
34
|
+
/** Display format. Default: "mm:ss". Pass a function for full control. */
|
|
35
|
+
format?: CountdownFormat;
|
|
36
|
+
/** Auto-start on mount. Default: true. */
|
|
37
|
+
autoStart?: boolean;
|
|
38
|
+
/** Fired every second with the remaining seconds (post-decrement). */
|
|
39
|
+
onTick?: (remaining: number) => void;
|
|
40
|
+
/** Fired once when remaining reaches 0. */
|
|
41
|
+
onComplete?: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Render-prop children. Called with the countdown state on every tick -
|
|
44
|
+
* lets you swap in any UI (e.g. a Button when complete) without needing
|
|
45
|
+
* a wrapper component. When omitted, the formatted time is rendered.
|
|
46
|
+
*/
|
|
47
|
+
children?: (state: CountdownState) => ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
style?: React.CSSProperties;
|
|
50
|
+
ref?: Ref<CountdownHandle>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const pad2 = (n: number) => String(n).padStart(2, "0");
|
|
54
|
+
|
|
55
|
+
const formatRemaining = (
|
|
56
|
+
remaining: number,
|
|
57
|
+
format: CountdownFormat,
|
|
58
|
+
): ReactNode => {
|
|
59
|
+
if (typeof format === "function") return format(remaining);
|
|
60
|
+
const seconds = remaining % 60;
|
|
61
|
+
const minutes = Math.floor(remaining / 60) % 60;
|
|
62
|
+
const hours = Math.floor(remaining / 3600);
|
|
63
|
+
switch (format) {
|
|
64
|
+
case "ss":
|
|
65
|
+
return String(remaining);
|
|
66
|
+
case "hh:mm:ss":
|
|
67
|
+
return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
|
|
68
|
+
case "mm:ss":
|
|
69
|
+
default:
|
|
70
|
+
return `${pad2(Math.floor(remaining / 60))}:${pad2(seconds)}`;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Countdown = ({
|
|
75
|
+
duration,
|
|
76
|
+
format = "mm:ss",
|
|
77
|
+
autoStart = true,
|
|
78
|
+
onTick,
|
|
79
|
+
onComplete,
|
|
80
|
+
children,
|
|
81
|
+
className,
|
|
82
|
+
style,
|
|
83
|
+
ref,
|
|
84
|
+
}: CountdownProps) => {
|
|
85
|
+
const [remaining, setRemaining] = useState(duration);
|
|
86
|
+
const [running, setRunning] = useState(autoStart);
|
|
87
|
+
|
|
88
|
+
// Latest-callback refs - keep the interval effect free of callback-identity
|
|
89
|
+
// deps so a parent re-render doesn't tear down and recreate the interval.
|
|
90
|
+
const onTickRef = useRef(onTick);
|
|
91
|
+
const onCompleteRef = useRef(onComplete);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
onTickRef.current = onTick;
|
|
94
|
+
onCompleteRef.current = onComplete;
|
|
95
|
+
}, [onTick, onComplete]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!running) return;
|
|
99
|
+
if (remaining <= 0) {
|
|
100
|
+
setRunning(false);
|
|
101
|
+
onCompleteRef.current?.();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const id = setInterval(() => {
|
|
105
|
+
setRemaining((r) => {
|
|
106
|
+
const next = r - 1;
|
|
107
|
+
onTickRef.current?.(next);
|
|
108
|
+
return next;
|
|
109
|
+
});
|
|
110
|
+
}, 1000);
|
|
111
|
+
return () => clearInterval(id);
|
|
112
|
+
}, [running, remaining]);
|
|
113
|
+
|
|
114
|
+
const start = useCallback(() => setRunning(true), []);
|
|
115
|
+
const pause = useCallback(() => setRunning(false), []);
|
|
116
|
+
const reset = useCallback(
|
|
117
|
+
(newDuration?: number) => {
|
|
118
|
+
setRemaining(newDuration ?? duration);
|
|
119
|
+
setRunning(true);
|
|
120
|
+
},
|
|
121
|
+
[duration],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
useImperativeHandle(ref, () => ({ start, pause, reset }), [start, pause, reset]);
|
|
125
|
+
|
|
126
|
+
if (typeof children === "function") {
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
{children({
|
|
130
|
+
remaining,
|
|
131
|
+
isRunning: running,
|
|
132
|
+
isComplete: remaining <= 0,
|
|
133
|
+
formatted: formatRemaining(remaining, format),
|
|
134
|
+
start,
|
|
135
|
+
pause,
|
|
136
|
+
reset,
|
|
137
|
+
})}
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<span
|
|
144
|
+
className={clsx(s.root, className)}
|
|
145
|
+
style={style}
|
|
146
|
+
role="timer"
|
|
147
|
+
aria-live="off"
|
|
148
|
+
>
|
|
149
|
+
{formatRemaining(remaining, format)}
|
|
150
|
+
</span>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
2
|
+
import { Input } from "../input-fields/input/input";
|
|
3
|
+
import s from "./data-table.module.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pre-built select-all / per-row checkbox column using `<Input type="checkbox">`.
|
|
7
|
+
* Handles indeterminate state on the header checkbox.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const cols = useMemo(
|
|
11
|
+
* () => [checkboxColumn<MyRow>(editingRowId), ...rest],
|
|
12
|
+
* [editingRowId]
|
|
13
|
+
* )
|
|
14
|
+
*/
|
|
15
|
+
export function checkboxColumn<TData extends { id: string }>(
|
|
16
|
+
editingRowId?: string | null,
|
|
17
|
+
): ColumnDef<TData> {
|
|
18
|
+
return {
|
|
19
|
+
id: "select",
|
|
20
|
+
// Compact fixed width keeps checkbox cells visually balanced while
|
|
21
|
+
// reserving enough room for focus ring rendering.
|
|
22
|
+
size: 42,
|
|
23
|
+
minSize: 42,
|
|
24
|
+
maxSize: 50,
|
|
25
|
+
enableSorting: false,
|
|
26
|
+
enableHiding: false,
|
|
27
|
+
enableResizing: false,
|
|
28
|
+
enableGrouping: false,
|
|
29
|
+
meta: {
|
|
30
|
+
align: "center",
|
|
31
|
+
truncate: false,
|
|
32
|
+
},
|
|
33
|
+
header: ({ table }) => (
|
|
34
|
+
<Input
|
|
35
|
+
type="checkbox"
|
|
36
|
+
checked={table.getIsAllRowsSelected()}
|
|
37
|
+
className={s.selection_checkbox}
|
|
38
|
+
disabled={editingRowId != null}
|
|
39
|
+
ref={(el: HTMLInputElement | null) => {
|
|
40
|
+
if (el)
|
|
41
|
+
el.indeterminate =
|
|
42
|
+
table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected();
|
|
43
|
+
}}
|
|
44
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
45
|
+
aria-label="Select all rows"
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
48
|
+
cell: ({ row }) => (
|
|
49
|
+
<Input
|
|
50
|
+
type="checkbox"
|
|
51
|
+
className={s.selection_checkbox}
|
|
52
|
+
checked={row.getIsSelected()}
|
|
53
|
+
disabled={!row.getCanSelect() || editingRowId != null}
|
|
54
|
+
onChange={row.getToggleSelectedHandler()}
|
|
55
|
+
aria-label={`Select row ${row.id}`}
|
|
56
|
+
/>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
}
|