wings-plugin-ui 0.0.0-stage → 0.1.1
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/LICENSE +18 -0
- package/README.md +179 -2
- package/package.json +44 -4
- package/plugin-ui.css +1292 -0
- package/ui.d.ts +277 -0
- package/ui.js +583 -0
package/ui.d.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/// <reference types="wings-plugin-sdk" />
|
|
2
|
+
// wings-plugin-ui: say what a page shows and where, and Wings decides how it looks. Every component renders
|
|
3
|
+
// `w-*` class names that Wings' plugin-ui.css styles, the interactive ones on Base UI's components, drawn as the app's
|
|
4
|
+
// own shadcn/ui ones. Your own components and CSS work too.
|
|
5
|
+
import type { ComponentPropsWithoutRef, JSX, ReactElement, ReactNode, Ref } from "react";
|
|
6
|
+
|
|
7
|
+
import type { ShellOptions } from "wings-plugin-sdk/shell";
|
|
8
|
+
|
|
9
|
+
// ---------- places ----------
|
|
10
|
+
|
|
11
|
+
/** Shows `element`, any React element, as the page of a sidebar. One call per page; calling again renders the new element. */
|
|
12
|
+
export function showSidebar(element: ReactElement, options?: ShellOptions): void;
|
|
13
|
+
/** Shows `element` as a view's page, in a modal or a window of its own. */
|
|
14
|
+
export function showView(element: ReactElement, options?: ShellOptions): void;
|
|
15
|
+
/** Shows `element` as a view's page in a tab next to the terminals. */
|
|
16
|
+
export function showTab(element: ReactElement, options?: ShellOptions): void;
|
|
17
|
+
/** Shows `element` as a sidebar's popover. */
|
|
18
|
+
export function showPopover(element: ReactElement, options?: ShellOptions): void;
|
|
19
|
+
/** Shows `element` as the plugin's page in Wings Settings. */
|
|
20
|
+
export function showSettings(element: ReactElement, options?: ShellOptions): void;
|
|
21
|
+
|
|
22
|
+
// ---------- talking to main ----------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The newest message of `type` the plugin's other pages broadcast, or null until one comes. Sends `hello`
|
|
26
|
+
* (`{ type: "hello" }` by default) once when the component mounts, so main answers with its state; pass null to
|
|
27
|
+
* send nothing.
|
|
28
|
+
*/
|
|
29
|
+
export function useMainState<T extends { type: string }>(type: T["type"], hello?: unknown): T | null;
|
|
30
|
+
|
|
31
|
+
// ---------- shared ----------
|
|
32
|
+
|
|
33
|
+
/** A colour that says something. */
|
|
34
|
+
export type Tone = "muted" | "info" | "success" | "warn" | "danger";
|
|
35
|
+
type Props<Tag extends keyof JSX.IntrinsicElements> = ComponentPropsWithoutRef<Tag>;
|
|
36
|
+
type As = "div" | "span" | "p" | "section" | "header" | "footer" | "strong" | "time" | "label" | "li";
|
|
37
|
+
/** The elements `Row` and `Stack` can be. */
|
|
38
|
+
type BoxAs = As | "form" | "ul" | "ol";
|
|
39
|
+
/** The elements `Card` can be. */
|
|
40
|
+
type CardAs = As | "form" | "article" | "ul" | "ol";
|
|
41
|
+
/** The props of element `E`, with `own` on top: where they share a name, `own` wins. */
|
|
42
|
+
type Own<E extends keyof JSX.IntrinsicElements, own> = Omit<Props<E>, keyof own> & own;
|
|
43
|
+
/** Spacing between children: none, xs (2px), s (4px), m (8px, the default) or l (12px), set by Wings. */
|
|
44
|
+
export type Gap = "none" | "xs" | "s" | "m" | "l";
|
|
45
|
+
|
|
46
|
+
// ---------- layout and text ----------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Children side by side, centred. `as` picks the element, like a `form` or a `ul`; the props and handlers are the
|
|
50
|
+
* element's, so `<Row as="form" onSubmit={(e) => e.currentTarget.requestSubmit()}>` has a `HTMLFormElement`.
|
|
51
|
+
*/
|
|
52
|
+
export function Row<E extends BoxAs = "div">(props: Own<E, { as?: E; gap?: Gap; align?: "start" | "baseline"; justify?: "end" | "between"; wrap?: boolean; grow?: boolean }>): ReactElement;
|
|
53
|
+
/** Children one under the other. As a `ul` or `ol` it's a list with no bullets, of `li` children. `as` types the props like `Row`'s. */
|
|
54
|
+
export function Stack<E extends BoxAs = "div">(props: Own<E, { as?: E; gap?: Gap; grow?: boolean }>): ReactElement;
|
|
55
|
+
/** Text with a tone and size, that can `grow` to fill a row or `truncate` with an ellipsis. */
|
|
56
|
+
export function Text(
|
|
57
|
+
props: Props<"span"> & {
|
|
58
|
+
as?: As;
|
|
59
|
+
tone?: Tone;
|
|
60
|
+
/** small is 12px, tiny 11px. */
|
|
61
|
+
size?: "small" | "tiny";
|
|
62
|
+
weight?: "medium" | "strong";
|
|
63
|
+
mono?: boolean;
|
|
64
|
+
/** Digits of equal width, for figures that change. */
|
|
65
|
+
numeric?: boolean;
|
|
66
|
+
/** One line, cut with an ellipsis. */
|
|
67
|
+
truncate?: boolean;
|
|
68
|
+
/** Up to three lines. */
|
|
69
|
+
clamp?: boolean;
|
|
70
|
+
grow?: boolean;
|
|
71
|
+
},
|
|
72
|
+
): ReactElement;
|
|
73
|
+
/** A section heading, an `h2`, with `actions` like chips or a button on its right. */
|
|
74
|
+
export function Heading(props: Props<"h2"> & { level?: 2 | 3 | 4; actions?: ReactNode }): ReactElement;
|
|
75
|
+
/** A page's or a card's title, like a sprint's name or a setup step's question: an `h1` unless `as` says otherwise. */
|
|
76
|
+
/** `truncate` cuts a title that's too long for its place to one line, with an ellipsis. */
|
|
77
|
+
export function Title(props: Props<"h1"> & { as?: "h1" | "h2" | "h3"; size?: "large"; truncate?: boolean }): ReactElement;
|
|
78
|
+
/** A card. `compact` for a summary line. As a `form` it holds a form; as a `ul` or `ol`, a list of `li`. `as` types the props like `Row`'s. */
|
|
79
|
+
export function Card<E extends CardAs = "div">(props: Own<E, { as?: E; compact?: boolean }>): ReactElement;
|
|
80
|
+
|
|
81
|
+
// ---------- figures ----------
|
|
82
|
+
|
|
83
|
+
/** A bar from 0 to 100, with the meter role. */
|
|
84
|
+
export function Meter(props: Props<"div"> & { percent: number; label: string; tone?: "warn" | "danger" }): ReactElement;
|
|
85
|
+
/** A labelled bar with its percentage and a `note` under it. `levels` are where it turns warn and danger, like `[75, 90]`. */
|
|
86
|
+
export function Gauge(props: Props<"div"> & { label: string; percent: number; note?: ReactNode; tone?: "warn" | "danger"; levels?: [number, number] }): ReactElement;
|
|
87
|
+
export interface Stat {
|
|
88
|
+
label: string;
|
|
89
|
+
value: ReactNode;
|
|
90
|
+
tone?: "warn" | "danger";
|
|
91
|
+
title?: string;
|
|
92
|
+
}
|
|
93
|
+
/** Small figures side by side, label over value. */
|
|
94
|
+
export function Stats(props: Props<"dl"> & { items: Stat[] }): ReactElement;
|
|
95
|
+
/** Label and value pairs, one per line. */
|
|
96
|
+
export function Properties(props: Props<"dl"> & { items: { label: string; value: ReactNode }[] }): ReactElement;
|
|
97
|
+
|
|
98
|
+
// ---------- lists ----------
|
|
99
|
+
|
|
100
|
+
/** A list of rows: a `ul`, or an `ol` when `ordered`. `compact` rows have no space between them, for long lists. */
|
|
101
|
+
export function List(props: Props<"ul"> & { ordered?: boolean; compact?: boolean }): ReactElement;
|
|
102
|
+
/** A row in a `List`. Holding a `ListButton`, the button is the row. */
|
|
103
|
+
export function ListItem(props: Props<"li"> & { align?: "start" }): ReactElement;
|
|
104
|
+
/** A row that's a button, inside a `ListItem`. */
|
|
105
|
+
export function ListButton(props: Props<"button"> & { align?: "baseline" }): ReactElement;
|
|
106
|
+
|
|
107
|
+
// ---------- status ----------
|
|
108
|
+
|
|
109
|
+
/** A status dot, green unless `tone` says otherwise. With a `label` it's announced; otherwise it's decoration. */
|
|
110
|
+
export function Dot(
|
|
111
|
+
props: Props<"span"> & {
|
|
112
|
+
tone?: Exclude<Tone, "success">;
|
|
113
|
+
/** A colour from your data, like a work item type's `#cc293d`, set as `--w-color`. Wins over `tone`. */
|
|
114
|
+
color?: string;
|
|
115
|
+
label?: string;
|
|
116
|
+
},
|
|
117
|
+
): ReactElement;
|
|
118
|
+
/** A spinner, 16px like the app's, in the colour of the text around it. With a `label` it's announced; otherwise it's decoration. */
|
|
119
|
+
export function Spinner(props: Props<"span"> & { size?: "large"; label?: string }): ReactElement;
|
|
120
|
+
/** shadcn/ui's Badge variants. */
|
|
121
|
+
export type BadgeVariant = "default" | "secondary" | "destructive" | "outline" | "ghost" | "link";
|
|
122
|
+
/** A badge, like a label on a command or a branch name, drawn as the app's Badge. `tone` is Wings' own: a colour that says something. */
|
|
123
|
+
export function Badge(props: Props<"span"> & { variant?: BadgeVariant; tone?: Exclude<Tone, "muted"> }): ReactElement;
|
|
124
|
+
/** What shows before the first data comes: "Loading…" unless `children` say otherwise. */
|
|
125
|
+
export function Loading(props: Props<"p">): ReactElement;
|
|
126
|
+
/** Nothing to show: what's missing, a `description` on what to do about it, and `children` under them, like a button. */
|
|
127
|
+
export function Empty(props: Omit<Props<"div">, "title"> & { title: ReactNode; description?: ReactNode }): ReactElement;
|
|
128
|
+
|
|
129
|
+
// ---------- buttons ----------
|
|
130
|
+
|
|
131
|
+
/** shadcn/ui's Button variants. */
|
|
132
|
+
export type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "destructive" | "link";
|
|
133
|
+
/** shadcn/ui's Button sizes. */
|
|
134
|
+
export type ButtonSize = "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg";
|
|
135
|
+
/**
|
|
136
|
+
* A button on Base UI's Button, `type="button"` unless you say otherwise. `variant` "default" is the filled one.
|
|
137
|
+
* `busy` shows a spinner and ignores clicks while it keeps the keyboard focus, unlike `disabled`: it's `aria-busy`
|
|
138
|
+
* and `aria-disabled`.
|
|
139
|
+
*/
|
|
140
|
+
export function Button(props: Props<"button"> & { variant?: ButtonVariant; size?: ButtonSize; busy?: boolean }): ReactElement;
|
|
141
|
+
/** A small quiet button. With `pressed` it's a toggle. */
|
|
142
|
+
export function Chip(props: Props<"button"> & { pressed?: boolean }): ReactElement;
|
|
143
|
+
export interface Option<V extends string> {
|
|
144
|
+
value: V;
|
|
145
|
+
label: ReactNode;
|
|
146
|
+
title?: string;
|
|
147
|
+
}
|
|
148
|
+
/** Chips that pick one of `options`, like a filter, in a group named `label`. `onChange` gets each click, the chip that's on too. */
|
|
149
|
+
export function Chips<V extends string>(
|
|
150
|
+
props: Omit<Props<"div">, "onChange"> & {
|
|
151
|
+
label: string;
|
|
152
|
+
/** Each chip's other props, like `id`, go to its button. */
|
|
153
|
+
options: (Option<V> & Omit<Props<"button">, "value" | "onClick" | "children">)[];
|
|
154
|
+
value: V;
|
|
155
|
+
onChange: (value: V) => void;
|
|
156
|
+
},
|
|
157
|
+
): ReactElement;
|
|
158
|
+
/**
|
|
159
|
+
* Tabs named `label` over the panel they show, `children`, on Base UI's Tabs. Arrow keys, Home and End move between
|
|
160
|
+
* tabs and show the one they land on. `variant` is shadcn/ui's: "default", a pill in a tray, or "line", an underline.
|
|
161
|
+
*/
|
|
162
|
+
export function Tabs<V extends string>(
|
|
163
|
+
props: Omit<Props<"div">, "onChange"> & { label: string; items: Option<V>[]; value: V; onChange: (value: V) => void; variant?: "default" | "line" },
|
|
164
|
+
): ReactElement;
|
|
165
|
+
|
|
166
|
+
// ---------- fields ----------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A labelled field with a `hint` or an `error` under it, for the `TextInput`, `SearchField`, `Select` or `Combobox` in
|
|
170
|
+
* `children`, on Base UI's Field. It takes one input: Base UI gives every input in a Field the first one's id, so a
|
|
171
|
+
* second input goes beside the Field.
|
|
172
|
+
*/
|
|
173
|
+
export function Field(
|
|
174
|
+
props: Props<"div"> & {
|
|
175
|
+
label: ReactNode;
|
|
176
|
+
hint?: ReactNode;
|
|
177
|
+
error?: ReactNode;
|
|
178
|
+
/** The input's id, and the label's `htmlFor`, for a page that puts focus back by id. Generated otherwise. */
|
|
179
|
+
id?: string;
|
|
180
|
+
/** The label and the input on one line. */
|
|
181
|
+
inline?: boolean;
|
|
182
|
+
},
|
|
183
|
+
): ReactElement;
|
|
184
|
+
/** Several controls under one label (a `fieldset` and its `legend`), like checkboxes, with a `hint` or an `error` under them. */
|
|
185
|
+
export function FieldGroup(props: Props<"fieldset"> & { label: ReactNode; hint?: ReactNode; error?: ReactNode }): ReactElement;
|
|
186
|
+
/** A text input. Outside a `Field`, give it a `label`. */
|
|
187
|
+
export function TextInput(props: Props<"input"> & { label?: string; ref?: Ref<HTMLInputElement> }): ReactElement;
|
|
188
|
+
/** A search box. `label` names it for screen readers. */
|
|
189
|
+
export function SearchField(props: Omit<Props<"input">, "type"> & { label?: string; ref?: Ref<HTMLInputElement> }): ReactElement;
|
|
190
|
+
/**
|
|
191
|
+
* A select of `options` on Base UI's Select: a button that opens a list in the page. `onChange` gets the value picked.
|
|
192
|
+
* Without a `value` it starts on `defaultValue`, or the first option. Its props are the button's, not a `select`'s.
|
|
193
|
+
*/
|
|
194
|
+
export function Select<V extends string>(
|
|
195
|
+
props: Omit<Props<"button">, "onChange" | "children" | "value" | "defaultValue"> & {
|
|
196
|
+
label?: string;
|
|
197
|
+
options: (Option<V> & { label: string; disabled?: boolean })[];
|
|
198
|
+
value?: V;
|
|
199
|
+
defaultValue?: V;
|
|
200
|
+
onChange?: (value: V) => void;
|
|
201
|
+
name?: string;
|
|
202
|
+
required?: boolean;
|
|
203
|
+
form?: string;
|
|
204
|
+
},
|
|
205
|
+
): ReactElement;
|
|
206
|
+
/**
|
|
207
|
+
* A checkbox with its label, `children`, on Base UI's Checkbox. `onChange` gets whether it's checked. Its element is
|
|
208
|
+
* a `button` with the `checkbox` role, and `id` is that button's.
|
|
209
|
+
*/
|
|
210
|
+
export function Checkbox(
|
|
211
|
+
props: Omit<Props<"button">, "onChange" | "type" | "children" | "value" | "defaultValue"> & {
|
|
212
|
+
children?: ReactNode;
|
|
213
|
+
checked?: boolean;
|
|
214
|
+
defaultChecked?: boolean;
|
|
215
|
+
onChange?: (checked: boolean) => void;
|
|
216
|
+
name?: string;
|
|
217
|
+
value?: string;
|
|
218
|
+
required?: boolean;
|
|
219
|
+
readOnly?: boolean;
|
|
220
|
+
},
|
|
221
|
+
): ReactElement;
|
|
222
|
+
export interface ComboboxOption {
|
|
223
|
+
value: string;
|
|
224
|
+
label: string;
|
|
225
|
+
/** A second, quieter part, like a project's client. */
|
|
226
|
+
detail?: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* A text field with `options` under it, on Base UI's Combobox: the list is a popup in the page. Arrows move, Enter picks, Escape and Tab close, and pressing outside
|
|
230
|
+
* closes it. With `freeText` the field keeps what you type (`value`, `onInput`) and starts with nothing
|
|
231
|
+
* highlighted; otherwise you type to find an option and the field shows `value`, what you picked, once closed.
|
|
232
|
+
* `filter` decides what matches what's typed: the label or the detail containing it, by default. Without
|
|
233
|
+
* `freeText` the field's text is selected when it gets focus and after a pick, so typing replaces the label. An
|
|
234
|
+
* `onKeyDown` you pass runs first; `preventDefault()` in it keeps the Combobox's own keys from running.
|
|
235
|
+
*/
|
|
236
|
+
export function Combobox(
|
|
237
|
+
props: Omit<Props<"input">, "value" | "onInput" | "type"> & {
|
|
238
|
+
label?: string;
|
|
239
|
+
value: string;
|
|
240
|
+
onInput?: (text: string) => void;
|
|
241
|
+
options: ComboboxOption[];
|
|
242
|
+
onPick: (option: ComboboxOption) => void;
|
|
243
|
+
/** Enter with no option highlighted, with the text typed. The Enter doesn't submit a form around it. */
|
|
244
|
+
onEnter?: (text: string) => void;
|
|
245
|
+
freeText?: boolean;
|
|
246
|
+
/** Open the list when the field gets focus, by default; false opens it on a click, a key or typing only, for a page that puts focus back by id after it renders. */
|
|
247
|
+
openOnFocus?: boolean;
|
|
248
|
+
/**
|
|
249
|
+
* When the list is shown. `"auto"` (default): on focus, a click, a key or typing, and Escape, Tab, blur to another
|
|
250
|
+
* element or a press outside close it. `"whileText"`: the list is shown exactly while the field has text and an
|
|
251
|
+
* option matches it, whether or not the field has focus. Escape, Tab and a press outside only clear the highlighted
|
|
252
|
+
* option (`aria-activedescendant`); emptying the text or picking closes it, and there's no `empty` message. A
|
|
253
|
+
* list left open after the field loses focus is a screen reader caveat, see the README.
|
|
254
|
+
*/
|
|
255
|
+
open?: "auto" | "whileText";
|
|
256
|
+
filter?: (option: ComboboxOption, query: string) => boolean;
|
|
257
|
+
/** What shows when nothing matches. */
|
|
258
|
+
empty?: ReactNode;
|
|
259
|
+
},
|
|
260
|
+
): ReactElement;
|
|
261
|
+
export interface MenuItem {
|
|
262
|
+
id: string;
|
|
263
|
+
label: ReactNode;
|
|
264
|
+
onSelect: () => void;
|
|
265
|
+
/** "destructive" draws it in the danger colour, like shadcn/ui's DropdownMenuItem. */
|
|
266
|
+
variant?: "default" | "destructive";
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* A button, showing `label`, that opens a menu of `items` under it, on Base UI's Menu: the menu is a popup in the
|
|
270
|
+
* page. Arrows, Home and End move, Enter or Space picks, Escape closes and puts focus back on the button. `align` end
|
|
271
|
+
* lines the menu up with the button's right. `variant` and `size` are the Button's.
|
|
272
|
+
*/
|
|
273
|
+
export function Menu(props: Omit<Props<"button">, "children"> & { label: ReactNode; items: MenuItem[]; align?: "end"; variant?: ButtonVariant; size?: ButtonSize }): ReactElement;
|
|
274
|
+
/** More behind a `summary` line that opens it, on Base UI's Collapsible. `open` is controlled: give it `onOpenChange`, or use `defaultOpen`. */
|
|
275
|
+
export function Disclosure(
|
|
276
|
+
props: Omit<Props<"div">, "children"> & { summary: ReactNode; children?: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void },
|
|
277
|
+
): ReactElement;
|