@surflane/ui 0.10.1 → 0.12.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/dist/app-shell.d.ts +10 -0
- package/dist/app-shell.js +7 -1
- package/dist/bar-list.d.ts +31 -0
- package/dist/bar-list.js +25 -0
- package/dist/bottom-navigation.d.ts +6 -2
- package/dist/bottom-navigation.js +2 -2
- package/dist/chart.d.ts +56 -3
- package/dist/chart.js +266 -48
- package/dist/css/bar-list.css +9 -0
- package/dist/css/chart.css +2 -0
- package/dist/css/dashboard.css +20 -0
- package/dist/css/rules/alert-dialog.css +4 -0
- package/dist/css/rules/app-shell.css +10 -0
- package/dist/css/rules/bar-list.css +101 -0
- package/dist/css/rules/bottom-navigation.css +16 -0
- package/dist/css/rules/card.css +5 -0
- package/dist/css/rules/chart.css +74 -3
- package/dist/css/rules/dashboard.css +66 -0
- package/dist/css/rules/date-picker.css +10 -3
- package/dist/css/rules/dialog.css +11 -0
- package/dist/css/rules/field-control.css +7 -0
- package/dist/css/rules/filter-bar.css +8 -1
- package/dist/css/rules/input-group.css +9 -0
- package/dist/css/rules/otp-field.css +7 -4
- package/dist/css/rules/panel.css +9 -1
- package/dist/css/rules/select.css +25 -1
- package/dist/css/rules/sizing.css +10 -0
- package/dist/css/rules/slider.css +4 -2
- package/dist/css/rules/stat.css +57 -0
- package/dist/css/rules/time-range-picker.css +14 -0
- package/dist/css/styles.css +3 -0
- package/dist/css/time-range-picker.css +14 -0
- package/dist/dashboard.d.ts +46 -0
- package/dist/dashboard.js +35 -0
- package/dist/icon.d.ts +10 -0
- package/dist/icon.js +11 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/select.d.ts +33 -6
- package/dist/select.js +94 -21
- package/dist/stat.d.ts +20 -0
- package/dist/stat.js +15 -1
- package/dist/time-range-picker.d.ts +42 -0
- package/dist/time-range-picker.js +69 -0
- package/package.json +18 -3
|
@@ -117,6 +117,22 @@
|
|
|
117
117
|
font-variant-numeric: tabular-nums;
|
|
118
118
|
text-align: center;
|
|
119
119
|
}
|
|
120
|
+
/* Status fills, each 4.5:1 against the canvas-colored count. */
|
|
121
|
+
.surf-bottom-nav-badge[data-tone='caution'] {
|
|
122
|
+
background: var(--surf-status-caution-fg);
|
|
123
|
+
}
|
|
124
|
+
.surf-bottom-nav-badge[data-tone='success'] {
|
|
125
|
+
background: var(--surf-status-success-fg);
|
|
126
|
+
}
|
|
127
|
+
.surf-bottom-nav-badge[data-tone='info'] {
|
|
128
|
+
background: var(--surf-status-info-fg);
|
|
129
|
+
}
|
|
130
|
+
.surf-bottom-nav-badge[data-tone='progress'] {
|
|
131
|
+
background: var(--surf-status-progress-fg);
|
|
132
|
+
}
|
|
133
|
+
.surf-bottom-nav-badge[data-tone='neutral'] {
|
|
134
|
+
background: var(--surf-status-neutral-fg);
|
|
135
|
+
}
|
|
120
136
|
.surf-bottom-nav-badge[data-dot] {
|
|
121
137
|
inset-block-start: 0;
|
|
122
138
|
min-inline-size: 0;
|
package/dist/css/rules/card.css
CHANGED
|
@@ -7,4 +7,9 @@
|
|
|
7
7
|
padding: var(--surf-surface-padding, var(--surf-ui-card-padding));
|
|
8
8
|
gap: var(--surf-ui-surface-padding);
|
|
9
9
|
}
|
|
10
|
+
:where([data-ui-size], [data-ui-size] *).surf-card-title {
|
|
11
|
+
font-size: var(--surf-ui-title-size);
|
|
12
|
+
line-height: var(--surf-ui-title-leading);
|
|
13
|
+
letter-spacing: var(--surf-ui-title-tracking);
|
|
14
|
+
}
|
|
10
15
|
}
|
package/dist/css/rules/chart.css
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
display: inline-flex;
|
|
55
55
|
align-items: center;
|
|
56
56
|
gap: var(--surf-space-2);
|
|
57
|
-
min-height:
|
|
57
|
+
min-height: var(--surf-control-height);
|
|
58
58
|
padding: var(--surf-space-1) var(--surf-space-2);
|
|
59
59
|
border: 0;
|
|
60
60
|
border-radius: var(--surf-radius-md);
|
|
@@ -66,9 +66,39 @@
|
|
|
66
66
|
cursor: pointer;
|
|
67
67
|
text-align: start;
|
|
68
68
|
}
|
|
69
|
-
.surf-chart-legend-button svg
|
|
69
|
+
.surf-chart-legend-button svg,
|
|
70
|
+
.surf-chart-legend-reference svg {
|
|
70
71
|
flex: none;
|
|
71
72
|
}
|
|
73
|
+
/* A threshold or band: named with its value, not a toggle. */
|
|
74
|
+
.surf-chart-legend-reference {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: var(--surf-space-2);
|
|
78
|
+
min-height: var(--surf-control-height);
|
|
79
|
+
padding: var(--surf-space-1) var(--surf-space-2);
|
|
80
|
+
color: var(--surf-ink-body);
|
|
81
|
+
font: var(--surf-weight-regular)
|
|
82
|
+
var(--surf-type-body-size)/var(--surf-type-body-leading)
|
|
83
|
+
var(--surf-font-body);
|
|
84
|
+
}
|
|
85
|
+
.surf-chart-legend-more {
|
|
86
|
+
font-variant-numeric: tabular-nums;
|
|
87
|
+
}
|
|
88
|
+
/* The series past legendLimit, one per row in a popover. */
|
|
89
|
+
.surf-chart-legend-list {
|
|
90
|
+
display: grid;
|
|
91
|
+
gap: var(--surf-space-1);
|
|
92
|
+
margin: 0;
|
|
93
|
+
padding: 0;
|
|
94
|
+
list-style: none;
|
|
95
|
+
}
|
|
96
|
+
.surf-chart-legend-list .surf-chart-legend-button {
|
|
97
|
+
inline-size: 100%;
|
|
98
|
+
}
|
|
99
|
+
.surf-chart-key[data-position='bottom'] .surf-chart-legend {
|
|
100
|
+
margin-inline-start: 0;
|
|
101
|
+
}
|
|
72
102
|
.surf-chart-legend-button span {
|
|
73
103
|
overflow-wrap: anywhere;
|
|
74
104
|
}
|
|
@@ -166,6 +196,10 @@
|
|
|
166
196
|
inline-size: var(--surf-space-6);
|
|
167
197
|
block-size: var(--surf-space-4);
|
|
168
198
|
}
|
|
199
|
+
.surf-chart-tooltip-total {
|
|
200
|
+
padding-block-start: var(--surf-space-1);
|
|
201
|
+
border-block-start: 1px solid var(--surf-line);
|
|
202
|
+
}
|
|
169
203
|
.surf-chart-tooltip-value {
|
|
170
204
|
white-space: nowrap;
|
|
171
205
|
margin-inline-start: auto;
|
|
@@ -231,8 +265,39 @@
|
|
|
231
265
|
.surf-chart-table .surf-scroll-viewport {
|
|
232
266
|
max-block-size: 320px;
|
|
233
267
|
}
|
|
268
|
+
/* Compact, for charts in dashboard cards: the plot, its legend and the floating
|
|
269
|
+
values show; the caption, keyboard help, readout and x-axis name stay for
|
|
270
|
+
assistive technology. */
|
|
271
|
+
.surf-chart[data-density='compact'] {
|
|
272
|
+
gap: var(--surf-space-2);
|
|
273
|
+
}
|
|
274
|
+
.surf-chart[data-density='compact']
|
|
275
|
+
:is(
|
|
276
|
+
.surf-chart-caption,
|
|
277
|
+
.surf-chart-help,
|
|
278
|
+
.surf-chart-inspector,
|
|
279
|
+
.surf-chart-x-label
|
|
280
|
+
) {
|
|
281
|
+
position: absolute;
|
|
282
|
+
inline-size: 1px;
|
|
283
|
+
block-size: 1px;
|
|
284
|
+
min-height: 0;
|
|
285
|
+
margin: 0;
|
|
286
|
+
padding: 0;
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
clip-path: inset(50%);
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
}
|
|
291
|
+
.surf-chart[data-density='compact']
|
|
292
|
+
:is(.surf-chart-legend-button, .surf-chart-legend-reference) {
|
|
293
|
+
min-height: var(--surf-space-6);
|
|
294
|
+
padding-block: 0;
|
|
295
|
+
font-size: var(--surf-type-compact-size);
|
|
296
|
+
line-height: var(--surf-type-compact-leading);
|
|
297
|
+
}
|
|
234
298
|
@media (max-width: 760px), (pointer: coarse) {
|
|
235
|
-
.surf-chart-legend-button
|
|
299
|
+
.surf-chart-legend-button,
|
|
300
|
+
.surf-chart[data-density='compact'] .surf-chart-legend-button {
|
|
236
301
|
min-height: var(--surf-control-target-size);
|
|
237
302
|
}
|
|
238
303
|
}
|
|
@@ -251,6 +316,12 @@
|
|
|
251
316
|
--surf-chart-category-4: CanvasText;
|
|
252
317
|
--surf-chart-category-5: CanvasText;
|
|
253
318
|
--surf-chart-category-6: CanvasText;
|
|
319
|
+
--surf-chart-category-7: CanvasText;
|
|
320
|
+
--surf-chart-category-8: CanvasText;
|
|
321
|
+
--surf-chart-category-9: CanvasText;
|
|
322
|
+
--surf-chart-category-10: CanvasText;
|
|
323
|
+
--surf-chart-category-11: CanvasText;
|
|
324
|
+
--surf-chart-category-12: CanvasText;
|
|
254
325
|
}
|
|
255
326
|
.surf-chart-plot .recharts-surface,
|
|
256
327
|
.surf-chart-mark {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Dashboard, with the styles of what it renders. */
|
|
2
|
+
@layer surflane.tokens, surflane.themes, surflane.base, surflane.components,
|
|
3
|
+
surflane.sizes;
|
|
4
|
+
|
|
5
|
+
@layer surflane.components {
|
|
6
|
+
/* As many columns as fit a panel's minimum width; one on a phone. */
|
|
7
|
+
.surf-dashboard {
|
|
8
|
+
--surf-dashboard-panel-min-width: 20rem;
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: repeat(
|
|
11
|
+
auto-fill,
|
|
12
|
+
minmax(min(100%, var(--surf-dashboard-panel-min-width)), 1fr)
|
|
13
|
+
);
|
|
14
|
+
gap: var(--surf-space-4);
|
|
15
|
+
min-inline-size: 0;
|
|
16
|
+
}
|
|
17
|
+
/* The header keeps its height; the content takes the rest of a stretched row. */
|
|
18
|
+
.surf-dashboard-panel {
|
|
19
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
20
|
+
align-content: start;
|
|
21
|
+
min-inline-size: 0;
|
|
22
|
+
}
|
|
23
|
+
.surf-dashboard-panel[data-span='2'] {
|
|
24
|
+
grid-column: span 2;
|
|
25
|
+
}
|
|
26
|
+
.surf-dashboard-panel[data-span='3'] {
|
|
27
|
+
grid-column: span 3;
|
|
28
|
+
}
|
|
29
|
+
.surf-dashboard-panel[data-span='full'] {
|
|
30
|
+
grid-column: 1 / -1;
|
|
31
|
+
}
|
|
32
|
+
/* As tall as an icon button, with or without one, so titles line up across a row. */
|
|
33
|
+
.surf-dashboard-panel-header {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: var(--surf-space-2);
|
|
37
|
+
min-inline-size: 0;
|
|
38
|
+
min-block-size: var(--surf-control-height);
|
|
39
|
+
}
|
|
40
|
+
.surf-dashboard-panel-header .surf-card-title {
|
|
41
|
+
min-inline-size: 0;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
text-overflow: ellipsis;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
}
|
|
46
|
+
.surf-dashboard-panel-actions {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: var(--surf-space-1);
|
|
50
|
+
margin-inline-start: auto;
|
|
51
|
+
}
|
|
52
|
+
.surf-dashboard-panel-body {
|
|
53
|
+
min-inline-size: 0;
|
|
54
|
+
min-block-size: 0;
|
|
55
|
+
}
|
|
56
|
+
.surf-dashboard-panel-empty {
|
|
57
|
+
margin: 0;
|
|
58
|
+
color: var(--surf-ink-muted);
|
|
59
|
+
}
|
|
60
|
+
/* A phone shows one panel a row, whatever it spans. */
|
|
61
|
+
@media (max-width: 760px) {
|
|
62
|
+
.surf-dashboard-panel[data-span] {
|
|
63
|
+
grid-column: auto;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -50,13 +50,20 @@
|
|
|
50
50
|
.surf-date-group:hover {
|
|
51
51
|
border-color: var(--surf-status-danger-fg);
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
53
|
+
/* Disabled fades the field, as in every other field: the label and the reason
|
|
54
|
+
beside it stay readable. */
|
|
56
55
|
:is(.surf-date-picker, .surf-time-field)[data-disabled] .surf-date-group {
|
|
56
|
+
opacity: var(--surf-control-disabled-opacity);
|
|
57
57
|
cursor: not-allowed;
|
|
58
58
|
background: var(--surf-control-disabled-background);
|
|
59
59
|
}
|
|
60
|
+
/* Read-only shows a value, not a box to type in: it sits on the surface plane
|
|
61
|
+
without the field's border, as text fields do. */
|
|
62
|
+
:is(.surf-date-picker, .surf-time-field)[data-readonly]:not([data-invalid])
|
|
63
|
+
.surf-date-group:not([data-focus-within], :focus-within) {
|
|
64
|
+
border-color: transparent;
|
|
65
|
+
background: var(--surf-surface);
|
|
66
|
+
}
|
|
60
67
|
.surf-date-input {
|
|
61
68
|
display: flex;
|
|
62
69
|
align-items: center;
|
|
@@ -158,9 +158,20 @@
|
|
|
158
158
|
) {
|
|
159
159
|
padding-inline: var(--surf-ui-card-padding);
|
|
160
160
|
}
|
|
161
|
+
/* Head and footer follow the size's card padding: 24/20px and 20px by default. */
|
|
161
162
|
:where([data-ui-size], [data-ui-size] *).surf-dialog-header {
|
|
163
|
+
padding-block: var(--surf-ui-card-padding)
|
|
164
|
+
calc(var(--surf-ui-card-padding) - var(--surf-space-1));
|
|
162
165
|
padding-inline-end: calc(
|
|
163
166
|
var(--surf-control-height) + var(--surf-space-4) * 2
|
|
164
167
|
);
|
|
165
168
|
}
|
|
169
|
+
:where([data-ui-size], [data-ui-size] *).surf-dialog-footer {
|
|
170
|
+
padding-block: calc(var(--surf-ui-card-padding) - var(--surf-space-1));
|
|
171
|
+
}
|
|
172
|
+
:where([data-ui-size], [data-ui-size] *).surf-dialog-header h2 {
|
|
173
|
+
font-size: var(--surf-ui-title-size);
|
|
174
|
+
line-height: var(--surf-ui-title-leading);
|
|
175
|
+
letter-spacing: var(--surf-ui-title-tracking);
|
|
176
|
+
}
|
|
166
177
|
}
|
|
@@ -62,6 +62,13 @@
|
|
|
62
62
|
cursor: not-allowed;
|
|
63
63
|
background: var(--surf-control-disabled-background);
|
|
64
64
|
}
|
|
65
|
+
/* Read-only shows a value, not a box to type in: the value sits on the surface
|
|
66
|
+
plane without the field's border. Its text stays at full ink, apart from a
|
|
67
|
+
disabled field, and focus still rings it, so the value stays copyable. */
|
|
68
|
+
.surf-input[readonly]:not(:focus, [aria-invalid='true']) {
|
|
69
|
+
border-color: transparent;
|
|
70
|
+
background: var(--surf-surface);
|
|
71
|
+
}
|
|
65
72
|
/* An invalid field keeps its edge while hovered; hover still tints the fill. */
|
|
66
73
|
.surf-input[aria-invalid='true'],
|
|
67
74
|
.surf-input[aria-invalid='true']:hover,
|
|
@@ -15,16 +15,23 @@
|
|
|
15
15
|
gap: var(--surf-space-4);
|
|
16
16
|
min-inline-size: 0;
|
|
17
17
|
}
|
|
18
|
+
/* A filter keeps its width; only the search field takes the room left in the row. */
|
|
18
19
|
.surf-filter-bar-field {
|
|
19
20
|
display: grid;
|
|
20
21
|
gap: var(--surf-space-2);
|
|
21
|
-
flex:
|
|
22
|
+
flex: 0 1 12rem;
|
|
22
23
|
min-inline-size: 0;
|
|
23
24
|
max-inline-size: 100%;
|
|
24
25
|
}
|
|
25
26
|
.surf-filter-bar-field[data-width='search'] {
|
|
26
27
|
flex: 2 1 20rem;
|
|
27
28
|
}
|
|
29
|
+
/* On a phone each field fills its row. */
|
|
30
|
+
@media (max-width: 760px) {
|
|
31
|
+
.surf-filter-bar-field {
|
|
32
|
+
flex-grow: 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
28
35
|
.surf-filter-bar-field > :is(.surf-control, .surf-select-trigger) {
|
|
29
36
|
inline-size: 100%;
|
|
30
37
|
min-inline-size: 0;
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
border-color: var(--surf-control-hover-border);
|
|
25
25
|
background: var(--surf-control-hover-background);
|
|
26
26
|
}
|
|
27
|
+
/* Read-only shows a value on the surface plane, without the field's border, as
|
|
28
|
+
text fields do. */
|
|
29
|
+
:is(.surf-combobox-group, .surf-number-group)[data-readonly]:not(
|
|
30
|
+
:focus-within,
|
|
31
|
+
[data-invalid]
|
|
32
|
+
) {
|
|
33
|
+
border-color: transparent;
|
|
34
|
+
background: var(--surf-surface);
|
|
35
|
+
}
|
|
27
36
|
.surf-combobox-group:focus-within,
|
|
28
37
|
.surf-number-group:focus-within {
|
|
29
38
|
border-color: var(--surf-control-focus);
|
|
@@ -29,12 +29,15 @@
|
|
|
29
29
|
font-variant-numeric: tabular-nums;
|
|
30
30
|
caret-color: var(--surf-control-focus);
|
|
31
31
|
}
|
|
32
|
+
/* A short dash, as a code is written elsewhere (481-920): muted ink, weaker than
|
|
33
|
+
the digits and the boxes but plainly there. */
|
|
32
34
|
.surf-otp-separator {
|
|
33
35
|
flex: none;
|
|
34
|
-
width: var(--surf-space-
|
|
35
|
-
height:
|
|
36
|
+
width: var(--surf-space-3);
|
|
37
|
+
height: 2px;
|
|
36
38
|
border: 0;
|
|
37
|
-
border-
|
|
39
|
+
border-radius: var(--surf-radius-full);
|
|
40
|
+
background: var(--surf-ink-muted);
|
|
38
41
|
}
|
|
39
42
|
@media (max-width: 760px) {
|
|
40
43
|
.surf-otp-field,
|
|
@@ -56,7 +59,7 @@
|
|
|
56
59
|
border-color: LinkText;
|
|
57
60
|
}
|
|
58
61
|
.surf-otp-separator {
|
|
59
|
-
|
|
62
|
+
background: CanvasText;
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
}
|
package/dist/css/rules/panel.css
CHANGED
|
@@ -217,10 +217,18 @@
|
|
|
217
217
|
) {
|
|
218
218
|
padding-inline: var(--surf-ui-card-padding);
|
|
219
219
|
}
|
|
220
|
-
/* The title keeps clear of the close button: its 16px inset, its width and a 16px gap.
|
|
220
|
+
/* The title keeps clear of the close button: its 16px inset, its width and a 16px gap.
|
|
221
|
+
* Above and below, the head follows the size's card padding: 24/20px by default. */
|
|
221
222
|
:where([data-ui-size], [data-ui-size] *).surf-panel-header {
|
|
223
|
+
padding-block: var(--surf-ui-card-padding)
|
|
224
|
+
calc(var(--surf-ui-card-padding) - var(--surf-space-1));
|
|
222
225
|
padding-inline-end: calc(
|
|
223
226
|
var(--surf-control-height) + var(--surf-space-4) * 2
|
|
224
227
|
);
|
|
225
228
|
}
|
|
229
|
+
:where([data-ui-size], [data-ui-size] *).surf-panel-title {
|
|
230
|
+
font-size: var(--surf-ui-title-size);
|
|
231
|
+
line-height: var(--surf-ui-title-leading);
|
|
232
|
+
letter-spacing: var(--surf-ui-title-tracking);
|
|
233
|
+
}
|
|
226
234
|
}
|
|
@@ -11,11 +11,35 @@
|
|
|
11
11
|
text-align: start;
|
|
12
12
|
cursor: pointer;
|
|
13
13
|
}
|
|
14
|
-
.surf-select-trigger > span:first-child
|
|
14
|
+
.surf-select-trigger > span:first-child,
|
|
15
|
+
.surf-select-value {
|
|
15
16
|
overflow: hidden;
|
|
16
17
|
text-overflow: ellipsis;
|
|
17
18
|
white-space: nowrap;
|
|
18
19
|
}
|
|
20
|
+
/* Content width: the trigger fits its longest option and keeps that width. */
|
|
21
|
+
.surf-select[data-width='content'],
|
|
22
|
+
.surf-select[data-width='content'] .surf-select-trigger {
|
|
23
|
+
width: auto;
|
|
24
|
+
}
|
|
25
|
+
.surf-select-sizer {
|
|
26
|
+
display: grid;
|
|
27
|
+
min-width: 0;
|
|
28
|
+
}
|
|
29
|
+
.surf-select-sizer > * {
|
|
30
|
+
grid-area: 1 / 1;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
text-overflow: ellipsis;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
}
|
|
35
|
+
.surf-select-sizer > [aria-hidden='true'] {
|
|
36
|
+
visibility: hidden;
|
|
37
|
+
}
|
|
38
|
+
.surf-select-separator {
|
|
39
|
+
block-size: 1px;
|
|
40
|
+
margin-block: var(--surf-space-1);
|
|
41
|
+
background: var(--surf-line);
|
|
42
|
+
}
|
|
19
43
|
.surf-select-icon {
|
|
20
44
|
display: flex;
|
|
21
45
|
align-items: center;
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
--surf-ui-slider-track: var(--surf-size-slider-track-small);
|
|
12
12
|
--surf-ui-file-padding: var(--surf-size-file-padding-small);
|
|
13
13
|
--surf-ui-content-gap: var(--surf-size-content-gap-small);
|
|
14
|
+
/* Surface titles (Card, Sheet, Dialog) step down one role at Small. */
|
|
15
|
+
--surf-ui-title-size: var(--surf-type-subsection-size);
|
|
16
|
+
--surf-ui-title-leading: var(--surf-type-subsection-leading);
|
|
17
|
+
--surf-ui-title-tracking: var(--surf-type-subsection-tracking);
|
|
14
18
|
|
|
15
19
|
--surf-ui-control-height: var(--surf-size-control-height-small);
|
|
16
20
|
--surf-ui-control-padding: var(--surf-size-control-padding-small);
|
|
@@ -51,6 +55,9 @@
|
|
|
51
55
|
--surf-ui-slider-track: var(--surf-size-slider-track-default);
|
|
52
56
|
--surf-ui-file-padding: var(--surf-size-file-padding-default);
|
|
53
57
|
--surf-ui-content-gap: var(--surf-size-content-gap-default);
|
|
58
|
+
--surf-ui-title-size: var(--surf-type-section-size);
|
|
59
|
+
--surf-ui-title-leading: var(--surf-type-section-leading);
|
|
60
|
+
--surf-ui-title-tracking: var(--surf-type-section-tracking);
|
|
54
61
|
|
|
55
62
|
--surf-ui-control-height: var(--surf-size-control-height-default);
|
|
56
63
|
--surf-ui-control-padding: var(--surf-size-control-padding-default);
|
|
@@ -91,6 +98,9 @@
|
|
|
91
98
|
--surf-ui-slider-track: var(--surf-size-slider-track-large);
|
|
92
99
|
--surf-ui-file-padding: var(--surf-size-file-padding-large);
|
|
93
100
|
--surf-ui-content-gap: var(--surf-size-content-gap-large);
|
|
101
|
+
--surf-ui-title-size: var(--surf-type-section-size);
|
|
102
|
+
--surf-ui-title-leading: var(--surf-type-section-leading);
|
|
103
|
+
--surf-ui-title-tracking: var(--surf-type-section-tracking);
|
|
94
104
|
|
|
95
105
|
--surf-ui-control-height: var(--surf-size-control-height-large);
|
|
96
106
|
--surf-ui-control-padding: var(--surf-size-control-padding-large);
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
}
|
|
39
39
|
.surf-slider-track {
|
|
40
40
|
width: 100%;
|
|
41
|
-
height:
|
|
41
|
+
height: var(--surf-size-slider-track-default);
|
|
42
42
|
border-radius: var(--surf-radius-full);
|
|
43
43
|
background: var(--surf-slider-track);
|
|
44
44
|
}
|
|
45
|
+
/* The pair Progress uses: a brand-mark fill on the surface track, 3:1 against both
|
|
46
|
+
the track and the sheet in either theme. */
|
|
45
47
|
.surf-slider-indicator {
|
|
46
|
-
background: var(--surf-brand);
|
|
48
|
+
background: var(--surf-brand-mark);
|
|
47
49
|
border-radius: inherit;
|
|
48
50
|
}
|
|
49
51
|
.surf-slider-thumb {
|
package/dist/css/rules/stat.css
CHANGED
|
@@ -49,6 +49,35 @@
|
|
|
49
49
|
.surf-stat-change[data-tone='down'] {
|
|
50
50
|
color: var(--surf-trend-down-fg);
|
|
51
51
|
}
|
|
52
|
+
/* The value's state: the tone's icon and text, on the value's line. */
|
|
53
|
+
.surf-stat-status {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: var(--surf-space-1);
|
|
57
|
+
color: var(--surf-status-neutral-fg);
|
|
58
|
+
font-size: var(--surf-type-label-size);
|
|
59
|
+
line-height: var(--surf-type-label-leading);
|
|
60
|
+
letter-spacing: var(--surf-type-label-tracking);
|
|
61
|
+
font-weight: var(--surf-weight-medium);
|
|
62
|
+
}
|
|
63
|
+
.surf-stat-status[data-tone='info'] {
|
|
64
|
+
color: var(--surf-status-info-fg);
|
|
65
|
+
}
|
|
66
|
+
.surf-stat-status[data-tone='success'] {
|
|
67
|
+
color: var(--surf-status-success-fg);
|
|
68
|
+
}
|
|
69
|
+
.surf-stat-status[data-tone='progress'] {
|
|
70
|
+
color: var(--surf-status-progress-fg);
|
|
71
|
+
}
|
|
72
|
+
/* Past a threshold the value takes the status color as well. */
|
|
73
|
+
.surf-stat-status[data-tone='caution'],
|
|
74
|
+
.surf-stat:has(> .surf-stat-status[data-tone='caution']) > .surf-stat-value {
|
|
75
|
+
color: var(--surf-status-caution-fg);
|
|
76
|
+
}
|
|
77
|
+
.surf-stat-status[data-tone='danger'],
|
|
78
|
+
.surf-stat:has(> .surf-stat-status[data-tone='danger']) > .surf-stat-value {
|
|
79
|
+
color: var(--surf-status-danger-fg);
|
|
80
|
+
}
|
|
52
81
|
.surf-stat-caption {
|
|
53
82
|
flex: 1 0 100%;
|
|
54
83
|
color: var(--surf-ink-muted);
|
|
@@ -56,4 +85,32 @@
|
|
|
56
85
|
line-height: var(--surf-type-caption-leading);
|
|
57
86
|
letter-spacing: var(--surf-type-caption-tracking);
|
|
58
87
|
}
|
|
88
|
+
/* A group: the shared label, then each value under its own name, side by side. */
|
|
89
|
+
.surf-stat-group {
|
|
90
|
+
display: grid;
|
|
91
|
+
gap: var(--surf-space-2);
|
|
92
|
+
min-width: 0;
|
|
93
|
+
}
|
|
94
|
+
.surf-stat-group-label {
|
|
95
|
+
color: var(--surf-ink-muted);
|
|
96
|
+
font-size: var(--surf-type-label-size);
|
|
97
|
+
line-height: var(--surf-type-label-leading);
|
|
98
|
+
letter-spacing: var(--surf-type-label-tracking);
|
|
99
|
+
}
|
|
100
|
+
/* Each value takes its own width and wraps to a new line when the row is full. */
|
|
101
|
+
.surf-stat-group-items {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
gap: var(--surf-space-3) var(--surf-space-6);
|
|
105
|
+
min-width: 0;
|
|
106
|
+
}
|
|
107
|
+
/* In a group each value stacks its lines, so it is only as wide as the widest. */
|
|
108
|
+
.surf-stat-group-items > .surf-stat {
|
|
109
|
+
display: grid;
|
|
110
|
+
justify-items: start;
|
|
111
|
+
align-content: start;
|
|
112
|
+
}
|
|
113
|
+
.surf-stat-group-items .surf-stat-label {
|
|
114
|
+
color: var(--surf-ink-body);
|
|
115
|
+
}
|
|
59
116
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Time range picker, with the styles of what it renders. */
|
|
2
|
+
@layer surflane.tokens, surflane.themes, surflane.base, surflane.components,
|
|
3
|
+
surflane.sizes;
|
|
4
|
+
|
|
5
|
+
@layer surflane.components {
|
|
6
|
+
/* The span, the interval and the button sit on one line in a header or toolbar. */
|
|
7
|
+
.surf-time-range {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: var(--surf-space-2);
|
|
12
|
+
max-inline-size: 100%;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/css/styles.css
CHANGED
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
@import './rules/app-shell.css';
|
|
63
63
|
@import './rules/tree.css';
|
|
64
64
|
@import './rules/date-picker.css';
|
|
65
|
+
@import './rules/time-range-picker.css';
|
|
65
66
|
@import './rules/chart.css';
|
|
66
67
|
@import '@surflane/css/foundations/layout.css';
|
|
67
68
|
@import './rules/layout.css';
|
|
@@ -73,8 +74,10 @@
|
|
|
73
74
|
@import './rules/list.css';
|
|
74
75
|
@import './rules/stat.css';
|
|
75
76
|
@import './rules/sparkline.css';
|
|
77
|
+
@import './rules/bar-list.css';
|
|
76
78
|
@import '@surflane/css/presentation/inline-message.css';
|
|
77
79
|
@import './rules/inline-message.css';
|
|
80
|
+
@import './rules/dashboard.css';
|
|
78
81
|
@import './rules/syntax-highlighter.css';
|
|
79
82
|
@import '@surflane/css/presentation/code.css';
|
|
80
83
|
@import './rules/code.css';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Time range picker, with the styles of what it renders. */
|
|
2
|
+
@layer surflane.tokens, surflane.themes, surflane.base, surflane.components,
|
|
3
|
+
surflane.sizes;
|
|
4
|
+
@import '@surflane/design-tokens/tokens.css';
|
|
5
|
+
@import '@surflane/css/presentation/base.css';
|
|
6
|
+
@import './rules/base.css';
|
|
7
|
+
@import './rules/field-control.css';
|
|
8
|
+
@import './rules/sizing.css';
|
|
9
|
+
@import './rules/popup.css';
|
|
10
|
+
@import './rules/item.css';
|
|
11
|
+
@import './rules/icon.css';
|
|
12
|
+
@import './rules/select.css';
|
|
13
|
+
@import './rules/button.css';
|
|
14
|
+
@import './rules/time-range-picker.css';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type DashboardRootProps = React.ComponentPropsWithoutRef<'div'>;
|
|
3
|
+
export type DashboardPanelLabels = {
|
|
4
|
+
/** The button that shows the description, given the title. */
|
|
5
|
+
about: (title: string) => string;
|
|
6
|
+
loading: string;
|
|
7
|
+
refreshing: string;
|
|
8
|
+
error: string;
|
|
9
|
+
retry: string;
|
|
10
|
+
empty: string;
|
|
11
|
+
};
|
|
12
|
+
export type DashboardPanelProps = Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> & {
|
|
13
|
+
title: string;
|
|
14
|
+
/** What the panel shows and how it is measured, behind an info button by the title. */
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
/** The heading level of the title. */
|
|
17
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
18
|
+
/** loading shows a placeholder until the first data arrives; error and empty
|
|
19
|
+
* replace the content with a message. */
|
|
20
|
+
state?: 'ready' | 'loading' | 'error' | 'empty';
|
|
21
|
+
/** New data is on its way: the content stays, a spinner shows by the title and the
|
|
22
|
+
* panel is busy for assistive technology. */
|
|
23
|
+
refreshing?: boolean;
|
|
24
|
+
/** Shows a retry button in the error message. */
|
|
25
|
+
onRetry?: () => void;
|
|
26
|
+
/** Why it failed or why it is empty, in place of the default text. */
|
|
27
|
+
message?: React.ReactNode;
|
|
28
|
+
/** Controls at the end of the header, such as a menu. */
|
|
29
|
+
actions?: React.ReactNode;
|
|
30
|
+
/** Columns the panel spans where they fit; a phone shows one. */
|
|
31
|
+
span?: 1 | 2 | 3 | 'full';
|
|
32
|
+
labels?: Partial<DashboardPanelLabels>;
|
|
33
|
+
};
|
|
34
|
+
export declare const Dashboard: {
|
|
35
|
+
Root: ({ className, ref, ...props }: DashboardRootProps & {
|
|
36
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
37
|
+
}) => React.JSX.Element;
|
|
38
|
+
Panel: ({ title, description, level, state, refreshing, onRetry, message, actions, span, labels, className, children, ref, ...props }: DashboardPanelProps & {
|
|
39
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
40
|
+
}) => React.JSX.Element;
|
|
41
|
+
};
|
|
42
|
+
export declare const DashboardRoot: ({ className, ref, ...props }: DashboardRootProps & {
|
|
43
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
44
|
+
}) => React.JSX.Element, DashboardPanel: ({ title, description, level, state, refreshing, onRetry, message, actions, span, labels, className, children, ref, ...props }: DashboardPanelProps & {
|
|
45
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
46
|
+
}) => React.JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Button } from './button.js';
|
|
5
|
+
import { Card } from './card.js';
|
|
6
|
+
import { Icon } from './icon.js';
|
|
7
|
+
import { InlineMessage } from './inline-message.js';
|
|
8
|
+
import { Popover } from './popover.js';
|
|
9
|
+
import { Skeleton } from './skeleton.js';
|
|
10
|
+
import { Spinner } from './spinner.js';
|
|
11
|
+
import { cx } from './class-names.js';
|
|
12
|
+
/** The panels of a dashboard on a responsive grid: as many columns as fit panels of
|
|
13
|
+
* --surf-dashboard-panel-min-width (20rem), one column on a phone. */
|
|
14
|
+
const Root = function DashboardRoot({ className, ref, ...props }) {
|
|
15
|
+
return (_jsx("div", { ...props, ref: ref, className: cx('surf-dashboard', className) }));
|
|
16
|
+
};
|
|
17
|
+
const panelLabels = {
|
|
18
|
+
about: (title) => `About ${title}`,
|
|
19
|
+
loading: 'Loading',
|
|
20
|
+
refreshing: 'Refreshing',
|
|
21
|
+
error: 'This panel could not load.',
|
|
22
|
+
retry: 'Retry',
|
|
23
|
+
empty: 'No data in this range.',
|
|
24
|
+
};
|
|
25
|
+
/** One panel of a dashboard: a card with its title, an optional description, the
|
|
26
|
+
* states of its data and the content. The content takes the room the row gives it. */
|
|
27
|
+
const Panel = function DashboardPanel({ title, description, level = 3, state = 'ready', refreshing = false, onRetry, message, actions, span, labels, className, children, ref, ...props }) {
|
|
28
|
+
const text = { ...panelLabels, ...labels };
|
|
29
|
+
const id = React.useId();
|
|
30
|
+
const busy = state === 'loading' || refreshing;
|
|
31
|
+
return (_jsxs(Card.Root, { role: 'group', "aria-labelledby": `${id}-title`, "aria-busy": busy || undefined, ...props, ref: ref, "data-span": span, "data-state": state, className: cx('surf-dashboard-panel', className), children: [_jsxs("div", { className: 'surf-dashboard-panel-header', children: [_jsx(Card.Title, { level: level, id: `${id}-title`, children: title }), description && (_jsxs(Popover.Root, { children: [_jsx(Popover.Trigger, { render: _jsx(Button, { variant: 'ghost', size: 'icon-small', "aria-label": text.about(title) }), children: _jsx(Icon, { name: 'info' }) }), _jsx(Popover.Content, { size: 'small', children: _jsx(Popover.Description, { children: description }) })] })), refreshing && state !== 'loading' && (_jsx(Spinner, { size: 'small', label: text.refreshing })), actions && (_jsx("div", { className: 'surf-dashboard-panel-actions', children: actions }))] }), _jsx("div", { className: 'surf-dashboard-panel-body', children: state === 'loading' ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { shape: 'block' }), _jsx("span", { className: 'surf-feedback-sr', children: text.loading })] })) : state === 'error' ? (_jsxs(InlineMessage.Root, { tone: 'danger', children: [_jsx(InlineMessage.Icon, { tone: 'danger' }), _jsxs(InlineMessage.Content, { children: [_jsx(InlineMessage.Description, { children: message ?? text.error }), onRetry && (_jsx(InlineMessage.Actions, { children: _jsx(Button, { variant: 'outline', size: 'small', onClick: onRetry, children: text.retry }) }))] })] })) : state === 'empty' ? (_jsx("p", { className: 'surf-dashboard-panel-empty', children: message ?? text.empty })) : (children) })] }));
|
|
32
|
+
};
|
|
33
|
+
export const Dashboard = { Root, Panel };
|
|
34
|
+
// Flat names for React Server Components, which cannot dot into client exports.
|
|
35
|
+
export const { Root: DashboardRoot, Panel: DashboardPanel } = Dashboard;
|