@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.
Files changed (58) hide show
  1. package/README.md +80 -8
  2. package/dist/css/components/buttons.css +1 -1
  3. package/dist/css/components/combobox.css +17 -1
  4. package/dist/css/components/inputs.css +3 -55
  5. package/dist/css/components/overlays.css +28 -59
  6. package/dist/css/components/primitives.css +10 -1
  7. package/dist/css/components/sidebar.css +3 -3
  8. package/dist/css/components/smart-search.css +91 -0
  9. package/dist/css/components/table.css +10 -8
  10. package/dist/css/components/tabs.css +2 -2
  11. package/dist/css/components/toast.css +4 -0
  12. package/dist/css/components/tooltip.css +5 -0
  13. package/dist/css/index.css +1 -0
  14. package/dist/css/tokens.css +100 -96
  15. package/dist/react/AppSidebar/AppSidebar.d.ts +50 -0
  16. package/dist/react/AppSidebar/AppSidebar.tsx +197 -0
  17. package/dist/react/AppTopbar/AppTopbar.d.ts +58 -0
  18. package/dist/react/AppTopbar/AppTopbar.tsx +203 -0
  19. package/dist/react/Atoms/Atoms.d.ts +149 -0
  20. package/dist/react/Atoms/Atoms.tsx +407 -0
  21. package/dist/react/Button/Button.d.ts +25 -0
  22. package/dist/react/Button/Button.tsx +83 -0
  23. package/dist/react/Callout/Callout.d.ts +29 -0
  24. package/dist/react/Callout/Callout.tsx +98 -0
  25. package/dist/react/Card/Card.d.ts +38 -0
  26. package/dist/react/Card/Card.tsx +93 -0
  27. package/dist/react/Combobox/Combobox.d.ts +52 -0
  28. package/dist/react/Combobox/Combobox.tsx +254 -0
  29. package/dist/react/DataTable/DataTable.d.ts +81 -0
  30. package/dist/react/DataTable/DataTable.tsx +308 -0
  31. package/dist/react/Drawer/Drawer.d.ts +35 -0
  32. package/dist/react/Drawer/Drawer.tsx +226 -0
  33. package/dist/react/Input/Input.d.ts +155 -0
  34. package/dist/react/Input/Input.tsx +478 -0
  35. package/dist/react/Modal/Modal.d.ts +27 -0
  36. package/dist/react/Modal/Modal.tsx +173 -0
  37. package/dist/react/NumberInput/NumberInput.d.ts +25 -0
  38. package/dist/react/NumberInput/NumberInput.tsx +117 -0
  39. package/dist/react/Pill/Pill.d.ts +31 -0
  40. package/dist/react/Pill/Pill.tsx +104 -0
  41. package/dist/react/SearchPill/SearchPill.d.ts +14 -0
  42. package/dist/react/SearchPill/SearchPill.tsx +40 -0
  43. package/dist/react/SmartSearch/SmartSearch.d.ts +65 -0
  44. package/dist/react/SmartSearch/SmartSearch.tsx +271 -0
  45. package/dist/react/Stepper/Stepper.d.ts +25 -0
  46. package/dist/react/Stepper/Stepper.tsx +94 -0
  47. package/dist/react/Tabs/Tabs.d.ts +32 -0
  48. package/dist/react/Tabs/Tabs.tsx +147 -0
  49. package/dist/react/Toast/Toast.d.ts +30 -0
  50. package/dist/react/Toast/Toast.tsx +94 -0
  51. package/dist/react/Tooltip/Tooltip.d.ts +23 -0
  52. package/dist/react/Tooltip/Tooltip.tsx +80 -0
  53. package/dist/react/ZeroState/ZeroState.d.ts +17 -0
  54. package/dist/react/ZeroState/ZeroState.tsx +45 -0
  55. package/dist/react/index.ts +35 -0
  56. package/dist/tokens/attrus-tokens.flat.json +1 -1
  57. package/dist/tokens/attrus-tokens.json +1 -1
  58. package/package.json +20 -2
@@ -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
- CABIROS — ATTRUS Design System
3
- colors_and_type.css
4
- ----
5
- Sober, serious, professional. Dashboards for financial
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
- DARK THEME — [data-theme="dark"] remaps the AMBIENT semantic
536
- tokens to a dark ink ramp. Components consume semantic tokens,
537
- so they flip automatically — no per-component dark CSS needed.
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 :root), distinct from the ambient
540
- surface. Raw palette + brand/accent scales are theme-agnostic.
541
- Toggle via JS (theme-toggle.js): sets data-theme on <html>,
542
- persists to localStorage, seeds from prefers-color-scheme.
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
- 9. SEMANTIC TYPE CLASSES — ready-to-use
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;