@fabrika/ui 0.4.0 → 0.4.1
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 +1 -1
- package/src/AppShell.tsx +13 -6
- package/src/DataTable.tsx +13 -1
- package/src/StatusPill.tsx +4 -2
- package/src/Tabs.tsx +9 -2
- package/src/components.css +83 -8
- package/src/index.ts +1 -0
- package/src/theme.css +19 -0
package/package.json
CHANGED
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
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
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)}>
|
package/src/StatusPill.tsx
CHANGED
|
@@ -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
|
|
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 (
|
package/src/components.css
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
335
|
-
font-weight: var(--fui-weight
|
|
336
|
-
color: var(--fui-
|
|
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-
|
|
717
|
-
font-weight: var(--fui-weight
|
|
718
|
-
color: var(--fui-
|
|
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
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;
|