@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,11 @@
|
|
|
1
|
+
export interface StepDef {
|
|
2
|
+
label: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
}
|
|
5
|
+
export type StepperOrientation = 'horizontal' | 'vertical';
|
|
6
|
+
export interface StepperProps {
|
|
7
|
+
steps: StepDef[];
|
|
8
|
+
active: number;
|
|
9
|
+
orientation?: StepperOrientation;
|
|
10
|
+
}
|
|
11
|
+
export declare function Stepper({ steps, active, orientation }: StepperProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
const List = styled.ol `
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: ${(p) => (p.$orientation === 'vertical' ? 'column' : 'row')};
|
|
6
|
+
align-items: ${(p) => (p.$orientation === 'vertical' ? 'stretch' : 'flex-start')};
|
|
7
|
+
list-style: none;
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
font-family: ${(p) => p.theme.font.body};
|
|
11
|
+
`;
|
|
12
|
+
const Item = styled.li `
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: ${(p) => (p.$orientation === 'vertical' ? 'row' : 'column')};
|
|
15
|
+
align-items: ${(p) => (p.$orientation === 'vertical' ? 'flex-start' : 'center')};
|
|
16
|
+
flex: ${(p) => (p.$orientation === 'horizontal' && !p.$last ? '1' : '0 0 auto')};
|
|
17
|
+
gap: ${(p) => p.theme.space[2]};
|
|
18
|
+
position: relative;
|
|
19
|
+
`;
|
|
20
|
+
const Circle = styled.span `
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
width: 36px;
|
|
25
|
+
height: 36px;
|
|
26
|
+
flex-shrink: 0;
|
|
27
|
+
border-radius: ${(p) => p.theme.radii.pill};
|
|
28
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
29
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
30
|
+
font-weight: 800;
|
|
31
|
+
font-size: 15px;
|
|
32
|
+
background: ${(p) => {
|
|
33
|
+
switch (p.$status) {
|
|
34
|
+
case 'complete':
|
|
35
|
+
return p.theme.colors.success;
|
|
36
|
+
case 'active':
|
|
37
|
+
return p.theme.colors.primary;
|
|
38
|
+
default:
|
|
39
|
+
return p.theme.colors.surface;
|
|
40
|
+
}
|
|
41
|
+
}};
|
|
42
|
+
color: ${(p) => {
|
|
43
|
+
switch (p.$status) {
|
|
44
|
+
case 'complete':
|
|
45
|
+
return p.theme.colors.bg;
|
|
46
|
+
case 'active':
|
|
47
|
+
return p.theme.colors.primaryInk;
|
|
48
|
+
default:
|
|
49
|
+
return p.theme.colors.textMuted;
|
|
50
|
+
}
|
|
51
|
+
}};
|
|
52
|
+
`;
|
|
53
|
+
const Connector = styled.span `
|
|
54
|
+
flex: 1;
|
|
55
|
+
align-self: ${(p) => (p.$orientation === 'vertical' ? 'stretch' : 'center')};
|
|
56
|
+
background: ${(p) => (p.$done ? p.theme.colors.success : p.theme.colors.borderSoft)};
|
|
57
|
+
${(p) => p.$orientation === 'vertical'
|
|
58
|
+
? `width: 2px; min-height: 24px; margin-left: 17px;`
|
|
59
|
+
: `height: 2px; min-width: 24px;`}
|
|
60
|
+
`;
|
|
61
|
+
const Labels = styled.div `
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
text-align: ${(p) => (p.$orientation === 'vertical' ? 'left' : 'center')};
|
|
65
|
+
gap: 2px;
|
|
66
|
+
`;
|
|
67
|
+
const Label = styled.span `
|
|
68
|
+
font-weight: ${(p) => (p.$status === 'upcoming' ? 600 : 800)};
|
|
69
|
+
font-size: 14px;
|
|
70
|
+
color: ${(p) => p.$status === 'upcoming' ? p.theme.colors.textMuted : p.theme.colors.text};
|
|
71
|
+
`;
|
|
72
|
+
const Description = styled.span `
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
color: ${(p) => p.theme.colors.textSubtle};
|
|
76
|
+
`;
|
|
77
|
+
const StepRow = styled.div `
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: ${(p) => (p.$orientation === 'vertical' ? 'column' : 'row')};
|
|
80
|
+
align-items: ${(p) => (p.$orientation === 'vertical' ? 'flex-start' : 'center')};
|
|
81
|
+
${(p) => (p.$orientation === 'horizontal' ? `width: 100%; justify-content: center;` : '')}
|
|
82
|
+
gap: ${(p) => p.theme.space[2]};
|
|
83
|
+
`;
|
|
84
|
+
function statusOf(index, active) {
|
|
85
|
+
if (index < active)
|
|
86
|
+
return 'complete';
|
|
87
|
+
if (index === active)
|
|
88
|
+
return 'active';
|
|
89
|
+
return 'upcoming';
|
|
90
|
+
}
|
|
91
|
+
export function Stepper({ steps, active, orientation = 'horizontal' }) {
|
|
92
|
+
return (_jsx(List, { "$orientation": orientation, children: steps.map((step, index) => {
|
|
93
|
+
const status = statusOf(index, active);
|
|
94
|
+
const isLast = index === steps.length - 1;
|
|
95
|
+
return (_jsxs(Item, { "$orientation": orientation, "$last": isLast, "data-status": status, "aria-current": status === 'active' ? 'step' : undefined, children: [_jsxs(StepRow, { "$orientation": orientation, children: [_jsx(Circle, { "$status": status, children: index + 1 }), _jsxs(Labels, { "$orientation": orientation, children: [_jsx(Label, { "$status": status, children: step.label }), step.description && _jsx(Description, { children: step.description })] })] }), !isLast && (_jsx(Connector, { "$orientation": orientation, "$done": index < active, "aria-hidden": "true" }))] }, step.label));
|
|
96
|
+
}) }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface SwitchProps extends Omit<React.ComponentPropsWithoutRef<'input'>, 'onChange' | 'type' | 'role'> {
|
|
3
|
+
checked: boolean;
|
|
4
|
+
onChange: (checked: boolean) => void;
|
|
5
|
+
label?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const Switch: React.ForwardRefExoticComponent<SwitchProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,58 @@
|
|
|
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 Root = styled.label `
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: ${(p) => p.theme.space[2]};
|
|
8
|
+
font-family: ${(p) => p.theme.font.body};
|
|
9
|
+
font-weight: 700;
|
|
10
|
+
color: ${(p) => p.theme.colors.text};
|
|
11
|
+
cursor: ${(p) => (p.$disabled ? 'not-allowed' : 'pointer')};
|
|
12
|
+
opacity: ${(p) => (p.$disabled ? 0.55 : 1)};
|
|
13
|
+
user-select: none;
|
|
14
|
+
`;
|
|
15
|
+
const HiddenInput = styled.input `
|
|
16
|
+
position: absolute;
|
|
17
|
+
width: 1px;
|
|
18
|
+
height: 1px;
|
|
19
|
+
padding: 0;
|
|
20
|
+
margin: -1px;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
clip: rect(0 0 0 0);
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
border: 0;
|
|
25
|
+
`;
|
|
26
|
+
const Track = styled.span `
|
|
27
|
+
position: relative;
|
|
28
|
+
display: inline-block;
|
|
29
|
+
width: 48px;
|
|
30
|
+
height: 26px;
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
33
|
+
border-radius: ${(p) => p.theme.radii.pill};
|
|
34
|
+
background: ${(p) => (p.$checked ? p.theme.colors.primary : p.theme.colors.surfaceMuted)};
|
|
35
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
36
|
+
transition: background 80ms ${(p) => p.theme.easing.out};
|
|
37
|
+
|
|
38
|
+
${HiddenInput}:focus-visible + & {
|
|
39
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
40
|
+
outline-offset: 2px;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
const Thumb = styled.span `
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: 1px;
|
|
46
|
+
left: 1px;
|
|
47
|
+
width: 20px;
|
|
48
|
+
height: 20px;
|
|
49
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
50
|
+
border-radius: ${(p) => p.theme.radii.pill};
|
|
51
|
+
background: ${(p) => p.theme.colors.surface};
|
|
52
|
+
transform: translateX(${(p) => (p.$checked ? '22px' : '0')});
|
|
53
|
+
transition: transform 80ms ${(p) => p.theme.easing.out};
|
|
54
|
+
`;
|
|
55
|
+
export const Switch = React.forwardRef(({ checked, onChange, label, disabled, ...rest }, ref) => {
|
|
56
|
+
return (_jsxs(Root, { "$disabled": disabled, children: [_jsx(HiddenInput, { ref: ref, type: "checkbox", role: "switch", "aria-checked": checked, checked: checked, disabled: disabled, onChange: (e) => onChange(e.target.checked), ...rest }), _jsx(Track, { "$checked": checked, "aria-hidden": "true", children: _jsx(Thumb, { "$checked": checked }) }), label != null ? _jsx("span", { children: label }) : null] }));
|
|
57
|
+
});
|
|
58
|
+
Switch.displayName = 'Switch';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const Table: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").TableHTMLAttributes<HTMLTableElement>, HTMLTableElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").TableHTMLAttributes<HTMLTableElement>, HTMLTableElement>, never>>> & string;
|
|
2
|
+
export declare const THead: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, never>>> & string;
|
|
3
|
+
export declare const TBody: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, never>>> & string;
|
|
4
|
+
export declare const Tr: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableRowElement>, HTMLTableRowElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableRowElement>, HTMLTableRowElement>, never>>> & string;
|
|
5
|
+
export declare const Th: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").ThHTMLAttributes<HTMLTableHeaderCellElement>, HTMLTableHeaderCellElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").ThHTMLAttributes<HTMLTableHeaderCellElement>, HTMLTableHeaderCellElement>, never>>> & string;
|
|
6
|
+
export declare const Td: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement>, never> & Partial<Pick<import("react").DetailedHTMLProps<import("react").TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement>, never>>> & string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
export const Table = styled.table `
|
|
3
|
+
width: 100%;
|
|
4
|
+
border-collapse: collapse;
|
|
5
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
6
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
font-family: ${(p) => p.theme.font.body};
|
|
9
|
+
font-size: 14px;
|
|
10
|
+
color: ${(p) => p.theme.colors.text};
|
|
11
|
+
background: ${(p) => p.theme.colors.surface};
|
|
12
|
+
`;
|
|
13
|
+
export const THead = styled.thead `
|
|
14
|
+
background: ${(p) => p.theme.colors.surfaceMuted};
|
|
15
|
+
`;
|
|
16
|
+
export const TBody = styled.tbody ``;
|
|
17
|
+
export const Tr = styled.tr `
|
|
18
|
+
transition: background 80ms ${(p) => p.theme.easing.out};
|
|
19
|
+
|
|
20
|
+
tbody &:hover {
|
|
21
|
+
background: ${(p) => p.theme.colors.surfaceSunken};
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
export const Th = styled.th `
|
|
25
|
+
text-align: left;
|
|
26
|
+
font-weight: 800;
|
|
27
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
|
|
28
|
+
border-bottom: 2px solid ${(p) => p.theme.colors.border};
|
|
29
|
+
color: ${(p) => p.theme.colors.text};
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
`;
|
|
32
|
+
export const Td = styled.td `
|
|
33
|
+
text-align: left;
|
|
34
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
|
|
35
|
+
border-bottom: 1px solid ${(p) => p.theme.colors.borderSoft};
|
|
36
|
+
color: ${(p) => p.theme.colors.text};
|
|
37
|
+
|
|
38
|
+
tr:last-child & {
|
|
39
|
+
border-bottom: none;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface TabsProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function Tabs({ value, onChange, children }: TabsProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export interface TabListProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare function TabList({ children, ...rest }: TabListProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export interface TabProps extends Omit<React.ComponentPropsWithoutRef<'button'>, 'value'> {
|
|
13
|
+
value: string;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare function Tab({ value, children, onClick, onKeyDown, disabled, ...rest }: TabProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export interface TabPanelProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'value'> {
|
|
18
|
+
value: string;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export declare function TabPanel({ value, children, ...rest }: TabPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { createContext, useContext, useCallback, useMemo, useRef, } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
const TabsContext = createContext(null);
|
|
5
|
+
function useTabsContext(component) {
|
|
6
|
+
const ctx = useContext(TabsContext);
|
|
7
|
+
if (!ctx) {
|
|
8
|
+
throw new Error(`<${component}> must be used within <Tabs>`);
|
|
9
|
+
}
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
let tabsIdCounter = 0;
|
|
13
|
+
export function Tabs({ value, onChange, children }) {
|
|
14
|
+
const baseIdRef = useRef('');
|
|
15
|
+
if (!baseIdRef.current) {
|
|
16
|
+
tabsIdCounter += 1;
|
|
17
|
+
baseIdRef.current = `stamp-tabs-${tabsIdCounter}`;
|
|
18
|
+
}
|
|
19
|
+
// Preserve registration order of tab values for arrow navigation.
|
|
20
|
+
const orderRef = useRef([]);
|
|
21
|
+
const refsRef = useRef(new Map());
|
|
22
|
+
// Track each tab's disabled state so arrow navigation can skip disabled tabs.
|
|
23
|
+
const disabledRef = useRef(new Map());
|
|
24
|
+
const register = useCallback((v, disabled) => {
|
|
25
|
+
if (!orderRef.current.includes(v)) {
|
|
26
|
+
orderRef.current = [...orderRef.current, v];
|
|
27
|
+
}
|
|
28
|
+
disabledRef.current.set(v, disabled);
|
|
29
|
+
}, []);
|
|
30
|
+
const unregister = useCallback((v) => {
|
|
31
|
+
orderRef.current = orderRef.current.filter((x) => x !== v);
|
|
32
|
+
refsRef.current.delete(v);
|
|
33
|
+
disabledRef.current.delete(v);
|
|
34
|
+
}, []);
|
|
35
|
+
const isDisabled = useCallback((v) => disabledRef.current.get(v) === true, []);
|
|
36
|
+
const setTabRef = useCallback((v, el) => {
|
|
37
|
+
if (el) {
|
|
38
|
+
refsRef.current.set(v, el);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
refsRef.current.delete(v);
|
|
42
|
+
}
|
|
43
|
+
}, []);
|
|
44
|
+
const focusByValue = useCallback((v) => {
|
|
45
|
+
const el = refsRef.current.get(v);
|
|
46
|
+
if (el)
|
|
47
|
+
el.focus();
|
|
48
|
+
}, []);
|
|
49
|
+
const getOrderedValues = useCallback(() => orderRef.current.slice(), []);
|
|
50
|
+
const ctx = useMemo(() => ({
|
|
51
|
+
value,
|
|
52
|
+
onChange,
|
|
53
|
+
baseId: baseIdRef.current,
|
|
54
|
+
register,
|
|
55
|
+
unregister,
|
|
56
|
+
focusByValue,
|
|
57
|
+
setTabRef,
|
|
58
|
+
getOrderedValues,
|
|
59
|
+
isDisabled,
|
|
60
|
+
}), [value, onChange, register, unregister, focusByValue, setTabRef, getOrderedValues, isDisabled]);
|
|
61
|
+
return _jsx(TabsContext.Provider, { value: ctx, children: children });
|
|
62
|
+
}
|
|
63
|
+
const StyledTabList = styled.div `
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: ${(p) => p.theme.space[2]};
|
|
66
|
+
border-bottom: 2px solid ${(p) => p.theme.colors.border};
|
|
67
|
+
margin-bottom: ${(p) => p.theme.space[4]};
|
|
68
|
+
`;
|
|
69
|
+
export function TabList({ children, ...rest }) {
|
|
70
|
+
return (_jsx(StyledTabList, { role: "tablist", ...rest, children: children }));
|
|
71
|
+
}
|
|
72
|
+
const StyledTab = styled.button `
|
|
73
|
+
appearance: none;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
font-family: ${(p) => p.theme.font.body};
|
|
76
|
+
font-weight: ${(p) => (p.$active ? 800 : 700)};
|
|
77
|
+
font-size: 0.95rem;
|
|
78
|
+
padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[4]};
|
|
79
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
80
|
+
border-bottom: none;
|
|
81
|
+
border-top-left-radius: ${(p) => p.theme.radii.md};
|
|
82
|
+
border-top-right-radius: ${(p) => p.theme.radii.md};
|
|
83
|
+
background: ${(p) => (p.$active ? p.theme.colors.surface : p.theme.colors.surfaceMuted)};
|
|
84
|
+
color: ${(p) => (p.$active ? p.theme.colors.text : p.theme.colors.textMuted)};
|
|
85
|
+
box-shadow: ${(p) => (p.$active ? p.theme.shadow.stampSm : p.theme.shadow.none)};
|
|
86
|
+
transition: background 80ms ${(p) => p.theme.easing.out},
|
|
87
|
+
color 80ms ${(p) => p.theme.easing.out};
|
|
88
|
+
|
|
89
|
+
&:hover:not(:disabled) {
|
|
90
|
+
color: ${(p) => p.theme.colors.text};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&:disabled {
|
|
94
|
+
opacity: 0.55;
|
|
95
|
+
cursor: not-allowed;
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
export function Tab({ value, children, onClick, onKeyDown, disabled, ...rest }) {
|
|
99
|
+
const ctx = useTabsContext('Tab');
|
|
100
|
+
const active = ctx.value === value;
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
ctx.register(value, disabled === true);
|
|
103
|
+
return () => ctx.unregister(value);
|
|
104
|
+
}, [ctx, value, disabled]);
|
|
105
|
+
const handleClick = (e) => {
|
|
106
|
+
onClick?.(e);
|
|
107
|
+
if (!e.defaultPrevented)
|
|
108
|
+
ctx.onChange(value);
|
|
109
|
+
};
|
|
110
|
+
const handleKeyDown = (e) => {
|
|
111
|
+
onKeyDown?.(e);
|
|
112
|
+
if (e.defaultPrevented)
|
|
113
|
+
return;
|
|
114
|
+
const order = ctx.getOrderedValues();
|
|
115
|
+
const idx = order.indexOf(value);
|
|
116
|
+
if (idx === -1)
|
|
117
|
+
return;
|
|
118
|
+
// Step over disabled tabs in the given direction (wrapping), so arrow
|
|
119
|
+
// navigation never lands on a disabled tab. Returns null if no enabled
|
|
120
|
+
// tab exists in that direction.
|
|
121
|
+
const seekEnabled = (start, step) => {
|
|
122
|
+
for (let i = 1; i <= order.length; i += 1) {
|
|
123
|
+
const candidate = (start + step * i + order.length * order.length) % order.length;
|
|
124
|
+
if (!ctx.isDisabled(order[candidate]))
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
// Scan from one end inward for the first/last enabled tab (Home/End).
|
|
130
|
+
const firstEnabled = (step) => {
|
|
131
|
+
const begin = step > 0 ? 0 : order.length - 1;
|
|
132
|
+
for (let i = 0; i < order.length; i += 1) {
|
|
133
|
+
const candidate = begin + step * i;
|
|
134
|
+
if (!ctx.isDisabled(order[candidate]))
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
let nextIdx = null;
|
|
140
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
141
|
+
nextIdx = seekEnabled(idx, 1);
|
|
142
|
+
}
|
|
143
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
144
|
+
nextIdx = seekEnabled(idx, -1);
|
|
145
|
+
}
|
|
146
|
+
else if (e.key === 'Home') {
|
|
147
|
+
nextIdx = firstEnabled(1);
|
|
148
|
+
}
|
|
149
|
+
else if (e.key === 'End') {
|
|
150
|
+
nextIdx = firstEnabled(-1);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
if (nextIdx !== null) {
|
|
157
|
+
const nextValue = order[nextIdx];
|
|
158
|
+
ctx.onChange(nextValue);
|
|
159
|
+
ctx.focusByValue(nextValue);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return (_jsx(StyledTab, { type: "button", role: "tab", id: `${ctx.baseId}-tab-${value}`, "aria-selected": active, "aria-controls": `${ctx.baseId}-panel-${value}`, tabIndex: active ? 0 : -1, disabled: disabled, "$active": active, ref: (el) => ctx.setTabRef(value, el), onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: children }));
|
|
163
|
+
}
|
|
164
|
+
const StyledTabPanel = styled.div `
|
|
165
|
+
&:focus-visible {
|
|
166
|
+
outline: 2px solid ${(p) => p.theme.colors.border};
|
|
167
|
+
outline-offset: 2px;
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
export function TabPanel({ value, children, ...rest }) {
|
|
171
|
+
const ctx = useTabsContext('TabPanel');
|
|
172
|
+
const active = ctx.value === value;
|
|
173
|
+
return (_jsx(StyledTabPanel, { role: "tabpanel", id: `${ctx.baseId}-panel-${value}`, "aria-labelledby": `${ctx.baseId}-tab-${value}`, hidden: !active, tabIndex: 0, ...rest, children: children }));
|
|
174
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
const Root = styled.span `
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: ${(p) => p.theme.space[1]};
|
|
8
|
+
padding: ${(p) => p.theme.space[1]} ${(p) => p.theme.space[2]};
|
|
9
|
+
font-family: ${(p) => p.theme.font.body};
|
|
10
|
+
font-size: 13px;
|
|
11
|
+
font-weight: 700;
|
|
12
|
+
line-height: 1.4;
|
|
13
|
+
color: ${(p) => p.theme.colors.text};
|
|
14
|
+
background: ${(p) => p.theme.colors.surfaceMuted};
|
|
15
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
16
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
17
|
+
`;
|
|
18
|
+
const RemoveButton = styled.button `
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
width: 18px;
|
|
23
|
+
height: 18px;
|
|
24
|
+
padding: 0;
|
|
25
|
+
margin: 0;
|
|
26
|
+
color: ${(p) => p.theme.colors.textMuted};
|
|
27
|
+
background: transparent;
|
|
28
|
+
border: none;
|
|
29
|
+
border-radius: ${(p) => p.theme.radii.xs};
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
transition: color 80ms ${(p) => p.theme.easing.out},
|
|
32
|
+
background 80ms ${(p) => p.theme.easing.out};
|
|
33
|
+
|
|
34
|
+
&:hover {
|
|
35
|
+
color: ${(p) => p.theme.colors.text};
|
|
36
|
+
background: ${(p) => p.theme.colors.surfaceSunken};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:focus-visible {
|
|
40
|
+
outline: 2px solid ${(p) => p.theme.colors.accent};
|
|
41
|
+
outline-offset: 1px;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
export const Tag = ({ children, onRemove, ...rest }) => {
|
|
45
|
+
const label = typeof children === 'string' ? children : undefined;
|
|
46
|
+
return (_jsxs(Root, { ...rest, children: [_jsx("span", { children: children }), onRemove != null ? (_jsx(RemoveButton, { type: "button", "aria-label": label != null ? `Remove ${label}` : 'Remove', onClick: onRemove, children: _jsx(X, { size: 14, strokeWidth: 3, "aria-hidden": "true" }) })) : null] }));
|
|
47
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ToastKind = 'success' | 'error' | 'info' | 'warn';
|
|
2
|
+
export interface ToastItem {
|
|
3
|
+
id: number;
|
|
4
|
+
kind: ToastKind;
|
|
5
|
+
msg: string;
|
|
6
|
+
duration?: number;
|
|
7
|
+
}
|
|
8
|
+
interface ToastInput {
|
|
9
|
+
kind: ToastKind;
|
|
10
|
+
msg: string;
|
|
11
|
+
duration?: number;
|
|
12
|
+
}
|
|
13
|
+
interface ToastStore {
|
|
14
|
+
toasts: ToastItem[];
|
|
15
|
+
push: (toast: ToastInput) => void;
|
|
16
|
+
dismiss: (id: number) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const useToastStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ToastStore>>;
|
|
19
|
+
export declare const toast: {
|
|
20
|
+
success: (msg: string) => void;
|
|
21
|
+
error: (msg: string) => void;
|
|
22
|
+
info: (msg: string) => void;
|
|
23
|
+
warn: (msg: string) => void;
|
|
24
|
+
};
|
|
25
|
+
export declare function ToastViewport(): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangle, CheckCircle2, Info, XCircle } from 'lucide-react';
|
|
3
|
+
import styled, { keyframes } from 'styled-components';
|
|
4
|
+
import { create } from 'zustand';
|
|
5
|
+
let id = 0;
|
|
6
|
+
export const useToastStore = create((set, get) => ({
|
|
7
|
+
toasts: [],
|
|
8
|
+
push: (toast) => {
|
|
9
|
+
const tid = ++id;
|
|
10
|
+
set({ toasts: [...get().toasts, { id: tid, ...toast }] });
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
set({ toasts: get().toasts.filter((t) => t.id !== tid) });
|
|
13
|
+
}, toast.duration ?? 4000);
|
|
14
|
+
},
|
|
15
|
+
dismiss: (tid) => set({ toasts: get().toasts.filter((t) => t.id !== tid) }),
|
|
16
|
+
}));
|
|
17
|
+
export const toast = {
|
|
18
|
+
success: (msg) => useToastStore.getState().push({ kind: 'success', msg }),
|
|
19
|
+
error: (msg) => useToastStore.getState().push({ kind: 'error', msg }),
|
|
20
|
+
info: (msg) => useToastStore.getState().push({ kind: 'info', msg }),
|
|
21
|
+
warn: (msg) => useToastStore.getState().push({ kind: 'warn', msg }),
|
|
22
|
+
};
|
|
23
|
+
const slide = keyframes `
|
|
24
|
+
from { transform: translateY(12px); opacity: 0; }
|
|
25
|
+
to { transform: none; opacity: 1; }
|
|
26
|
+
`;
|
|
27
|
+
const Stack = styled.div `
|
|
28
|
+
position: fixed;
|
|
29
|
+
bottom: 24px;
|
|
30
|
+
right: 24px;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 10px;
|
|
34
|
+
z-index: 100;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
`;
|
|
37
|
+
const Pill = styled.div `
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 10px;
|
|
41
|
+
padding: 12px 16px;
|
|
42
|
+
border-radius: ${(p) => p.theme.radii.md};
|
|
43
|
+
background: ${(p) => p.theme.colors.surface};
|
|
44
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
45
|
+
box-shadow: ${(p) => p.theme.shadow.stamp};
|
|
46
|
+
color: ${(p) => p.theme.colors.text};
|
|
47
|
+
font-size: 0.9375rem;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
animation: ${slide} 160ms ${(p) => p.theme.easing.out};
|
|
50
|
+
pointer-events: auto;
|
|
51
|
+
min-width: 240px;
|
|
52
|
+
`;
|
|
53
|
+
const ColoredPill = styled(Pill) `
|
|
54
|
+
background: ${(p) => p.$kind === 'success'
|
|
55
|
+
? p.theme.colors.incomeSoft
|
|
56
|
+
: p.$kind === 'error'
|
|
57
|
+
? p.theme.colors.expenseSoft
|
|
58
|
+
: p.$kind === 'warn'
|
|
59
|
+
? p.theme.colors.primarySoft
|
|
60
|
+
: p.theme.colors.surface};
|
|
61
|
+
`;
|
|
62
|
+
const iconMap = {
|
|
63
|
+
success: CheckCircle2,
|
|
64
|
+
error: XCircle,
|
|
65
|
+
warn: AlertTriangle,
|
|
66
|
+
info: Info,
|
|
67
|
+
};
|
|
68
|
+
export function ToastViewport() {
|
|
69
|
+
const toasts = useToastStore((s) => s.toasts);
|
|
70
|
+
return (_jsx(Stack, { "aria-live": "polite", children: toasts.map((t) => {
|
|
71
|
+
const Icon = iconMap[t.kind] ?? iconMap.info;
|
|
72
|
+
return (_jsxs(ColoredPill, { "$kind": t.kind, children: [_jsx(Icon, { size: 18 }), _jsx("span", { children: t.msg })] }, t.id));
|
|
73
|
+
}) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type Placement } from '@floating-ui/react';
|
|
3
|
+
export interface TooltipProps {
|
|
4
|
+
content: React.ReactNode;
|
|
5
|
+
placement?: Placement;
|
|
6
|
+
children: React.ReactElement;
|
|
7
|
+
}
|
|
8
|
+
export declare function Tooltip({ content, placement, children }: TooltipProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
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, useHover, useFocus, useDismiss, useRole, useInteractions, FloatingPortal, } from '@floating-ui/react';
|
|
5
|
+
const TooltipBubble = styled.div `
|
|
6
|
+
background: ${(p) => p.theme.colors.text};
|
|
7
|
+
color: ${(p) => p.theme.colors.bg};
|
|
8
|
+
border: 2px solid ${(p) => p.theme.colors.border};
|
|
9
|
+
border-radius: ${(p) => p.theme.radii.sm};
|
|
10
|
+
box-shadow: ${(p) => p.theme.shadow.stampSm};
|
|
11
|
+
padding: ${(p) => p.theme.space[1]} ${(p) => p.theme.space[2]};
|
|
12
|
+
font-family: ${(p) => p.theme.font.body};
|
|
13
|
+
font-size: 13px;
|
|
14
|
+
font-weight: 700;
|
|
15
|
+
line-height: 1.3;
|
|
16
|
+
max-width: 240px;
|
|
17
|
+
z-index: 1000;
|
|
18
|
+
`;
|
|
19
|
+
export function Tooltip({ content, placement = 'top', children }) {
|
|
20
|
+
const [open, setOpen] = React.useState(false);
|
|
21
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
22
|
+
open,
|
|
23
|
+
onOpenChange: setOpen,
|
|
24
|
+
placement,
|
|
25
|
+
whileElementsMounted: autoUpdate,
|
|
26
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
27
|
+
});
|
|
28
|
+
const hover = useHover(context, { move: false });
|
|
29
|
+
const focus = useFocus(context);
|
|
30
|
+
const dismiss = useDismiss(context);
|
|
31
|
+
const role = useRole(context, { role: 'tooltip' });
|
|
32
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
33
|
+
hover,
|
|
34
|
+
focus,
|
|
35
|
+
dismiss,
|
|
36
|
+
role,
|
|
37
|
+
]);
|
|
38
|
+
const child = React.Children.only(children);
|
|
39
|
+
return (_jsxs(_Fragment, { children: [React.cloneElement(child, getReferenceProps({
|
|
40
|
+
ref: refs.setReference,
|
|
41
|
+
...child.props,
|
|
42
|
+
})), open && (_jsx(FloatingPortal, { children: _jsx(TooltipBubble, { ref: refs.setFloating, style: floatingStyles, ...getFloatingProps(), children: content }) }))] }));
|
|
43
|
+
}
|