@attrus-ds/core 0.1.0 → 0.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/README.md +80 -8
- package/dist/css/components/buttons.css +1 -1
- package/dist/css/components/combobox.css +17 -1
- package/dist/css/components/inputs.css +3 -55
- package/dist/css/components/overlays.css +28 -59
- package/dist/css/components/primitives.css +10 -1
- package/dist/css/components/sidebar.css +3 -3
- package/dist/css/components/smart-search.css +91 -0
- package/dist/css/components/table.css +10 -8
- package/dist/css/components/tabs.css +2 -2
- package/dist/css/components/toast.css +4 -0
- package/dist/css/components/tooltip.css +5 -0
- package/dist/css/index.css +1 -0
- package/dist/css/tokens.css +100 -96
- package/dist/react/AppSidebar/AppSidebar.d.ts +50 -0
- package/dist/react/AppSidebar/AppSidebar.tsx +197 -0
- package/dist/react/AppTopbar/AppTopbar.d.ts +58 -0
- package/dist/react/AppTopbar/AppTopbar.tsx +203 -0
- package/dist/react/Atoms/Atoms.d.ts +149 -0
- package/dist/react/Atoms/Atoms.tsx +407 -0
- package/dist/react/Button/Button.d.ts +25 -0
- package/dist/react/Button/Button.tsx +83 -0
- package/dist/react/Callout/Callout.d.ts +29 -0
- package/dist/react/Callout/Callout.tsx +98 -0
- package/dist/react/Card/Card.d.ts +38 -0
- package/dist/react/Card/Card.tsx +93 -0
- package/dist/react/Combobox/Combobox.d.ts +52 -0
- package/dist/react/Combobox/Combobox.tsx +254 -0
- package/dist/react/DataTable/DataTable.d.ts +81 -0
- package/dist/react/DataTable/DataTable.tsx +308 -0
- package/dist/react/Drawer/Drawer.d.ts +35 -0
- package/dist/react/Drawer/Drawer.tsx +226 -0
- package/dist/react/Input/Input.d.ts +155 -0
- package/dist/react/Input/Input.tsx +478 -0
- package/dist/react/Modal/Modal.d.ts +27 -0
- package/dist/react/Modal/Modal.tsx +173 -0
- package/dist/react/NumberInput/NumberInput.d.ts +25 -0
- package/dist/react/NumberInput/NumberInput.tsx +117 -0
- package/dist/react/Pill/Pill.d.ts +31 -0
- package/dist/react/Pill/Pill.tsx +104 -0
- package/dist/react/SearchPill/SearchPill.d.ts +14 -0
- package/dist/react/SearchPill/SearchPill.tsx +40 -0
- package/dist/react/SmartSearch/SmartSearch.d.ts +65 -0
- package/dist/react/SmartSearch/SmartSearch.tsx +271 -0
- package/dist/react/Stepper/Stepper.d.ts +25 -0
- package/dist/react/Stepper/Stepper.tsx +94 -0
- package/dist/react/Tabs/Tabs.d.ts +32 -0
- package/dist/react/Tabs/Tabs.tsx +147 -0
- package/dist/react/Toast/Toast.d.ts +30 -0
- package/dist/react/Toast/Toast.tsx +94 -0
- package/dist/react/Tooltip/Tooltip.d.ts +23 -0
- package/dist/react/Tooltip/Tooltip.tsx +80 -0
- package/dist/react/ZeroState/ZeroState.d.ts +17 -0
- package/dist/react/ZeroState/ZeroState.tsx +45 -0
- package/dist/react/index.ts +35 -0
- package/dist/tokens/attrus-tokens.flat.json +1 -1
- package/dist/tokens/attrus-tokens.json +1 -1
- package/package.json +20 -2
package/dist/css/tokens.css
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
+
/* @attrus/cabiros — design tokens + type classes (concatenated).
|
|
2
|
+
Generated by scripts/build.mjs from src/tokens/ and src/typography/. */
|
|
3
|
+
|
|
1
4
|
/* ============================================================
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
operations across LATAM. Cool bluish grays. Deep navy.
|
|
7
|
-
Primary brand blue. Liquid-glass-friendly translucency.
|
|
5
|
+
ATTRUS / Cabiros — BASE PALETTE (raw color tokens)
|
|
6
|
+
The primitive layer. Theme-agnostic. Referenced only by the
|
|
7
|
+
semantic layer (color-semantic.css) and effect layers — never
|
|
8
|
+
consumed directly by components.
|
|
8
9
|
============================================================ */
|
|
9
10
|
|
|
10
|
-
/* ---------- Webfont: Lato ---------- */
|
|
11
|
-
|
|
12
11
|
:root {
|
|
13
|
-
/* ============================================================
|
|
14
|
-
1. BASE PALETTE — raw color tokens
|
|
15
|
-
============================================================ */
|
|
16
|
-
|
|
17
12
|
/* --- Brand (from manual: Lato + 4 HEX stops) --- */
|
|
18
13
|
--brand-attrus-ink: #0F0F1A; /* almost-black, blue-tinted */
|
|
19
14
|
--brand-attrus-navy: #002142; /* deep navy */
|
|
@@ -146,13 +141,17 @@
|
|
|
146
141
|
--color-info-700: #144B8C;
|
|
147
142
|
--color-info-800: #0D3A6E;
|
|
148
143
|
--color-info-900: #072951;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ============================================================
|
|
147
|
+
ATTRUS / Cabiros — SEMANTIC COLOR TOKENS
|
|
148
|
+
Reference the base palette (palette.css). This is the layer
|
|
149
|
+
components consume.
|
|
150
|
+
Roles: default | secondary | muted | accent | placeholder
|
|
151
|
+
Contexts: fg (text/icon), bg (surface), br (border)
|
|
152
|
+
============================================================ */
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
2. SEMANTIC TOKENS — reference the base palette
|
|
152
|
-
Roles: default | secondary | muted | accent | placeholder
|
|
153
|
-
Contexts: fg (text/icon), bg (surface), br (border)
|
|
154
|
-
============================================================ */
|
|
155
|
-
|
|
154
|
+
:root {
|
|
156
155
|
/* Default (light) theme — the 60% whites/light grays */
|
|
157
156
|
--color-background-canvas: var(--color-neutral-25); /* app background */
|
|
158
157
|
--color-background-surface: var(--color-neutral-0); /* card / container */
|
|
@@ -274,10 +273,13 @@
|
|
|
274
273
|
--color-feedback-danger-foreground-on-inverse-muted: color-mix(in srgb, var(--color-danger-100) 70%, var(--color-foreground-inverse-muted));
|
|
275
274
|
--color-feedback-info-foreground-on-inverse: var(--color-info-100);
|
|
276
275
|
--color-feedback-info-foreground-on-inverse-muted: color-mix(in srgb, var(--color-info-100) 70%, var(--color-foreground-inverse-muted));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ============================================================
|
|
279
|
+
ATTRUS / Cabiros — GRADIENTS (primary brand + layers)
|
|
280
|
+
============================================================ */
|
|
277
281
|
|
|
278
|
-
|
|
279
|
-
3. GRADIENTS — primary brand + layers
|
|
280
|
-
============================================================ */
|
|
282
|
+
:root {
|
|
281
283
|
--grad-brand: linear-gradient(135deg, #001A4D 0%, #003399 50%, #3399FF 100%);
|
|
282
284
|
--grad-brand-soft: linear-gradient(135deg, #002A80 0%, #003399 100%);
|
|
283
285
|
--grad-ink: linear-gradient(160deg, #0B1230 0%, #001A4D 55%, #002A80 100%);
|
|
@@ -302,16 +304,19 @@
|
|
|
302
304
|
--grad-data-brand: linear-gradient(180deg,
|
|
303
305
|
color-mix(in srgb, var(--color-brand-accent-500) 30%, transparent) 0%,
|
|
304
306
|
color-mix(in srgb, var(--color-brand-accent-500) 0%, transparent) 100%);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ============================================================
|
|
310
|
+
ATTRUS / Cabiros — DATA VISUALIZATION (chart palettes & structure)
|
|
311
|
+
Categorical = qualitative series (distinct, on-brand, cool).
|
|
312
|
+
Sequential = single-hue magnitude ramp (Sky).
|
|
313
|
+
Diverging = danger ← neutral → success (variance/heatmap).
|
|
314
|
+
Structure = grid / axis / track / reference (semantic → theme).
|
|
315
|
+
Categorical gets dark-theme overrides (lift the dark hues, in
|
|
316
|
+
theme-dark.css); ramps + structure follow semantic tokens.
|
|
317
|
+
============================================================ */
|
|
305
318
|
|
|
306
|
-
|
|
307
|
-
3b. DATA VISUALIZATION — chart palettes & structure
|
|
308
|
-
Categorical = qualitative series (distinct, on-brand, cool).
|
|
309
|
-
Sequential = single-hue magnitude ramp (Sky).
|
|
310
|
-
Diverging = danger ← neutral → success (variance/heatmap).
|
|
311
|
-
Structure = grid / axis / track / reference (semantic → theme).
|
|
312
|
-
Categorical gets dark-theme overrides (lift the dark hues);
|
|
313
|
-
ramps + structure follow semantic tokens automatically.
|
|
314
|
-
============================================================ */
|
|
319
|
+
:root {
|
|
315
320
|
/* Categorical — order chosen for max adjacent contrast */
|
|
316
321
|
--dataviz-cat-1: #003399; /* Royal — brand-anchored series 1 */
|
|
317
322
|
--dataviz-cat-2: #3399FF; /* Sky */
|
|
@@ -350,11 +355,14 @@
|
|
|
350
355
|
--dataviz-axis-label: var(--color-foreground-muted); /* tick labels */
|
|
351
356
|
--dataviz-track: var(--color-background-subtle); /* empty bar / gauge track */
|
|
352
357
|
--dataviz-reference: var(--color-foreground-muted); /* dashed target/avg line */
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ============================================================
|
|
361
|
+
ATTRUS / Cabiros — LIQUID GLASS (transparent / frosted layers)
|
|
362
|
+
Use with backdrop-filter: blur().
|
|
363
|
+
============================================================ */
|
|
353
364
|
|
|
354
|
-
|
|
355
|
-
4. LIQUID GLASS — transparent / frosted layers
|
|
356
|
-
Use with backdrop-filter: blur().
|
|
357
|
-
============================================================ */
|
|
365
|
+
:root {
|
|
358
366
|
--glass-white: color-mix(in srgb, var(--color-pure-white) 58%, transparent);
|
|
359
367
|
--glass-white-hi: color-mix(in srgb, var(--color-pure-white) 78%, transparent);
|
|
360
368
|
--glass-white-lo: color-mix(in srgb, var(--color-pure-white) 38%, transparent);
|
|
@@ -367,10 +375,14 @@
|
|
|
367
375
|
--glass-blur-lg: saturate(160%) blur(28px);
|
|
368
376
|
--glass-border: 1px solid color-mix(in srgb, var(--color-pure-white) 45%, transparent);
|
|
369
377
|
--glass-border-dark: 1px solid color-mix(in srgb, var(--color-pure-white) 10%, transparent);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* ============================================================
|
|
381
|
+
ATTRUS / Cabiros — RADII
|
|
382
|
+
Mostly softly rounded; buttons fully rounded.
|
|
383
|
+
============================================================ */
|
|
370
384
|
|
|
371
|
-
|
|
372
|
-
5. RADII — mostly softly rounded; buttons fully rounded
|
|
373
|
-
============================================================ */
|
|
385
|
+
:root {
|
|
374
386
|
--radius-2xs: 2px; /* never-square minimum — tiny chips, ticks, dividers */
|
|
375
387
|
--radius-xs: 4px;
|
|
376
388
|
--radius-sm: 6px;
|
|
@@ -382,10 +394,13 @@
|
|
|
382
394
|
--radius-xl: 20px; /* hero / large panels */
|
|
383
395
|
--radius-2xl: 28px;
|
|
384
396
|
--radius-pill: 999px; /* BUTTONS — fully rounded */
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* ============================================================
|
|
400
|
+
ATTRUS / Cabiros — SHADOWS (soft, blue-tinted, multi-layer)
|
|
401
|
+
============================================================ */
|
|
385
402
|
|
|
386
|
-
|
|
387
|
-
6. SHADOWS — soft, blue-tinted, multi-layer
|
|
388
|
-
============================================================ */
|
|
403
|
+
:root {
|
|
389
404
|
--shadow-xs: 0 1px 2px color-mix(in srgb, var(--color-shadow-tint) 5%, transparent);
|
|
390
405
|
--shadow-sm: 0 1px 2px color-mix(in srgb, var(--color-shadow-tint) 6%, transparent),
|
|
391
406
|
0 2px 6px color-mix(in srgb, var(--color-shadow-tint) 4%, transparent);
|
|
@@ -427,10 +442,14 @@
|
|
|
427
442
|
0 10px 30px color-mix(in srgb, var(--color-shadow-tint) 12%, transparent);
|
|
428
443
|
--shadow-glass-dark: 0 1px 0 color-mix(in srgb, var(--color-pure-white) 8%, transparent) inset,
|
|
429
444
|
0 10px 40px color-mix(in srgb, var(--color-pure-black) 35%, transparent);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* ============================================================
|
|
448
|
+
ATTRUS / Cabiros — SPACING
|
|
449
|
+
4px base; dense, data-heavy layouts.
|
|
450
|
+
============================================================ */
|
|
430
451
|
|
|
431
|
-
|
|
432
|
-
7. SPACING — 4px base; dense, data-heavy layouts
|
|
433
|
-
============================================================ */
|
|
452
|
+
:root {
|
|
434
453
|
--space-0: 0;
|
|
435
454
|
--space-1: 2px;
|
|
436
455
|
--space-2: 4px;
|
|
@@ -448,10 +467,13 @@
|
|
|
448
467
|
--space-12: 64px;
|
|
449
468
|
--space-14: 80px;
|
|
450
469
|
--space-16: 96px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ============================================================
|
|
473
|
+
ATTRUS / Cabiros — ICONOGRAPHY (Lucide; sized to pair with type scale)
|
|
474
|
+
============================================================ */
|
|
451
475
|
|
|
452
|
-
|
|
453
|
-
7b. ICONOGRAPHY — Lucide; sized to pair with type scale
|
|
454
|
-
============================================================ */
|
|
476
|
+
:root {
|
|
455
477
|
--icon-xs: 12px; /* helper text, micro caption (paired w/ 11–12px text) */
|
|
456
478
|
--icon-sm: 14px; /* input affix, dense inline (paired w/ 13px text) */
|
|
457
479
|
--icon-md: 16px; /* default — buttons, nav, body (paired w/ 14px text) */
|
|
@@ -462,10 +484,13 @@
|
|
|
462
484
|
--icon-stroke: 1.75; /* default — slightly bolder than 1.5, calmer than 2 */
|
|
463
485
|
--icon-stroke-bold: 2; /* close buttons, X marks, indicators where weight matters */
|
|
464
486
|
--icon-stroke-thin: 1.5; /* decorative / large hero icons where 1.75 looks heavy */
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* ============================================================
|
|
490
|
+
ATTRUS / Cabiros — MOTION (durations + easings for transitions/animations)
|
|
491
|
+
============================================================ */
|
|
465
492
|
|
|
466
|
-
|
|
467
|
-
7c. MOTION — durations + easings for transitions/animations
|
|
468
|
-
============================================================ */
|
|
493
|
+
:root {
|
|
469
494
|
/* Easing — single curve across the system; "out-expo-ish" for snappy UI */
|
|
470
495
|
--ease-standard: cubic-bezier(.2, .8, .2, 1);
|
|
471
496
|
--ease-emphasis: cubic-bezier(.2, .9, .15, 1); /* slightly more overshoot — for entries */
|
|
@@ -483,10 +508,15 @@
|
|
|
483
508
|
--transition-fast: 140ms var(--ease-standard); /* default for buttons/inputs */
|
|
484
509
|
--transition-base: 200ms var(--ease-standard);
|
|
485
510
|
--transition-slow: 320ms var(--ease-standard);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* ============================================================
|
|
514
|
+
ATTRUS / Cabiros — TYPOGRAPHY TOKENS (Lato family per brand manual)
|
|
515
|
+
Font families, weights, type scale, line heights, letter spacing.
|
|
516
|
+
Ready-to-use type CLASSES live in ../typography/index.css.
|
|
517
|
+
============================================================ */
|
|
486
518
|
|
|
487
|
-
|
|
488
|
-
8. TYPOGRAPHY — Lato family per brand manual
|
|
489
|
-
============================================================ */
|
|
519
|
+
:root {
|
|
490
520
|
--font-sans: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
491
521
|
--font-mono: 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
|
|
492
522
|
--font-display: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
@@ -529,18 +559,20 @@
|
|
|
529
559
|
--ls-body: 0em; /* <16px body */
|
|
530
560
|
--ls-label: 0.02em; /* small caps / labels */
|
|
531
561
|
--ls-caps: 0.08em;
|
|
532
|
-
}
|
|
533
|
-
|
|
562
|
+
}
|
|
563
|
+
|
|
534
564
|
/* ============================================================
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
565
|
+
ATTRUS / Cabiros — DARK THEME
|
|
566
|
+
[data-theme="dark"] remaps the AMBIENT semantic tokens to a dark
|
|
567
|
+
ink ramp. Components consume semantic tokens, so they flip
|
|
568
|
+
automatically — no per-component dark CSS needed.
|
|
538
569
|
Philosophy: the INVERSE layer stays a deliberate, brand-tinted
|
|
539
|
-
dark even here (see note in
|
|
540
|
-
surface. Raw palette + brand/accent scales are theme-agnostic.
|
|
541
|
-
Toggle via JS
|
|
542
|
-
|
|
570
|
+
dark even here (see note in color-semantic.css), distinct from the
|
|
571
|
+
ambient surface. Raw palette + brand/accent scales are theme-agnostic.
|
|
572
|
+
Toggle via JS: set data-theme on <html>, persist to localStorage,
|
|
573
|
+
seed from prefers-color-scheme.
|
|
543
574
|
============================================================ */
|
|
575
|
+
|
|
544
576
|
[data-theme="dark"] {
|
|
545
577
|
color-scheme: dark;
|
|
546
578
|
|
|
@@ -603,10 +635,12 @@
|
|
|
603
635
|
--dataviz-cat-6: #E87BA1; /* Rose, lightened */
|
|
604
636
|
--dataviz-cat-7: #3FC4DC; /* Cyan, lightened */
|
|
605
637
|
--dataviz-cat-8: #A6B0C4; /* Slate, lightened */
|
|
606
|
-
}
|
|
607
|
-
|
|
638
|
+
}
|
|
639
|
+
|
|
608
640
|
/* ============================================================
|
|
609
|
-
|
|
641
|
+
ATTRUS / Cabiros — SEMANTIC TYPE CLASSES (ready-to-use)
|
|
642
|
+
Base document styles + the display/heading/body/utility classes.
|
|
643
|
+
Consumes typography + color tokens (see ../tokens).
|
|
610
644
|
============================================================ */
|
|
611
645
|
|
|
612
646
|
html, body {
|
|
@@ -657,35 +691,5 @@ h6, .h6 { font-weight: var(--w-semibold); font-size: var(--fs-lg); line-height:
|
|
|
657
691
|
}
|
|
658
692
|
.inverse .fg-secondary { color: var(--color-foreground-inverse-secondary); }
|
|
659
693
|
.inverse .color-foreground-muted { color: var(--color-foreground-inverse-muted); }
|
|
660
|
-
.inverse .fg-accent { color: var(--color-foreground-inverse-accent); }
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
/* ---- doc-back link (fixed top-left on every preview page) ---- */
|
|
664
|
-
.doc-back {
|
|
665
|
-
position: fixed;
|
|
666
|
-
top: var(--space-5);
|
|
667
|
-
left: var(--space-5);
|
|
668
|
-
z-index: 1000;
|
|
669
|
-
display: inline-flex;
|
|
670
|
-
align-items: center;
|
|
671
|
-
gap: var(--space-2);
|
|
672
|
-
font-family: var(--font-sans);
|
|
673
|
-
font-size: var(--fs-xs);
|
|
674
|
-
font-weight: var(--w-semibold);
|
|
675
|
-
letter-spacing: 0;
|
|
676
|
-
text-transform: none;
|
|
677
|
-
color: var(--color-foreground-secondary);
|
|
678
|
-
text-decoration: none;
|
|
679
|
-
padding: var(--space-2) var(--space-3);
|
|
680
|
-
border: 1px solid var(--color-border-default);
|
|
681
|
-
border-radius: var(--radius-pill);
|
|
682
|
-
background: var(--color-background-surface);
|
|
683
|
-
box-shadow: var(--shadow-sm);
|
|
684
|
-
transition: var(--transition-fast);
|
|
685
|
-
}
|
|
686
|
-
.doc-back:hover {
|
|
687
|
-
color: var(--color-foreground-default);
|
|
688
|
-
border-color: var(--color-border-strong);
|
|
689
|
-
box-shadow: var(--shadow-md);
|
|
690
|
-
}
|
|
691
|
-
.doc-back svg, .doc-back i { width: 14px; height: 14px; stroke-width: var(--icon-stroke); }
|
|
694
|
+
.inverse .fg-accent { color: var(--color-foreground-inverse-accent); }
|
|
695
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SidebarChildItem {
|
|
4
|
+
key: string;
|
|
5
|
+
label: React.ReactNode;
|
|
6
|
+
count?: number | string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SidebarItem {
|
|
10
|
+
key: string;
|
|
11
|
+
label: React.ReactNode;
|
|
12
|
+
/** 15px icon slot (.ic). */
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
/** Status badge text (e.g. "NEW") — mutually exclusive with count. */
|
|
15
|
+
badge?: string;
|
|
16
|
+
badgeTone?: 'alert' | 'warning';
|
|
17
|
+
/** Numeric trailing count. */
|
|
18
|
+
count?: number | string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Sub-items — renders a collapsible .sb-group. */
|
|
21
|
+
children?: SidebarChildItem[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SidebarSection {
|
|
25
|
+
/** Uppercase section label; omit for the first unlabelled block. */
|
|
26
|
+
label?: React.ReactNode;
|
|
27
|
+
items: SidebarItem[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AppSidebarProps {
|
|
31
|
+
sections: SidebarSection[];
|
|
32
|
+
/** Key of the active item/sub-item. */
|
|
33
|
+
active?: string;
|
|
34
|
+
onNav?: (key: string) => void;
|
|
35
|
+
/** Full-height app rail (.sb-app) instead of card framing. */
|
|
36
|
+
app?: boolean;
|
|
37
|
+
/** 60px icon rail. */
|
|
38
|
+
collapsed?: boolean;
|
|
39
|
+
/** Renders the header collapse toggle. */
|
|
40
|
+
onToggleCollapse?: () => void;
|
|
41
|
+
/** Header brand slot (logo img). */
|
|
42
|
+
logo?: React.ReactNode;
|
|
43
|
+
/** Footer slot (.sb-foot markup is the caller's). */
|
|
44
|
+
foot?: React.ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
style?: React.CSSProperties;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export declare const AppSidebar: React.FC<AppSidebarProps>;
|
|
50
|
+
export default AppSidebar;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS AppSidebar — typed wrapper over the canonical `.sb` system
|
|
5
|
+
* (preview/components/sidebar.css). Single visual source shared with
|
|
6
|
+
* the live app shell and the doc page. Sections, items with badge OR
|
|
7
|
+
* count, collapsible groups, disabled rows, collapsed 60px icon rail.
|
|
8
|
+
* (Named AppSidebar — "Shell"/"Sidebar" collide with legacy app files.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface SidebarChildItem {
|
|
12
|
+
key: string;
|
|
13
|
+
label: React.ReactNode;
|
|
14
|
+
count?: number | string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SidebarItem {
|
|
18
|
+
key: string;
|
|
19
|
+
label: React.ReactNode;
|
|
20
|
+
/** 15px icon slot (.ic). */
|
|
21
|
+
icon?: React.ReactNode;
|
|
22
|
+
/** Status badge text (e.g. "NEW") — mutually exclusive with count. */
|
|
23
|
+
badge?: string;
|
|
24
|
+
badgeTone?: 'alert' | 'warning';
|
|
25
|
+
/** Numeric trailing count. */
|
|
26
|
+
count?: number | string;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Sub-items — renders a collapsible .sb-group. */
|
|
29
|
+
children?: SidebarChildItem[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SidebarSection {
|
|
33
|
+
/** Uppercase section label; omit for the first unlabelled block. */
|
|
34
|
+
label?: React.ReactNode;
|
|
35
|
+
items: SidebarItem[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AppSidebarProps {
|
|
39
|
+
sections: SidebarSection[];
|
|
40
|
+
/** Key of the active item/sub-item. */
|
|
41
|
+
active?: string;
|
|
42
|
+
onNav?: (key: string) => void;
|
|
43
|
+
/** Full-height app rail (.sb-app) instead of card framing. */
|
|
44
|
+
app?: boolean;
|
|
45
|
+
/** 60px icon rail. */
|
|
46
|
+
collapsed?: boolean;
|
|
47
|
+
/** Renders the header collapse toggle. */
|
|
48
|
+
onToggleCollapse?: () => void;
|
|
49
|
+
/** Header brand slot (logo img). */
|
|
50
|
+
logo?: React.ReactNode;
|
|
51
|
+
/** Footer slot (.sb-foot markup is the caller's). */
|
|
52
|
+
foot?: React.ReactNode;
|
|
53
|
+
className?: string;
|
|
54
|
+
style?: React.CSSProperties;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PanelGlyph: React.FC = () => (
|
|
58
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
59
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
60
|
+
<line x1="9" y1="3" x2="9" y2="21" />
|
|
61
|
+
</svg>
|
|
62
|
+
);
|
|
63
|
+
const ChevGlyph: React.FC = () => (
|
|
64
|
+
<svg className="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
65
|
+
<path d="m9 18 6-6-6-6" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const AppSidebar: React.FC<AppSidebarProps> = ({
|
|
70
|
+
sections,
|
|
71
|
+
active,
|
|
72
|
+
onNav,
|
|
73
|
+
app = false,
|
|
74
|
+
collapsed = false,
|
|
75
|
+
onToggleCollapse,
|
|
76
|
+
logo,
|
|
77
|
+
foot,
|
|
78
|
+
className,
|
|
79
|
+
style,
|
|
80
|
+
}) => {
|
|
81
|
+
const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>({});
|
|
82
|
+
|
|
83
|
+
const toggleGroup = (key: string) =>
|
|
84
|
+
setOpenGroups((g) => ({ ...g, [key]: !g[key] }));
|
|
85
|
+
|
|
86
|
+
const renderItem = (item: SidebarItem) => {
|
|
87
|
+
const isGroup = !!(item.children && item.children.length);
|
|
88
|
+
const childActive = isGroup && item.children!.some((c) => c.key === active);
|
|
89
|
+
const expanded = isGroup ? (openGroups[item.key] ?? childActive) : false;
|
|
90
|
+
const itemCls = [
|
|
91
|
+
'sb-item',
|
|
92
|
+
item.key === active ? 'active' : '',
|
|
93
|
+
item.disabled ? 'disabled' : '',
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' ');
|
|
97
|
+
|
|
98
|
+
const row = (
|
|
99
|
+
<div
|
|
100
|
+
className={itemCls}
|
|
101
|
+
role="button"
|
|
102
|
+
tabIndex={item.disabled ? undefined : 0}
|
|
103
|
+
aria-disabled={item.disabled || undefined}
|
|
104
|
+
aria-current={item.key === active ? 'page' : undefined}
|
|
105
|
+
onClick={() => {
|
|
106
|
+
if (item.disabled) return;
|
|
107
|
+
if (isGroup) toggleGroup(item.key);
|
|
108
|
+
else if (onNav) onNav(item.key);
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{item.icon != null ? <span className="ic">{item.icon}</span> : null}
|
|
112
|
+
<span className="label">{item.label}</span>
|
|
113
|
+
{item.badge ? (
|
|
114
|
+
<span className={['sb-badge', item.badgeTone || ''].filter(Boolean).join(' ')}>{item.badge}</span>
|
|
115
|
+
) : item.count != null ? (
|
|
116
|
+
<span className="sb-count">{item.count}</span>
|
|
117
|
+
) : null}
|
|
118
|
+
{isGroup ? <ChevGlyph /> : null}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!isGroup) return <React.Fragment key={item.key}>{row}</React.Fragment>;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div key={item.key} className="sb-group" aria-expanded={expanded}>
|
|
126
|
+
{row}
|
|
127
|
+
<div className="sb-children">
|
|
128
|
+
<div className="sb-children-inner">
|
|
129
|
+
{item.children!.map((c) => (
|
|
130
|
+
<div
|
|
131
|
+
key={c.key}
|
|
132
|
+
className={['sb-subitem', c.key === active ? 'active' : ''].filter(Boolean).join(' ')}
|
|
133
|
+
role="button"
|
|
134
|
+
tabIndex={0}
|
|
135
|
+
aria-current={c.key === active ? 'page' : undefined}
|
|
136
|
+
onClick={() => onNav && onNav(c.key)}
|
|
137
|
+
>
|
|
138
|
+
<span className="label">{c.label}</span>
|
|
139
|
+
{c.count != null ? <span className="sb-count">{c.count}</span> : null}
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<aside
|
|
150
|
+
className={['sb', app ? 'sb-app' : '', collapsed ? 'collapsed' : '', className || ''].filter(Boolean).join(' ')}
|
|
151
|
+
style={style}
|
|
152
|
+
onKeyDown={(e) => {
|
|
153
|
+
// Rail keyboard nav (doc a11y contract): ↑/↓ move between items
|
|
154
|
+
// (wrapping), Home/End jump; Enter/Space activate via role=button.
|
|
155
|
+
const nav = e.currentTarget;
|
|
156
|
+
const target = e.target as HTMLElement;
|
|
157
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
158
|
+
if (target.matches('.sb-item, .sb-subitem')) { e.preventDefault(); target.click(); }
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Home' && e.key !== 'End') return;
|
|
162
|
+
const rows = Array.from(nav.querySelectorAll<HTMLElement>('.sb-item:not(.disabled), .sb-subitem'))
|
|
163
|
+
.filter((el) => el.offsetParent !== null); // skip collapsed-group children
|
|
164
|
+
if (!rows.length) return;
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
const idx = rows.indexOf(target);
|
|
167
|
+
let next = 0;
|
|
168
|
+
if (e.key === 'ArrowDown') next = idx === -1 ? 0 : (idx + 1) % rows.length;
|
|
169
|
+
else if (e.key === 'ArrowUp') next = idx === -1 ? rows.length - 1 : (idx - 1 + rows.length) % rows.length;
|
|
170
|
+
else if (e.key === 'End') next = rows.length - 1;
|
|
171
|
+
rows[next].focus();
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{(logo != null || onToggleCollapse) && (
|
|
175
|
+
<div className="sb-head">
|
|
176
|
+
{logo}
|
|
177
|
+
{onToggleCollapse ? (
|
|
178
|
+
<button type="button" className="toggle" aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'} onClick={onToggleCollapse}>
|
|
179
|
+
<PanelGlyph />
|
|
180
|
+
</button>
|
|
181
|
+
) : null}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
<div className="sb-body">
|
|
185
|
+
{sections.map((s, i) => (
|
|
186
|
+
<React.Fragment key={i}>
|
|
187
|
+
{s.label != null ? <div className="sb-section">{s.label}</div> : null}
|
|
188
|
+
{s.items.map(renderItem)}
|
|
189
|
+
</React.Fragment>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
{foot}
|
|
193
|
+
</aside>
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default AppSidebar;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface AppTopbarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Left CTA slot — the canonical Button (primary). */
|
|
5
|
+
primary?: React.ReactNode;
|
|
6
|
+
/** Search slot — the canonical SearchPill (grows to fill). */
|
|
7
|
+
search?: React.ReactNode;
|
|
8
|
+
/** Right-aligned cluster (.tb-spacer): TopbarControl/Live/Bell/Account. */
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TopbarControlProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
13
|
+
/** Leading slot (icon, flag…). */
|
|
14
|
+
icon?: React.ReactNode;
|
|
15
|
+
/** Square icon-only control (.tb-control.icon). */
|
|
16
|
+
iconOnly?: boolean;
|
|
17
|
+
/** Trailing disclosure chevron. */
|
|
18
|
+
chev?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TopbarLiveProps {
|
|
22
|
+
label?: React.ReactNode;
|
|
23
|
+
/** Elapsed mono clock (e.g. "0:19"). */
|
|
24
|
+
clock?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TopbarBellProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
28
|
+
/** Unread red dot. */
|
|
29
|
+
badge?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TopbarAccountItem {
|
|
33
|
+
key: string;
|
|
34
|
+
label: React.ReactNode;
|
|
35
|
+
icon?: React.ReactNode;
|
|
36
|
+
danger?: boolean;
|
|
37
|
+
/** Renders a separator before this item. */
|
|
38
|
+
sep?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TopbarAccountProps {
|
|
42
|
+
initials: string;
|
|
43
|
+
name: React.ReactNode;
|
|
44
|
+
company: React.ReactNode;
|
|
45
|
+
/** Second line in the dropdown head (e.g. "Acme Corp · Admin"). */
|
|
46
|
+
detail?: React.ReactNode;
|
|
47
|
+
items?: TopbarAccountItem[];
|
|
48
|
+
onSelect?: (key: string) => void;
|
|
49
|
+
/** Hide name/company in the trigger (mobile). */
|
|
50
|
+
compact?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export declare const AppTopbar: React.FC<AppTopbarProps>;
|
|
54
|
+
export declare const TopbarControl: React.FC<TopbarControlProps>;
|
|
55
|
+
export declare const TopbarLive: React.FC<TopbarLiveProps>;
|
|
56
|
+
export declare const TopbarBell: React.FC<TopbarBellProps>;
|
|
57
|
+
export declare const TopbarAccount: React.FC<TopbarAccountProps>;
|
|
58
|
+
export default AppTopbar;
|