wings-plugin-ui 0.0.0-stage → 0.1.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/LICENSE +18 -0
- package/README.md +179 -2
- package/package.json +44 -4
- package/plugin-ui.css +1292 -0
- package/ui.d.ts +276 -0
- package/ui.js +578 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Josip Papež
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,180 @@
|
|
|
1
|
-
#
|
|
1
|
+
# wings-plugin-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React components for [Wings](https://github.com/josippapez/Wings) plugin pages, and the stylesheet Wings draws them
|
|
4
|
+
with. The components are Base UI's, like the Wings app's own shadcn/ui ones, with shadcn's names and variants.
|
|
5
|
+
Build and pack the plugin with [wings-plugin-sdk](../plugin-sdk/README.md).
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install --save-dev wings-plugin-sdk wings-plugin-ui react react-dom @types/react @types/react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`wings-plugin-ui` lets a page say what it shows and where, and Wings decides how it looks. A page built from
|
|
12
|
+
it never touches the DOM, writes HTML or lays itself out with CSS, so when Wings moves or redesigns a place,
|
|
13
|
+
your page follows without a release.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Card, Gauge, Heading, showSidebar, useMainState } from "wings-plugin-ui";
|
|
17
|
+
|
|
18
|
+
type Usage = { type: "usage"; percent: number };
|
|
19
|
+
|
|
20
|
+
function Usage() {
|
|
21
|
+
// Says hello to your main script once, then shows the newest `usage` message it broadcasts.
|
|
22
|
+
const usage = useMainState<Usage>("usage");
|
|
23
|
+
if (!usage) return null;
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<Heading>Usage</Heading>
|
|
27
|
+
<Card>
|
|
28
|
+
<Gauge label="Today" percent={usage.percent} levels={[75, 90]} />
|
|
29
|
+
</Card>
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
showSidebar(<Usage />);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Places
|
|
38
|
+
|
|
39
|
+
`showSidebar`, `showPopover`, `showView`, `showTab` and `showSettings` take any React element, your own
|
|
40
|
+
components too, and mount it in that place's shell: Wings' padding, spacing and scrolling for a sidebar, a
|
|
41
|
+
popover, a view (in a modal or a window), a tab or the settings page. Call one per page. `{ fill: true }` lays
|
|
42
|
+
the page out edge to edge with no padding, for a page that scrolls on its own, like a canvas.
|
|
43
|
+
|
|
44
|
+
You don't write the page's HTML. For `src/sidebar.tsx` that calls one of these, `wings-plugin build` writes
|
|
45
|
+
`sidebar.html` with a mount point, its CSS and its script, titled like the manifest's page. A `sidebar.html` you
|
|
46
|
+
write yourself wins: the build only writes a page that's missing or that it wrote before (it carries a
|
|
47
|
+
`Generated by wings-plugin build` line). A popover's page takes its sidebar's title, the name Wings gives the
|
|
48
|
+
popover too. A page that builds its DOM itself, without React, gets the same shell
|
|
49
|
+
from `mountShell(place, options)` in `wings-plugin-sdk/shell`, which returns the element to fill.
|
|
50
|
+
|
|
51
|
+
## Talking to main
|
|
52
|
+
|
|
53
|
+
`useMainState<T>(type, hello?)` returns the newest message of `type` your plugin's other pages broadcast, or
|
|
54
|
+
`null` until one comes. It sends `hello` (`{ type: "hello" }` by default, `null` for none) once when the
|
|
55
|
+
component mounts, so main answers with what it has. `useBroadcast(listener)` from `wings-plugin-sdk/react` gets every
|
|
56
|
+
message.
|
|
57
|
+
|
|
58
|
+
## Components
|
|
59
|
+
|
|
60
|
+
| Component | Props | Renders |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `Row`, `Stack` | `gap` (`none`, `xs`, `s`, `m`, `l`), `grow`; `Row` also `align`, `justify`, `wrap`; `as` (`form`, `ul`, `ol` too) | Children side by side or one under the other; as a `ul` or `ol`, a list with no bullets. With `as`, the props and handlers are the element's: `<Row as="form" onSubmit={(e) => e.currentTarget.requestSubmit()}>` has a `HTMLFormElement` |
|
|
63
|
+
| `Text` | `tone` (`muted`, `info`, `success`, `warn`, `danger`), `size` (`small`, `tiny`), `weight` (`medium`, `strong`), `mono`, `numeric`, `truncate`, `clamp`, `grow`, `as` | A span, or the element in `as` |
|
|
64
|
+
| `Title` | `as` (`h1`, `h2`, `h3`), `size` (`large`), `truncate` | A page's or a card's title, like a sprint's name or a setup step's question |
|
|
65
|
+
| `Heading` | `level` (2, 3, 4), `actions` | A small section heading, `h2` by default, with chips or a button on its right |
|
|
66
|
+
| `Card` | `compact`, `as` (`form`, `ul`, `ol` too) | A card, a card that's a form, or a list. Typed for its `as` element like `Row` and `Stack` |
|
|
67
|
+
| `Gauge`, `Meter` | `label`, `percent`, `note`, `tone` or `levels` | A labelled bar with its percentage, or the bar alone, with the meter role |
|
|
68
|
+
| `Stats` | `items: { label, value, tone? }[]` | Small figures side by side, label over value |
|
|
69
|
+
| `Properties` | `items: { label, value }[]` | Label and value pairs |
|
|
70
|
+
| `List`, `ListItem`, `ListButton` | `ordered` and `compact` on `List`, `align` on the rows | A list of rows; a `ListButton` in a `ListItem` makes the row a button |
|
|
71
|
+
| `Dot`, `Spinner` | `tone`, `size`, `label`; `Dot` also `color` | A status dot or a spinner, announced only with a `label`. A dot's `color` comes from your data, like a work item type's `#cc293d` |
|
|
72
|
+
| `Pill`, `Tag` | `tone` | A filled pill, or an outlined tag like a branch |
|
|
73
|
+
| `Loading`, `Empty` | `Empty`: `title`, `detail` | Before the first data, and nothing to show |
|
|
74
|
+
| `Button` | `variant` (`primary`, `ghost`, `danger`), `size` (`small`), `busy` | A button, `type="button"`; `busy` shows a spinner and ignores clicks, keeping focus |
|
|
75
|
+
| `Chip`, `Chips` | `pressed`; `Chips`: `label`, `options`, `value`, `onChange` | A small quiet button, and a group of them that picks one, with `aria-pressed`. An option's other props, like `id`, go to its chip |
|
|
76
|
+
| `Tabs` | `label`, `items`, `value`, `onChange`, `children` | Tabs with arrow keys, Home and End, over their panel |
|
|
77
|
+
| `Field` | `label`, `hint`, `error`, `id`, `inline` | A labelled field that names and describes the input in it. `id` becomes the input's, for a page that puts focus back by id; `inline` puts the label beside it |
|
|
78
|
+
| `FieldGroup` | `label`, `hint`, `error` | Several controls under one label, like checkboxes: a `fieldset` with its `legend`, described by the hint; an error marks each checkbox invalid |
|
|
79
|
+
| `TextInput`, `SearchField`, `Select` | `label`; `Select`: `options`, `value`, `onChange` | Inputs Wings styles; outside a `Field`, `label` names them |
|
|
80
|
+
| `Checkbox` | `checked`, `onChange`, `children` | A checkbox with its label |
|
|
81
|
+
| `Combobox` | `label`, `value`, `onInput`, `options: { value, label, detail? }[]`, `onPick`, `onEnter`, `onKeyDown`, `freeText`, `openOnFocus`, `open`, `filter`, `empty` | A text field with suggestions under it: arrows, Enter, Escape and Tab, and a press outside closes it. With `freeText` it keeps what you type; otherwise it picks one of `options`. `openOnFocus={false}` for a page that puts focus back into it after a render. Without `freeText` the picked label is selected when the field gets focus and after a pick, so typing replaces it. `onEnter(text)` runs on Enter when no option is highlighted, with the typed text, and stops the Enter from submitting a form around it. An `onKeyDown` you pass runs first; `e.preventDefault()` in it skips the combobox's own keys. ArrowDown on a closed combobox opens it and highlights the first option, as in the [WAI-ARIA APG combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/). `open="whileText"` keeps the list shown for as long as the field has text and an option matches, even after Escape, Tab or a press outside (those only clear the highlighted option) and it closes when the text is emptied or an option is picked; `aria-expanded` follows whether the list is shown. Accessibility caveat: the APG pattern has Escape dismiss the popup and treats it as shown only while the user is working in the combobox, so this mode departs from it. Use it only where the suggestions are the page's main content, such as a people search that is useful to keep in view, and not for a field where a stray list would cover other controls |
|
|
82
|
+
| `Menu` | `label`, `items: { id, label, onSelect, tone? }[]`, `align`, `variant`, `size` | A button that opens a menu: arrows, Home, End, Enter, Escape |
|
|
83
|
+
| `Disclosure` | `summary` | More behind a line that opens it |
|
|
84
|
+
|
|
85
|
+
Every component passes other props, like `id`, `title`, `className`, `onClick` and `aria-*`, to its element.
|
|
86
|
+
`TextInput` and `SearchField` pass `ref` to the input on React 19.
|
|
87
|
+
|
|
88
|
+
A button that's working on something is `busy`: it shows a spinner, ignores clicks and says so with `aria-busy`
|
|
89
|
+
and `aria-disabled`, and unlike `disabled` it keeps the keyboard focus:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<Button variant="primary" busy={saving} onClick={save}>
|
|
93
|
+
Save
|
|
94
|
+
</Button>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Ours or your own
|
|
98
|
+
|
|
99
|
+
The components are there for a page that looks like the rest of Wings without effort, not a rule. Write your
|
|
100
|
+
own components next to them, and your own CSS: import it from the page, `import "./sidebar.css"`, and the build
|
|
101
|
+
bundles it into `sidebar.css` and links it from the generated page. Match Wings with its variables, which every
|
|
102
|
+
page has:
|
|
103
|
+
|
|
104
|
+
| Variable | What |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `--wings-fg`, `--wings-muted` | Text, and quieter text |
|
|
107
|
+
| `--wings-surface`, `--wings-hover`, `--wings-pressed`, `--wings-input` | Card, hovered, pressed and input backgrounds |
|
|
108
|
+
| `--wings-hairline`, `--wings-hairline-strong` | Borders |
|
|
109
|
+
| `--wings-ring` | The focus ring |
|
|
110
|
+
| `--wings-primary`, `--wings-primary-fg` | The primary button, and its text |
|
|
111
|
+
| `--wings-info`, `--wings-success`, `--wings-warning`, `--wings-danger` | Tones |
|
|
112
|
+
| `--wings-radius` | A card's corners |
|
|
113
|
+
| `--wings-menu-bg`, `--wings-menu-ring`, `--wings-menu-active`, `--wings-shadow` | A raised layer like a menu or a list of suggestions, as Wings' own menus draw it: its opaque background, its edge, the highlighted option and its shadow |
|
|
114
|
+
| `--wings-font`, `--wings-mono` | Fonts |
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// src/sidebar.tsx
|
|
118
|
+
import "./sidebar.css";
|
|
119
|
+
|
|
120
|
+
function Banner({ children }: { children: string }) {
|
|
121
|
+
return <div className="banner">{children}</div>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
/* src/sidebar.css */
|
|
127
|
+
.banner {
|
|
128
|
+
padding: 8px 10px;
|
|
129
|
+
border-radius: var(--wings-radius);
|
|
130
|
+
background: color-mix(in srgb, var(--wings-info) 14%, transparent);
|
|
131
|
+
color: var(--wings-info);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Wings' base styles cover plain elements (buttons, inputs, selects and the page's font and colours), the `w-*`
|
|
136
|
+
classes and two older helpers, `.muted` and `.card`; nothing else, so your other classes are yours. Your CSS
|
|
137
|
+
loads after Wings', so it wins where you style a component, like `.my-list .w-list-button { height: 24px; }`.
|
|
138
|
+
|
|
139
|
+
## The class contract
|
|
140
|
+
|
|
141
|
+
The components render plain elements with `w-*` class names and data attributes, like
|
|
142
|
+
`<div class="w-card" data-size="compact">`, and no styles of their own. Wings styles them in its
|
|
143
|
+
`plugin-ui.css`, which ships with the app, so a Wings update restyles every plugin. The names are a versioned
|
|
144
|
+
public contract. `Meter` sets `--w-value`, how full it is, and a `Dot` with a `color` sets `--w-color`; both are data
|
|
145
|
+
rather than styles.
|
|
146
|
+
|
|
147
|
+
`w-shell` with `data-place` (`sidebar`, `popover`, `view`, `tab`, `settings`) and `data-fill`; `w-row`,
|
|
148
|
+
`w-stack` with `data-gap`, `data-align`, `data-justify`, `data-wrap`, `data-grow`; `w-text` with `data-tone`,
|
|
149
|
+
`data-size`, `data-weight`, `data-mono`, `data-numeric`, `data-truncate`, `data-clamp`, `data-grow`;
|
|
150
|
+
`w-title` with `data-size`; `w-heading`, `w-heading-row`; `w-card` with `data-size`; `w-gauge`, `w-gauge-label`, `w-gauge-value`,
|
|
151
|
+
`w-gauge-note`, `w-meter` with `data-tone`, `w-meter-fill`; `w-stats`, `w-stat-label`, `w-stat-value`;
|
|
152
|
+
`w-props`, `w-prop`, `w-prop-label`, `w-prop-value`; `w-list` with `data-density`, `w-list-item`,
|
|
153
|
+
`w-list-button` with `data-align`; `w-dot` with `data-tone` or `data-color`; `w-pill`, `w-tag` with `data-tone`; `w-spinner` with `data-size`;
|
|
154
|
+
`w-loading`, `w-empty`, `w-empty-detail`; `w-button` with `data-variant`, `data-size`, `aria-disabled`; `w-chips`, `w-chip`;
|
|
155
|
+
`w-tabs`, `w-tab`; `w-field` with `data-layout`, `w-field-row`, `w-field-label`, `w-field-hint`,
|
|
156
|
+
`w-field-group`, `w-input`, `w-search`, `w-select`, `w-check`; `w-combobox`, `w-listbox`, `w-option`,
|
|
157
|
+
`w-option-label`, `w-option-detail`, `w-option-empty`; `w-menu-root`, `w-menu` with `data-align`, `w-menu-item`
|
|
158
|
+
with `data-tone`; `w-disclosure`. A `ul` or `ol` with `w-row`, `w-stack` or `w-card` loses its bullets.
|
|
159
|
+
|
|
160
|
+
## Tailwind or any CSS
|
|
161
|
+
|
|
162
|
+
Wings' styles sit in CSS cascade layers, declared before anything else on the page, in this order:
|
|
163
|
+
`wings-base, theme, base, wings-components, components, utilities`. Any CSS a page brings that isn't in a layer
|
|
164
|
+
wins over all of Wings'. Tailwind v4 fits in as it is: `@import "tailwindcss";` puts its reset in `base`, under the
|
|
165
|
+
components here, and its utilities in `utilities`, over them. So a page can use Tailwind for its own parts next to
|
|
166
|
+
these components, and both look right. Build Tailwind's CSS with its CLI before `wings-plugin build` and import
|
|
167
|
+
it from the page. Rem units mean 16px, the browser's root size, while the page's text is 13px.
|
|
168
|
+
|
|
169
|
+
## Linked packages
|
|
170
|
+
|
|
171
|
+
With the packages linked from a local folder (`npm link`, or `link:` in `package.json`) rather than installed,
|
|
172
|
+
TypeScript looks for React next to their folder. Point it at yours in `tsconfig.json`; the build already
|
|
173
|
+
does:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
"paths": {
|
|
177
|
+
"react": ["./node_modules/@types/react"],
|
|
178
|
+
"react/*": ["./node_modules/@types/react/*"]
|
|
179
|
+
}
|
|
180
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wings-plugin-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React components and the stylesheet for Wings plugin pages, on Base UI like the Wings app's shadcn/ui components.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "./ui.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./ui.d.ts",
|
|
11
|
+
"default": "./ui.js"
|
|
12
|
+
},
|
|
13
|
+
"./plugin-ui.css": "./plugin-ui.css"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"ui.js",
|
|
17
|
+
"ui.d.ts",
|
|
18
|
+
"plugin-ui.css",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/josippapez/Wings.git",
|
|
25
|
+
"directory": "packages/plugin-ui"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "tsc -p tsconfig.json && node --test"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@base-ui/react": "^1.8.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=19",
|
|
35
|
+
"react-dom": ">=19",
|
|
36
|
+
"wings-plugin-sdk": "^0.6.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^19.1.8",
|
|
40
|
+
"jsdom": "^30.1.2",
|
|
41
|
+
"react": "^19.1.0",
|
|
42
|
+
"react-dom": "^19.1.0",
|
|
43
|
+
"typescript": "~6.0.3",
|
|
44
|
+
"wings-plugin-sdk": "link:../plugin-sdk"
|
|
45
|
+
}
|
|
46
|
+
}
|