@surflane/ui 0.6.0 → 0.8.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/README.md +1 -1
- package/dist/code.js +3 -1
- package/dist/css/rules/app-shell.css +23 -16
- package/dist/css/rules/chart.css +8 -0
- package/dist/css/rules/choice.css +13 -1
- package/dist/css/rules/combobox.css +4 -0
- package/dist/css/rules/date-picker.css +7 -1
- package/dist/css/rules/field-control.css +12 -1
- package/dist/css/rules/file-upload.css +31 -20
- package/dist/css/rules/input-group.css +8 -1
- package/dist/css/rules/scroll-area.css +7 -0
- package/dist/css/rules/select.css +6 -0
- package/dist/css/rules/slider.css +2 -1
- package/dist/file-upload.js +47 -47
- package/dist/pagination.js +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Import the stylesheets once at the application root. `all.css` brings the tokens
|
|
|
18
18
|
@import '@surflane/ui/all.css';
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
To ship only what you use, import component stylesheets instead, such as `@surflane/ui/button.css` and `@surflane/ui/dialog.css`. Each brings the tokens and the styles of what its component renders. The rules sit in `surflane.*` cascade layers, so your unlayered CSS overrides them. Set `data-theme="light"` or `data-theme="dark"` on `<html>` before the first paint and add `pui-typography` to `<body>`. The [install guide](https://surflane.page/install) walks through Vite, Next.js, React Router and Astro, with or without Tailwind CSS.
|
|
21
|
+
To ship only what you use, import component stylesheets instead, such as `@surflane/ui/button.css` and `@surflane/ui/dialog.css`. Each brings the tokens and the styles of what its component renders. The rules sit in `surflane.*` cascade layers, so your unlayered CSS overrides them. Set `data-theme="light"` or `data-theme="dark"` on `<html>` before the first paint and add `pui-typography` to `<body>`. With AppShell, which fills the window, also give `<body>` no margin and the `--chrome` background. The [install guide](https://surflane.page/install) walks through Vite, Next.js, React Router and Astro, with or without Tailwind CSS.
|
|
22
22
|
|
|
23
23
|
```tsx
|
|
24
24
|
import {
|
package/dist/code.js
CHANGED
|
@@ -74,7 +74,9 @@ export const CodeBlock = /* @__PURE__ */ React.forwardRef(function CodeBlock({ c
|
|
|
74
74
|
}, copyFeedbackDuration);
|
|
75
75
|
}
|
|
76
76
|
const icon = state === 'copied' || state === 'failed' ? state : 'idle';
|
|
77
|
-
return (_jsxs("div", { ...props, ref: ref, "data-wrap": wrap || undefined, className: cx('pui-code-block', className), children: [_jsxs("div", { className: 'pui-code-toolbar', children: [_jsxs("span", { className: 'pui-code-label', children: [label, language && _jsx("span", { className: 'pui-code-language', children: language })] }), _jsxs(Button, { ref: button, type: 'button', variant: 'outline', size: 'sm', className: 'pui-code-copy', onClick: copy,
|
|
77
|
+
return (_jsxs("div", { ...props, ref: ref, "data-wrap": wrap || undefined, className: cx('pui-code-block', className), children: [_jsxs("div", { className: 'pui-code-toolbar', children: [_jsxs("span", { className: 'pui-code-label', children: [label, language && _jsx("span", { className: 'pui-code-language', children: language })] }), _jsxs(Button, { ref: button, type: 'button', variant: 'outline', size: 'sm', className: 'pui-code-copy', onClick: copy,
|
|
78
|
+
// A pending copy blocks a second start but keeps focus on the button.
|
|
79
|
+
disabled: state === 'busy', focusableWhenDisabled: true, "aria-busy": state === 'busy' || undefined, "data-copy-state": icon === 'idle' ? undefined : icon, "data-copy-used": used || undefined, children: [_jsx(Icon, { name: copyIcons[icon], className: 'pui-code-copy-icon' }, icon), _jsx("span", { "aria-live": 'polite', children: _jsx("span", { className: 'pui-code-copy-label', children: state === 'copied'
|
|
78
80
|
? copiedLabel
|
|
79
81
|
: state === 'failed'
|
|
80
82
|
? copyErrorLabel
|
|
@@ -3,10 +3,18 @@
|
|
|
3
3
|
surflane.sizes;
|
|
4
4
|
|
|
5
5
|
@layer surflane.components {
|
|
6
|
-
/* Application frame slots: navigation state and product routing remain caller-owned.
|
|
6
|
+
/* Application frame slots: navigation state and product routing remain caller-owned.
|
|
7
|
+
The shell is never rounded: its edge is the window's, or the edge of a container
|
|
8
|
+
that frames it and clips it. Only the page surface inside it is rounded. */
|
|
7
9
|
.pui-app-shell {
|
|
8
|
-
--app-shell-radius: var(--radius-sheet);
|
|
9
10
|
--app-shell-inset: var(--space-3);
|
|
11
|
+
/* --app-shell-radius, if a caller still sets it, keeps deriving the page radius. */
|
|
12
|
+
--app-shell-page-radius: max(
|
|
13
|
+
0px,
|
|
14
|
+
calc(
|
|
15
|
+
var(--app-shell-radius, var(--radius-sheet)) - var(--app-shell-inset)
|
|
16
|
+
)
|
|
17
|
+
);
|
|
10
18
|
--app-shell-sidebar-width: 216px;
|
|
11
19
|
/* The collapsed sidebar's width. Set it on the shell for a narrower rail. */
|
|
12
20
|
--app-shell-rail-width: 64px;
|
|
@@ -17,7 +25,6 @@
|
|
|
17
25
|
min-inline-size: 0;
|
|
18
26
|
padding: var(--app-shell-inset);
|
|
19
27
|
background: var(--chrome);
|
|
20
|
-
border-radius: var(--app-shell-radius);
|
|
21
28
|
}
|
|
22
29
|
.pui-app-shell[data-sidebar='collapsed'] {
|
|
23
30
|
--app-shell-sidebar-width: var(--app-shell-rail-width);
|
|
@@ -43,10 +50,7 @@
|
|
|
43
50
|
min-block-size: 0;
|
|
44
51
|
padding: var(--space-6);
|
|
45
52
|
background: var(--canvas);
|
|
46
|
-
border-radius:
|
|
47
|
-
0px,
|
|
48
|
-
calc(var(--app-shell-radius) - var(--app-shell-inset))
|
|
49
|
-
);
|
|
53
|
+
border-radius: var(--app-shell-page-radius);
|
|
50
54
|
overflow-wrap: anywhere;
|
|
51
55
|
}
|
|
52
56
|
.pui-app-shell-footer {
|
|
@@ -98,16 +102,24 @@
|
|
|
98
102
|
clip-path: inset(50%);
|
|
99
103
|
overflow: hidden;
|
|
100
104
|
}
|
|
105
|
+
/* On a phone the frame costs more room than it gives: the page meets the screen's
|
|
106
|
+
edges, and only the header keeps its inset. */
|
|
101
107
|
@media (max-width: 760px) {
|
|
102
108
|
.pui-app-shell {
|
|
103
109
|
grid-template-columns: minmax(0, 1fr);
|
|
104
110
|
grid-template-rows: auto minmax(0, 1fr);
|
|
111
|
+
gap: 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
}
|
|
114
|
+
.pui-app-shell > .pui-app-shell-header {
|
|
115
|
+
padding: var(--app-shell-inset);
|
|
105
116
|
}
|
|
106
117
|
.pui-app-shell-sidebar {
|
|
107
118
|
display: none;
|
|
108
119
|
}
|
|
109
120
|
.pui-app-shell-content {
|
|
110
121
|
padding: var(--space-4);
|
|
122
|
+
border-radius: 0;
|
|
111
123
|
}
|
|
112
124
|
}
|
|
113
125
|
@media (max-width: 760px), (pointer: coarse) {
|
|
@@ -163,19 +175,14 @@
|
|
|
163
175
|
margin-inline-start: 0;
|
|
164
176
|
padding: var(--space-6);
|
|
165
177
|
background: var(--canvas);
|
|
166
|
-
border-radius:
|
|
167
|
-
0px,
|
|
168
|
-
calc(var(--app-shell-radius) - var(--app-shell-inset))
|
|
169
|
-
);
|
|
178
|
+
border-radius: var(--app-shell-page-radius);
|
|
170
179
|
}
|
|
171
|
-
/*
|
|
180
|
+
/* On a phone the page meets the screen's edges, below the mobile header. */
|
|
172
181
|
@media (max-width: 760px) {
|
|
173
182
|
.pui-app-shell-page {
|
|
174
|
-
margin:
|
|
183
|
+
margin: 0;
|
|
175
184
|
padding: var(--space-4);
|
|
176
|
-
|
|
177
|
-
.pui-app-shell:has(> .pui-app-shell-header) .pui-app-shell-page {
|
|
178
|
-
margin-block-start: 0;
|
|
185
|
+
border-radius: 0;
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
.pui-app-shell[data-layout='workspace'] .pui-app-shell-footer {
|
package/dist/css/rules/chart.css
CHANGED
|
@@ -87,6 +87,14 @@
|
|
|
87
87
|
outline-offset: var(--focus-ring-offset);
|
|
88
88
|
border-radius: var(--radius-sm);
|
|
89
89
|
}
|
|
90
|
+
/* The browser's SVG styles ring any :focus, so a tap or click would draw its own
|
|
91
|
+
ring: around the surface, or around the layer a tapped line or point sits in,
|
|
92
|
+
since Recharts gives each layer tabindex -1. Only the surface is a keyboard
|
|
93
|
+
target, and it rings for keyboard focus only, like every other control. */
|
|
94
|
+
.pui-chart-plot .recharts-surface:focus:not(:focus-visible),
|
|
95
|
+
.pui-chart-plot .recharts-surface :focus {
|
|
96
|
+
outline: none;
|
|
97
|
+
}
|
|
90
98
|
.pui-chart-axis-label {
|
|
91
99
|
margin: 0;
|
|
92
100
|
color: var(--ink-muted);
|
|
@@ -81,9 +81,21 @@
|
|
|
81
81
|
.pui-switch[data-readonly] {
|
|
82
82
|
cursor: default;
|
|
83
83
|
}
|
|
84
|
+
/* An invalid control keeps its edge through every hover, checked or not; hover
|
|
85
|
+
still tints the fill. Each selector matches the hover rule it must outrank. */
|
|
84
86
|
:is(.pui-checkbox, .pui-radio, .pui-switch)[data-invalid],
|
|
87
|
+
:is(.pui-checkbox, .pui-radio, .pui-switch)[data-invalid]:hover,
|
|
88
|
+
:is(.pui-checkbox, .pui-radio, .pui-switch)[data-invalid]:is(
|
|
89
|
+
[data-checked],
|
|
90
|
+
[data-indeterminate]
|
|
91
|
+
):hover,
|
|
92
|
+
.pui-choice-label:hover
|
|
93
|
+
:is(.pui-checkbox, .pui-radio, .pui-switch)[data-invalid],
|
|
85
94
|
.pui-choice-label:hover
|
|
86
|
-
:is(.pui-checkbox, .pui-radio, .pui-switch)[data-invalid]
|
|
95
|
+
:is(.pui-checkbox, .pui-radio, .pui-switch)[data-invalid]:is(
|
|
96
|
+
[data-checked],
|
|
97
|
+
[data-indeterminate]
|
|
98
|
+
) {
|
|
87
99
|
border-color: var(--status-danger-fg);
|
|
88
100
|
}
|
|
89
101
|
@media (forced-colors: active) {
|
|
@@ -143,6 +143,10 @@
|
|
|
143
143
|
outline: 1px solid CanvasText;
|
|
144
144
|
outline-offset: -1px;
|
|
145
145
|
}
|
|
146
|
+
/* The group draws the field's focus, so its input draws none of its own. */
|
|
147
|
+
.pui-combobox-group .pui-combobox-input:focus {
|
|
148
|
+
outline: none;
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
.pui-combobox-group > .pui-combobox-input,
|
|
148
152
|
.pui-combobox-action {
|
|
@@ -40,7 +40,13 @@
|
|
|
40
40
|
border-color: var(--control-focus);
|
|
41
41
|
box-shadow: 0 0 0 var(--control-focus-ring-size) var(--control-focus-ring);
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
/* An invalid field keeps its edge while hovered; hover still tints the fill. */
|
|
44
|
+
:is(.pui-date-picker, .pui-time-field)[data-invalid] .pui-date-group,
|
|
45
|
+
:is(.pui-date-picker, .pui-time-field)[data-invalid]:not(
|
|
46
|
+
[data-disabled],
|
|
47
|
+
[data-readonly]
|
|
48
|
+
)
|
|
49
|
+
.pui-date-group:hover {
|
|
44
50
|
border-color: var(--status-danger-fg);
|
|
45
51
|
}
|
|
46
52
|
:is(.pui-date-picker, .pui-time-field)[data-disabled] {
|
|
@@ -53,10 +53,21 @@
|
|
|
53
53
|
cursor: not-allowed;
|
|
54
54
|
background: var(--control-disabled-background);
|
|
55
55
|
}
|
|
56
|
+
/* An invalid field keeps its edge while hovered; hover still tints the fill. */
|
|
56
57
|
.pui-input[aria-invalid='true'],
|
|
57
|
-
.pui-
|
|
58
|
+
.pui-input[aria-invalid='true']:hover,
|
|
59
|
+
.pui-select-trigger[aria-invalid='true'],
|
|
60
|
+
.pui-select-trigger[aria-invalid='true']:hover {
|
|
58
61
|
border-color: var(--status-danger-fg);
|
|
59
62
|
}
|
|
63
|
+
/* Forced colors drop the border color and the halo, so focus becomes an outline. */
|
|
64
|
+
@media (forced-colors: active) {
|
|
65
|
+
.pui-input:focus,
|
|
66
|
+
.pui-select-trigger:focus-visible {
|
|
67
|
+
outline: var(--focus-ring-width) solid Highlight;
|
|
68
|
+
outline-offset: var(--focus-ring-offset);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
@media (prefers-reduced-motion: reduce) {
|
|
61
72
|
.pui-input,
|
|
62
73
|
.pui-select-trigger {
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
clip-path: inset(50%);
|
|
18
18
|
white-space: nowrap;
|
|
19
19
|
}
|
|
20
|
+
/* One plane: the picker row, then a row per file. Rows meet at a line, and the
|
|
21
|
+
plane keeps one inset on every edge. */
|
|
20
22
|
.pui-file-dropzone {
|
|
23
|
+
--file-inset: var(--space-5);
|
|
21
24
|
display: flex;
|
|
22
|
-
flex-
|
|
23
|
-
|
|
24
|
-
gap: var(--space-3);
|
|
25
|
-
padding: var(--space-5);
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
padding-inline: var(--file-inset);
|
|
26
27
|
border: 1px solid transparent;
|
|
27
28
|
border-radius: var(--radius-lg);
|
|
28
29
|
background: var(--surface);
|
|
@@ -31,6 +32,13 @@
|
|
|
31
32
|
.pui-file-upload[data-dragging] .pui-file-dropzone {
|
|
32
33
|
background: var(--selected);
|
|
33
34
|
}
|
|
35
|
+
.pui-file-picker {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: var(--space-3);
|
|
40
|
+
padding-block: var(--file-inset);
|
|
41
|
+
}
|
|
34
42
|
.pui-file-trigger {
|
|
35
43
|
flex-direction: row;
|
|
36
44
|
width: fit-content;
|
|
@@ -74,18 +82,30 @@
|
|
|
74
82
|
}
|
|
75
83
|
.pui-file-list {
|
|
76
84
|
display: grid;
|
|
77
|
-
gap: var(--space-2);
|
|
78
85
|
list-style: none;
|
|
79
86
|
padding: 0;
|
|
80
87
|
margin: 0;
|
|
81
88
|
}
|
|
89
|
+
/* The file's details lead its row and its actions end it, wrapping below when the
|
|
90
|
+
row is narrow. Progress and an upload error run under the whole row. */
|
|
82
91
|
.pui-file-item {
|
|
92
|
+
display: flex;
|
|
93
|
+
flex-wrap: wrap;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: var(--space-3) var(--space-4);
|
|
83
96
|
min-width: 0;
|
|
84
|
-
padding: var(--space-
|
|
85
|
-
border-
|
|
86
|
-
|
|
97
|
+
padding-block: var(--space-3);
|
|
98
|
+
border-top: 1px solid var(--line);
|
|
99
|
+
}
|
|
100
|
+
.pui-file-item:last-child {
|
|
101
|
+
padding-block-end: var(--file-inset);
|
|
102
|
+
}
|
|
103
|
+
.pui-file-item > :not(.pui-file-info, .pui-file-actions) {
|
|
104
|
+
flex-basis: 100%;
|
|
87
105
|
}
|
|
88
106
|
.pui-file-info {
|
|
107
|
+
flex: 1 1 auto;
|
|
108
|
+
min-width: 0;
|
|
89
109
|
display: flex;
|
|
90
110
|
flex-wrap: wrap;
|
|
91
111
|
align-items: center;
|
|
@@ -107,13 +127,9 @@
|
|
|
107
127
|
display: flex;
|
|
108
128
|
flex-wrap: wrap;
|
|
109
129
|
gap: var(--space-2);
|
|
110
|
-
margin-top: var(--space-3);
|
|
111
|
-
}
|
|
112
|
-
.pui-file-item .pui-gauge {
|
|
113
|
-
margin-top: var(--space-3);
|
|
114
130
|
}
|
|
115
131
|
.pui-file-upload-error {
|
|
116
|
-
margin:
|
|
132
|
+
margin: 0;
|
|
117
133
|
color: var(--status-danger-fg);
|
|
118
134
|
font-size: var(--type-compact-size);
|
|
119
135
|
line-height: var(--type-compact-leading);
|
|
@@ -126,7 +142,7 @@
|
|
|
126
142
|
}
|
|
127
143
|
@media (max-width: 760px) {
|
|
128
144
|
.pui-file-dropzone {
|
|
129
|
-
|
|
145
|
+
--file-inset: var(--space-4);
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
@media (forced-colors: active) {
|
|
@@ -137,11 +153,6 @@
|
|
|
137
153
|
.pui-file-upload[data-dragging] .pui-file-dropzone {
|
|
138
154
|
border-color: Highlight;
|
|
139
155
|
}
|
|
140
|
-
.pui-file-item {
|
|
141
|
-
border: 1px solid CanvasText;
|
|
142
|
-
background: Canvas;
|
|
143
|
-
color: CanvasText;
|
|
144
|
-
}
|
|
145
156
|
}
|
|
146
157
|
@media (prefers-reduced-motion: reduce) {
|
|
147
158
|
.pui-file-dropzone {
|
|
@@ -151,6 +162,6 @@
|
|
|
151
162
|
}
|
|
152
163
|
@layer surflane.sizes {
|
|
153
164
|
:where([data-ui-size], [data-ui-size] *).pui-file-dropzone {
|
|
154
|
-
|
|
165
|
+
--file-inset: var(--ui-file-padding);
|
|
155
166
|
}
|
|
156
167
|
}
|
|
@@ -64,6 +64,13 @@
|
|
|
64
64
|
}
|
|
65
65
|
.pui-combobox-group[data-invalid],
|
|
66
66
|
.pui-number-group[data-invalid] {
|
|
67
|
-
border-color: var(--status-danger-
|
|
67
|
+
border-color: var(--status-danger-fg);
|
|
68
|
+
}
|
|
69
|
+
@media (forced-colors: active) {
|
|
70
|
+
.pui-combobox-group:focus-within,
|
|
71
|
+
.pui-number-group:focus-within {
|
|
72
|
+
outline: var(--focus-ring-width) solid Highlight;
|
|
73
|
+
outline-offset: var(--focus-ring-offset);
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
}
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
max-width: 100%;
|
|
13
13
|
overscroll-behavior: auto;
|
|
14
14
|
scrollbar-width: none !important;
|
|
15
|
+
outline: none;
|
|
16
|
+
}
|
|
17
|
+
/* A labeled scroller takes keyboard focus. Its root draws the shared ring, since
|
|
18
|
+
the viewport's edge fade would clip a ring drawn on the viewport itself. */
|
|
19
|
+
.pui-scroll-root:has(> .pui-scroll-viewport:focus-visible) {
|
|
20
|
+
outline: var(--focus-ring-width) solid var(--interaction-focus);
|
|
21
|
+
outline-offset: var(--focus-ring-offset);
|
|
15
22
|
}
|
|
16
23
|
.pui-scroll-viewport::-webkit-scrollbar {
|
|
17
24
|
display: none;
|
|
@@ -55,6 +55,12 @@
|
|
|
55
55
|
.pui-select-item[data-disabled] {
|
|
56
56
|
opacity: var(--control-disabled-opacity);
|
|
57
57
|
}
|
|
58
|
+
@media (forced-colors: active) {
|
|
59
|
+
.pui-select-item[data-highlighted] {
|
|
60
|
+
outline: 1px solid CanvasText;
|
|
61
|
+
outline-offset: -1px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
58
64
|
.pui-select-check {
|
|
59
65
|
position: absolute;
|
|
60
66
|
inset-inline-start: var(--space-3);
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
border-radius: var(--radius-full);
|
|
55
55
|
background: var(--canvas);
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/* The thumb's input is focused on pointer use too; only keyboard focus rings. */
|
|
58
|
+
.pui-slider-thumb:has(:focus-visible) {
|
|
58
59
|
outline: var(--focus-ring-width) solid var(--interaction-focus);
|
|
59
60
|
outline-offset: var(--focus-ring-offset);
|
|
60
61
|
}
|
package/dist/file-upload.js
CHANGED
|
@@ -296,51 +296,51 @@ export const FileUpload = /* @__PURE__ */ React.forwardRef(function FileUpload({
|
|
|
296
296
|
event.preventDefault();
|
|
297
297
|
select(Array.from(event.dataTransfer.files), 'drop');
|
|
298
298
|
}
|
|
299
|
-
}, children: [_jsx(Field.Control, { ref: input, id: inputId, name: name, form: form, type: 'file', accept: accept, capture: capture, multiple: multiple, required: required, disabled: disabled, className: 'pui-file-native', "aria-describedby": errorId, onKeyDown: (event) => {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
299
|
+
}, children: [_jsxs("div", { className: 'pui-file-picker', children: [_jsx(Field.Control, { ref: input, id: inputId, name: name, form: form, type: 'file', accept: accept, capture: capture, multiple: multiple, required: required, disabled: disabled, className: 'pui-file-native', "aria-describedby": errorId, onKeyDown: (event) => {
|
|
300
|
+
if (event.key === 'Enter')
|
|
301
|
+
event.preventBaseUIHandler();
|
|
302
|
+
}, onChange: (event) => {
|
|
303
|
+
if (!notifying.current) {
|
|
304
|
+
event.preventBaseUIHandler();
|
|
305
|
+
select(Array.from(event.currentTarget.files ?? []), 'select');
|
|
306
|
+
}
|
|
307
|
+
} }), _jsxs("label", { htmlFor: inputId, className: 'pui-button pui-file-trigger', "data-variant": 'outline', "data-disabled": disabled || undefined, children: [_jsx(Icon, { name: 'upload' }), labels.choose] }), _jsx("p", { className: 'pui-file-drop-hint', children: labels.drop })] }), files.length > 0 && (_jsx("ul", { className: 'pui-file-list', role: 'list', children: files.map((file) => {
|
|
308
|
+
const state = metadata.get(file) ?? {
|
|
309
|
+
status: 'ready',
|
|
310
|
+
progress: null,
|
|
311
|
+
};
|
|
312
|
+
if (!identities.current.has(file))
|
|
313
|
+
identities.current.set(file, `${uid}-${++nextId.current}`);
|
|
314
|
+
const itemId = identities.current.get(file);
|
|
315
|
+
return (_jsxs("li", { className: 'pui-file-item', children: [_jsxs("div", { className: 'pui-file-info', children: [_jsx("strong", { children: file.name }), _jsx("span", { children: sizeLabel(file.size) }), _jsx(StatusBadge, { tone: state.status === 'success'
|
|
316
|
+
? 'success'
|
|
317
|
+
: state.status === 'error'
|
|
318
|
+
? 'danger'
|
|
319
|
+
: state.status === 'uploading'
|
|
320
|
+
? 'progress'
|
|
321
|
+
: 'neutral', children: labels.status[state.status] })] }), _jsxs("div", { className: 'pui-file-actions', children: [transport && state.status !== 'success' && (_jsx(Button, { ref: (node) => {
|
|
322
|
+
if (node)
|
|
323
|
+
primaryButtons.current.set(file, node);
|
|
324
|
+
else
|
|
325
|
+
primaryButtons.current.delete(file);
|
|
326
|
+
}, type: 'button', size: 'sm', variant: 'outline', disabled: disabled, "aria-describedby": state.error ? `${itemId}-error` : undefined, "aria-label": `${state.status === 'uploading' ? labels.cancel : state.status === 'ready' ? labels.upload : labels.retry} ${file.name}`, onClick: () => {
|
|
327
|
+
if (state.status === 'uploading')
|
|
328
|
+
cancel(file);
|
|
329
|
+
else
|
|
330
|
+
void upload(file);
|
|
331
|
+
}, children: state.status === 'uploading'
|
|
332
|
+
? labels.cancel
|
|
333
|
+
: state.status === 'ready'
|
|
334
|
+
? labels.upload
|
|
335
|
+
: labels.retry })), _jsx(Button, { type: 'button', size: 'sm', variant: 'ghost', ref: (node) => {
|
|
336
|
+
if (node)
|
|
337
|
+
removeButtons.current.set(file, node);
|
|
338
|
+
else
|
|
339
|
+
removeButtons.current.delete(file);
|
|
340
|
+
}, disabled: disabled, "aria-label": `${labels.remove} ${file.name}`, onClick: () => {
|
|
341
|
+
setRejections([]);
|
|
342
|
+
if (commit(files.filter((current) => current !== file), 'remove'))
|
|
343
|
+
focusPicker.current = file;
|
|
344
|
+
}, children: labels.remove })] }), state.status === 'uploading' && (_jsx(Progress.Root, { value: state.progress, "aria-label": labels.progress(file), children: _jsx(Progress.Track, { children: _jsx(Progress.Indicator, {}) }) })), state.error && (_jsx("p", { id: `${itemId}-error`, className: 'pui-file-upload-error', children: state.error }))] }, itemId));
|
|
345
|
+
}) }))] }), _jsx("div", { id: errorId, role: 'alert', className: 'pui-file-errors', children: rejections.length > 0 && (_jsx("ul", { children: rejections.map((rejection, index) => (_jsx("li", { children: labels.rejection(rejection) }, index))) })) }), _jsx(Field.Error, {}), _jsx("span", { className: 'pui-feedback-sr', role: 'status', children: announcement })] }));
|
|
346
346
|
});
|
package/dist/pagination.js
CHANGED
|
@@ -32,8 +32,10 @@ export const Pagination = /* @__PURE__ */ React.forwardRef(function Pagination({
|
|
|
32
32
|
'aria-label': name,
|
|
33
33
|
'aria-current': currentPage ? 'page' : undefined,
|
|
34
34
|
};
|
|
35
|
+
// An inactive page is a link with no destination: it keeps its name and reads as
|
|
36
|
+
// unavailable, as a disabled Button does. A plain span could carry neither.
|
|
35
37
|
if (getPageHref)
|
|
36
|
-
return inactive ? (_jsx("
|
|
38
|
+
return inactive ? (_jsx("a", { ...attributes, role: 'link', "aria-disabled": 'true', children: children })) : (_jsx("a", { ...attributes, href: getPageHref(target), children: children }));
|
|
37
39
|
return (_jsx(Button, { ...attributes, variant: 'ghost', type: 'button', disabled: inactive, onClick: () => {
|
|
38
40
|
if (target !== current)
|
|
39
41
|
onPageChange?.(target);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surflane/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Surflane React components built on Base UI, with data, chart, code and Markdown components.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design-system",
|
|
@@ -407,7 +407,7 @@
|
|
|
407
407
|
"peerDependencies": {
|
|
408
408
|
"@headless-tree/core": "^1.8.0",
|
|
409
409
|
"@headless-tree/react": "^1.8.0",
|
|
410
|
-
"@surflane/design-tokens": "^0.
|
|
410
|
+
"@surflane/design-tokens": "^0.8.0",
|
|
411
411
|
"@tanstack/react-virtual": "^3.14.21",
|
|
412
412
|
"react": "^19.0.0",
|
|
413
413
|
"react-aria-components": "^1.22.1",
|