@fabrika/ui 0.4.0 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "React component kit for the paved road: accessible admin components styled through CSS custom properties.",
6
6
  "license": "MIT",
package/src/AppShell.tsx CHANGED
@@ -15,14 +15,19 @@ export interface AppShellProps {
15
15
  /** The signed-in caller, shown at the end of the top bar. */
16
16
  readonly user?: ReactNode
17
17
  readonly actions?: ReactNode
18
+ /** The top bar: shown when there is a `user` or `actions`, unless `false`. */
19
+ readonly topBar?: boolean
20
+ /** The current navigation item: a soft accent (the default), or `filled` with the accent and an inverted count. */
21
+ readonly navStyle?: 'soft' | 'filled'
18
22
  readonly children: ReactNode
19
23
  readonly navLabel?: string
20
24
  readonly className?: string
21
25
  }
22
26
 
23
- export function AppShell({ brand, nav, user, actions, children, navLabel = 'Main', className }: AppShellProps) {
27
+ export function AppShell({ brand, nav, user, actions, topBar, navStyle = 'soft', children, navLabel = 'Main', className }: AppShellProps) {
28
+ const showTopBar = topBar !== false && (user !== undefined || actions !== undefined)
24
29
  return (
25
- <div className={classNames('fui-app', 'fui-shell', className)}>
30
+ <div className={classNames('fui-app', 'fui-shell', navStyle === 'filled' && 'fui-shell--nav-filled', className)}>
26
31
  <div className="fui-shell__sidebar">
27
32
  <div className="fui-shell__brand">{brand}</div>
28
33
  <nav className="fui-shell__nav" aria-label={navLabel}>
@@ -43,10 +48,12 @@ export function AppShell({ brand, nav, user, actions, children, navLabel = 'Main
43
48
  </nav>
44
49
  </div>
45
50
  <div className="fui-shell__body">
46
- <header className="fui-shell__topbar">
47
- <div className="fui-shell__actions">{actions}</div>
48
- {user !== undefined && <div className="fui-shell__user">{user}</div>}
49
- </header>
51
+ {showTopBar && (
52
+ <header className="fui-shell__topbar">
53
+ <div className="fui-shell__actions">{actions}</div>
54
+ {user !== undefined && <div className="fui-shell__user">{user}</div>}
55
+ </header>
56
+ )}
50
57
  <main className="fui-shell__main">{children}</main>
51
58
  </div>
52
59
  </div>
package/src/DataTable.tsx CHANGED
@@ -10,8 +10,14 @@ export interface Column<Row> {
10
10
  readonly align?: 'start' | 'end'
11
11
  /** Takes the width the other columns leave; the remaining columns shrink to their content. */
12
12
  readonly grow?: boolean
13
+ /** A width of the column scale (`--fui-column-width-<size>`), on its head and its cells. */
14
+ readonly width?: ColumnWidth
15
+ /** Its cells wrap their text instead of keeping it on one line. */
16
+ readonly wrap?: boolean
13
17
  }
14
18
 
19
+ export type ColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
20
+
15
21
  export interface TableSelection {
16
22
  readonly selected: ReadonlySet<string>
17
23
  readonly onChange: (next: ReadonlySet<string>) => void
@@ -99,7 +105,13 @@ export function DataTable<Row>({
99
105
  }
100
106
 
101
107
  const cellClassName = (column: Column<Row>) =>
102
- classNames('fui-table__cell', column.align === 'end' && 'fui-table__cell--end', column.grow && 'fui-table__cell--grow')
108
+ classNames(
109
+ 'fui-table__cell',
110
+ column.align === 'end' && 'fui-table__cell--end',
111
+ column.grow && 'fui-table__cell--grow',
112
+ column.width !== undefined && `fui-table__cell--width-${column.width}`,
113
+ column.wrap && 'fui-table__cell--wrap',
114
+ )
103
115
 
104
116
  return (
105
117
  <div className={classNames('fui-data-table', className)}>
@@ -4,14 +4,16 @@ import type { Tone } from './tone'
4
4
 
5
5
  export interface StatusPillProps {
6
6
  readonly tone?: Tone
7
+ /** The tone's dot before the text; `false` leaves it out. */
8
+ readonly dot?: boolean
7
9
  readonly children: ReactNode
8
10
  readonly className?: string
9
11
  }
10
12
 
11
- export function StatusPill({ tone = 'neutral', children, className }: StatusPillProps) {
13
+ export function StatusPill({ tone = 'neutral', dot = true, children, className }: StatusPillProps) {
12
14
  return (
13
15
  <span className={classNames('fui-pill', `fui-pill--${tone}`, className)}>
14
- <span className="fui-pill__dot" aria-hidden="true" />
16
+ {dot && <span className="fui-pill__dot" aria-hidden="true" />}
15
17
  {children}
16
18
  </span>
17
19
  )
package/src/Tabs.tsx CHANGED
@@ -13,6 +13,8 @@ export interface TabsProps<K extends string> {
13
13
  readonly items: readonly TabItem<K>[]
14
14
  readonly active: K
15
15
  readonly onChange: (key: K) => void
16
+ /** `underline` (the default) marks the active tab by a line under it; `boxed` puts each tab in a box, its count after a dot. */
17
+ readonly variant?: 'underline' | 'boxed'
16
18
  readonly className?: string
17
19
  }
18
20
 
@@ -32,7 +34,7 @@ function targetIndex(key: string, current: number, length: number): number | und
32
34
  }
33
35
 
34
36
  /** A WAI-ARIA tab list with automatic activation: moving focus with the keyboard also selects. */
35
- export function Tabs<K extends string>({ label, items, active, onChange, className }: TabsProps<K>) {
37
+ export function Tabs<K extends string>({ label, items, active, onChange, variant = 'underline', className }: TabsProps<K>) {
36
38
  const tabs = useRef(new Map<K, HTMLButtonElement>())
37
39
  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
38
40
  const current = items.findIndex((item) => tabs.current.get(item.key) === event.target)
@@ -45,7 +47,12 @@ export function Tabs<K extends string>({ label, items, active, onChange, classNa
45
47
  if (item.key !== active) onChange(item.key)
46
48
  }
47
49
  return (
48
- <div className={classNames('fui-tabs', className)} role="tablist" aria-label={label} onKeyDown={handleKeyDown}>
50
+ <div
51
+ className={classNames('fui-tabs', variant === 'boxed' && 'fui-tabs--boxed', className)}
52
+ role="tablist"
53
+ aria-label={label}
54
+ onKeyDown={handleKeyDown}
55
+ >
49
56
  {items.map((item) => {
50
57
  const selected = item.key === active
51
58
  return (
@@ -114,6 +114,20 @@
114
114
  background: var(--fui-accent-soft);
115
115
  }
116
116
 
117
+ .fui-app .fui-shell--nav-filled .fui-shell__nav-link--active,
118
+ .fui-app .fui-shell--nav-filled .fui-shell__nav-link--active:hover,
119
+ .fui-app.fui-shell--nav-filled .fui-shell__nav-link--active,
120
+ .fui-app.fui-shell--nav-filled .fui-shell__nav-link--active:hover {
121
+ color: var(--fui-accent-contrast);
122
+ background: var(--fui-accent);
123
+ }
124
+
125
+ .fui-shell--nav-filled .fui-shell__nav-link--active .fui-count-badge {
126
+ color: var(--fui-accent);
127
+ background: var(--fui-accent-contrast);
128
+ border-color: var(--fui-accent-contrast);
129
+ }
130
+
117
131
  .fui-shell__body {
118
132
  display: flex;
119
133
  flex-direction: column;
@@ -146,7 +160,7 @@
146
160
  .fui-shell__main {
147
161
  width: 100%;
148
162
  max-width: var(--fui-content-max-width);
149
- padding: var(--fui-space-6);
163
+ padding: var(--fui-main-padding);
150
164
  }
151
165
 
152
166
  @media (max-width: 768px) {
@@ -177,7 +191,7 @@
177
191
  }
178
192
 
179
193
  .fui-shell__main {
180
- padding: var(--fui-space-4);
194
+ padding: var(--fui-main-padding-narrow);
181
195
  }
182
196
  }
183
197
 
@@ -278,6 +292,40 @@
278
292
  border-bottom-color: var(--fui-accent);
279
293
  }
280
294
 
295
+ /* Boxed tabs: each tab framed, the active one filled; the count follows a dot instead of a badge. */
296
+ .fui-tabs--boxed {
297
+ gap: var(--fui-space-2);
298
+ border-bottom: 0;
299
+ }
300
+
301
+ .fui-tabs--boxed .fui-tabs__tab {
302
+ margin-bottom: 0;
303
+ background: var(--fui-surface);
304
+ border: var(--fui-border-width) solid var(--fui-border);
305
+ border-radius: var(--fui-radius-md);
306
+ }
307
+
308
+ .fui-tabs--boxed .fui-tabs__tab--active {
309
+ color: var(--fui-accent-contrast);
310
+ background: var(--fui-accent);
311
+ border-color: var(--fui-accent);
312
+ }
313
+
314
+ .fui-tabs--boxed .fui-count-badge {
315
+ min-width: 0;
316
+ padding: 0;
317
+ font-size: inherit;
318
+ font-weight: inherit;
319
+ color: inherit;
320
+ background: none;
321
+ border: 0;
322
+ }
323
+
324
+ .fui-tabs--boxed .fui-count-badge::before {
325
+ content: '·' / '';
326
+ margin-right: var(--fui-space-2);
327
+ }
328
+
281
329
  /* FilterBar */
282
330
 
283
331
  .fui-filter-bar {
@@ -331,9 +379,9 @@
331
379
  }
332
380
 
333
381
  .fui-field__label {
334
- font-size: var(--fui-text-sm);
335
- font-weight: var(--fui-weight-medium);
336
- color: var(--fui-text);
382
+ font-size: var(--fui-label-size);
383
+ font-weight: var(--fui-label-weight);
384
+ color: var(--fui-label-color);
337
385
  }
338
386
 
339
387
  .fui-field__required {
@@ -713,10 +761,12 @@
713
761
  z-index: var(--fui-z-sticky);
714
762
  height: var(--fui-row-height);
715
763
  padding: 0 var(--fui-space-3);
716
- font-size: var(--fui-text-sm);
717
- font-weight: var(--fui-weight-medium);
718
- color: var(--fui-text-muted);
764
+ font-size: var(--fui-table-head-size);
765
+ font-weight: var(--fui-table-head-weight);
766
+ color: var(--fui-table-head-color);
719
767
  text-align: left;
768
+ text-transform: var(--fui-table-head-transform);
769
+ letter-spacing: var(--fui-table-head-tracking);
720
770
  white-space: nowrap;
721
771
  background: var(--fui-surface-muted);
722
772
  border-bottom: var(--fui-border-width) solid var(--fui-border);
@@ -745,6 +795,31 @@
745
795
  white-space: normal;
746
796
  }
747
797
 
798
+ .fui-table__cell--wrap {
799
+ white-space: normal;
800
+ overflow-wrap: anywhere;
801
+ }
802
+
803
+ .fui-table__cell--width-xs {
804
+ width: var(--fui-column-width-xs);
805
+ }
806
+
807
+ .fui-table__cell--width-sm {
808
+ width: var(--fui-column-width-sm);
809
+ }
810
+
811
+ .fui-table__cell--width-md {
812
+ width: var(--fui-column-width-md);
813
+ }
814
+
815
+ .fui-table__cell--width-lg {
816
+ width: var(--fui-column-width-lg);
817
+ }
818
+
819
+ .fui-table__cell--width-xl {
820
+ width: var(--fui-column-width-xl);
821
+ }
822
+
748
823
  .fui-table__select {
749
824
  width: 1px;
750
825
  padding-right: 0;
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export { Card, type CardProps, Section, type SectionProps } from './Card'
4
4
  export { Columns, type ColumnsProps, SidePanel, type SidePanelProps } from './Columns'
5
5
  export {
6
6
  type Column,
7
+ type ColumnWidth,
7
8
  DataTable,
8
9
  type DataTableProps,
9
10
  type LoadMore,
package/src/theme.css CHANGED
@@ -69,6 +69,25 @@
69
69
 
70
70
  --fui-sidebar-width: 232px;
71
71
  --fui-shell-min-height: 100vh;
72
+ /* The main region's padding: wide screens, then below the breakpoint. */
73
+ --fui-main-padding: var(--fui-space-6);
74
+ --fui-main-padding-narrow: var(--fui-space-4);
75
+ /* A field's label. */
76
+ --fui-label-size: var(--fui-text-sm);
77
+ --fui-label-weight: var(--fui-weight-medium);
78
+ --fui-label-color: var(--fui-text);
79
+ /* A table's column heads: `uppercase` and a tracking such as 0.04em give small capitals. */
80
+ --fui-table-head-size: var(--fui-text-sm);
81
+ --fui-table-head-weight: var(--fui-weight-medium);
82
+ --fui-table-head-color: var(--fui-text-muted);
83
+ --fui-table-head-transform: none;
84
+ --fui-table-head-tracking: normal;
85
+ /* The widths a DataTable column takes by its `width`. */
86
+ --fui-column-width-xs: 64px;
87
+ --fui-column-width-sm: 120px;
88
+ --fui-column-width-md: 180px;
89
+ --fui-column-width-lg: 260px;
90
+ --fui-column-width-xl: 360px;
72
91
  --fui-side-panel-width: 320px;
73
92
  --fui-content-max-width: 1280px;
74
93
  --fui-dialog-width: 480px;