@harismawan/stamp-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/GlobalStyles.d.ts +1 -0
- package/dist/GlobalStyles.js +96 -0
- package/dist/components/Accordion.d.ts +12 -0
- package/dist/components/Accordion.js +85 -0
- package/dist/components/Alert.d.ts +9 -0
- package/dist/components/Alert.js +88 -0
- package/dist/components/Avatar.d.ts +12 -0
- package/dist/components/Avatar.js +67 -0
- package/dist/components/Badge.d.ts +7 -0
- package/dist/components/Badge.js +40 -0
- package/dist/components/Breadcrumb.d.ts +10 -0
- package/dist/components/Breadcrumb.js +50 -0
- package/dist/components/Button.d.ts +6 -0
- package/dist/components/Button.js +95 -0
- package/dist/components/Card.d.ts +8 -0
- package/dist/components/Card.js +55 -0
- package/dist/components/Checkbox.d.ts +8 -0
- package/dist/components/Checkbox.js +49 -0
- package/dist/components/ColorPicker.d.ts +7 -0
- package/dist/components/ColorPicker.js +61 -0
- package/dist/components/ConfirmDialog.d.ts +9 -0
- package/dist/components/ConfirmDialog.js +58 -0
- package/dist/components/Divider.d.ts +6 -0
- package/dist/components/Divider.js +45 -0
- package/dist/components/Drawer.d.ts +10 -0
- package/dist/components/Drawer.js +92 -0
- package/dist/components/DropdownMenu.d.ts +20 -0
- package/dist/components/DropdownMenu.js +139 -0
- package/dist/components/EmptyState.d.ts +8 -0
- package/dist/components/EmptyState.js +43 -0
- package/dist/components/IconPicker.d.ts +7 -0
- package/dist/components/IconPicker.js +45 -0
- package/dist/components/Input.d.ts +6 -0
- package/dist/components/Input.js +63 -0
- package/dist/components/Modal.d.ts +8 -0
- package/dist/components/Modal.js +104 -0
- package/dist/components/NumberInput.d.ts +23 -0
- package/dist/components/NumberInput.js +83 -0
- package/dist/components/Pagination.d.ts +7 -0
- package/dist/components/Pagination.js +81 -0
- package/dist/components/Popover.d.ts +8 -0
- package/dist/components/Popover.js +39 -0
- package/dist/components/Progress.d.ts +9 -0
- package/dist/components/Progress.js +41 -0
- package/dist/components/Radio.d.ts +14 -0
- package/dist/components/Radio.js +72 -0
- package/dist/components/Skeleton.d.ts +34 -0
- package/dist/components/Skeleton.js +39 -0
- package/dist/components/Slider.d.ts +10 -0
- package/dist/components/Slider.js +47 -0
- package/dist/components/Spinner.d.ts +6 -0
- package/dist/components/Spinner.js +52 -0
- package/dist/components/Stat.d.ts +9 -0
- package/dist/components/Stat.js +44 -0
- package/dist/components/Stepper.d.ts +11 -0
- package/dist/components/Stepper.js +97 -0
- package/dist/components/Switch.d.ts +8 -0
- package/dist/components/Switch.js +58 -0
- package/dist/components/Table.d.ts +6 -0
- package/dist/components/Table.js +41 -0
- package/dist/components/Tabs.d.ts +21 -0
- package/dist/components/Tabs.js +174 -0
- package/dist/components/Tag.d.ts +6 -0
- package/dist/components/Tag.js +47 -0
- package/dist/components/Toast.d.ts +26 -0
- package/dist/components/Toast.js +74 -0
- package/dist/components/Tooltip.d.ts +8 -0
- package/dist/components/Tooltip.js +43 -0
- package/dist/components/layout/index.d.ts +56 -0
- package/dist/components/layout/index.js +72 -0
- package/dist/hooks/useClickOutside.d.ts +2 -0
- package/dist/hooks/useClickOutside.js +25 -0
- package/dist/hooks/useDisclosure.d.ts +8 -0
- package/dist/hooks/useDisclosure.js +9 -0
- package/dist/hooks/useThemeStore.d.ts +20 -0
- package/dist/hooks/useThemeStore.js +7 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +43 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +14 -0
- package/dist/theme.d.ts +193 -0
- package/dist/theme.js +156 -0
- package/package.json +48 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
const sizeMap = {
|
|
3
|
+
sm: css `
|
|
4
|
+
padding: 8px 14px;
|
|
5
|
+
font-size: 0.875rem;
|
|
6
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
7
|
+
`,
|
|
8
|
+
md: css `
|
|
9
|
+
padding: 11px 20px;
|
|
10
|
+
font-size: 0.9375rem;
|
|
11
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
12
|
+
`,
|
|
13
|
+
lg: css `
|
|
14
|
+
padding: 14px 26px;
|
|
15
|
+
font-size: 1rem;
|
|
16
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
17
|
+
`,
|
|
18
|
+
};
|
|
19
|
+
const stamp = css `
|
|
20
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
21
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
22
|
+
transition:
|
|
23
|
+
transform 80ms ${(p) => p.theme.easing.out},
|
|
24
|
+
box-shadow 80ms ${(p) => p.theme.easing.out},
|
|
25
|
+
background 80ms ${(p) => p.theme.easing.out};
|
|
26
|
+
|
|
27
|
+
&:hover:not(:disabled):not([aria-disabled='true']) {
|
|
28
|
+
transform: translate(2px, 2px);
|
|
29
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
30
|
+
}
|
|
31
|
+
&:active:not(:disabled):not([aria-disabled='true']) {
|
|
32
|
+
transform: translate(4px, 4px);
|
|
33
|
+
box-shadow: none;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const variantMap = {
|
|
37
|
+
primary: css `
|
|
38
|
+
${stamp};
|
|
39
|
+
background: ${(p) => p.theme.colors.primary};
|
|
40
|
+
color: ${(p) => p.theme.colors.primaryInk};
|
|
41
|
+
&:hover:not(:disabled):not([aria-disabled='true']) {
|
|
42
|
+
background: ${(p) => p.theme.colors.primaryHover};
|
|
43
|
+
color: ${(p) => p.theme.colors.primaryInk};
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
ghost: css `
|
|
47
|
+
background: transparent;
|
|
48
|
+
color: ${(p) => p.theme.colors.text};
|
|
49
|
+
border: 2px solid transparent;
|
|
50
|
+
&:hover:not(:disabled):not([aria-disabled='true']) {
|
|
51
|
+
background: ${(p) => p.theme.colors.surfaceMuted};
|
|
52
|
+
color: ${(p) => p.theme.colors.text};
|
|
53
|
+
}
|
|
54
|
+
`,
|
|
55
|
+
outline: css `
|
|
56
|
+
${stamp};
|
|
57
|
+
background: ${(p) => p.theme.colors.surface};
|
|
58
|
+
color: ${(p) => p.theme.colors.text};
|
|
59
|
+
&:hover:not(:disabled):not([aria-disabled='true']) {
|
|
60
|
+
color: ${(p) => p.theme.colors.text};
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
danger: css `
|
|
64
|
+
${stamp};
|
|
65
|
+
background: ${(p) => p.theme.colors.danger};
|
|
66
|
+
color: ${(p) => p.theme.colors.surface};
|
|
67
|
+
&:hover:not(:disabled):not([aria-disabled='true']) {
|
|
68
|
+
color: ${(p) => p.theme.colors.surface};
|
|
69
|
+
}
|
|
70
|
+
`,
|
|
71
|
+
};
|
|
72
|
+
export const Button = styled.button.attrs((p) => p.as === 'a'
|
|
73
|
+
? {}
|
|
74
|
+
: { type: p.type ?? 'button' }) `
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
gap: ${(p) => p.theme.space[2]};
|
|
79
|
+
font-family: inherit;
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
letter-spacing: -0.005em;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
${(p) => sizeMap[p.$size ?? 'md']};
|
|
85
|
+
${(p) => variantMap[p.$variant ?? 'primary']};
|
|
86
|
+
${(p) => p.$full && 'width: 100%;'}
|
|
87
|
+
|
|
88
|
+
&:disabled,
|
|
89
|
+
&[aria-disabled='true'] {
|
|
90
|
+
opacity: 0.55;
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
transform: none;
|
|
93
|
+
box-shadow: none;
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CardProps {
|
|
2
|
+
$hover?: boolean;
|
|
3
|
+
$accent?: boolean;
|
|
4
|
+
$flat?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare const Card: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof CardProps> & CardProps, never> & Partial<Pick<import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof CardProps> & CardProps, never>>> & string;
|
|
7
|
+
export declare const CardTitle: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>>> & string;
|
|
8
|
+
export declare const CardValue: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>>> & string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
export const Card = styled.div `
|
|
3
|
+
background: ${(p) => p.theme.colors.surface};
|
|
4
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
5
|
+
border-radius: ${(p) => p.theme.radii.lg};
|
|
6
|
+
padding: ${(p) => p.theme.space[6]};
|
|
7
|
+
min-width: 0;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
container-type: inline-size;
|
|
10
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
11
|
+
transition:
|
|
12
|
+
transform 120ms ${(p) => p.theme.easing.out},
|
|
13
|
+
box-shadow 120ms ${(p) => p.theme.easing.out};
|
|
14
|
+
|
|
15
|
+
${(p) => p.$hover &&
|
|
16
|
+
`
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
&:hover {
|
|
19
|
+
transform: translate(-1px, -1px);
|
|
20
|
+
box-shadow: ${p.theme.shadow.stampLg};
|
|
21
|
+
}
|
|
22
|
+
`}
|
|
23
|
+
|
|
24
|
+
${(p) => p.$accent &&
|
|
25
|
+
`
|
|
26
|
+
background: ${p.theme.colors.primary};
|
|
27
|
+
color: ${p.theme.colors.primaryInk};
|
|
28
|
+
`}
|
|
29
|
+
|
|
30
|
+
${(p) => p.$flat &&
|
|
31
|
+
`
|
|
32
|
+
box-shadow: none;
|
|
33
|
+
`}
|
|
34
|
+
`;
|
|
35
|
+
export const CardTitle = styled.div `
|
|
36
|
+
font-size: 0.8125rem;
|
|
37
|
+
font-weight: 700;
|
|
38
|
+
letter-spacing: 0.06em;
|
|
39
|
+
text-transform: uppercase;
|
|
40
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
41
|
+
margin-bottom: ${(p) => p.theme.space[2]};
|
|
42
|
+
`;
|
|
43
|
+
export const CardValue = styled.div `
|
|
44
|
+
font-family: ${(p) => p.theme.font.mono};
|
|
45
|
+
font-size: clamp(1.25rem, 2.4cqi + 0.6rem, 1.75rem);
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
color: ${(p) => p.theme.colors.text};
|
|
48
|
+
font-variant-numeric: tabular-nums;
|
|
49
|
+
font-feature-settings: 'tnum' 1;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
max-width: 100%;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface CheckboxProps extends Omit<React.ComponentPropsWithoutRef<'input'>, 'onChange' | 'type'> {
|
|
3
|
+
checked: boolean;
|
|
4
|
+
onChange: (checked: boolean) => void;
|
|
5
|
+
label?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { Check } from 'lucide-react';
|
|
5
|
+
const Root = styled.label `
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: ${(p) => p.theme.space[2]};
|
|
9
|
+
font-family: ${(p) => p.theme.font.body};
|
|
10
|
+
font-weight: 700;
|
|
11
|
+
color: ${(p) => p.theme.colors.text};
|
|
12
|
+
cursor: ${(p) => (p.$disabled ? 'not-allowed' : 'pointer')};
|
|
13
|
+
opacity: ${(p) => (p.$disabled ? 0.55 : 1)};
|
|
14
|
+
user-select: none;
|
|
15
|
+
`;
|
|
16
|
+
const HiddenInput = styled.input `
|
|
17
|
+
position: absolute;
|
|
18
|
+
width: 1px;
|
|
19
|
+
height: 1px;
|
|
20
|
+
padding: 0;
|
|
21
|
+
margin: -1px;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
clip: rect(0 0 0 0);
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
border: 0;
|
|
26
|
+
`;
|
|
27
|
+
const Box = styled.span `
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
width: 22px;
|
|
32
|
+
height: 22px;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
35
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
36
|
+
background: ${(p) => (p.$checked ? p.theme.colors.primary : p.theme.colors.surface)};
|
|
37
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
38
|
+
color: ${(p) => p.theme.colors.primaryInk};
|
|
39
|
+
transition: background 80ms ${(p) => p.theme.easing.out};
|
|
40
|
+
|
|
41
|
+
${HiddenInput}:focus-visible + & {
|
|
42
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
43
|
+
outline-offset: 2px;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
export const Checkbox = React.forwardRef(({ checked, onChange, label, disabled, ...rest }, ref) => {
|
|
47
|
+
return (_jsxs(Root, { "$disabled": disabled, children: [_jsx(HiddenInput, { ref: ref, type: "checkbox", checked: checked, disabled: disabled, onChange: (e) => onChange(e.target.checked), ...rest }), _jsx(Box, { "$checked": checked, "$disabled": disabled, "aria-hidden": "true", children: checked ? _jsx(Check, { size: 16, strokeWidth: 3 }) : null }), label != null ? _jsx("span", { children: label }) : null] }));
|
|
48
|
+
});
|
|
49
|
+
Checkbox.displayName = 'Checkbox';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const DEFAULT_SWATCHES: string[];
|
|
2
|
+
export interface ColorPickerProps {
|
|
3
|
+
value?: string;
|
|
4
|
+
onChange: (hex: string) => void;
|
|
5
|
+
swatches?: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function ColorPicker({ value, onChange, swatches }: ColorPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Check } from 'lucide-react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
export const DEFAULT_SWATCHES = [
|
|
5
|
+
'#111111',
|
|
6
|
+
'#6B7280',
|
|
7
|
+
'#EF4444',
|
|
8
|
+
'#F59E0B',
|
|
9
|
+
'#10B981',
|
|
10
|
+
'#3B82F6',
|
|
11
|
+
'#8B5CF6',
|
|
12
|
+
'#EC4899',
|
|
13
|
+
];
|
|
14
|
+
// Relative luminance (WCAG-ish) of a hex color, 0 (black) .. 1 (white).
|
|
15
|
+
// Used to pick a contrasting check-mark color on each swatch.
|
|
16
|
+
function luminance(hex) {
|
|
17
|
+
const m = hex.replace('#', '');
|
|
18
|
+
const full = m.length === 3
|
|
19
|
+
? m
|
|
20
|
+
.split('')
|
|
21
|
+
.map((c) => c + c)
|
|
22
|
+
.join('')
|
|
23
|
+
: m.padEnd(6, '0').slice(0, 6);
|
|
24
|
+
const r = parseInt(full.slice(0, 2), 16) / 255;
|
|
25
|
+
const g = parseInt(full.slice(2, 4), 16) / 255;
|
|
26
|
+
const b = parseInt(full.slice(4, 6), 16) / 255;
|
|
27
|
+
const lin = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
28
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
29
|
+
}
|
|
30
|
+
// Dark swatches get a white check; light swatches get a near-black check so the
|
|
31
|
+
// 'selected' indicator stays visible regardless of the swatch color.
|
|
32
|
+
function checkColor(hex) {
|
|
33
|
+
return luminance(hex) < 0.5 ? '#fff' : '#111';
|
|
34
|
+
}
|
|
35
|
+
const Grid = styled.div `
|
|
36
|
+
display: grid;
|
|
37
|
+
grid-template-columns: repeat(6, 1fr);
|
|
38
|
+
gap: ${(p) => p.theme.space[2]};
|
|
39
|
+
`;
|
|
40
|
+
const Swatch = styled.button `
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
height: 40px;
|
|
45
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
46
|
+
background: ${(p) => p.$bg};
|
|
47
|
+
border: ${(p) => (p.$active ? '3px' : '2px')} solid ${(p) => p.theme.colors.border};
|
|
48
|
+
color: ${(p) => p.$check};
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
transition: transform 80ms ${(p) => p.theme.easing.out};
|
|
51
|
+
&:hover {
|
|
52
|
+
transform: translate(-1px, -1px);
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
export function ColorPicker({ value, onChange, swatches = DEFAULT_SWATCHES }) {
|
|
56
|
+
const cur = (value ?? '').toUpperCase();
|
|
57
|
+
return (_jsx(Grid, { children: swatches.map((hex) => {
|
|
58
|
+
const active = cur === hex.toUpperCase();
|
|
59
|
+
return (_jsx(Swatch, { type: "button", "$bg": hex, "$active": active, "$check": checkColor(hex), "aria-pressed": active, "aria-label": hex, onClick: () => onChange(hex), children: active && _jsx(Check, { size: 16, strokeWidth: 2.6 }) }, hex));
|
|
60
|
+
}) }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ConfirmOptions {
|
|
2
|
+
title?: string;
|
|
3
|
+
message?: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
cancelLabel?: string;
|
|
6
|
+
destructive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function confirmDialog(opts?: ConfirmOptions): Promise<boolean>;
|
|
9
|
+
export declare function ConfirmViewport(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangle } from 'lucide-react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { create } from 'zustand';
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { Modal } from './Modal';
|
|
7
|
+
const useConfirmStore = create((set, get) => ({
|
|
8
|
+
pending: null,
|
|
9
|
+
open: (opts) => new Promise((resolve) => {
|
|
10
|
+
const prev = get().pending;
|
|
11
|
+
if (prev)
|
|
12
|
+
prev.resolve(false);
|
|
13
|
+
set({ pending: { ...opts, resolve } });
|
|
14
|
+
}),
|
|
15
|
+
resolve: (result) => {
|
|
16
|
+
const p = get().pending;
|
|
17
|
+
if (!p)
|
|
18
|
+
return;
|
|
19
|
+
p.resolve(result);
|
|
20
|
+
set({ pending: null });
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
export function confirmDialog(opts = {}) {
|
|
24
|
+
return useConfirmStore.getState().open(opts);
|
|
25
|
+
}
|
|
26
|
+
const Body = styled.div `
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: 14px;
|
|
29
|
+
align-items: center;
|
|
30
|
+
margin-bottom: 20px;
|
|
31
|
+
`;
|
|
32
|
+
const IconWrap = styled.div `
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
width: 40px;
|
|
35
|
+
height: 40px;
|
|
36
|
+
display: grid;
|
|
37
|
+
place-items: center;
|
|
38
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
39
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
40
|
+
background: ${(p) => p.theme.colors.expenseSoft ?? p.theme.colors.surfaceMuted};
|
|
41
|
+
color: ${(p) => p.theme.colors.expense ?? p.theme.colors.text};
|
|
42
|
+
`;
|
|
43
|
+
const Message = styled.div `
|
|
44
|
+
font-size: 0.9375rem;
|
|
45
|
+
line-height: 1.5;
|
|
46
|
+
color: ${(p) => p.theme.colors.text};
|
|
47
|
+
`;
|
|
48
|
+
const Actions = styled.div `
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: flex-end;
|
|
51
|
+
gap: 12px;
|
|
52
|
+
`;
|
|
53
|
+
export function ConfirmViewport() {
|
|
54
|
+
const pending = useConfirmStore((s) => s.pending);
|
|
55
|
+
const resolve = useConfirmStore((s) => s.resolve);
|
|
56
|
+
const open = !!pending;
|
|
57
|
+
return (_jsxs(Modal, { open: open, onClose: () => resolve(false), title: pending?.title ?? 'Are you sure?', children: [_jsxs(Body, { children: [_jsx(IconWrap, { children: _jsx(AlertTriangle, { size: 20, strokeWidth: 2.5 }) }), _jsx(Message, { children: pending?.message ?? 'This action cannot be undone.' })] }), _jsxs(Actions, { children: [_jsx(Button, { type: "button", "$variant": "outline", onClick: () => resolve(false), children: pending?.cancelLabel ?? 'Cancel' }), _jsx(Button, { type: "button", "$variant": pending?.destructive === false ? 'primary' : 'danger', onClick: () => resolve(true), children: pending?.confirmLabel ?? 'Delete' })] })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface DividerProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
3
|
+
orientation?: 'horizontal' | 'vertical';
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const Divider: React.ForwardRefExoticComponent<DividerProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
const Vertical = styled.div `
|
|
5
|
+
align-self: stretch;
|
|
6
|
+
width: 0;
|
|
7
|
+
min-height: 16px;
|
|
8
|
+
border-left: 2px solid ${(p) => p.theme.colors.border};
|
|
9
|
+
`;
|
|
10
|
+
const Horizontal = styled.div `
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: ${(p) => p.theme.space[3]};
|
|
14
|
+
width: 100%;
|
|
15
|
+
|
|
16
|
+
&::before,
|
|
17
|
+
&::after {
|
|
18
|
+
content: '';
|
|
19
|
+
flex: 1;
|
|
20
|
+
border-top: 2px solid ${(p) => p.theme.colors.border};
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
const PlainHorizontal = styled.div `
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 0;
|
|
26
|
+
border-top: 2px solid ${(p) => p.theme.colors.border};
|
|
27
|
+
`;
|
|
28
|
+
const Label = styled.span `
|
|
29
|
+
font-family: ${(p) => p.theme.font.body};
|
|
30
|
+
font-size: 12px;
|
|
31
|
+
font-weight: 800;
|
|
32
|
+
letter-spacing: 0.06em;
|
|
33
|
+
text-transform: uppercase;
|
|
34
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
35
|
+
`;
|
|
36
|
+
export const Divider = React.forwardRef(({ orientation = 'horizontal', label, ...rest }, ref) => {
|
|
37
|
+
if (orientation === 'vertical') {
|
|
38
|
+
return _jsx(Vertical, { ref: ref, role: "separator", "aria-orientation": "vertical", ...rest });
|
|
39
|
+
}
|
|
40
|
+
if (label != null) {
|
|
41
|
+
return (_jsx(Horizontal, { ref: ref, role: "separator", "aria-orientation": "horizontal", ...rest, children: _jsx(Label, { children: label }) }));
|
|
42
|
+
}
|
|
43
|
+
return _jsx(PlainHorizontal, { ref: ref, role: "separator", "aria-orientation": "horizontal", ...rest });
|
|
44
|
+
});
|
|
45
|
+
Divider.displayName = 'Divider';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type DrawerSide = 'left' | 'right' | 'top' | 'bottom';
|
|
3
|
+
export interface DrawerProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
side?: DrawerSide;
|
|
7
|
+
title?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function Drawer({ open, onClose, side, title, children }: DrawerProps): React.ReactPortal | null;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
const Overlay = styled.div `
|
|
6
|
+
position: fixed;
|
|
7
|
+
inset: 0;
|
|
8
|
+
background: ${(p) => p.theme.colors.overlay};
|
|
9
|
+
z-index: 1100;
|
|
10
|
+
display: flex;
|
|
11
|
+
`;
|
|
12
|
+
const Panel = styled.div `
|
|
13
|
+
position: fixed;
|
|
14
|
+
background: ${(p) => p.theme.colors.surface};
|
|
15
|
+
color: ${(p) => p.theme.colors.text};
|
|
16
|
+
font-family: ${(p) => p.theme.font.body};
|
|
17
|
+
box-shadow: ${(p) => p.theme.shadow.stampLg};
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
z-index: 1101;
|
|
21
|
+
outline: none;
|
|
22
|
+
transition: transform 80ms ${(p) => p.theme.easing.out};
|
|
23
|
+
|
|
24
|
+
${(p) => {
|
|
25
|
+
switch (p.$side) {
|
|
26
|
+
case 'left':
|
|
27
|
+
return `
|
|
28
|
+
top: 0; left: 0; bottom: 0;
|
|
29
|
+
width: min(360px, 90vw);
|
|
30
|
+
border-right: 2px solid ${p.theme.colors.border};
|
|
31
|
+
`;
|
|
32
|
+
case 'top':
|
|
33
|
+
return `
|
|
34
|
+
top: 0; left: 0; right: 0;
|
|
35
|
+
height: min(360px, 90vh);
|
|
36
|
+
border-bottom: 2px solid ${p.theme.colors.border};
|
|
37
|
+
`;
|
|
38
|
+
case 'bottom':
|
|
39
|
+
return `
|
|
40
|
+
bottom: 0; left: 0; right: 0;
|
|
41
|
+
height: min(360px, 90vh);
|
|
42
|
+
border-top: 2px solid ${p.theme.colors.border};
|
|
43
|
+
`;
|
|
44
|
+
case 'right':
|
|
45
|
+
default:
|
|
46
|
+
return `
|
|
47
|
+
top: 0; right: 0; bottom: 0;
|
|
48
|
+
width: min(360px, 90vw);
|
|
49
|
+
border-left: 2px solid ${p.theme.colors.border};
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
`;
|
|
54
|
+
const Header = styled.div `
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
padding: ${(p) => p.theme.space[4]};
|
|
58
|
+
border-bottom: 2px solid ${(p) => p.theme.colors.border};
|
|
59
|
+
font-weight: 800;
|
|
60
|
+
font-size: 18px;
|
|
61
|
+
`;
|
|
62
|
+
const Body = styled.div `
|
|
63
|
+
padding: ${(p) => p.theme.space[4]};
|
|
64
|
+
overflow: auto;
|
|
65
|
+
flex: 1;
|
|
66
|
+
`;
|
|
67
|
+
export function Drawer({ open, onClose, side = 'right', title, children }) {
|
|
68
|
+
const panelRef = React.useRef(null);
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (!open)
|
|
71
|
+
return;
|
|
72
|
+
const onKeyDown = (e) => {
|
|
73
|
+
if (e.key === 'Escape') {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
onClose();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
document.addEventListener('keydown', onKeyDown);
|
|
79
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
80
|
+
}, [open, onClose]);
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
if (open && panelRef.current) {
|
|
83
|
+
panelRef.current.focus();
|
|
84
|
+
}
|
|
85
|
+
}, [open]);
|
|
86
|
+
if (!open)
|
|
87
|
+
return null;
|
|
88
|
+
return createPortal(_jsx(Overlay, { "data-testid": "drawer-overlay", onMouseDown: (e) => {
|
|
89
|
+
if (e.target === e.currentTarget)
|
|
90
|
+
onClose();
|
|
91
|
+
}, children: _jsxs(Panel, { ref: panelRef, role: "dialog", "aria-modal": "true", "aria-label": title, "data-side": side, "$side": side, tabIndex: -1, onMouseDown: (e) => e.stopPropagation(), children: [title && _jsx(Header, { children: title }), _jsx(Body, { children: children })] }) }), document.body);
|
|
92
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type Placement } from '@floating-ui/react';
|
|
3
|
+
export interface MenuProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
placement?: Placement;
|
|
6
|
+
}
|
|
7
|
+
export declare function Menu({ children, placement }: MenuProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export interface MenuButtonProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare const MenuButton: React.ForwardRefExoticComponent<MenuButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
12
|
+
export interface MenuListProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export declare function MenuList({ children }: MenuListProps): import("react/jsx-runtime").JSX.Element | null;
|
|
16
|
+
export interface MenuItemProps {
|
|
17
|
+
onSelect: () => void;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function MenuItem({ onSelect, children }: MenuItemProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { useFloating, autoUpdate, offset, flip, shift, useClick, useDismiss, useRole, useListNavigation, useInteractions, useListItem, FloatingList, FloatingPortal, FloatingFocusManager, } from '@floating-ui/react';
|
|
5
|
+
const MenuContext = React.createContext(null);
|
|
6
|
+
function useMenuContext() {
|
|
7
|
+
const ctx = React.useContext(MenuContext);
|
|
8
|
+
if (!ctx) {
|
|
9
|
+
throw new Error('DropdownMenu compound components must be used within <Menu>');
|
|
10
|
+
}
|
|
11
|
+
return ctx;
|
|
12
|
+
}
|
|
13
|
+
export function Menu({ children, placement = 'bottom-start' }) {
|
|
14
|
+
const [open, setOpen] = React.useState(false);
|
|
15
|
+
const [activeIndex, setActiveIndex] = React.useState(null);
|
|
16
|
+
const listRef = React.useRef([]);
|
|
17
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
18
|
+
open,
|
|
19
|
+
onOpenChange: setOpen,
|
|
20
|
+
placement,
|
|
21
|
+
whileElementsMounted: autoUpdate,
|
|
22
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
23
|
+
});
|
|
24
|
+
const click = useClick(context);
|
|
25
|
+
const dismiss = useDismiss(context);
|
|
26
|
+
const role = useRole(context, { role: 'menu' });
|
|
27
|
+
const listNavigation = useListNavigation(context, {
|
|
28
|
+
listRef,
|
|
29
|
+
activeIndex,
|
|
30
|
+
onNavigate: setActiveIndex,
|
|
31
|
+
loop: true,
|
|
32
|
+
});
|
|
33
|
+
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
34
|
+
click,
|
|
35
|
+
dismiss,
|
|
36
|
+
role,
|
|
37
|
+
listNavigation,
|
|
38
|
+
]);
|
|
39
|
+
const value = React.useMemo(() => ({
|
|
40
|
+
open,
|
|
41
|
+
setOpen,
|
|
42
|
+
activeIndex,
|
|
43
|
+
setActiveIndex,
|
|
44
|
+
getReferenceProps,
|
|
45
|
+
getFloatingProps,
|
|
46
|
+
getItemProps,
|
|
47
|
+
refs,
|
|
48
|
+
floatingStyles,
|
|
49
|
+
context,
|
|
50
|
+
listRef,
|
|
51
|
+
}), [open, activeIndex, getReferenceProps, getFloatingProps, getItemProps, refs, floatingStyles, context]);
|
|
52
|
+
return _jsx(MenuContext.Provider, { value: value, children: children });
|
|
53
|
+
}
|
|
54
|
+
const TriggerButton = styled.button `
|
|
55
|
+
font-family: ${(p) => p.theme.font.body};
|
|
56
|
+
font-weight: 800;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
color: ${(p) => p.theme.colors.text};
|
|
59
|
+
background: ${(p) => p.theme.colors.surface};
|
|
60
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
61
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
62
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
63
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
transition: transform 80ms ${(p) => p.theme.easing.out},
|
|
66
|
+
box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
67
|
+
|
|
68
|
+
&:hover:not(:disabled) {
|
|
69
|
+
transform: translate(2px, 2px);
|
|
70
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
71
|
+
}
|
|
72
|
+
&:active:not(:disabled) {
|
|
73
|
+
transform: translate(4px, 4px);
|
|
74
|
+
box-shadow: ${(p) => p.theme.shadow.none};
|
|
75
|
+
}
|
|
76
|
+
&:disabled {
|
|
77
|
+
opacity: 0.55;
|
|
78
|
+
cursor: not-allowed;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
export const MenuButton = React.forwardRef(function MenuButton({ children, ...rest }, ref) {
|
|
82
|
+
const { refs, getReferenceProps } = useMenuContext();
|
|
83
|
+
return (_jsx(TriggerButton, { type: "button", ref: (node) => {
|
|
84
|
+
refs.setReference(node);
|
|
85
|
+
if (typeof ref === 'function')
|
|
86
|
+
ref(node);
|
|
87
|
+
else if (ref)
|
|
88
|
+
ref.current = node;
|
|
89
|
+
}, ...getReferenceProps(rest), children: children }));
|
|
90
|
+
});
|
|
91
|
+
const List = styled.div `
|
|
92
|
+
background: ${(p) => p.theme.colors.surface};
|
|
93
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
94
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
95
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
96
|
+
padding: ${(p) => p.theme.space[1]};
|
|
97
|
+
min-width: 180px;
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: ${(p) => p.theme.space[1]};
|
|
101
|
+
z-index: 1000;
|
|
102
|
+
outline: none;
|
|
103
|
+
`;
|
|
104
|
+
export function MenuList({ children }) {
|
|
105
|
+
const { open, refs, floatingStyles, context, getFloatingProps, listRef, setActiveIndex } = useMenuContext();
|
|
106
|
+
if (!open)
|
|
107
|
+
return null;
|
|
108
|
+
return (_jsx(FloatingPortal, { children: _jsx(FloatingFocusManager, { context: context, modal: false, children: _jsx(List, { ref: refs.setFloating, style: floatingStyles, ...getFloatingProps(), children: _jsx(FloatingList, { elementsRef: listRef, children: children }) }) }) }));
|
|
109
|
+
}
|
|
110
|
+
const Item = styled.div `
|
|
111
|
+
font-family: ${(p) => p.theme.font.body};
|
|
112
|
+
font-weight: 700;
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
color: ${(p) => p.theme.colors.text};
|
|
115
|
+
background: ${(p) => (p.$active ? p.theme.colors.primarySoft : 'transparent')};
|
|
116
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
117
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
user-select: none;
|
|
120
|
+
outline: none;
|
|
121
|
+
`;
|
|
122
|
+
export function MenuItem({ onSelect, children }) {
|
|
123
|
+
const { activeIndex, getItemProps, setOpen } = useMenuContext();
|
|
124
|
+
const { ref, index } = useListItem();
|
|
125
|
+
const isActive = activeIndex === index;
|
|
126
|
+
const handleSelect = () => {
|
|
127
|
+
onSelect();
|
|
128
|
+
setOpen(false);
|
|
129
|
+
};
|
|
130
|
+
return (_jsx(Item, { role: "menuitem", tabIndex: isActive ? 0 : -1, "$active": isActive, ref: ref, ...getItemProps({
|
|
131
|
+
onClick: handleSelect,
|
|
132
|
+
onKeyDown: (e) => {
|
|
133
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
handleSelect();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
}), children: children }));
|
|
139
|
+
}
|