@attrus-ds/core 1.1.0 → 1.2.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/dist/css/components/buttons.css +51 -0
- package/dist/css/components/callout.css +55 -5
- package/dist/css/components/combobox.css +18 -0
- package/dist/css/components/filters.css +123 -0
- package/dist/css/components/inputs.css +52 -0
- package/dist/css/components/sidebar.css +8 -0
- package/dist/css/components/status-pills.css +22 -3
- package/dist/css/components/table.css +121 -7
- package/dist/css/components/toggle-chip.css +9 -0
- package/dist/css/index.css +1 -0
- package/dist/css/tokens.css +7 -0
- package/dist/react/Combobox/Combobox.d.ts +3 -0
- package/dist/react/Combobox/Combobox.tsx +18 -4
- package/dist/react/DTFilters/DTFilters.d.ts +34 -0
- package/dist/react/DTFilters/DTFilters.tsx +237 -0
- package/dist/react/DataTable/DataTable.d.ts +15 -3
- package/dist/react/DataTable/DataTable.tsx +73 -17
- package/dist/react/DateRangePicker/DateRangePicker.d.ts +24 -0
- package/dist/react/DateRangePicker/DateRangePicker.tsx +140 -0
- package/dist/react/Input/Input.tsx +1 -1
- package/dist/react/Pill/Pill.tsx +3 -1
- package/dist/react/index.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ATTRUS DateRangePicker — a period control for a table toolbar.
|
|
6
|
+
*
|
|
7
|
+
* Trigger + panel: presets on the left, two date fields on the right. Values are
|
|
8
|
+
* ISO dates ("2026-03-10"); an empty string means "open on that side".
|
|
9
|
+
*
|
|
10
|
+
* `portal` (default true) renders the panel in <body> with fixed coordinates —
|
|
11
|
+
* a .dt-wrap clips overflow, so an inline panel would be cut by the table.
|
|
12
|
+
*/
|
|
13
|
+
export interface DateRangePreset { label: string; from: string; to: string; }
|
|
14
|
+
|
|
15
|
+
export interface DateRangePickerProps {
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
onChange: (from: string, to: string) => void;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** sm = 32px trigger, for a table toolbar. */
|
|
21
|
+
size?: 'sm' | 'md';
|
|
22
|
+
portal?: boolean;
|
|
23
|
+
/** Defaults to last 7 / 30 days, this month, last month — relative to today. */
|
|
24
|
+
presets?: DateRangePreset[];
|
|
25
|
+
locale?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
'aria-label'?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const iso = (d: Date) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
31
|
+
|
|
32
|
+
export function defaultDatePresets(today = new Date()): DateRangePreset[] {
|
|
33
|
+
const t = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
34
|
+
const minus = (n: number) => { const d = new Date(t); d.setDate(d.getDate() - n); return d; };
|
|
35
|
+
const mStart = new Date(t.getFullYear(), t.getMonth(), 1);
|
|
36
|
+
const lmStart = new Date(t.getFullYear(), t.getMonth() - 1, 1);
|
|
37
|
+
const lmEnd = new Date(t.getFullYear(), t.getMonth(), 0);
|
|
38
|
+
return [
|
|
39
|
+
{ label: 'Last 7 days', from: iso(minus(6)), to: iso(t) },
|
|
40
|
+
{ label: 'Last 30 days', from: iso(minus(29)), to: iso(t) },
|
|
41
|
+
{ label: 'This month', from: iso(mStart), to: iso(t) },
|
|
42
|
+
{ label: 'Last month', from: iso(lmStart), to: iso(lmEnd) },
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatDateRange(from: string, to: string, locale = 'en-US'): string {
|
|
47
|
+
if (!from && !to) return '';
|
|
48
|
+
const f = (s: string) => {
|
|
49
|
+
const [y, m, d] = s.split('-').map(Number);
|
|
50
|
+
return new Date(y, m - 1, d).toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
51
|
+
};
|
|
52
|
+
return (from ? f(from) : '…') + ' – ' + (to ? f(to) : '…');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CalendarRange = () => (
|
|
56
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
57
|
+
<rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18M17 14h-6M13 18H7M7 14h.01M17 18h.01" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
|
62
|
+
from, to, onChange, placeholder = 'Period', size = 'md', portal = true, presets, locale = 'en-US', className, ...rest
|
|
63
|
+
}) => {
|
|
64
|
+
const [open, setOpen] = React.useState(false);
|
|
65
|
+
const [pos, setPos] = React.useState<{ left: number; top: number } | null>(null);
|
|
66
|
+
const wrapRef = React.useRef<HTMLDivElement>(null);
|
|
67
|
+
const panelRef = React.useRef<HTMLDivElement>(null);
|
|
68
|
+
const list = presets || defaultDatePresets();
|
|
69
|
+
const label = formatDateRange(from, to, locale);
|
|
70
|
+
|
|
71
|
+
React.useLayoutEffect(() => {
|
|
72
|
+
if (!open || !portal) return undefined;
|
|
73
|
+
const place = () => {
|
|
74
|
+
const r = wrapRef.current?.getBoundingClientRect();
|
|
75
|
+
if (r) setPos({ left: Math.min(r.left, window.innerWidth - 16 - (panelRef.current?.offsetWidth || 0)), top: r.bottom + 8 });
|
|
76
|
+
};
|
|
77
|
+
place();
|
|
78
|
+
window.addEventListener('scroll', place, true);
|
|
79
|
+
window.addEventListener('resize', place);
|
|
80
|
+
return () => { window.removeEventListener('scroll', place, true); window.removeEventListener('resize', place); };
|
|
81
|
+
}, [open, portal]);
|
|
82
|
+
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
if (!open) return undefined;
|
|
85
|
+
const onDoc = (e: MouseEvent) => {
|
|
86
|
+
const t = e.target as Node;
|
|
87
|
+
if (wrapRef.current?.contains(t) || panelRef.current?.contains(t)) return;
|
|
88
|
+
setOpen(false);
|
|
89
|
+
};
|
|
90
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
|
|
91
|
+
document.addEventListener('mousedown', onDoc);
|
|
92
|
+
document.addEventListener('keydown', onKey);
|
|
93
|
+
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
|
|
94
|
+
}, [open]);
|
|
95
|
+
|
|
96
|
+
const panel = (
|
|
97
|
+
<div ref={panelRef} className="date-range-panel" role="dialog" aria-label={rest['aria-label'] || placeholder}
|
|
98
|
+
style={portal ? { position: 'fixed', left: pos ? pos.left : -9999, top: pos ? pos.top : 0 } : undefined}>
|
|
99
|
+
<div className="date-range-presets">
|
|
100
|
+
{list.map((p) => (
|
|
101
|
+
<button key={p.label} type="button"
|
|
102
|
+
className={['date-range-preset', p.from === from && p.to === to ? 'is-active' : ''].filter(Boolean).join(' ')}
|
|
103
|
+
onClick={() => { onChange(p.from, p.to); setOpen(false); }}>{p.label}</button>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
<div className="date-range-fields">
|
|
107
|
+
<label className="date-range-field">From
|
|
108
|
+
<input className="input input-sm" type="date" value={from} max={to || undefined} onChange={(e) => onChange(e.target.value, to)} />
|
|
109
|
+
</label>
|
|
110
|
+
<label className="date-range-field">To
|
|
111
|
+
<input className="input input-sm" type="date" value={to} min={from || undefined} onChange={(e) => onChange(from, e.target.value)} />
|
|
112
|
+
</label>
|
|
113
|
+
<div className="date-range-actions">
|
|
114
|
+
{from || to ? <button type="button" className="btn btn-ghost btn-sm" onClick={() => onChange('', '')}>Clear</button> : null}
|
|
115
|
+
<button type="button" className="btn btn-secondary btn-sm" onClick={() => setOpen(false)}>Done</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div ref={wrapRef}
|
|
123
|
+
className={['date-range', size === 'sm' ? 'date-range-sm' : '', portal ? 'is-portal' : '', className || ''].filter(Boolean).join(' ')}>
|
|
124
|
+
<button type="button" className="date-trigger" aria-haspopup="dialog" aria-expanded={open}
|
|
125
|
+
aria-label={rest['aria-label'] ? rest['aria-label'] + (label ? ': ' + label : '') : undefined}
|
|
126
|
+
onClick={() => setOpen((o) => !o)}>
|
|
127
|
+
{/* Canonical .date-trigger anatomy: icon + text grouped in ONE leading span
|
|
128
|
+
(the trigger is space-between), and a trailing chevron. */}
|
|
129
|
+
<span className="date-range-label">
|
|
130
|
+
<CalendarRange />
|
|
131
|
+
{label ? <span>{label}</span> : <span className="placeholder">{placeholder}</span>}
|
|
132
|
+
</span>
|
|
133
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
|
|
134
|
+
</button>
|
|
135
|
+
{open ? (portal && typeof document !== 'undefined' ? createPortal(panel, document.body) : panel) : null}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default DateRangePicker;
|
|
@@ -176,7 +176,7 @@ export const Input: React.FC<InputProps> = ({
|
|
|
176
176
|
// wins over static text on the same side: a cell is either a label or a
|
|
177
177
|
// control, never both.
|
|
178
178
|
control = (
|
|
179
|
-
<div className={['group', groupShape === 'pill' ? 'is-pill' : '', groupShape === 'underline' ? 'underline' : '', status === 'error' ? 'is-error' : ''].filter(Boolean).join(' ')}>
|
|
179
|
+
<div className={['group', size === 'sm' ? 'group-sm' : '', groupShape === 'pill' ? 'is-pill' : '', groupShape === 'underline' ? 'underline' : '', status === 'error' ? 'is-error' : ''].filter(Boolean).join(' ')}>
|
|
180
180
|
{before != null ? renderAddon(before, 'l') : prefix != null ? <span className="fix l">{prefix}</span> : null}
|
|
181
181
|
<input id={inputId} readOnly={readOnlyQuiet || rest.readOnly} {...rest} {...a11y} />
|
|
182
182
|
{after != null ? renderAddon(after, 'r') : suffix != null ? <span className="fix r">{suffix}</span> : null}
|
package/dist/react/Pill/Pill.tsx
CHANGED
|
@@ -64,7 +64,9 @@ export const Pill: React.FC<PillProps> = ({
|
|
|
64
64
|
<React.Fragment>
|
|
65
65
|
{dot ? <span className="dot" /> : null}
|
|
66
66
|
{icon}
|
|
67
|
-
{children != null ?
|
|
67
|
+
{children != null ? (
|
|
68
|
+
<span className={typeof children === 'string' && /[A-Z]/.test(children) && children === children.toUpperCase() ? 'pill-label is-caps' : 'pill-label'}>{children}</span>
|
|
69
|
+
) : null}
|
|
68
70
|
{onRemove ? (
|
|
69
71
|
<span
|
|
70
72
|
className="pill-x"
|
package/dist/react/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export * from "./Callout/Callout";
|
|
|
21
21
|
export * from "./Card/Card";
|
|
22
22
|
export * from "./Combobox/Combobox";
|
|
23
23
|
export * from "./DataTable/DataTable";
|
|
24
|
+
export * from "./DateRangePicker/DateRangePicker";
|
|
25
|
+
export * from "./DTFilters/DTFilters";
|
|
24
26
|
export * from "./Drawer/Drawer";
|
|
25
27
|
export * from "./Input/Input";
|
|
26
28
|
export * from "./Modal/Modal";
|
package/package.json
CHANGED