@attrus-ds/core 1.0.10 → 1.0.12
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/action-bar.css +93 -0
- package/dist/css/components/ambient.css +97 -0
- package/dist/css/components/animated-bg.css +261 -0
- package/dist/css/components/buttons.css +4 -4
- package/dist/css/components/callout.css +4 -3
- package/dist/css/components/cards.css +4 -3
- package/dist/css/components/combobox.css +16 -13
- package/dist/css/components/inputs.css +99 -15
- package/dist/css/components/number-input.css +6 -3
- package/dist/css/components/primitives.css +20 -15
- package/dist/css/components/sel-card.css +212 -0
- package/dist/css/components/sidebar.css +11 -9
- package/dist/css/components/smart-search.css +13 -11
- package/dist/css/components/status-pills.css +1 -1
- package/dist/css/components/stepper.css +81 -4
- package/dist/css/components/table.css +168 -7
- package/dist/css/components/tabs.css +4 -3
- package/dist/css/components/toast.css +2 -2
- package/dist/css/components/toggle-chip.css +137 -0
- package/dist/css/components/tooltip.css +8 -5
- package/dist/css/components/topbar.css +8 -7
- package/dist/css/index.css +5 -0
- package/dist/css/tokens.css +126 -22
- package/dist/react/ActionBar/ActionBar.d.ts +19 -0
- package/dist/react/ActionBar/ActionBar.tsx +54 -0
- package/dist/react/AmbientBackdrop/AmbientBackdrop.d.ts +17 -0
- package/dist/react/AmbientBackdrop/AmbientBackdrop.tsx +53 -0
- package/dist/react/AnimatedBackdrop/AnimatedBackdrop.d.ts +22 -0
- package/dist/react/AnimatedBackdrop/AnimatedBackdrop.tsx +317 -0
- package/dist/react/Callout/Callout.d.ts +1 -1
- package/dist/react/Callout/Callout.tsx +1 -1
- package/dist/react/Combobox/Combobox.d.ts +2 -0
- package/dist/react/Combobox/Combobox.tsx +85 -39
- package/dist/react/DataTable/DataTable.tsx +3 -3
- package/dist/react/Drawer/Drawer.tsx +7 -1
- package/dist/react/Input/Input.d.ts +1 -1
- package/dist/react/Input/Input.tsx +1 -1
- package/dist/react/Modal/Modal.tsx +8 -1
- package/dist/react/SelectableCard/SelectableCard.d.ts +39 -0
- package/dist/react/SelectableCard/SelectableCard.tsx +94 -0
- package/dist/react/SmartSearch/SmartSearch.tsx +2 -2
- package/dist/react/Stepper/Stepper.d.ts +3 -0
- package/dist/react/Stepper/Stepper.tsx +7 -0
- package/dist/react/Toast/Toast.d.ts +1 -1
- package/dist/react/Toast/Toast.tsx +1 -1
- package/dist/react/ToggleChip/ToggleChip.d.ts +29 -0
- package/dist/react/ToggleChip/ToggleChip.tsx +76 -0
- package/dist/react/Tooltip/Tooltip.d.ts +1 -1
- package/dist/react/Tooltip/Tooltip.tsx +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS AmbientBackdrop — typed wrapper over the canonical `.ambient`
|
|
5
|
+
* (preview/components/ambient.css): the official decorative layer of soft
|
|
6
|
+
* radial brand blobs behind hero / marketing content.
|
|
7
|
+
*
|
|
8
|
+
* Use this instead of hand-rolling radial gradients on every landing: the blobs
|
|
9
|
+
* derive from the product's brand + accent tokens, the readability scrim is
|
|
10
|
+
* built in (mandatory — copy drops below AA over a bare blob), and motion
|
|
11
|
+
* respects prefers-reduced-motion.
|
|
12
|
+
*/
|
|
13
|
+
export interface AmbientBackdropProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
/** How many blobs to render (1–3). */
|
|
16
|
+
blobs?: 1 | 2 | 3;
|
|
17
|
+
/** Blob opacity preset. */
|
|
18
|
+
intensity?: 'subtle' | 'default' | 'strong';
|
|
19
|
+
/** Dark hero: ink background + ink scrim. */
|
|
20
|
+
onInverse?: boolean;
|
|
21
|
+
/** Drop the readability veil. Only for purely decorative strips with no text. */
|
|
22
|
+
noScrim?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const AmbientBackdrop: React.FC<AmbientBackdropProps> = ({
|
|
26
|
+
children,
|
|
27
|
+
blobs = 2,
|
|
28
|
+
intensity = 'default',
|
|
29
|
+
onInverse = false,
|
|
30
|
+
noScrim = false,
|
|
31
|
+
className,
|
|
32
|
+
...rest
|
|
33
|
+
}) => {
|
|
34
|
+
const cls = [
|
|
35
|
+
'ambient',
|
|
36
|
+
intensity === 'subtle' ? 'is-subtle' : '',
|
|
37
|
+
intensity === 'strong' ? 'is-strong' : '',
|
|
38
|
+
onInverse ? 'on-inverse' : '',
|
|
39
|
+
className || '',
|
|
40
|
+
].filter(Boolean).join(' ');
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={cls} {...rest}>
|
|
44
|
+
{Array.from({ length: blobs }, (_, i) => (
|
|
45
|
+
<div key={i} className={`ambient-blob is-${i + 1}`} aria-hidden="true" />
|
|
46
|
+
))}
|
|
47
|
+
{noScrim ? null : <div className="ambient-scrim" aria-hidden="true" />}
|
|
48
|
+
<div className="ambient-content">{children}</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default AmbientBackdrop;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export type AnimatedBackdropVariant =
|
|
4
|
+
| 'aurora' | 'mesh' | 'grid' | 'ripple' | 'beams' | 'orbits' | 'apex' | 'triangles';
|
|
5
|
+
|
|
6
|
+
export interface AnimatedBackdropProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
/** Which background to render. */
|
|
9
|
+
variant?: AnimatedBackdropVariant;
|
|
10
|
+
/** Layer opacity preset. */
|
|
11
|
+
intensity?: 'subtle' | 'default' | 'strong';
|
|
12
|
+
/** Dark hero: ink base + ink scrim. */
|
|
13
|
+
onInverse?: boolean;
|
|
14
|
+
/** Drop the readability veil. Only for decorative strips with no text. */
|
|
15
|
+
noScrim?: boolean;
|
|
16
|
+
/** Veil direction — 'diagonal' anchors copy left, 'vertical' suits centred copy. */
|
|
17
|
+
scrim?: 'diagonal' | 'vertical';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export declare const AnimatedBackdrop: React.FC<AnimatedBackdropProps>;
|
|
21
|
+
|
|
22
|
+
export default AnimatedBackdrop;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS AnimatedBackdrop — typed wrapper over the canonical `.abg`
|
|
5
|
+
* (preview/components/animated-bg.css): eight full-bleed animated backgrounds
|
|
6
|
+
* for hero, onboarding and marketing surfaces.
|
|
7
|
+
*
|
|
8
|
+
* Companion to `AmbientBackdrop` (plain drifting blobs). Reach for this one
|
|
9
|
+
* when the surface needs motion vocabulary — aurora, cursor mesh, scanning
|
|
10
|
+
* grid, ripples, beams, orbits, apex bloom, drifting triangles.
|
|
11
|
+
*
|
|
12
|
+
* Every tint derives from the product's brand/accent tokens, so a backdrop
|
|
13
|
+
* reskins with the active product. The readability scrim is on by default —
|
|
14
|
+
* copy drops below AA over a bare layer.
|
|
15
|
+
*/
|
|
16
|
+
export type AnimatedBackdropVariant =
|
|
17
|
+
| 'aurora' | 'mesh' | 'grid' | 'ripple' | 'beams' | 'orbits' | 'apex' | 'triangles';
|
|
18
|
+
|
|
19
|
+
export interface AnimatedBackdropProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
/** Which background to render. */
|
|
22
|
+
variant?: AnimatedBackdropVariant;
|
|
23
|
+
/** Layer opacity preset. */
|
|
24
|
+
intensity?: 'subtle' | 'default' | 'strong';
|
|
25
|
+
/** Dark hero: ink base + ink scrim. */
|
|
26
|
+
onInverse?: boolean;
|
|
27
|
+
/** Drop the readability veil. Only for decorative strips with no text. */
|
|
28
|
+
noScrim?: boolean;
|
|
29
|
+
/** Veil direction — 'diagonal' anchors copy left, 'vertical' suits centred copy. */
|
|
30
|
+
scrim?: 'diagonal' | 'vertical';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const RIPPLE_DELAYS = [0, -1.8, -3.6, -5.4, -7.2, -9, -10.8, -12.6, -14.4, -16.2];
|
|
34
|
+
const BEAM_WEIGHTS = [1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2];
|
|
35
|
+
const ORBIT_SIZES = [130, 200, 280, 380, 490, 610, 740, 880, 1030, 1190];
|
|
36
|
+
const GLINT_ROWS = [25, 43.75, 62.5, 81.25];
|
|
37
|
+
const GLINT_COLS = [11.54, 23.08, 34.62, 50, 61.54, 73.08, 84.62];
|
|
38
|
+
/* The ATTRUS apex, as a single path — traced by the light beam. */
|
|
39
|
+
const APEX_PATH = 'M108.233 6.74605C105.184 2.677 100.34 0 94.8819 0C89.4234 0 84.4464 2.73056 81.3961 6.8799L80.6203 8.03098L0 129.352H27.1854L90.0923 32.6594L94.9622 25.1637L99.7248 32.6325L161.615 129.352H189.176L109.304 8.37901L108.233 6.74605Z';
|
|
40
|
+
|
|
41
|
+
export const AnimatedBackdrop: React.FC<AnimatedBackdropProps> = ({
|
|
42
|
+
children,
|
|
43
|
+
variant = 'aurora',
|
|
44
|
+
intensity = 'default',
|
|
45
|
+
onInverse = false,
|
|
46
|
+
noScrim = false,
|
|
47
|
+
scrim = 'diagonal',
|
|
48
|
+
className,
|
|
49
|
+
...rest
|
|
50
|
+
}) => {
|
|
51
|
+
const hostRef = React.useRef<HTMLDivElement>(null);
|
|
52
|
+
|
|
53
|
+
/* Mesh only: feed the cursor position to the mask that reveals the bright dots. */
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (variant !== 'mesh') return undefined;
|
|
56
|
+
const el = hostRef.current;
|
|
57
|
+
if (!el) return undefined;
|
|
58
|
+
const onMove = (e: PointerEvent) => {
|
|
59
|
+
const r = el.getBoundingClientRect();
|
|
60
|
+
el.style.setProperty('--mx', e.clientX - r.left + 'px');
|
|
61
|
+
el.style.setProperty('--my', e.clientY - r.top + 'px');
|
|
62
|
+
};
|
|
63
|
+
el.addEventListener('pointermove', onMove);
|
|
64
|
+
return () => el.removeEventListener('pointermove', onMove);
|
|
65
|
+
}, [variant]);
|
|
66
|
+
|
|
67
|
+
const cls = [
|
|
68
|
+
'abg',
|
|
69
|
+
'abg-' + variant,
|
|
70
|
+
intensity === 'subtle' ? 'is-subtle' : '',
|
|
71
|
+
intensity === 'strong' ? 'is-strong' : '',
|
|
72
|
+
onInverse ? 'on-inverse' : '',
|
|
73
|
+
className || '',
|
|
74
|
+
].filter(Boolean).join(' ');
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div ref={hostRef} className={cls} {...rest}>
|
|
78
|
+
{renderLayers(variant)}
|
|
79
|
+
{noScrim ? null : <div className={'abg-scrim' + (scrim === 'vertical' ? ' is-vertical' : '')} aria-hidden="true" />}
|
|
80
|
+
<div className="abg-content">{children}</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function renderLayers(variant: AnimatedBackdropVariant): React.ReactNode {
|
|
86
|
+
const svgBase: React.CSSProperties = { position: 'absolute', inset: 0, width: '100%', height: '100%' };
|
|
87
|
+
|
|
88
|
+
if (variant === 'aurora') {
|
|
89
|
+
return (
|
|
90
|
+
<div className="abg-layer is-tinted" aria-hidden="true">
|
|
91
|
+
<div className="abg-blob is-1" />
|
|
92
|
+
<div className="abg-blob is-2" />
|
|
93
|
+
<div className="abg-blob is-3" />
|
|
94
|
+
<div className="abg-blob is-4" />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (variant === 'mesh') {
|
|
100
|
+
return (
|
|
101
|
+
<React.Fragment>
|
|
102
|
+
<div className="abg-layer is-tinted" aria-hidden="true">
|
|
103
|
+
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" style={svgBase}>
|
|
104
|
+
<defs>
|
|
105
|
+
<pattern id="abg-dots" width="6" height="6" patternUnits="userSpaceOnUse">
|
|
106
|
+
<circle cx="1" cy="1" r=".62" fill="var(--color-brand-accent-500)" opacity=".22" />
|
|
107
|
+
</pattern>
|
|
108
|
+
<radialGradient id="abg-fade" cx="62%" cy="70%" r="72%">
|
|
109
|
+
<stop offset="30%" stopColor="#fff" stopOpacity="1" />
|
|
110
|
+
<stop offset="100%" stopColor="#fff" stopOpacity="0" />
|
|
111
|
+
</radialGradient>
|
|
112
|
+
<mask id="abg-mask"><rect width="640" height="400" fill="url(#abg-fade)" /></mask>
|
|
113
|
+
</defs>
|
|
114
|
+
<rect width="640" height="400" fill="url(#abg-dots)" mask="url(#abg-mask)" />
|
|
115
|
+
</svg>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="abg-dotglow" aria-hidden="true">
|
|
118
|
+
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" style={svgBase}>
|
|
119
|
+
<defs>
|
|
120
|
+
<pattern id="abg-dots-hi" width="6" height="6" patternUnits="userSpaceOnUse">
|
|
121
|
+
<circle cx="1" cy="1" r=".85" fill="var(--color-brand-accent-300)" />
|
|
122
|
+
</pattern>
|
|
123
|
+
</defs>
|
|
124
|
+
<rect width="640" height="400" fill="url(#abg-dots-hi)" />
|
|
125
|
+
</svg>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="abg-layer is-tinted" aria-hidden="true" style={{ background: 'radial-gradient(90% 80% at 72% 74%, color-mix(in srgb, var(--color-brand-secondary-500) 26%, transparent), transparent 62%)' }} />
|
|
128
|
+
</React.Fragment>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (variant === 'grid') {
|
|
133
|
+
return (
|
|
134
|
+
<React.Fragment>
|
|
135
|
+
<div className="abg-layer" aria-hidden="true" style={{ background: 'linear-gradient(155deg, var(--abg-ink) 10%, color-mix(in srgb, var(--abg-base) 78%, var(--abg-ink)) 100%)' }} />
|
|
136
|
+
<div className="abg-layer abg-gridlines is-tinted" aria-hidden="true" />
|
|
137
|
+
<div className="abg-layer" aria-hidden="true" style={{ overflow: 'hidden' }}>
|
|
138
|
+
{GLINT_ROWS.map((pct, i) => (
|
|
139
|
+
<div key={'r' + i} className="abg-glint-x" style={{
|
|
140
|
+
top: pct + '%',
|
|
141
|
+
width: 14 + (i % 3) * 6 + '%',
|
|
142
|
+
opacity: 0.34 + (i % 3) * 0.04,
|
|
143
|
+
animationDuration: 11 + (i % 4) * 3 + 's',
|
|
144
|
+
animationDelay: i * 2.4 + 's',
|
|
145
|
+
}} />
|
|
146
|
+
))}
|
|
147
|
+
{GLINT_COLS.map((pct, i) => (
|
|
148
|
+
<div key={'c' + i} className="abg-glint-y" style={{
|
|
149
|
+
left: pct + '%',
|
|
150
|
+
height: 14 + (i % 3) * 6 + '%',
|
|
151
|
+
opacity: 0.3 + (i % 3) * 0.04,
|
|
152
|
+
animationDuration: 13 + (i % 4) * 3 + 's',
|
|
153
|
+
animationDelay: i * 1.7 + 0.7 + 's',
|
|
154
|
+
}} />
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
<div className="abg-layer abg-vignette" aria-hidden="true" />
|
|
158
|
+
</React.Fragment>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (variant === 'ripple') {
|
|
163
|
+
return (
|
|
164
|
+
<React.Fragment>
|
|
165
|
+
<div className="abg-layer" aria-hidden="true" style={{ background: 'radial-gradient(125% 105% at 62% 46%, color-mix(in srgb, var(--abg-base) 42%, var(--abg-ink)), var(--abg-ink) 76%)' }} />
|
|
166
|
+
<div className="abg-layer is-tinted" aria-hidden="true">
|
|
167
|
+
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" style={svgBase}>
|
|
168
|
+
<g fill="none" stroke="var(--color-brand-accent-500)" strokeWidth="1.2">
|
|
169
|
+
{RIPPLE_DELAYS.map((d, i) => {
|
|
170
|
+
const soft = i % 2 === 1;
|
|
171
|
+
return (
|
|
172
|
+
<ellipse key={i} cx="420" cy="200" rx="380" ry="262"
|
|
173
|
+
stroke={soft ? 'var(--color-brand-accent-300)' : undefined}
|
|
174
|
+
strokeWidth={soft ? '.8' : undefined}
|
|
175
|
+
opacity={soft ? '.4' : undefined}
|
|
176
|
+
style={{
|
|
177
|
+
transformOrigin: '420px 200px',
|
|
178
|
+
animation: (soft ? 'abg-ripple-soft' : 'abg-ripple') + ' 18s var(--ease-standard) ' + d + 's infinite',
|
|
179
|
+
}} />
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</g>
|
|
183
|
+
<circle cx="420" cy="200" r="5.5" fill="var(--color-brand-accent-300)" opacity=".28" />
|
|
184
|
+
</svg>
|
|
185
|
+
</div>
|
|
186
|
+
</React.Fragment>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (variant === 'beams') {
|
|
191
|
+
return (
|
|
192
|
+
<React.Fragment>
|
|
193
|
+
<div className="abg-layer" aria-hidden="true" style={{ background: 'linear-gradient(180deg, color-mix(in srgb, var(--abg-base) 62%, var(--abg-ink)) 0%, var(--abg-ink) 78%)' }} />
|
|
194
|
+
<div className="abg-layer abg-tracks is-tinted" aria-hidden="true">
|
|
195
|
+
{BEAM_WEIGHTS.map((w, i) => {
|
|
196
|
+
const strong = w >= 3;
|
|
197
|
+
return (
|
|
198
|
+
<div key={i} className="abg-track" style={{
|
|
199
|
+
background: 'linear-gradient(90deg, transparent, color-mix(in srgb, var(--color-brand-accent-500) ' + (3 + w * 2) + '%, transparent) 45%, color-mix(in srgb, var(--color-brand-accent-500) ' + (2 + w) + '%, transparent))',
|
|
200
|
+
}}>
|
|
201
|
+
<div className={'abg-beam' + (strong ? ' is-strong' : '')} style={{
|
|
202
|
+
width: 22 + w * 5 + '%',
|
|
203
|
+
opacity: 0.16 + w * 0.06,
|
|
204
|
+
animationDuration: 6 + (i % 5) * 1.8 + w * 0.5 + 's',
|
|
205
|
+
animationDelay: i * 0.74 + 's',
|
|
206
|
+
}} />
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
</div>
|
|
211
|
+
<div className="abg-layer is-tinted" aria-hidden="true" style={{ inset: 'auto 0 auto auto', top: '20%', bottom: '20%', right: 0, width: '24%', filter: 'blur(30px)', background: 'radial-gradient(ellipse at 100% 50%, color-mix(in srgb, var(--color-brand-accent-500) 26%, transparent), transparent 74%)' }} />
|
|
212
|
+
<div className="abg-layer abg-vignette" aria-hidden="true" />
|
|
213
|
+
</React.Fragment>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (variant === 'orbits') {
|
|
218
|
+
return (
|
|
219
|
+
<React.Fragment>
|
|
220
|
+
<div className="abg-layer" aria-hidden="true" style={{ background: 'radial-gradient(120% 100% at 62% 50%, color-mix(in srgb, var(--abg-base) 38%, var(--abg-ink)), var(--abg-ink) 76%)' }} />
|
|
221
|
+
<div className="abg-layer is-tinted" aria-hidden="true">
|
|
222
|
+
<div className="abg-hub">
|
|
223
|
+
{ORBIT_SIZES.map((d, i) => (
|
|
224
|
+
<div key={i} className="abg-ring" style={{
|
|
225
|
+
left: -d / 2 + 'px', top: -d / 2 + 'px', width: d + 'px', height: d + 'px',
|
|
226
|
+
borderColor: 'color-mix(in srgb, var(--color-brand-accent-500) ' + Math.max(4, 24 - i * 2.4).toFixed(1) + '%, transparent)',
|
|
227
|
+
animationDuration: 18 + i * 7 + 's',
|
|
228
|
+
animationDirection: i % 2 ? 'reverse' : 'normal',
|
|
229
|
+
}}>
|
|
230
|
+
<span className="abg-sat" style={{ opacity: Math.max(0.25, 0.9 - i * 0.07) }} />
|
|
231
|
+
</div>
|
|
232
|
+
))}
|
|
233
|
+
<span className="abg-core" />
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</React.Fragment>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (variant === 'apex') {
|
|
241
|
+
/* Layered strokes of growing width and shifting dash offset read as one
|
|
242
|
+
beam with a bloom, without a filter per stroke. */
|
|
243
|
+
const beams = Array.from({ length: 12 }, (_, i) => ({
|
|
244
|
+
w: (0.45 + i * 0.035).toFixed(2),
|
|
245
|
+
dash: (480 - i * 21) + ' ' + (148 + i * 21),
|
|
246
|
+
delay: (-1.548 - i * 0.19).toFixed(3),
|
|
247
|
+
opacity: (0.012 + i * 0.0022).toFixed(4),
|
|
248
|
+
}));
|
|
249
|
+
return (
|
|
250
|
+
<React.Fragment>
|
|
251
|
+
<div className="abg-layer" aria-hidden="true" style={{ background: 'radial-gradient(125% 105% at 66% 44%, color-mix(in srgb, var(--abg-base) 52%, var(--abg-ink)), var(--abg-ink) 78%)' }} />
|
|
252
|
+
<div className="abg-layer is-tinted" aria-hidden="true">
|
|
253
|
+
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" style={svgBase}>
|
|
254
|
+
<defs>
|
|
255
|
+
<path id="abg-apex-src" d={APEX_PATH} />
|
|
256
|
+
<filter id="abg-apex-bloom" x="-40%" y="-40%" width="180%" height="180%">
|
|
257
|
+
<feGaussianBlur stdDeviation="4.5" result="b1" />
|
|
258
|
+
<feMerge><feMergeNode in="b1" /><feMergeNode in="SourceGraphic" /></feMerge>
|
|
259
|
+
</filter>
|
|
260
|
+
</defs>
|
|
261
|
+
<g transform="translate(352 118) scale(1.05)">
|
|
262
|
+
<use href="#abg-apex-src" fill="var(--color-brand-accent-500)" opacity=".012" />
|
|
263
|
+
<use href="#abg-apex-src" fill="none" stroke="var(--color-brand-accent-500)" strokeWidth=".7" strokeLinejoin="round" opacity=".07" />
|
|
264
|
+
<g filter="url(#abg-apex-bloom)">
|
|
265
|
+
{beams.map((b, i) => (
|
|
266
|
+
<use key={i} href="#abg-apex-src" fill="none"
|
|
267
|
+
stroke="var(--color-brand-accent-400)"
|
|
268
|
+
strokeWidth={b.w} strokeLinecap="round" strokeLinejoin="round"
|
|
269
|
+
opacity={b.opacity} strokeDasharray={b.dash}
|
|
270
|
+
style={{ animation: 'abg-apex-glint 6s linear ' + b.delay + 's infinite' }} />
|
|
271
|
+
))}
|
|
272
|
+
<use href="#abg-apex-src" fill="none" stroke="var(--color-brand-accent-200)"
|
|
273
|
+
strokeWidth="1.1" strokeLinecap="round" strokeLinejoin="round"
|
|
274
|
+
opacity=".5" strokeDasharray="18 610"
|
|
275
|
+
style={{ animation: 'abg-apex-glint 6s linear infinite' }} />
|
|
276
|
+
</g>
|
|
277
|
+
</g>
|
|
278
|
+
</svg>
|
|
279
|
+
</div>
|
|
280
|
+
</React.Fragment>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* triangles */
|
|
285
|
+
return (
|
|
286
|
+
<React.Fragment>
|
|
287
|
+
<div className="abg-layer" aria-hidden="true" style={{ background: 'radial-gradient(120% 100% at 60% 46%, color-mix(in srgb, var(--abg-base) 34%, var(--abg-ink)), var(--abg-ink) 78%)' }} />
|
|
288
|
+
<div className="abg-layer is-tinted" aria-hidden="true">
|
|
289
|
+
<svg viewBox="0 0 640 400" preserveAspectRatio="xMidYMid slice" style={svgBase}>
|
|
290
|
+
<defs>
|
|
291
|
+
<filter id="abg-tri-soft" x="-25%" y="-25%" width="150%" height="150%">
|
|
292
|
+
<feGaussianBlur stdDeviation="18" />
|
|
293
|
+
</filter>
|
|
294
|
+
</defs>
|
|
295
|
+
<g style={{ transformOrigin: '120px 300px', animation: 'abg-tri-a 34s linear infinite alternate' }}>
|
|
296
|
+
<polygon points="120,-60 460,320 -220,320" fill="color-mix(in srgb, var(--color-brand-secondary-500) 9%, transparent)" filter="url(#abg-tri-soft)" />
|
|
297
|
+
</g>
|
|
298
|
+
<g style={{ transformOrigin: '520px 90px', animation: 'abg-tri-b 30s linear infinite alternate' }}>
|
|
299
|
+
<polygon points="520,-120 880,300 160,300" fill="none" stroke="var(--color-brand-accent-500)" strokeWidth="1" opacity=".14" />
|
|
300
|
+
</g>
|
|
301
|
+
<g style={{ transformOrigin: '380px 340px', animation: 'abg-tri-c 38s linear infinite alternate' }}>
|
|
302
|
+
<polygon points="380,120 700,560 60,560" fill="color-mix(in srgb, var(--color-brand-accent-500) 6%, transparent)" filter="url(#abg-tri-soft)" />
|
|
303
|
+
</g>
|
|
304
|
+
<g style={{ transformOrigin: '80px 120px', animation: 'abg-tri-d 26s linear infinite alternate' }}>
|
|
305
|
+
<polygon points="80,-40 300,300 -140,300" fill="none" stroke="var(--color-brand-accent-300)" strokeWidth=".8" opacity=".1" />
|
|
306
|
+
</g>
|
|
307
|
+
<g style={{ transformOrigin: '600px 260px', animation: 'abg-tri-a 44s linear -20s infinite alternate' }}>
|
|
308
|
+
<polygon points="600,60 900,520 300,520" fill="color-mix(in srgb, var(--dataviz-cat-3) 5%, transparent)" filter="url(#abg-tri-soft)" />
|
|
309
|
+
</g>
|
|
310
|
+
</svg>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="abg-layer abg-vignette" aria-hidden="true" />
|
|
313
|
+
</React.Fragment>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export default AnimatedBackdrop;
|
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
export type CalloutTone = 'info' | 'warn' | 'success' | 'danger' | 'neutral';
|
|
4
4
|
|
|
5
|
-
export interface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
export interface CalloutProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
6
6
|
/** Semantic tone. Default 'info' (brand royal tint). */
|
|
7
7
|
tone?: CalloutTone;
|
|
8
8
|
/** Compact density for dense panels & forms. */
|
|
@@ -13,7 +13,7 @@ import * as React from 'react';
|
|
|
13
13
|
|
|
14
14
|
export type CalloutTone = 'info' | 'warn' | 'success' | 'danger' | 'neutral';
|
|
15
15
|
|
|
16
|
-
export interface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
export interface CalloutProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
17
17
|
/** Semantic tone. Default 'info' (brand royal tint). */
|
|
18
18
|
tone?: CalloutTone;
|
|
19
19
|
/** Compact density for dense panels & forms. */
|
|
@@ -43,6 +43,8 @@ export interface ComboboxProps {
|
|
|
43
43
|
emptyTitle?: React.ReactNode;
|
|
44
44
|
emptySub?: React.ReactNode;
|
|
45
45
|
disabled?: boolean;
|
|
46
|
+
/** Gallery/doc rendering: popover stays inline (not portaled). */
|
|
47
|
+
static?: boolean;
|
|
46
48
|
className?: string;
|
|
47
49
|
style?: React.CSSProperties;
|
|
48
50
|
'aria-label'?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* ATTRUS Combobox — typed wrapper over the canonical `.combobox`
|
|
@@ -49,6 +50,9 @@ export interface ComboboxProps {
|
|
|
49
50
|
emptyTitle?: React.ReactNode;
|
|
50
51
|
emptySub?: React.ReactNode;
|
|
51
52
|
disabled?: boolean;
|
|
53
|
+
/** Gallery/doc rendering: popover stays inline (absolute under the trigger),
|
|
54
|
+
not portaled. Use inside relative frames for side-by-side examples. */
|
|
55
|
+
static?: boolean;
|
|
52
56
|
className?: string;
|
|
53
57
|
style?: React.CSSProperties;
|
|
54
58
|
'aria-label'?: string;
|
|
@@ -88,6 +92,7 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
88
92
|
emptyTitle = 'No matches',
|
|
89
93
|
emptySub,
|
|
90
94
|
disabled = false,
|
|
95
|
+
static: isStatic = false,
|
|
91
96
|
className,
|
|
92
97
|
style,
|
|
93
98
|
...rest
|
|
@@ -96,11 +101,32 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
96
101
|
const [q, setQ] = React.useState('');
|
|
97
102
|
const [active, setActive] = React.useState<string | null>(null);
|
|
98
103
|
const wrapRef = React.useRef<HTMLDivElement>(null);
|
|
104
|
+
const popRef = React.useRef<HTMLDivElement>(null);
|
|
105
|
+
const [pos, setPos] = React.useState<{ left: number; top: number; width: number } | null>(null);
|
|
106
|
+
|
|
107
|
+
// Position the portaled popover under the trigger (fixed coords from rect).
|
|
108
|
+
React.useLayoutEffect(() => {
|
|
109
|
+
if (!open || isStatic) return undefined;
|
|
110
|
+
const place = () => {
|
|
111
|
+
const r = wrapRef.current?.getBoundingClientRect();
|
|
112
|
+
if (r) setPos({ left: r.left, top: r.bottom + 8, width: r.width });
|
|
113
|
+
};
|
|
114
|
+
place();
|
|
115
|
+
window.addEventListener('scroll', place, true);
|
|
116
|
+
window.addEventListener('resize', place);
|
|
117
|
+
return () => {
|
|
118
|
+
window.removeEventListener('scroll', place, true);
|
|
119
|
+
window.removeEventListener('resize', place);
|
|
120
|
+
};
|
|
121
|
+
}, [open, isStatic]);
|
|
99
122
|
|
|
100
123
|
React.useEffect(() => {
|
|
101
124
|
if (!open) return;
|
|
102
125
|
const onDoc = (e: MouseEvent) => {
|
|
103
|
-
|
|
126
|
+
const t = e.target as Node;
|
|
127
|
+
if (wrapRef.current && wrapRef.current.contains(t)) return;
|
|
128
|
+
if (popRef.current && popRef.current.contains(t)) return;
|
|
129
|
+
setOpen(false);
|
|
104
130
|
};
|
|
105
131
|
const onKey = (e: KeyboardEvent) => {
|
|
106
132
|
if (e.key === 'Escape') setOpen(false);
|
|
@@ -130,7 +156,7 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
130
156
|
? selectable[Math.min(idx + 1, selectable.length - 1)]
|
|
131
157
|
: selectable[Math.max(idx - 1, 0)];
|
|
132
158
|
setActive(next.value);
|
|
133
|
-
const list = wrapRef.current?.querySelector('.combobox-list');
|
|
159
|
+
const list = popRef.current?.querySelector('.combobox-list') || wrapRef.current?.querySelector('.combobox-list');
|
|
134
160
|
const el = list?.querySelector(`[data-value="${next.value}"]`) as HTMLElement | null;
|
|
135
161
|
if (list && el) {
|
|
136
162
|
const lt = (list as HTMLElement).scrollTop;
|
|
@@ -188,7 +214,40 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
188
214
|
);
|
|
189
215
|
});
|
|
190
216
|
|
|
191
|
-
|
|
217
|
+
const popoverInner = (
|
|
218
|
+
<React.Fragment>
|
|
219
|
+
{searchable ? (
|
|
220
|
+
<div className="combobox-search">
|
|
221
|
+
<svg className="combobox-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
222
|
+
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
223
|
+
</svg>
|
|
224
|
+
<input autoFocus placeholder={searchPlaceholder} value={q} onChange={(e) => setQ(e.target.value)} />
|
|
225
|
+
</div>
|
|
226
|
+
) : null}
|
|
227
|
+
{filters != null ? <div className="combobox-filters">{filters}</div> : null}
|
|
228
|
+
{meta != null ? <div className="combobox-meta">{meta}</div> : null}
|
|
229
|
+
{filtered.length === 0 ? (
|
|
230
|
+
<div className="combobox-empty">
|
|
231
|
+
<strong>{emptyTitle}</strong>
|
|
232
|
+
{emptySub}
|
|
233
|
+
</div>
|
|
234
|
+
) : (
|
|
235
|
+
<ul className="combobox-list" role="listbox">
|
|
236
|
+
{rendered}
|
|
237
|
+
</ul>
|
|
238
|
+
)}
|
|
239
|
+
{footMeta != null ? <div className="combobox-foot-meta">{footMeta}</div> : null}
|
|
240
|
+
{footer ? (
|
|
241
|
+
<div className="combobox-footer">
|
|
242
|
+
<button type="button" className="combobox-footer-action" onClick={onFooterClick}>
|
|
243
|
+
{footer}
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
) : null}
|
|
247
|
+
</React.Fragment>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const tree = (
|
|
192
251
|
<div
|
|
193
252
|
ref={wrapRef}
|
|
194
253
|
className={['combobox', open ? 'is-open' : '', status === 'error' ? 'is-invalid' : '', className || ''].filter(Boolean).join(' ')}
|
|
@@ -210,45 +269,32 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
210
269
|
</span>
|
|
211
270
|
<Chevron />
|
|
212
271
|
</button>
|
|
213
|
-
{open ? (
|
|
214
|
-
<div className="combobox-popover">
|
|
215
|
-
{searchable ? (
|
|
216
|
-
<div className="combobox-search">
|
|
217
|
-
<svg className="combobox-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
218
|
-
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
|
219
|
-
</svg>
|
|
220
|
-
<input
|
|
221
|
-
autoFocus
|
|
222
|
-
placeholder={searchPlaceholder}
|
|
223
|
-
value={q}
|
|
224
|
-
onChange={(e) => setQ(e.target.value)}
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
) : null}
|
|
228
|
-
{filters != null ? <div className="combobox-filters">{filters}</div> : null}
|
|
229
|
-
{meta != null ? <div className="combobox-meta">{meta}</div> : null}
|
|
230
|
-
{filtered.length === 0 ? (
|
|
231
|
-
<div className="combobox-empty">
|
|
232
|
-
<strong>{emptyTitle}</strong>
|
|
233
|
-
{emptySub}
|
|
234
|
-
</div>
|
|
235
|
-
) : (
|
|
236
|
-
<ul className="combobox-list" role="listbox">
|
|
237
|
-
{rendered}
|
|
238
|
-
</ul>
|
|
239
|
-
)}
|
|
240
|
-
{footMeta != null ? <div className="combobox-foot-meta">{footMeta}</div> : null}
|
|
241
|
-
{footer ? (
|
|
242
|
-
<div className="combobox-footer">
|
|
243
|
-
<button type="button" className="combobox-footer-action" onClick={onFooterClick}>
|
|
244
|
-
{footer}
|
|
245
|
-
</button>
|
|
246
|
-
</div>
|
|
247
|
-
) : null}
|
|
248
|
-
</div>
|
|
272
|
+
{open && isStatic ? (
|
|
273
|
+
<div className="combobox-popover" ref={popRef}>{popoverInner}</div>
|
|
249
274
|
) : null}
|
|
250
275
|
</div>
|
|
251
276
|
);
|
|
277
|
+
|
|
278
|
+
// Portal the popover to <body> (fixed coords) so it escapes any overflow /
|
|
279
|
+
// stacking ancestor. Static (gallery/doc) keeps it inline under the trigger.
|
|
280
|
+
if (isStatic || typeof document === 'undefined') return tree;
|
|
281
|
+
return (
|
|
282
|
+
<React.Fragment>
|
|
283
|
+
{tree}
|
|
284
|
+
{open && pos
|
|
285
|
+
? createPortal(
|
|
286
|
+
<div
|
|
287
|
+
className={['combobox', 'is-open', status === 'error' ? 'is-invalid' : ''].filter(Boolean).join(' ')}
|
|
288
|
+
style={{ position: 'fixed', left: pos.left, top: pos.top, width: pos.width, margin: 0, zIndex: 7000 }}
|
|
289
|
+
onKeyDown={onWrapKeyDown}
|
|
290
|
+
>
|
|
291
|
+
<div className="combobox-popover" ref={popRef} style={{ position: 'static' }}>{popoverInner}</div>
|
|
292
|
+
</div>,
|
|
293
|
+
document.body
|
|
294
|
+
)
|
|
295
|
+
: null}
|
|
296
|
+
</React.Fragment>
|
|
297
|
+
);
|
|
252
298
|
};
|
|
253
299
|
|
|
254
300
|
export default Combobox;
|
|
@@ -247,11 +247,11 @@ export default DataTable;
|
|
|
247
247
|
Typed cell-pattern helpers — compose inside a column's render().
|
|
248
248
|
---------------------------------------------------------------- */
|
|
249
249
|
|
|
250
|
-
/** Stacked primary + secondary lines — `.dt-cell-stack` (.pri / .
|
|
250
|
+
/** Stacked primary + secondary lines — `.dt-cell-stack` (.pri / .sub). */
|
|
251
251
|
export const DTCellStack: React.FC<{ pri: React.ReactNode; sec?: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>> = ({ pri, sec, className, ...rest }) => (
|
|
252
252
|
<div className={['dt-cell-stack', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
253
253
|
<span className="pri">{pri}</span>
|
|
254
|
-
{sec != null ? <span className="
|
|
254
|
+
{sec != null ? <span className="sub">{sec}</span> : null}
|
|
255
255
|
</div>
|
|
256
256
|
);
|
|
257
257
|
|
|
@@ -261,7 +261,7 @@ export const DTCellLead: React.FC<{ lead: React.ReactNode; pri: React.ReactNode;
|
|
|
261
261
|
{lead}
|
|
262
262
|
<div className="dt-cell-stack">
|
|
263
263
|
<span className="pri">{pri}</span>
|
|
264
|
-
{sec != null ? <span className="
|
|
264
|
+
{sec != null ? <span className="sub">{sec}</span> : null}
|
|
265
265
|
</div>
|
|
266
266
|
</div>
|
|
267
267
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* ATTRUS Drawer — typed wrapper over the canonical `.drawer` + `.scrim`
|
|
@@ -173,7 +174,7 @@ export const Drawer: React.FC<DrawerProps> = ({
|
|
|
173
174
|
.filter(Boolean)
|
|
174
175
|
.join(' ');
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
const overlay = (
|
|
177
178
|
<React.Fragment>
|
|
178
179
|
{isStatic ? null : <div className={['scrim', shown ? 'is-open' : ''].filter(Boolean).join(' ')} onClick={onClose} />}
|
|
179
180
|
<div
|
|
@@ -221,6 +222,11 @@ export const Drawer: React.FC<DrawerProps> = ({
|
|
|
221
222
|
</div>
|
|
222
223
|
</React.Fragment>
|
|
223
224
|
);
|
|
225
|
+
|
|
226
|
+
// Portal the live overlay to <body> so the scrim + panel escape any clipping /
|
|
227
|
+
// stacking ancestor. Static (gallery/doc) stays inline in its relative frame.
|
|
228
|
+
if (isStatic || typeof document === 'undefined') return overlay;
|
|
229
|
+
return createPortal(overlay, document.body);
|
|
224
230
|
};
|
|
225
231
|
|
|
226
232
|
export default Drawer;
|
|
@@ -110,7 +110,7 @@ export interface OtpProps {
|
|
|
110
110
|
}
|
|
111
111
|
export declare const Otp: React.FC<OtpProps>;
|
|
112
112
|
|
|
113
|
-
export interface FileDropProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
113
|
+
export interface FileDropProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
114
114
|
onFiles?: (files: File[]) => void;
|
|
115
115
|
multiple?: boolean;
|
|
116
116
|
accept?: string;
|