wings-plugin-sdk 0.1.0 → 0.2.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 +174 -22
- package/bin.mjs +10 -4
- package/build.d.ts +12 -3
- package/build.mjs +136 -9
- package/package.json +27 -5
- package/shell.d.ts +14 -0
- package/shell.js +38 -0
- package/ui-css.d.ts +4 -0
- package/ui-css.js +547 -0
- package/ui.d.ts +144 -0
- package/ui.js +290 -0
- package/wings.d.ts +9 -3
package/ui-css.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// The wings-plugin-sdk/ui styles, for Wings versions whose plugin-ui.css doesn't have them yet. Wings' own copy
|
|
2
|
+
// in plugin-ui.css wins whenever it's there, so a Wings update restyles every plugin. Keep both the same.
|
|
3
|
+
|
|
4
|
+
/** The version of the classes this package renders. Wings' plugin-ui.css sets it as `--wings-ui`. */
|
|
5
|
+
export const UI_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export const fallbackCss = `/* wings-ui 1 start
|
|
8
|
+
The classes wings-plugin-sdk/ui renders, a public contract: the names and data attributes stay, and Wings may
|
|
9
|
+
restyle them. wings-plugin-sdk carries a copy of this block for older Wings without --wings-ui, so keep the
|
|
10
|
+
two the same (apps/desktop/src/lib/plugin-ui.test.ts checks). Bump --wings-ui when a class is added. */
|
|
11
|
+
|
|
12
|
+
/* The page's place: a sidebar, popover, view (modal, window or tab) or settings page. */
|
|
13
|
+
.w-shell {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 10px;
|
|
17
|
+
padding: 14px;
|
|
18
|
+
}
|
|
19
|
+
.w-shell[data-place="popover"] {
|
|
20
|
+
gap: 12px;
|
|
21
|
+
padding: 12px 14px 14px;
|
|
22
|
+
}
|
|
23
|
+
.w-shell[data-place="settings"] {
|
|
24
|
+
gap: 12px;
|
|
25
|
+
}
|
|
26
|
+
/* A page that lays itself out edge to edge, like a canvas, fills the frame and scrolls on its own. */
|
|
27
|
+
.w-shell[data-fill] {
|
|
28
|
+
height: 100vh;
|
|
29
|
+
gap: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Layout. */
|
|
35
|
+
.w-row {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
.w-stack {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 8px;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
:is(.w-row, .w-stack)[data-gap="none"] {
|
|
48
|
+
gap: 0;
|
|
49
|
+
}
|
|
50
|
+
:is(.w-row, .w-stack)[data-gap="xs"] {
|
|
51
|
+
gap: 2px;
|
|
52
|
+
}
|
|
53
|
+
:is(.w-row, .w-stack)[data-gap="s"] {
|
|
54
|
+
gap: 4px;
|
|
55
|
+
}
|
|
56
|
+
:is(.w-row, .w-stack)[data-gap="l"] {
|
|
57
|
+
gap: 12px;
|
|
58
|
+
}
|
|
59
|
+
.w-row[data-align="start"] {
|
|
60
|
+
align-items: flex-start;
|
|
61
|
+
}
|
|
62
|
+
.w-row[data-align="baseline"] {
|
|
63
|
+
align-items: baseline;
|
|
64
|
+
}
|
|
65
|
+
.w-row[data-justify="end"] {
|
|
66
|
+
justify-content: flex-end;
|
|
67
|
+
}
|
|
68
|
+
.w-row[data-justify="between"] {
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
}
|
|
71
|
+
.w-row[data-wrap] {
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
}
|
|
74
|
+
:is(.w-row, .w-stack, .w-text)[data-grow] {
|
|
75
|
+
flex: 1;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Text. */
|
|
80
|
+
.w-text[data-tone="muted"] {
|
|
81
|
+
color: var(--wings-muted);
|
|
82
|
+
}
|
|
83
|
+
.w-text[data-tone="info"] {
|
|
84
|
+
color: var(--wings-info);
|
|
85
|
+
}
|
|
86
|
+
.w-text[data-tone="success"] {
|
|
87
|
+
color: var(--wings-success);
|
|
88
|
+
}
|
|
89
|
+
.w-text[data-tone="warn"] {
|
|
90
|
+
color: var(--wings-warning);
|
|
91
|
+
}
|
|
92
|
+
.w-text[data-tone="danger"] {
|
|
93
|
+
color: var(--wings-danger);
|
|
94
|
+
}
|
|
95
|
+
.w-text[data-size="small"] {
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
}
|
|
98
|
+
.w-text[data-size="tiny"] {
|
|
99
|
+
font-size: 11px;
|
|
100
|
+
}
|
|
101
|
+
.w-text[data-weight="medium"] {
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
}
|
|
104
|
+
.w-text[data-weight="strong"] {
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
}
|
|
107
|
+
.w-text[data-mono] {
|
|
108
|
+
font-family: var(--wings-mono);
|
|
109
|
+
}
|
|
110
|
+
.w-text[data-numeric] {
|
|
111
|
+
font-variant-numeric: tabular-nums;
|
|
112
|
+
}
|
|
113
|
+
.w-text[data-truncate] {
|
|
114
|
+
display: block;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
text-overflow: ellipsis;
|
|
117
|
+
white-space: nowrap;
|
|
118
|
+
}
|
|
119
|
+
.w-text[data-clamp] {
|
|
120
|
+
display: -webkit-box;
|
|
121
|
+
-webkit-line-clamp: 3;
|
|
122
|
+
-webkit-box-orient: vertical;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
:is(.w-row, .w-list-item, .w-list-button) > .w-text[data-truncate] {
|
|
126
|
+
min-width: 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* A section heading, with its controls on the right. */
|
|
130
|
+
.w-heading {
|
|
131
|
+
margin-top: 6px;
|
|
132
|
+
font-size: 12px;
|
|
133
|
+
font-weight: 500;
|
|
134
|
+
color: var(--wings-muted);
|
|
135
|
+
}
|
|
136
|
+
.w-heading-row {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 8px;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
.w-heading-row > .w-heading {
|
|
143
|
+
flex: 1;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* A card, and the quieter one for a summary line. */
|
|
148
|
+
.w-card {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 10px;
|
|
152
|
+
min-width: 0;
|
|
153
|
+
padding: 10px 12px;
|
|
154
|
+
border: 1px solid var(--wings-hairline);
|
|
155
|
+
border-radius: var(--wings-radius);
|
|
156
|
+
background: var(--wings-surface);
|
|
157
|
+
}
|
|
158
|
+
.w-card[data-size="compact"] {
|
|
159
|
+
gap: 6px;
|
|
160
|
+
padding: 8px 10px;
|
|
161
|
+
font-size: 12px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* A labelled bar, and the bar on its own. --w-value is how full it is. */
|
|
165
|
+
.w-gauge {
|
|
166
|
+
display: grid;
|
|
167
|
+
grid-template-columns: 1fr auto;
|
|
168
|
+
gap: 4px 8px;
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
}
|
|
171
|
+
.w-gauge-value {
|
|
172
|
+
font-variant-numeric: tabular-nums;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
}
|
|
175
|
+
.w-gauge-note {
|
|
176
|
+
grid-column: 1 / -1;
|
|
177
|
+
color: var(--wings-muted);
|
|
178
|
+
}
|
|
179
|
+
.w-meter {
|
|
180
|
+
grid-column: 1 / -1;
|
|
181
|
+
height: 4px;
|
|
182
|
+
border-radius: 2px;
|
|
183
|
+
background: var(--wings-hairline-strong);
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
}
|
|
186
|
+
.w-meter-fill {
|
|
187
|
+
display: block;
|
|
188
|
+
width: var(--w-value, 0%);
|
|
189
|
+
height: 100%;
|
|
190
|
+
border-radius: 2px;
|
|
191
|
+
background: var(--wings-info);
|
|
192
|
+
}
|
|
193
|
+
.w-meter[data-tone="warn"] > .w-meter-fill {
|
|
194
|
+
background: var(--wings-warning);
|
|
195
|
+
}
|
|
196
|
+
.w-meter[data-tone="danger"] > .w-meter-fill {
|
|
197
|
+
background: var(--wings-danger);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* A row of small figures, label over value. */
|
|
201
|
+
.w-stats {
|
|
202
|
+
display: grid;
|
|
203
|
+
grid-auto-flow: column;
|
|
204
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
205
|
+
gap: 8px;
|
|
206
|
+
margin: 0;
|
|
207
|
+
padding: 10px 12px;
|
|
208
|
+
border: 1px solid var(--wings-hairline);
|
|
209
|
+
border-radius: var(--wings-radius);
|
|
210
|
+
background: var(--wings-surface);
|
|
211
|
+
}
|
|
212
|
+
.w-stat-label {
|
|
213
|
+
font-size: 11px;
|
|
214
|
+
color: var(--wings-muted);
|
|
215
|
+
}
|
|
216
|
+
.w-stat-value {
|
|
217
|
+
margin: 2px 0 0;
|
|
218
|
+
font-weight: 500;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
text-overflow: ellipsis;
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
}
|
|
223
|
+
.w-stat-value[data-tone="warn"] {
|
|
224
|
+
color: var(--wings-warning);
|
|
225
|
+
}
|
|
226
|
+
.w-stat-value[data-tone="danger"] {
|
|
227
|
+
color: var(--wings-danger);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Label and value pairs, like a work item's fields. */
|
|
231
|
+
.w-props {
|
|
232
|
+
display: flex;
|
|
233
|
+
flex-direction: column;
|
|
234
|
+
gap: 6px;
|
|
235
|
+
margin: 0;
|
|
236
|
+
}
|
|
237
|
+
.w-prop {
|
|
238
|
+
display: grid;
|
|
239
|
+
grid-template-columns: 92px minmax(0, 1fr);
|
|
240
|
+
gap: 8px;
|
|
241
|
+
align-items: start;
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
line-height: 22px;
|
|
244
|
+
}
|
|
245
|
+
.w-prop-label {
|
|
246
|
+
color: var(--wings-muted);
|
|
247
|
+
}
|
|
248
|
+
.w-prop-value {
|
|
249
|
+
min-width: 0;
|
|
250
|
+
margin: 0;
|
|
251
|
+
overflow-wrap: anywhere;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Lists: rows that light up under the pointer, or rows that are a button. */
|
|
255
|
+
.w-list {
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: column;
|
|
258
|
+
margin: 0;
|
|
259
|
+
padding: 0;
|
|
260
|
+
list-style: none;
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
}
|
|
263
|
+
.w-list-item {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 8px;
|
|
267
|
+
min-width: 0;
|
|
268
|
+
padding: 4px 6px;
|
|
269
|
+
border-radius: 6px;
|
|
270
|
+
}
|
|
271
|
+
.w-list[data-density="compact"] > .w-list-item {
|
|
272
|
+
padding: 0 6px;
|
|
273
|
+
}
|
|
274
|
+
.w-list-item[data-align="start"] {
|
|
275
|
+
align-items: flex-start;
|
|
276
|
+
}
|
|
277
|
+
.w-list-item:hover {
|
|
278
|
+
background: var(--wings-hover);
|
|
279
|
+
}
|
|
280
|
+
.w-list-item:has(> .w-list-button) {
|
|
281
|
+
display: block;
|
|
282
|
+
padding: 0;
|
|
283
|
+
}
|
|
284
|
+
.w-list-item:has(> .w-list-button):hover {
|
|
285
|
+
background: none;
|
|
286
|
+
}
|
|
287
|
+
.w-list-button {
|
|
288
|
+
display: flex;
|
|
289
|
+
align-items: center;
|
|
290
|
+
justify-content: flex-start;
|
|
291
|
+
gap: 8px;
|
|
292
|
+
width: 100%;
|
|
293
|
+
min-width: 0;
|
|
294
|
+
height: auto;
|
|
295
|
+
padding: 5px 6px;
|
|
296
|
+
border: 0;
|
|
297
|
+
border-radius: 6px;
|
|
298
|
+
background: none;
|
|
299
|
+
font-size: inherit;
|
|
300
|
+
font-weight: inherit;
|
|
301
|
+
text-align: left;
|
|
302
|
+
}
|
|
303
|
+
.w-list-button[data-align="baseline"] {
|
|
304
|
+
align-items: baseline;
|
|
305
|
+
}
|
|
306
|
+
.w-list-button:hover {
|
|
307
|
+
background: var(--wings-hover);
|
|
308
|
+
}
|
|
309
|
+
.w-list-button:active {
|
|
310
|
+
transform: none;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Status: a dot, a spinner, a filled pill and an outlined tag. */
|
|
314
|
+
.w-dot {
|
|
315
|
+
flex: none;
|
|
316
|
+
width: 6px;
|
|
317
|
+
height: 6px;
|
|
318
|
+
border-radius: 50%;
|
|
319
|
+
background: var(--wings-success);
|
|
320
|
+
}
|
|
321
|
+
.w-dot[data-tone="muted"] {
|
|
322
|
+
background: var(--wings-muted);
|
|
323
|
+
}
|
|
324
|
+
.w-dot[data-tone="info"] {
|
|
325
|
+
background: var(--wings-info);
|
|
326
|
+
}
|
|
327
|
+
.w-dot[data-tone="warn"] {
|
|
328
|
+
background: var(--wings-warning);
|
|
329
|
+
}
|
|
330
|
+
.w-dot[data-tone="danger"] {
|
|
331
|
+
background: var(--wings-danger);
|
|
332
|
+
}
|
|
333
|
+
.w-spinner {
|
|
334
|
+
flex: none;
|
|
335
|
+
width: 10px;
|
|
336
|
+
height: 10px;
|
|
337
|
+
border: 2px solid var(--wings-hairline-strong);
|
|
338
|
+
border-top-color: var(--wings-fg);
|
|
339
|
+
border-radius: 50%;
|
|
340
|
+
animation: w-spin 0.8s linear infinite;
|
|
341
|
+
}
|
|
342
|
+
.w-spinner[data-size="large"] {
|
|
343
|
+
width: 14px;
|
|
344
|
+
height: 14px;
|
|
345
|
+
}
|
|
346
|
+
@keyframes w-spin {
|
|
347
|
+
to {
|
|
348
|
+
transform: rotate(360deg);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
.w-pill {
|
|
352
|
+
flex: none;
|
|
353
|
+
padding: 0 6px;
|
|
354
|
+
border-radius: 999px;
|
|
355
|
+
background: var(--wings-hover);
|
|
356
|
+
font-size: 11px;
|
|
357
|
+
font-weight: 600;
|
|
358
|
+
line-height: 18px;
|
|
359
|
+
white-space: nowrap;
|
|
360
|
+
}
|
|
361
|
+
.w-pill[data-tone="info"] {
|
|
362
|
+
color: var(--wings-info);
|
|
363
|
+
background: color-mix(in srgb, var(--wings-info) 14%, transparent);
|
|
364
|
+
}
|
|
365
|
+
.w-pill[data-tone="success"] {
|
|
366
|
+
color: var(--wings-success);
|
|
367
|
+
background: color-mix(in srgb, var(--wings-success) 14%, transparent);
|
|
368
|
+
}
|
|
369
|
+
.w-pill[data-tone="warn"] {
|
|
370
|
+
color: var(--wings-warning);
|
|
371
|
+
background: color-mix(in srgb, var(--wings-warning) 14%, transparent);
|
|
372
|
+
}
|
|
373
|
+
.w-pill[data-tone="danger"] {
|
|
374
|
+
color: var(--wings-danger);
|
|
375
|
+
background: color-mix(in srgb, var(--wings-danger) 14%, transparent);
|
|
376
|
+
}
|
|
377
|
+
.w-tag {
|
|
378
|
+
flex: none;
|
|
379
|
+
max-width: 100%;
|
|
380
|
+
overflow: hidden;
|
|
381
|
+
text-overflow: ellipsis;
|
|
382
|
+
white-space: nowrap;
|
|
383
|
+
padding: 0 5px;
|
|
384
|
+
border: 1px solid var(--wings-hairline-strong);
|
|
385
|
+
border-radius: 5px;
|
|
386
|
+
font-size: 10.5px;
|
|
387
|
+
line-height: 15px;
|
|
388
|
+
color: var(--wings-muted);
|
|
389
|
+
}
|
|
390
|
+
.w-tag[data-tone="info"] {
|
|
391
|
+
border-color: color-mix(in srgb, var(--wings-info) 45%, transparent);
|
|
392
|
+
color: var(--wings-info);
|
|
393
|
+
}
|
|
394
|
+
.w-tag[data-tone="success"] {
|
|
395
|
+
border-color: color-mix(in srgb, var(--wings-success) 45%, transparent);
|
|
396
|
+
color: var(--wings-success);
|
|
397
|
+
}
|
|
398
|
+
.w-tag[data-tone="warn"] {
|
|
399
|
+
border-color: color-mix(in srgb, var(--wings-warning) 45%, transparent);
|
|
400
|
+
color: var(--wings-warning);
|
|
401
|
+
}
|
|
402
|
+
.w-tag[data-tone="danger"] {
|
|
403
|
+
border-color: color-mix(in srgb, var(--wings-danger) 45%, transparent);
|
|
404
|
+
color: var(--wings-danger);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* Nothing to show yet, or nothing at all. */
|
|
408
|
+
.w-loading {
|
|
409
|
+
color: var(--wings-muted);
|
|
410
|
+
}
|
|
411
|
+
.w-empty {
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-direction: column;
|
|
414
|
+
gap: 4px;
|
|
415
|
+
padding: 24px 4px;
|
|
416
|
+
text-align: center;
|
|
417
|
+
}
|
|
418
|
+
.w-empty-detail {
|
|
419
|
+
color: var(--wings-muted);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* Buttons. Plain buttons already look like Wings; these add the variants and sizes. */
|
|
423
|
+
.w-button[data-variant="primary"] {
|
|
424
|
+
border-color: transparent;
|
|
425
|
+
background: var(--wings-primary);
|
|
426
|
+
color: var(--wings-primary-fg);
|
|
427
|
+
}
|
|
428
|
+
.w-button[data-variant="primary"]:hover {
|
|
429
|
+
background: color-mix(in srgb, var(--wings-primary) 85%, transparent);
|
|
430
|
+
}
|
|
431
|
+
.w-button[data-variant="danger"] {
|
|
432
|
+
border-color: transparent;
|
|
433
|
+
background: color-mix(in srgb, var(--wings-danger) 85%, transparent);
|
|
434
|
+
color: #1b0f0d;
|
|
435
|
+
}
|
|
436
|
+
.w-button[data-variant="ghost"] {
|
|
437
|
+
border-color: transparent;
|
|
438
|
+
background: transparent;
|
|
439
|
+
}
|
|
440
|
+
.w-button[data-variant="ghost"]:hover {
|
|
441
|
+
background: var(--wings-hover);
|
|
442
|
+
}
|
|
443
|
+
.w-button[data-size="small"] {
|
|
444
|
+
height: 24px;
|
|
445
|
+
padding: 0 8px;
|
|
446
|
+
font-size: 12px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* Chips: small quiet buttons, and a group of them that filters what shows. */
|
|
450
|
+
.w-chips {
|
|
451
|
+
display: flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
gap: 2px;
|
|
454
|
+
min-width: 0;
|
|
455
|
+
}
|
|
456
|
+
.w-chip {
|
|
457
|
+
height: 22px;
|
|
458
|
+
padding: 0 8px;
|
|
459
|
+
border: 0;
|
|
460
|
+
border-radius: 6px;
|
|
461
|
+
background: none;
|
|
462
|
+
font-size: 11px;
|
|
463
|
+
color: var(--wings-muted);
|
|
464
|
+
}
|
|
465
|
+
.w-chip:hover,
|
|
466
|
+
.w-chip[aria-pressed="true"] {
|
|
467
|
+
background: var(--wings-pressed);
|
|
468
|
+
}
|
|
469
|
+
.w-chip[aria-pressed="true"] {
|
|
470
|
+
color: var(--wings-fg);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Tabs over a panel. */
|
|
474
|
+
.w-tabs {
|
|
475
|
+
display: flex;
|
|
476
|
+
gap: 2px;
|
|
477
|
+
border-bottom: 1px solid var(--wings-hairline);
|
|
478
|
+
}
|
|
479
|
+
.w-tab {
|
|
480
|
+
height: 28px;
|
|
481
|
+
padding: 0 10px;
|
|
482
|
+
border: 0;
|
|
483
|
+
border-bottom: 2px solid transparent;
|
|
484
|
+
border-radius: 0;
|
|
485
|
+
background: none;
|
|
486
|
+
font-size: 12px;
|
|
487
|
+
color: var(--wings-muted);
|
|
488
|
+
}
|
|
489
|
+
.w-tab:hover {
|
|
490
|
+
background: none;
|
|
491
|
+
color: var(--wings-fg);
|
|
492
|
+
}
|
|
493
|
+
.w-tab:active {
|
|
494
|
+
transform: none;
|
|
495
|
+
}
|
|
496
|
+
.w-tab[aria-selected="true"] {
|
|
497
|
+
border-bottom-color: var(--wings-fg);
|
|
498
|
+
color: var(--wings-fg);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Form fields. Inputs, selects and text areas already look like Wings. */
|
|
502
|
+
.w-field {
|
|
503
|
+
display: flex;
|
|
504
|
+
flex-direction: column;
|
|
505
|
+
gap: 4px;
|
|
506
|
+
}
|
|
507
|
+
.w-field-label {
|
|
508
|
+
font-size: 12px;
|
|
509
|
+
font-weight: 500;
|
|
510
|
+
}
|
|
511
|
+
.w-field-hint {
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
color: var(--wings-muted);
|
|
514
|
+
}
|
|
515
|
+
.w-field-hint[data-tone="danger"] {
|
|
516
|
+
color: var(--wings-danger);
|
|
517
|
+
}
|
|
518
|
+
.w-check {
|
|
519
|
+
display: flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
gap: 6px;
|
|
522
|
+
min-width: 0;
|
|
523
|
+
font-size: 12px;
|
|
524
|
+
cursor: pointer;
|
|
525
|
+
}
|
|
526
|
+
.w-check > input {
|
|
527
|
+
flex: none;
|
|
528
|
+
width: auto;
|
|
529
|
+
height: auto;
|
|
530
|
+
margin: 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* More behind a summary line. */
|
|
534
|
+
.w-disclosure > summary {
|
|
535
|
+
padding: 4px 6px;
|
|
536
|
+
font-size: 12px;
|
|
537
|
+
color: var(--wings-muted);
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
@media (prefers-reduced-motion: reduce) {
|
|
542
|
+
.w-spinner {
|
|
543
|
+
animation: none;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/* wings-ui 1 end */
|
|
547
|
+
`;
|
package/ui.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/// <reference path="./wings.d.ts" />
|
|
2
|
+
// wings-plugin-sdk/ui: say what a page shows and where, and Wings decides how it looks. Every component renders
|
|
3
|
+
// plain elements with `w-*` class names that Wings' plugin-ui.css styles. Your own components and CSS work too.
|
|
4
|
+
import type { ComponentPropsWithoutRef, JSX, ReactElement, ReactNode, Ref } from "react";
|
|
5
|
+
|
|
6
|
+
import type { Place, ShellOptions } from "./shell";
|
|
7
|
+
|
|
8
|
+
export type { Place, ShellOptions } from "./shell";
|
|
9
|
+
export { mountShell } from "./shell";
|
|
10
|
+
export { useBroadcast } from "./react";
|
|
11
|
+
|
|
12
|
+
// ---------- places ----------
|
|
13
|
+
|
|
14
|
+
/** Shows `element`, any React element, as the page of a sidebar. One call per page; calling again renders the new element. */
|
|
15
|
+
export function showSidebar(element: ReactElement, options?: ShellOptions): void;
|
|
16
|
+
/** Shows `element` as a view's page, in a modal or a window of its own. */
|
|
17
|
+
export function showView(element: ReactElement, options?: ShellOptions): void;
|
|
18
|
+
/** Shows `element` as a view's page in a tab next to the terminals. */
|
|
19
|
+
export function showTab(element: ReactElement, options?: ShellOptions): void;
|
|
20
|
+
/** Shows `element` as a sidebar's popover. */
|
|
21
|
+
export function showPopover(element: ReactElement, options?: ShellOptions): void;
|
|
22
|
+
/** Shows `element` as the plugin's page in Wings Settings. */
|
|
23
|
+
export function showSettings(element: ReactElement, options?: ShellOptions): void;
|
|
24
|
+
|
|
25
|
+
// ---------- talking to main ----------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The newest message of `type` the plugin's other pages broadcast, or null until one comes. Sends `hello`
|
|
29
|
+
* (`{ type: "hello" }` by default) once when the component mounts, so main answers with its state; pass null to
|
|
30
|
+
* send nothing.
|
|
31
|
+
*/
|
|
32
|
+
export function useMainState<T extends { type: string }>(type: T["type"], hello?: unknown): T | null;
|
|
33
|
+
|
|
34
|
+
// ---------- shared ----------
|
|
35
|
+
|
|
36
|
+
/** A colour that says something. */
|
|
37
|
+
export type Tone = "muted" | "info" | "success" | "warn" | "danger";
|
|
38
|
+
type Props<Tag extends keyof JSX.IntrinsicElements> = ComponentPropsWithoutRef<Tag>;
|
|
39
|
+
type As = "div" | "span" | "p" | "section" | "header" | "footer" | "strong" | "time" | "label" | "li";
|
|
40
|
+
/** Spacing between children: none, xs (2px), s (4px), m (8px, the default) or l (12px), set by Wings. */
|
|
41
|
+
export type Gap = "none" | "xs" | "s" | "m" | "l";
|
|
42
|
+
|
|
43
|
+
// ---------- layout and text ----------
|
|
44
|
+
|
|
45
|
+
/** Children side by side, centred. */
|
|
46
|
+
export function Row(props: Props<"div"> & { as?: As; gap?: Gap; align?: "start" | "baseline"; justify?: "end" | "between"; wrap?: boolean; grow?: boolean }): ReactElement;
|
|
47
|
+
/** Children one under the other. */
|
|
48
|
+
export function Stack(props: Props<"div"> & { as?: As; gap?: Gap; grow?: boolean }): ReactElement;
|
|
49
|
+
/** Text with a tone and size, that can `grow` to fill a row or `truncate` with an ellipsis. */
|
|
50
|
+
export function Text(
|
|
51
|
+
props: Props<"span"> & {
|
|
52
|
+
as?: As;
|
|
53
|
+
tone?: Tone;
|
|
54
|
+
/** small is 12px, tiny 11px. */
|
|
55
|
+
size?: "small" | "tiny";
|
|
56
|
+
weight?: "medium" | "strong";
|
|
57
|
+
mono?: boolean;
|
|
58
|
+
/** Digits of equal width, for figures that change. */
|
|
59
|
+
numeric?: boolean;
|
|
60
|
+
/** One line, cut with an ellipsis. */
|
|
61
|
+
truncate?: boolean;
|
|
62
|
+
/** Up to three lines. */
|
|
63
|
+
clamp?: boolean;
|
|
64
|
+
grow?: boolean;
|
|
65
|
+
},
|
|
66
|
+
): ReactElement;
|
|
67
|
+
/** A section heading, an `h2`, with `actions` like chips or a button on its right. */
|
|
68
|
+
export function Heading(props: Props<"h2"> & { actions?: ReactNode }): ReactElement;
|
|
69
|
+
/** A card. `compact` for a summary line. */
|
|
70
|
+
export function Card(props: Props<"div"> & { as?: As; compact?: boolean }): ReactElement;
|
|
71
|
+
|
|
72
|
+
// ---------- figures ----------
|
|
73
|
+
|
|
74
|
+
/** A bar from 0 to 100, with the meter role. */
|
|
75
|
+
export function Meter(props: Props<"div"> & { percent: number; label: string; tone?: "warn" | "danger" }): ReactElement;
|
|
76
|
+
/** A labelled bar with its percentage and a `note` under it. `levels` are where it turns warn and danger, like `[75, 90]`. */
|
|
77
|
+
export function Gauge(props: Props<"div"> & { label: string; percent: number; note?: ReactNode; tone?: "warn" | "danger"; levels?: [number, number] }): ReactElement;
|
|
78
|
+
export interface Stat {
|
|
79
|
+
label: string;
|
|
80
|
+
value: ReactNode;
|
|
81
|
+
tone?: "warn" | "danger";
|
|
82
|
+
title?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Small figures side by side, label over value. */
|
|
85
|
+
export function Stats(props: Props<"dl"> & { items: Stat[] }): ReactElement;
|
|
86
|
+
/** Label and value pairs, one per line. */
|
|
87
|
+
export function Properties(props: Props<"dl"> & { items: { label: string; value: ReactNode }[] }): ReactElement;
|
|
88
|
+
|
|
89
|
+
// ---------- lists ----------
|
|
90
|
+
|
|
91
|
+
/** A list of rows: a `ul`, or an `ol` when `ordered`. `compact` rows have no space between them, for long lists. */
|
|
92
|
+
export function List(props: Props<"ul"> & { ordered?: boolean; compact?: boolean }): ReactElement;
|
|
93
|
+
/** A row in a `List`. Holding a `ListButton`, the button is the row. */
|
|
94
|
+
export function ListItem(props: Props<"li"> & { align?: "start" }): ReactElement;
|
|
95
|
+
/** A row that's a button, inside a `ListItem`. */
|
|
96
|
+
export function ListButton(props: Props<"button"> & { align?: "baseline" }): ReactElement;
|
|
97
|
+
|
|
98
|
+
// ---------- status ----------
|
|
99
|
+
|
|
100
|
+
/** A status dot, green unless `tone` says otherwise. With a `label` it's announced; otherwise it's decoration. */
|
|
101
|
+
export function Dot(props: Props<"span"> & { tone?: Exclude<Tone, "success">; label?: string }): ReactElement;
|
|
102
|
+
/** A spinner. With a `label` it's announced; otherwise it's decoration. */
|
|
103
|
+
export function Spinner(props: Props<"span"> & { size?: "large"; label?: string }): ReactElement;
|
|
104
|
+
/** A filled pill, like a label on a command. */
|
|
105
|
+
export function Pill(props: Props<"span"> & { tone?: Exclude<Tone, "muted"> }): ReactElement;
|
|
106
|
+
/** An outlined tag, like a branch name. */
|
|
107
|
+
export function Tag(props: Props<"span"> & { tone?: Exclude<Tone, "muted"> }): ReactElement;
|
|
108
|
+
/** What shows before the first data comes: "Loading…" unless `children` say otherwise. */
|
|
109
|
+
export function Loading(props: Props<"p">): ReactElement;
|
|
110
|
+
/** Nothing to show: what's missing, and a `detail` line on what to do about it. */
|
|
111
|
+
export function Empty(props: Omit<Props<"div">, "title"> & { title: ReactNode; detail?: ReactNode }): ReactElement;
|
|
112
|
+
|
|
113
|
+
// ---------- buttons ----------
|
|
114
|
+
|
|
115
|
+
/** A button, `type="button"` unless you say otherwise. */
|
|
116
|
+
export function Button(props: Props<"button"> & { variant?: "primary" | "ghost" | "danger"; size?: "small" }): ReactElement;
|
|
117
|
+
/** A small quiet button. With `pressed` it's a toggle. */
|
|
118
|
+
export function Chip(props: Props<"button"> & { pressed?: boolean }): ReactElement;
|
|
119
|
+
export interface Option<V extends string> {
|
|
120
|
+
value: V;
|
|
121
|
+
label: ReactNode;
|
|
122
|
+
title?: string;
|
|
123
|
+
}
|
|
124
|
+
/** Chips that pick one of `options`, like a filter, in a group named `label`. `onChange` gets each click, the chip that's on too. */
|
|
125
|
+
export function Chips<V extends string>(props: Omit<Props<"div">, "onChange"> & { label: string; options: Option<V>[]; value: V; onChange: (value: V) => void }): ReactElement;
|
|
126
|
+
/** Tabs named `label` over the panel they show, `children`. Arrow keys, Home and End move between tabs. */
|
|
127
|
+
export function Tabs<V extends string>(props: Omit<Props<"div">, "onChange"> & { label: string; items: Option<V>[]; value: V; onChange: (value: V) => void }): ReactElement;
|
|
128
|
+
|
|
129
|
+
// ---------- fields ----------
|
|
130
|
+
|
|
131
|
+
/** A labelled field with a `hint` or an `error` under it, for the `TextInput`, `SearchField` or `Select` in `children`. */
|
|
132
|
+
export function Field(props: Props<"div"> & { label: ReactNode; hint?: ReactNode; error?: ReactNode }): ReactElement;
|
|
133
|
+
/** A text input. Outside a `Field`, give it a `label`. */
|
|
134
|
+
export function TextInput(props: Props<"input"> & { label?: string; ref?: Ref<HTMLInputElement> }): ReactElement;
|
|
135
|
+
/** A search box. `label` names it for screen readers. */
|
|
136
|
+
export function SearchField(props: Omit<Props<"input">, "type"> & { label?: string; ref?: Ref<HTMLInputElement> }): ReactElement;
|
|
137
|
+
/** A select of `options`. `onChange` gets the value picked. */
|
|
138
|
+
export function Select<V extends string>(
|
|
139
|
+
props: Omit<Props<"select">, "onChange" | "children"> & { label?: string; options: (Option<V> & { label: string; disabled?: boolean })[]; value?: V; onChange?: (value: V) => void },
|
|
140
|
+
): ReactElement;
|
|
141
|
+
/** A checkbox with its label, `children`. `onChange` gets whether it's checked. */
|
|
142
|
+
export function Checkbox(props: Omit<Props<"input">, "onChange" | "type"> & { onChange?: (checked: boolean) => void }): ReactElement;
|
|
143
|
+
/** More behind a `summary` line that opens it. */
|
|
144
|
+
export function Disclosure(props: Props<"details"> & { summary: ReactNode }): ReactElement;
|