@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.
Files changed (45) hide show
  1. package/dist/app-shell.d.ts +10 -0
  2. package/dist/app-shell.js +7 -1
  3. package/dist/bar-list.d.ts +31 -0
  4. package/dist/bar-list.js +25 -0
  5. package/dist/bottom-navigation.d.ts +6 -2
  6. package/dist/bottom-navigation.js +2 -2
  7. package/dist/chart.d.ts +56 -3
  8. package/dist/chart.js +266 -48
  9. package/dist/css/bar-list.css +9 -0
  10. package/dist/css/chart.css +2 -0
  11. package/dist/css/dashboard.css +20 -0
  12. package/dist/css/rules/alert-dialog.css +4 -0
  13. package/dist/css/rules/app-shell.css +10 -0
  14. package/dist/css/rules/bar-list.css +101 -0
  15. package/dist/css/rules/bottom-navigation.css +16 -0
  16. package/dist/css/rules/card.css +5 -0
  17. package/dist/css/rules/chart.css +74 -3
  18. package/dist/css/rules/dashboard.css +66 -0
  19. package/dist/css/rules/date-picker.css +10 -3
  20. package/dist/css/rules/dialog.css +11 -0
  21. package/dist/css/rules/field-control.css +7 -0
  22. package/dist/css/rules/filter-bar.css +8 -1
  23. package/dist/css/rules/input-group.css +9 -0
  24. package/dist/css/rules/otp-field.css +7 -4
  25. package/dist/css/rules/panel.css +9 -1
  26. package/dist/css/rules/select.css +25 -1
  27. package/dist/css/rules/sizing.css +10 -0
  28. package/dist/css/rules/slider.css +4 -2
  29. package/dist/css/rules/stat.css +57 -0
  30. package/dist/css/rules/time-range-picker.css +14 -0
  31. package/dist/css/styles.css +3 -0
  32. package/dist/css/time-range-picker.css +14 -0
  33. package/dist/dashboard.d.ts +46 -0
  34. package/dist/dashboard.js +35 -0
  35. package/dist/icon.d.ts +10 -0
  36. package/dist/icon.js +11 -0
  37. package/dist/index.d.ts +6 -3
  38. package/dist/index.js +5 -2
  39. package/dist/select.d.ts +33 -6
  40. package/dist/select.js +94 -21
  41. package/dist/stat.d.ts +20 -0
  42. package/dist/stat.js +15 -1
  43. package/dist/time-range-picker.d.ts +42 -0
  44. package/dist/time-range-picker.js +69 -0
  45. 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;
@@ -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
  }
@@ -54,7 +54,7 @@
54
54
  display: inline-flex;
55
55
  align-items: center;
56
56
  gap: var(--surf-space-2);
57
- min-height: 36px;
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
- :is(.surf-date-picker, .surf-time-field)[data-disabled] {
54
- opacity: var(--surf-control-disabled-opacity);
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: 1 1 12rem;
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-2);
35
- height: 0;
36
+ width: var(--surf-space-3);
37
+ height: 2px;
36
38
  border: 0;
37
- border-top: 1px solid var(--surf-line);
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
- border-color: CanvasText;
62
+ background: CanvasText;
60
63
  }
61
64
  }
62
65
  }
@@ -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: 6px;
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 {
@@ -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
+ }
@@ -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;