@opencode/ui 2.0.30 → 2.0.32
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/context/dialog.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function useDialogLayer(): {
|
|
|
20
20
|
} | undefined;
|
|
21
21
|
export declare function DialogProvider(props: ParentProps): JSX.Element;
|
|
22
22
|
export declare function useDialog(): {
|
|
23
|
+
/** The top dialog that is open. One that has started closing no longer counts, though it waits to be disposed. */
|
|
23
24
|
readonly active: Active | undefined;
|
|
24
25
|
/**
|
|
25
26
|
* id lets the caller close this dialog later rather than whichever is on top. Opening is deferred; once
|
package/package.json
CHANGED
|
@@ -7,17 +7,23 @@
|
|
|
7
7
|
|
|
8
8
|
[data-slot="animated-number-value"] {
|
|
9
9
|
display: inline-flex;
|
|
10
|
-
|
|
10
|
+
/* Digits keep numeric order in RTL documents. */
|
|
11
|
+
direction: ltr;
|
|
11
12
|
align-items: baseline;
|
|
12
13
|
justify-content: flex-end;
|
|
13
14
|
line-height: inherit;
|
|
14
15
|
width: var(--animated-number-width, 1ch);
|
|
15
16
|
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-slot="animated-number-value"][data-resizing="true"] {
|
|
16
20
|
transition: width var(--tool-motion-spring-ms, 560ms) var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
[data-slot="animated-number-digit"] {
|
|
20
24
|
display: inline-grid;
|
|
25
|
+
/* Digits outside the width are clipped at the start edge, not squeezed. */
|
|
26
|
+
flex-shrink: 0;
|
|
21
27
|
width: 1ch;
|
|
22
28
|
height: 1em;
|
|
23
29
|
line-height: 1em;
|
|
@@ -56,6 +62,11 @@
|
|
|
56
62
|
line-height: 1em;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
[data-slot="animated-number-static"]::before,
|
|
66
|
+
[data-slot="animated-number-cell"]::before {
|
|
67
|
+
content: attr(data-digit);
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
[data-slot="animated-number-digit"][data-animating="true"] [data-slot="animated-number-static"] {
|
|
60
71
|
visibility: hidden;
|
|
61
72
|
}
|
|
@@ -63,8 +74,10 @@
|
|
|
63
74
|
[data-slot="animated-number-strip"] {
|
|
64
75
|
display: inline-flex;
|
|
65
76
|
flex-direction: column;
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
/* Not transform: a composited roll lands on whole pixels, then the digit shifts as it settles. */
|
|
78
|
+
position: relative;
|
|
79
|
+
top: calc(var(--animated-number-offset, 10) * -1em);
|
|
80
|
+
transition-property: top;
|
|
68
81
|
transition-duration: var(--animated-number-duration, 560ms);
|
|
69
82
|
transition-timing-function: var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
70
83
|
}
|
|
@@ -76,7 +89,7 @@
|
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
@media (prefers-reduced-motion: reduce) {
|
|
79
|
-
[data-component="animated-number"] [data-slot="animated-number-value"] {
|
|
92
|
+
[data-component="animated-number"] [data-slot="animated-number-value"][data-resizing] {
|
|
80
93
|
transition-duration: 0ms;
|
|
81
94
|
}
|
|
82
95
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { For,
|
|
1
|
+
import { For, createEffect, createMemo, on } from "solid-js"
|
|
2
2
|
import { createStore } from "solid-js/store"
|
|
3
3
|
|
|
4
4
|
const TRACK = Array.from({ length: 30 }, (_, index) => index % 10)
|
|
@@ -26,6 +26,7 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
|
|
|
26
26
|
const step = () => state.step
|
|
27
27
|
const animating = () => state.animating
|
|
28
28
|
let last = props.value
|
|
29
|
+
let strip: HTMLSpanElement | undefined
|
|
29
30
|
|
|
30
31
|
createEffect(
|
|
31
32
|
on(
|
|
@@ -34,15 +35,19 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
|
|
|
34
35
|
const delta = spin(last, next, props.direction)
|
|
35
36
|
last = next
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// An unchanged digit keeps any roll it is in the middle of.
|
|
39
|
+
if (!delta) return
|
|
40
|
+
|
|
41
|
+
const target = step() + delta
|
|
42
|
+
|
|
43
|
+
// A detached digit never finishes a roll, and rapid changes can run off the track, so both settle at once.
|
|
44
|
+
if (!strip?.isConnected || target < 0 || target >= TRACK.length) {
|
|
45
|
+
setState({ animating: false, step: next + 10 })
|
|
40
46
|
|
|
41
47
|
return
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
setState(
|
|
45
|
-
setState("step", (value) => value + delta)
|
|
50
|
+
setState({ animating: true, step: target })
|
|
46
51
|
},
|
|
47
52
|
{ defer: true },
|
|
48
53
|
),
|
|
@@ -50,8 +55,9 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
|
|
|
50
55
|
|
|
51
56
|
return (
|
|
52
57
|
<span data-slot="animated-number-digit" data-animating={animating() ? "true" : "false"}>
|
|
53
|
-
<span data-slot="animated-number-static"
|
|
58
|
+
<span data-slot="animated-number-static" data-digit={props.value} />
|
|
54
59
|
<span
|
|
60
|
+
ref={strip}
|
|
55
61
|
data-slot="animated-number-strip"
|
|
56
62
|
onTransitionEnd={() => {
|
|
57
63
|
setState("animating", false)
|
|
@@ -62,7 +68,7 @@ function Digit(props: { value: number; direction: 1 | -1 }) {
|
|
|
62
68
|
"--animated-number-duration": `var(--tool-motion-odometer-ms, ${DURATION}ms)`,
|
|
63
69
|
}}
|
|
64
70
|
>
|
|
65
|
-
<For each={TRACK}>{(value) => <span data-slot="animated-number-cell"
|
|
71
|
+
<For each={TRACK}>{(value) => <span data-slot="animated-number-cell" data-digit={value} />}</For>
|
|
66
72
|
</span>
|
|
67
73
|
</span>
|
|
68
74
|
)
|
|
@@ -75,13 +81,18 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
|
|
|
75
81
|
return Math.max(0, Math.round(props.value))
|
|
76
82
|
})
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
// A spare leading zero waits outside the clipped width to roll in. A digit that leaves stays until the width has
|
|
85
|
+
// shrunk past it, so it rolls to zero instead of vanishing.
|
|
86
|
+
const [state, setState] = createStore<{ value: number; direction: 1 | -1; places: number; resizing: boolean }>({
|
|
79
87
|
value: target(),
|
|
80
|
-
direction: 1
|
|
88
|
+
direction: 1,
|
|
89
|
+
places: String(target()).length + 1,
|
|
90
|
+
resizing: false,
|
|
81
91
|
})
|
|
82
92
|
|
|
83
93
|
const value = () => state.value
|
|
84
94
|
const direction = () => state.direction
|
|
95
|
+
let box: HTMLSpanElement | undefined
|
|
85
96
|
|
|
86
97
|
createEffect(
|
|
87
98
|
on(
|
|
@@ -91,8 +102,22 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
|
|
|
91
102
|
|
|
92
103
|
if (next === current) return
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
const digits = String(next).length
|
|
106
|
+
|
|
107
|
+
// A detached number cannot ease its width and would never hear it end, so it settles at once.
|
|
108
|
+
if (!box?.isConnected) {
|
|
109
|
+
setState({ direction: next > current ? 1 : -1, value: next, places: digits + 1, resizing: false })
|
|
110
|
+
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Only a change to the digit count eases the width, so a late font swap or zoom never animates it.
|
|
115
|
+
setState({
|
|
116
|
+
direction: next > current ? 1 : -1,
|
|
117
|
+
value: next,
|
|
118
|
+
places: Math.max(state.places, digits + 1),
|
|
119
|
+
resizing: state.resizing || digits !== String(current).length,
|
|
120
|
+
})
|
|
96
121
|
},
|
|
97
122
|
{ defer: true },
|
|
98
123
|
),
|
|
@@ -100,22 +125,26 @@ export function AnimatedNumber(props: { value: number; class?: string }) {
|
|
|
100
125
|
|
|
101
126
|
const label = createMemo(() => value().toString())
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const code = char.charCodeAt(0) - 48
|
|
106
|
-
|
|
107
|
-
if (code < 0 || code > 9) return 0
|
|
108
|
-
|
|
109
|
-
return code
|
|
110
|
-
}).reverse(),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
const width = createMemo(() => `${digits().length}ch`)
|
|
128
|
+
// Place values, most significant first. Keying by place keeps each digit rolling as digits appear and leave.
|
|
129
|
+
const places = createMemo(() => Array.from({ length: state.places }, (_, index) => state.places - 1 - index))
|
|
114
130
|
|
|
115
131
|
return (
|
|
116
|
-
<span data-component="animated-number" class={props.class}
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
<span data-component="animated-number" class={props.class}>
|
|
133
|
+
{/* The value is the only text: screen readers and copy read it once, in order. CSS draws the digits. */}
|
|
134
|
+
<span class="sr-only">{label()}</span>
|
|
135
|
+
<span
|
|
136
|
+
ref={box}
|
|
137
|
+
data-slot="animated-number-value"
|
|
138
|
+
data-resizing={state.resizing ? "true" : "false"}
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
style={{ "--animated-number-width": `${label().length}ch` }}
|
|
141
|
+
onTransitionEnd={(event) => {
|
|
142
|
+
if (event.target === event.currentTarget) setState({ places: label().length + 1, resizing: false })
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
<For each={places()}>
|
|
146
|
+
{(place) => <Digit value={Math.floor(value() / 10 ** place) % 10} direction={direction()} />}
|
|
147
|
+
</For>
|
|
119
148
|
</span>
|
|
120
149
|
</span>
|
|
121
150
|
)
|
package/src/context/dialog.tsx
CHANGED
|
@@ -47,6 +47,9 @@ function init() {
|
|
|
47
47
|
const closing = new Map<string, ReturnType<typeof setTimeout> | undefined>()
|
|
48
48
|
// The same ids, reactive, so the top dialog that stays open owns the focus trap during an exit animation.
|
|
49
49
|
const [exiting, setExiting] = createSignal<ReadonlySet<string>>(new Set())
|
|
50
|
+
// The top dialog that stays open. A dialog without an exit animation leaves the DOM as soon as it starts closing, so
|
|
51
|
+
// it stops being the active dialog then, not when it is disposed.
|
|
52
|
+
const top = () => stack().findLast((item) => !exiting().has(item.id))
|
|
50
53
|
const lock = { value: false }
|
|
51
54
|
const state = { disposed: false }
|
|
52
55
|
|
|
@@ -161,7 +164,7 @@ function init() {
|
|
|
161
164
|
|
|
162
165
|
return (
|
|
163
166
|
<Kobalte
|
|
164
|
-
modal={
|
|
167
|
+
modal={top()?.id === id}
|
|
165
168
|
open={!closing()}
|
|
166
169
|
onOpenChange={(open: boolean) => {
|
|
167
170
|
if (!open) dismiss(id)
|
|
@@ -231,6 +234,7 @@ function init() {
|
|
|
231
234
|
|
|
232
235
|
return {
|
|
233
236
|
stack,
|
|
237
|
+
top,
|
|
234
238
|
close,
|
|
235
239
|
show,
|
|
236
240
|
push,
|
|
@@ -302,8 +306,9 @@ export function useDialog() {
|
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
return {
|
|
309
|
+
/** The top dialog that is open. One that has started closing no longer counts, though it waits to be disposed. */
|
|
305
310
|
get active() {
|
|
306
|
-
return ctx.
|
|
311
|
+
return ctx.top()
|
|
307
312
|
},
|
|
308
313
|
/**
|
|
309
314
|
* id lets the caller close this dialog later rather than whichever is on top. Opening is deferred; once
|
package/src/styles/file-tree.css
CHANGED
|
@@ -134,59 +134,93 @@
|
|
|
134
134
|
inset: 0;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
/* Every row ends in the change letter or a blank end that sticks inside the pane while long rows
|
|
138
|
+
scroll. It fades the label out over 16px like tab titles and redraws the row's rounded end, so a
|
|
139
|
+
row that runs past the pane edge looks like a row that fits. */
|
|
140
|
+
[data-component="file-tree-v2"] :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"]) {
|
|
141
|
+
--file-tree-v2-end-inset: 8px;
|
|
142
|
+
|
|
139
143
|
flex: none;
|
|
144
|
+
align-self: stretch;
|
|
140
145
|
position: sticky;
|
|
141
|
-
inset-inline-end:
|
|
146
|
+
inset-inline-end: var(--file-tree-v2-end-inset);
|
|
142
147
|
z-index: 1;
|
|
143
|
-
display: flex;
|
|
144
|
-
flex-direction: row;
|
|
145
|
-
align-items: center;
|
|
146
|
-
justify-content: center;
|
|
147
|
-
width: 16px;
|
|
148
|
-
height: 16px;
|
|
149
|
-
font-size: 11px;
|
|
150
|
-
font-weight: 530;
|
|
151
|
-
line-height: 1;
|
|
152
|
-
letter-spacing: -0.04px;
|
|
153
|
-
text-align: center;
|
|
154
|
-
text-transform: uppercase;
|
|
155
|
-
font-variant-numeric: tabular-nums lining-nums;
|
|
156
|
-
font-feature-settings:
|
|
157
|
-
"tnum" on,
|
|
158
|
-
"lnum" on;
|
|
159
|
-
background:
|
|
160
|
-
linear-gradient(var(--file-tree-v2-row-overlay), var(--file-tree-v2-row-overlay)), var(--v2-background-bg-base);
|
|
161
148
|
}
|
|
162
149
|
|
|
163
|
-
[data-component="file-tree-v2"] [data-slot="file-tree-v2-
|
|
150
|
+
[data-component="file-tree-v2"] [data-slot="file-tree-v2-end"] {
|
|
151
|
+
--file-tree-v2-end-inset: 10px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
[data-component="file-tree-v2"] :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::before,
|
|
155
|
+
[data-component="file-tree-v2"] :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::after {
|
|
164
156
|
content: "";
|
|
165
157
|
position: absolute;
|
|
166
158
|
inset-block: 0;
|
|
159
|
+
pointer-events: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[data-component="file-tree-v2"] :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::before {
|
|
167
163
|
inset-inline-end: 100%;
|
|
168
164
|
width: 16px;
|
|
169
|
-
pointer-events: none;
|
|
170
165
|
background:
|
|
171
166
|
linear-gradient(to right, transparent, var(--file-tree-v2-row-overlay)),
|
|
172
167
|
linear-gradient(to right, transparent, var(--v2-background-bg-base));
|
|
173
168
|
}
|
|
174
169
|
|
|
175
|
-
[data-component="file-tree-v2"]:dir(rtl)
|
|
170
|
+
[data-component="file-tree-v2"]:dir(rtl)
|
|
171
|
+
:is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::before {
|
|
176
172
|
background:
|
|
177
173
|
linear-gradient(to left, transparent, var(--file-tree-v2-row-overlay)),
|
|
178
174
|
linear-gradient(to left, transparent, var(--v2-background-bg-base));
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
pointer-events: none;
|
|
177
|
+
/* The row through its rounded end. The shadow paints the pane past it, outside its corners too. */
|
|
178
|
+
[data-component="file-tree-v2"] :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::after {
|
|
179
|
+
inset-inline: 0 calc(-1 * var(--file-tree-v2-end-inset));
|
|
180
|
+
z-index: -1;
|
|
181
|
+
border-start-end-radius: 6px;
|
|
182
|
+
border-end-end-radius: 6px;
|
|
188
183
|
background:
|
|
189
184
|
linear-gradient(var(--file-tree-v2-row-overlay), var(--file-tree-v2-row-overlay)), var(--v2-background-bg-base);
|
|
185
|
+
box-shadow: 8px 0 0 6px var(--v2-background-bg-base);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
[data-component="file-tree-v2"]:dir(rtl) :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::after {
|
|
189
|
+
box-shadow: -8px 0 0 6px var(--v2-background-bg-base);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@keyframes file-tree-v2-end-fade {
|
|
193
|
+
to {
|
|
194
|
+
opacity: 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Scrolled to the end, every label is whole, so the fade clears. */
|
|
199
|
+
@supports (animation-timeline: scroll()) {
|
|
200
|
+
[data-component="file-tree-v2"] :is([data-slot="file-tree-v2-change"], [data-slot="file-tree-v2-end"])::before {
|
|
201
|
+
animation: file-tree-v2-end-fade linear both;
|
|
202
|
+
animation-timeline: scroll(nearest inline);
|
|
203
|
+
animation-range: calc(100% - 16px) 100%;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"] {
|
|
208
|
+
box-sizing: border-box;
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: row;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
width: 16px;
|
|
214
|
+
font-size: 11px;
|
|
215
|
+
font-weight: 530;
|
|
216
|
+
line-height: 1;
|
|
217
|
+
letter-spacing: -0.04px;
|
|
218
|
+
text-align: center;
|
|
219
|
+
text-transform: uppercase;
|
|
220
|
+
font-variant-numeric: tabular-nums lining-nums;
|
|
221
|
+
font-feature-settings:
|
|
222
|
+
"tnum" on,
|
|
223
|
+
"lnum" on;
|
|
190
224
|
}
|
|
191
225
|
|
|
192
226
|
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="modified"] {
|