@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,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface EmptyStateProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
3
|
+
icon?: React.ReactNode;
|
|
4
|
+
title: React.ReactNode;
|
|
5
|
+
description?: React.ReactNode;
|
|
6
|
+
action?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare const EmptyState: React.FC<EmptyStateProps>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
const Root = styled.div `
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
text-align: center;
|
|
9
|
+
gap: ${(p) => p.theme.space[3]};
|
|
10
|
+
padding: ${(p) => p.theme.space[8]} ${(p) => p.theme.space[5]};
|
|
11
|
+
font-family: ${(p) => p.theme.font.body};
|
|
12
|
+
`;
|
|
13
|
+
const IconWrap = styled.div `
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
width: 56px;
|
|
18
|
+
height: 56px;
|
|
19
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
20
|
+
background: ${(p) => p.theme.colors.surfaceMuted};
|
|
21
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
22
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
23
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
24
|
+
`;
|
|
25
|
+
const Title = styled.h3 `
|
|
26
|
+
margin: 0;
|
|
27
|
+
font-size: 18px;
|
|
28
|
+
font-weight: 800;
|
|
29
|
+
color: ${(p) => p.theme.colors.text};
|
|
30
|
+
`;
|
|
31
|
+
const Description = styled.p `
|
|
32
|
+
margin: 0;
|
|
33
|
+
max-width: 42ch;
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
37
|
+
`;
|
|
38
|
+
const ActionWrap = styled.div `
|
|
39
|
+
margin-top: ${(p) => p.theme.space[1]};
|
|
40
|
+
`;
|
|
41
|
+
export const EmptyState = ({ icon, title, description, action, ...rest }) => {
|
|
42
|
+
return (_jsxs(Root, { ...rest, children: [icon != null ? _jsx(IconWrap, { "aria-hidden": "true", children: icon }) : null, _jsx(Title, { children: title }), description != null ? _jsx(Description, { children: description }) : null, action != null ? _jsx(ActionWrap, { children: action }) : null] }));
|
|
43
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const DEFAULT_ICONS: string[];
|
|
2
|
+
export interface IconPickerProps {
|
|
3
|
+
value?: string;
|
|
4
|
+
onChange: (name: string) => void;
|
|
5
|
+
icons?: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function IconPicker({ value, onChange, icons }: IconPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as LucideIcons from 'lucide-react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
export const DEFAULT_ICONS = [
|
|
5
|
+
'Star',
|
|
6
|
+
'Heart',
|
|
7
|
+
'Tag',
|
|
8
|
+
'Folder',
|
|
9
|
+
'Bookmark',
|
|
10
|
+
'Bell',
|
|
11
|
+
'Flag',
|
|
12
|
+
'Home',
|
|
13
|
+
'Settings',
|
|
14
|
+
'Zap',
|
|
15
|
+
'Smile',
|
|
16
|
+
'Globe',
|
|
17
|
+
];
|
|
18
|
+
const Grid = styled.div `
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-template-columns: repeat(6, 1fr);
|
|
21
|
+
gap: ${(p) => p.theme.space[2]};
|
|
22
|
+
`;
|
|
23
|
+
const Tile = styled.button `
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
height: 40px;
|
|
28
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
29
|
+
background: ${(p) => (p.$active ? p.theme.colors.primary : p.theme.colors.surface)};
|
|
30
|
+
border: ${(p) => (p.$active ? '3px' : '2px')} solid ${(p) => p.theme.colors.border};
|
|
31
|
+
color: ${(p) => p.theme.colors.text};
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: transform 80ms ${(p) => p.theme.easing.out};
|
|
34
|
+
&:hover {
|
|
35
|
+
transform: translate(-1px, -1px);
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
const registry = LucideIcons;
|
|
39
|
+
export function IconPicker({ value, onChange, icons = DEFAULT_ICONS }) {
|
|
40
|
+
return (_jsx(Grid, { children: icons.map((name) => {
|
|
41
|
+
const Icon = registry[name] ?? registry.CircleAlert;
|
|
42
|
+
const active = value === name;
|
|
43
|
+
return (_jsx(Tile, { type: "button", "$active": active, "aria-pressed": active, "aria-label": name, onClick: () => onChange(name), children: _jsx(Icon, { size: 18, strokeWidth: 2.2 }) }, name));
|
|
44
|
+
}) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const FieldWrap: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, never>>> & string;
|
|
2
|
+
export declare const FieldLabel: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>>> & string;
|
|
3
|
+
export declare const Input: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never>>> & string;
|
|
4
|
+
export declare const Select: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>, never>>> & string;
|
|
5
|
+
export declare const Textarea: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, never>>> & string;
|
|
6
|
+
export declare const FieldError: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>>> & string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
export const FieldWrap = styled.label `
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: ${(p) => p.theme.space[2]};
|
|
6
|
+
`;
|
|
7
|
+
export const FieldLabel = styled.span `
|
|
8
|
+
font-size: 0.875rem;
|
|
9
|
+
font-weight: 700;
|
|
10
|
+
color: ${(p) => p.theme.colors.text};
|
|
11
|
+
`;
|
|
12
|
+
const baseInput = css `
|
|
13
|
+
font-family: inherit;
|
|
14
|
+
font-size: 1rem;
|
|
15
|
+
width: 100%;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
background: ${(p) => p.theme.colors.surface};
|
|
18
|
+
color: ${(p) => p.theme.colors.text};
|
|
19
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
20
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
21
|
+
padding: 11px 14px;
|
|
22
|
+
transition: box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
23
|
+
|
|
24
|
+
&::placeholder {
|
|
25
|
+
color: ${(p) => p.theme.colors.textSubtle};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&:focus {
|
|
29
|
+
outline: none;
|
|
30
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:disabled {
|
|
34
|
+
opacity: 0.6;
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
export const Input = styled.input `
|
|
38
|
+
${baseInput};
|
|
39
|
+
`;
|
|
40
|
+
export const Select = styled.select `
|
|
41
|
+
${baseInput};
|
|
42
|
+
appearance: none;
|
|
43
|
+
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
44
|
+
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
45
|
+
background-position:
|
|
46
|
+
calc(100% - 18px) 50%,
|
|
47
|
+
calc(100% - 13px) 50%;
|
|
48
|
+
background-size:
|
|
49
|
+
5px 5px,
|
|
50
|
+
5px 5px;
|
|
51
|
+
background-repeat: no-repeat;
|
|
52
|
+
padding-right: 36px;
|
|
53
|
+
`;
|
|
54
|
+
export const Textarea = styled.textarea `
|
|
55
|
+
${baseInput};
|
|
56
|
+
min-height: 96px;
|
|
57
|
+
resize: vertical;
|
|
58
|
+
`;
|
|
59
|
+
export const FieldError = styled.span `
|
|
60
|
+
font-size: 0.8125rem;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
color: ${(p) => p.theme.colors.danger};
|
|
63
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ModalProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onClose?: () => void;
|
|
4
|
+
title?: React.ReactNode;
|
|
5
|
+
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare function Modal({ open, onClose, title, size, children }: ModalProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { useEffect, useId, useRef } from 'react';
|
|
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
|
+
display: grid;
|
|
10
|
+
place-items: center;
|
|
11
|
+
z-index: 50;
|
|
12
|
+
padding: ${(p) => p.theme.space[5]};
|
|
13
|
+
animation: rfade 120ms ${(p) => p.theme.easing.out};
|
|
14
|
+
|
|
15
|
+
@keyframes rfade {
|
|
16
|
+
from {
|
|
17
|
+
opacity: 0;
|
|
18
|
+
}
|
|
19
|
+
to {
|
|
20
|
+
opacity: 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
const Panel = styled.div `
|
|
25
|
+
background: ${(p) => p.theme.colors.surface};
|
|
26
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
27
|
+
border-radius: ${(p) => p.theme.radii.lg};
|
|
28
|
+
box-shadow: ${(p) => p.theme.shadow.stampLg};
|
|
29
|
+
width: 100%;
|
|
30
|
+
max-width: ${(p) => (p.$size === 'lg' ? '720px' : p.$size === 'md' ? '600px' : '440px')};
|
|
31
|
+
max-height: calc(100vh - 32px);
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
animation: rrise 140ms ${(p) => p.theme.easing.out};
|
|
35
|
+
|
|
36
|
+
@keyframes rrise {
|
|
37
|
+
from {
|
|
38
|
+
transform: translate(-4px, -4px);
|
|
39
|
+
}
|
|
40
|
+
to {
|
|
41
|
+
transform: none;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
const Header = styled.div `
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: space-between;
|
|
49
|
+
padding: ${(p) => p.theme.space[5]} ${(p) => p.theme.space[6]};
|
|
50
|
+
border-bottom: 2px solid ${(p) => p.theme.colors.border};
|
|
51
|
+
`;
|
|
52
|
+
const Title = styled.h3 `
|
|
53
|
+
font-size: 1.125rem;
|
|
54
|
+
font-weight: 800;
|
|
55
|
+
`;
|
|
56
|
+
const Body = styled.div `
|
|
57
|
+
padding: ${(p) => p.theme.space[6]};
|
|
58
|
+
overflow-y: auto;
|
|
59
|
+
`;
|
|
60
|
+
const CloseBtn = styled.button `
|
|
61
|
+
display: grid;
|
|
62
|
+
place-items: center;
|
|
63
|
+
width: 32px;
|
|
64
|
+
height: 32px;
|
|
65
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
66
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
67
|
+
background: ${(p) => p.theme.colors.danger};
|
|
68
|
+
color: ${(p) => p.theme.colors.surface};
|
|
69
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
70
|
+
transition:
|
|
71
|
+
transform 80ms ${(p) => p.theme.easing.out},
|
|
72
|
+
box-shadow 80ms ${(p) => p.theme.easing.out},
|
|
73
|
+
background 80ms ${(p) => p.theme.easing.out};
|
|
74
|
+
&:hover {
|
|
75
|
+
transform: translate(2px, 2px);
|
|
76
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
77
|
+
}
|
|
78
|
+
&:active {
|
|
79
|
+
transform: translate(4px, 4px);
|
|
80
|
+
box-shadow: none;
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
export function Modal({ open, onClose, title, size = 'md', children }) {
|
|
84
|
+
const panelRef = useRef(null);
|
|
85
|
+
const titleId = useId();
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!open)
|
|
88
|
+
return;
|
|
89
|
+
const onKey = (e) => {
|
|
90
|
+
if (e.key === 'Escape')
|
|
91
|
+
onClose?.();
|
|
92
|
+
};
|
|
93
|
+
document.addEventListener('keydown', onKey);
|
|
94
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
95
|
+
}, [open, onClose]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (open && panelRef.current) {
|
|
98
|
+
panelRef.current.focus();
|
|
99
|
+
}
|
|
100
|
+
}, [open]);
|
|
101
|
+
if (!open)
|
|
102
|
+
return null;
|
|
103
|
+
return (_jsx(Overlay, { onClick: () => onClose?.(), children: _jsxs(Panel, { ref: panelRef, "$size": size, onClick: (e) => e.stopPropagation(), role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, tabIndex: -1, children: [_jsxs(Header, { children: [_jsx(Title, { id: titleId, children: title }), _jsx(CloseBtn, { type: "button", onClick: () => onClose?.(), "aria-label": "Close", children: _jsx(X, { size: 16, strokeWidth: 2.5 }) })] }), _jsx(Body, { children: children })] }) }));
|
|
104
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface NumberInputProps extends Omit<React.ComponentPropsWithoutRef<'input'>, 'value' | 'onChange'> {
|
|
2
|
+
value?: string | number;
|
|
3
|
+
onChange?: (e: {
|
|
4
|
+
target: {
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
}) => void;
|
|
8
|
+
thousandSep?: string;
|
|
9
|
+
decimalSep?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Integer-only text input that groups the value with a thousand separator
|
|
13
|
+
* (default '.'). Stores/emits the raw integer-digit string via
|
|
14
|
+
* onChange({ target: { value } }). A typed-friendly, generalized replacement
|
|
15
|
+
* for a money/number field.
|
|
16
|
+
*
|
|
17
|
+
* Decimals are out of scope: this field formats and emits integers only. If a
|
|
18
|
+
* non-integer value is supplied (e.g. 1234.56), the fractional part is dropped
|
|
19
|
+
* (-> "1234") rather than merged into the integer. The decimalSep prop is
|
|
20
|
+
* accepted for API symmetry and is honoured when splitting off that fractional
|
|
21
|
+
* part from the incoming value.
|
|
22
|
+
*/
|
|
23
|
+
export declare const NumberInput: import("react").ForwardRefExoticComponent<NumberInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useState } from 'react';
|
|
3
|
+
import { Input } from './Input';
|
|
4
|
+
/**
|
|
5
|
+
* Reduce a string to its signed integer digits ('-' optional, digits only).
|
|
6
|
+
* The caller is responsible for having already discarded any fractional part;
|
|
7
|
+
* this is the single source of truth for digit/sign extraction.
|
|
8
|
+
*/
|
|
9
|
+
function keepIntegerDigits(s) {
|
|
10
|
+
const neg = s.trim().startsWith('-');
|
|
11
|
+
const digits = s.replace(/\D/g, '');
|
|
12
|
+
if (!digits)
|
|
13
|
+
return neg ? '-' : '';
|
|
14
|
+
return (neg ? '-' : '') + digits;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalize the controlled `value` prop into a raw integer-digit string.
|
|
18
|
+
* The prop is an ordinary number-like value, so a decimal separator ('.' for
|
|
19
|
+
* JS numbers, or the configured decimalSep) splits off a fractional part that
|
|
20
|
+
* this integer-only field intentionally drops — e.g. 1234.56 -> "1234".
|
|
21
|
+
*/
|
|
22
|
+
function propToRaw(value, decimalSep) {
|
|
23
|
+
if (value == null || value === '')
|
|
24
|
+
return '';
|
|
25
|
+
let s = String(value);
|
|
26
|
+
// Split at the first decimal boundary and discard the fractional digits.
|
|
27
|
+
const dot = s.indexOf('.');
|
|
28
|
+
const sep = decimalSep ? s.indexOf(decimalSep) : -1;
|
|
29
|
+
const cut = dot === -1 ? sep : sep === -1 ? dot : Math.min(dot, sep);
|
|
30
|
+
if (cut !== -1)
|
|
31
|
+
s = s.slice(0, cut);
|
|
32
|
+
return keepIntegerDigits(s);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Normalize what the user typed into the field back into raw integer digits.
|
|
36
|
+
* The displayed value only ever contains digits, a sign and thousand
|
|
37
|
+
* separators (never a decimal), so every non-digit is stripped.
|
|
38
|
+
*/
|
|
39
|
+
function inputToRaw(formatted) {
|
|
40
|
+
if (formatted == null || formatted === '')
|
|
41
|
+
return '';
|
|
42
|
+
return keepIntegerDigits(String(formatted));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Group a raw integer-digit string into thousands for display.
|
|
46
|
+
*/
|
|
47
|
+
function formatThousand(raw, thousandSep) {
|
|
48
|
+
if (!raw || raw === '-')
|
|
49
|
+
return raw;
|
|
50
|
+
const neg = raw.startsWith('-');
|
|
51
|
+
const body = neg ? raw.slice(1) : raw;
|
|
52
|
+
if (!body)
|
|
53
|
+
return neg ? '-' : '';
|
|
54
|
+
return (neg ? '-' : '') + body.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Integer-only text input that groups the value with a thousand separator
|
|
58
|
+
* (default '.'). Stores/emits the raw integer-digit string via
|
|
59
|
+
* onChange({ target: { value } }). A typed-friendly, generalized replacement
|
|
60
|
+
* for a money/number field.
|
|
61
|
+
*
|
|
62
|
+
* Decimals are out of scope: this field formats and emits integers only. If a
|
|
63
|
+
* non-integer value is supplied (e.g. 1234.56), the fractional part is dropped
|
|
64
|
+
* (-> "1234") rather than merged into the integer. The decimalSep prop is
|
|
65
|
+
* accepted for API symmetry and is honoured when splitting off that fractional
|
|
66
|
+
* part from the incoming value.
|
|
67
|
+
*/
|
|
68
|
+
export const NumberInput = forwardRef(function NumberInput({ value, onChange, thousandSep = '.', decimalSep = ',', ...rest }, ref) {
|
|
69
|
+
// Track the raw digit-string internally so masking survives a controlled
|
|
70
|
+
// `value` that is not echoed back synchronously per keystroke. The buffer is
|
|
71
|
+
// re-seeded whenever the incoming `value` prop diverges from it.
|
|
72
|
+
const propRaw = propToRaw(value, decimalSep);
|
|
73
|
+
const [raw, setRaw] = useState(propRaw);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setRaw((prev) => (prev === propRaw ? prev : propRaw));
|
|
76
|
+
}, [propRaw]);
|
|
77
|
+
const display = formatThousand(raw, thousandSep);
|
|
78
|
+
return (_jsx(Input, { ref: ref, type: "text", inputMode: "numeric", autoComplete: "off", value: display, onChange: (e) => {
|
|
79
|
+
const next = inputToRaw(e.target.value);
|
|
80
|
+
setRaw(next);
|
|
81
|
+
onChange?.({ target: { value: next } });
|
|
82
|
+
}, ...rest }));
|
|
83
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PaginationProps {
|
|
2
|
+
page: number;
|
|
3
|
+
pageCount: number;
|
|
4
|
+
onChange: (page: number) => void;
|
|
5
|
+
siblingCount?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function Pagination({ page, pageCount, onChange, siblingCount }: PaginationProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
const ELLIPSIS = '…';
|
|
5
|
+
function range(start, end) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (let i = start; i <= end; i++)
|
|
8
|
+
out.push(i);
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
function buildPages(page, pageCount, siblingCount) {
|
|
12
|
+
// boundary pages (1, last) + current +/- siblings + 2 ellipsis slots
|
|
13
|
+
const totalSlots = siblingCount * 2 + 5;
|
|
14
|
+
if (pageCount <= totalSlots) {
|
|
15
|
+
return range(1, pageCount);
|
|
16
|
+
}
|
|
17
|
+
const leftSibling = Math.max(page - siblingCount, 1);
|
|
18
|
+
const rightSibling = Math.min(page + siblingCount, pageCount);
|
|
19
|
+
const showLeftEllipsis = leftSibling > 2;
|
|
20
|
+
const showRightEllipsis = rightSibling < pageCount - 1;
|
|
21
|
+
if (!showLeftEllipsis && showRightEllipsis) {
|
|
22
|
+
const leftCount = 3 + 2 * siblingCount;
|
|
23
|
+
return [...range(1, leftCount), ELLIPSIS, pageCount];
|
|
24
|
+
}
|
|
25
|
+
if (showLeftEllipsis && !showRightEllipsis) {
|
|
26
|
+
const rightCount = 3 + 2 * siblingCount;
|
|
27
|
+
return [1, ELLIPSIS, ...range(pageCount - rightCount + 1, pageCount)];
|
|
28
|
+
}
|
|
29
|
+
return [1, ELLIPSIS, ...range(leftSibling, rightSibling), ELLIPSIS, pageCount];
|
|
30
|
+
}
|
|
31
|
+
const Nav = styled.nav `
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: ${(p) => p.theme.space[1]};
|
|
35
|
+
font-family: ${(p) => p.theme.font.body};
|
|
36
|
+
`;
|
|
37
|
+
const PageButton = styled.button `
|
|
38
|
+
min-width: 36px;
|
|
39
|
+
height: 36px;
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
font-weight: 800;
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
color: ${(p) => (p.$active ? p.theme.colors.primaryInk : p.theme.colors.text)};
|
|
46
|
+
background: ${(p) => (p.$active ? p.theme.colors.primary : p.theme.colors.surface)};
|
|
47
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
48
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
49
|
+
box-shadow: ${(p) => (p.$active ? p.theme.shadow.none : p.theme.shadow.stampSm)};
|
|
50
|
+
padding: 0 ${(p) => p.theme.space[2]};
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
transition: transform 80ms ${(p) => p.theme.easing.out},
|
|
53
|
+
box-shadow 80ms ${(p) => p.theme.easing.out};
|
|
54
|
+
|
|
55
|
+
&:hover:not(:disabled) {
|
|
56
|
+
transform: translate(2px, 2px);
|
|
57
|
+
box-shadow: ${(p) => p.theme.shadow.none};
|
|
58
|
+
}
|
|
59
|
+
&:active:not(:disabled) {
|
|
60
|
+
transform: translate(4px, 4px);
|
|
61
|
+
box-shadow: ${(p) => p.theme.shadow.none};
|
|
62
|
+
}
|
|
63
|
+
&:disabled {
|
|
64
|
+
opacity: 0.55;
|
|
65
|
+
cursor: not-allowed;
|
|
66
|
+
transform: none;
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
const Ellipsis = styled.span `
|
|
70
|
+
min-width: 36px;
|
|
71
|
+
height: 36px;
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
font-weight: 800;
|
|
76
|
+
color: ${(p) => p.theme.colors.textSubtle};
|
|
77
|
+
`;
|
|
78
|
+
export function Pagination({ page, pageCount, onChange, siblingCount = 1 }) {
|
|
79
|
+
const pages = buildPages(page, pageCount, siblingCount);
|
|
80
|
+
return (_jsxs(Nav, { "aria-label": "Pagination", children: [_jsx(PageButton, { type: "button", "aria-label": "Previous page", disabled: page <= 1, onClick: () => onChange(page - 1), children: _jsx(ChevronLeft, { size: 18 }) }), pages.map((p, i) => p === ELLIPSIS ? (_jsx(Ellipsis, { "aria-hidden": "true", children: ELLIPSIS }, `e-${i}`)) : (_jsx(PageButton, { type: "button", "$active": p === page, "aria-current": p === page ? 'page' : undefined, onClick: () => onChange(p), children: p }, p))), _jsx(PageButton, { type: "button", "aria-label": "Next page", disabled: page >= pageCount, onClick: () => onChange(page + 1), children: _jsx(ChevronRight, { size: 18 }) })] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type Placement } from '@floating-ui/react';
|
|
3
|
+
export interface PopoverProps {
|
|
4
|
+
trigger: React.ReactElement;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
placement?: Placement;
|
|
7
|
+
}
|
|
8
|
+
export declare function Popover({ trigger, children, placement }: PopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } 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, useInteractions, FloatingPortal, FloatingFocusManager, } from '@floating-ui/react';
|
|
5
|
+
const Panel = styled.div `
|
|
6
|
+
background: ${(p) => p.theme.colors.surface};
|
|
7
|
+
color: ${(p) => p.theme.colors.text};
|
|
8
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
9
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
10
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
11
|
+
padding: ${(p) => p.theme.space[4]};
|
|
12
|
+
font-family: ${(p) => p.theme.font.body};
|
|
13
|
+
min-width: 200px;
|
|
14
|
+
z-index: 1000;
|
|
15
|
+
outline: none;
|
|
16
|
+
`;
|
|
17
|
+
export function Popover({ trigger, children, placement = 'bottom' }) {
|
|
18
|
+
const [open, setOpen] = React.useState(false);
|
|
19
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
20
|
+
open,
|
|
21
|
+
onOpenChange: setOpen,
|
|
22
|
+
placement,
|
|
23
|
+
whileElementsMounted: autoUpdate,
|
|
24
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
25
|
+
});
|
|
26
|
+
const click = useClick(context);
|
|
27
|
+
const dismiss = useDismiss(context);
|
|
28
|
+
const role = useRole(context, { role: 'dialog' });
|
|
29
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
30
|
+
click,
|
|
31
|
+
dismiss,
|
|
32
|
+
role,
|
|
33
|
+
]);
|
|
34
|
+
const child = React.Children.only(trigger);
|
|
35
|
+
return (_jsxs(_Fragment, { children: [React.cloneElement(child, getReferenceProps({
|
|
36
|
+
ref: refs.setReference,
|
|
37
|
+
...child.props,
|
|
38
|
+
})), open && (_jsx(FloatingPortal, { children: _jsx(FloatingFocusManager, { context: context, modal: false, children: _jsx(Panel, { ref: refs.setFloating, style: floatingStyles, ...getFloatingProps(), children: children }) }) }))] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type ProgressVariant = 'primary' | 'success' | 'danger';
|
|
3
|
+
export interface ProgressProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'role'> {
|
|
4
|
+
value: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
$variant?: ProgressVariant;
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const Progress: React.ForwardRefExoticComponent<ProgressProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
const variantColor = {
|
|
5
|
+
primary: (p) => p.theme.colors.primary,
|
|
6
|
+
success: (p) => p.theme.colors.income,
|
|
7
|
+
danger: (p) => p.theme.colors.expense,
|
|
8
|
+
};
|
|
9
|
+
const Wrap = styled.div `
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: ${(p) => p.theme.space[1]};
|
|
13
|
+
width: 100%;
|
|
14
|
+
font-family: ${(p) => p.theme.font.body};
|
|
15
|
+
`;
|
|
16
|
+
const Label = styled.span `
|
|
17
|
+
font-size: 13px;
|
|
18
|
+
font-weight: 700;
|
|
19
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
20
|
+
`;
|
|
21
|
+
const Track = styled.div `
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 18px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
background: ${(p) => p.theme.colors.surfaceMuted};
|
|
26
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
27
|
+
border-radius: ${(p) => p.theme.radii.pill};
|
|
28
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
29
|
+
`;
|
|
30
|
+
const Fill = styled.div `
|
|
31
|
+
height: 100%;
|
|
32
|
+
background: ${(p) => variantColor[p.$variant](p)};
|
|
33
|
+
border-right: 2px solid ${(p) => p.theme.colors.border};
|
|
34
|
+
transition: width 80ms ${(p) => p.theme.easing.out};
|
|
35
|
+
`;
|
|
36
|
+
export const Progress = React.forwardRef(({ value, max = 100, $variant = 'primary', label, ...rest }, ref) => {
|
|
37
|
+
const clamped = Math.max(0, Math.min(value, max));
|
|
38
|
+
const pct = max > 0 ? (clamped / max) * 100 : 0;
|
|
39
|
+
return (_jsxs(Wrap, { ref: ref, ...rest, children: [label != null ? _jsx(Label, { children: label }) : null, _jsx(Track, { role: "progressbar", "aria-valuenow": clamped, "aria-valuemin": 0, "aria-valuemax": max, "aria-label": label, children: _jsx(Fill, { "$variant": $variant, "data-testid": "progress-fill", style: { width: `${pct}%` } }) })] }));
|
|
40
|
+
});
|
|
41
|
+
Progress.displayName = 'Progress';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface RadioGroupProps {
|
|
3
|
+
name: string;
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare const RadioGroup: React.FC<RadioGroupProps>;
|
|
9
|
+
export interface RadioProps {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const Radio: React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLInputElement>>;
|