@leef-l/macos-ui-kit 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/README.md +215 -2
- package/dist/assets/Inter Font License.txt +93 -0
- package/dist/assets/Inter-Medium.ttf +0 -0
- package/dist/component.d.ts +47 -0
- package/dist/controls/badge.d.ts +27 -0
- package/dist/controls/button.d.ts +41 -0
- package/dist/controls/checkbox.d.ts +40 -0
- package/dist/controls/combobox.d.ts +105 -0
- package/dist/controls/datepicker.d.ts +42 -0
- package/dist/controls/disclosure.d.ts +40 -0
- package/dist/controls/numberfield.d.ts +60 -0
- package/dist/controls/progress.d.ts +33 -0
- package/dist/controls/segmented.d.ts +64 -0
- package/dist/controls/select.d.ts +42 -0
- package/dist/controls/slider.d.ts +49 -0
- package/dist/controls/spinner.d.ts +24 -0
- package/dist/controls/splitview.d.ts +76 -0
- package/dist/controls/switch.d.ts +47 -0
- package/dist/controls/table.d.ts +103 -0
- package/dist/controls/textarea.d.ts +49 -0
- package/dist/controls/textfield.d.ts +63 -0
- package/dist/dom.d.ts +35 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +2053 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/sidebar.d.ts +47 -0
- package/dist/layout/statusbar.d.ts +30 -0
- package/dist/layout/tabview.d.ts +74 -0
- package/dist/layout/titlebar.d.ts +49 -0
- package/dist/layout/toolbar.d.ts +54 -0
- package/dist/layout/traffic-lights.d.ts +37 -0
- package/dist/macos-ui-kit.css +2 -0
- package/dist/macos-ui-kit.iife.js +2 -0
- package/dist/macos-ui-kit.iife.js.map +1 -0
- package/dist/mobile/shell.d.ts +59 -0
- package/dist/overlay.d.ts +41 -0
- package/dist/overlays/dialog.d.ts +46 -0
- package/dist/overlays/menu.d.ts +58 -0
- package/dist/overlays/popover.d.ts +90 -0
- package/dist/overlays/sheet.d.ts +56 -0
- package/dist/overlays/toast.d.ts +37 -0
- package/dist/overlays/tooltip.d.ts +48 -0
- package/dist/style.css.d.ts +2 -0
- package/dist/theme.d.ts +100 -0
- package/package.json +55 -4
package/README.md
CHANGED
|
@@ -1,3 +1,216 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @leef-l/macos-ui-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
macOS-style UI components in dependency-free TypeScript. No React, no Vue, no
|
|
4
|
+
virtual DOM — just `document.createElement` wrappers over two-layer design
|
|
5
|
+
tokens, so one import gives you the whole design system.
|
|
6
|
+
|
|
7
|
+
- **30 components** — 17 controls, 6 layout, 6 overlays, 1 mobile shell
|
|
8
|
+
- **Zero runtime dependencies**
|
|
9
|
+
- **Light DOM only** — no Shadow DOM, so native window dragging and screen
|
|
10
|
+
readers both work
|
|
11
|
+
- **Runtime theming** — four built-in themes plus custom token overrides
|
|
12
|
+
- **Density scaling** — one `--mac-scale` factor resizes the whole interface
|
|
13
|
+
|
|
14
|
+
## Scope: frontend only
|
|
15
|
+
|
|
16
|
+
This package is **UI components and nothing else**. It contains no backend, no
|
|
17
|
+
server, no data layer and no build-time code generation.
|
|
18
|
+
|
|
19
|
+
- **No Go, no Wails runtime.** `package.json` declares no `dependencies` and no
|
|
20
|
+
`peerDependencies`; the built bundle has no imports at all. Nothing in `src/`
|
|
21
|
+
reads a `_wails` global, an IPC bridge, or a native API.
|
|
22
|
+
- **No framework.** Just DOM APIs. It runs in any browser, in an Electron or
|
|
23
|
+
Tauri webview, in a Wails window, or in a plain HTML page opened from disk.
|
|
24
|
+
- **No network calls.** Components never fetch anything. What you render is what
|
|
25
|
+
you pass in.
|
|
26
|
+
|
|
27
|
+
Wails v3 is a *target*, not a requirement: the drag regions and the mobile shell
|
|
28
|
+
were tuned for a frameless desktop window, and that is where the kit looks best.
|
|
29
|
+
In a normal browser those behaviours simply do nothing instead of failing.
|
|
30
|
+
|
|
31
|
+
Where the kit does touch host behaviour, it is through standard web APIs only:
|
|
32
|
+
|
|
33
|
+
| Behaviour | How it works | Outside a desktop shell |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Window dragging | `--wails-draggable` is a CSS custom property on the title bar | Ignored; nothing to drag |
|
|
36
|
+
| Double-click to zoom | Listens for `dblclick` | Does nothing |
|
|
37
|
+
| Mobile detection | `matchMedia("(pointer: coarse)")` + a width query | Works everywhere |
|
|
38
|
+
| Sidebar / overlays | Plain DOM + CSS | Works everywhere |
|
|
39
|
+
|
|
40
|
+
The Go application in this repository is a separate thing: a demo host that
|
|
41
|
+
happens to use these components. You do not need it, and nothing in this package
|
|
42
|
+
imports from it.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
### From npm (after the first publish)
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install @leef-l/macos-ui-kit
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Straight from a GitHub release — no npm account, no publish needed
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm install https://github.com/leef-l/macos-ui-kit/releases/download/v0.1.0/leef-l-macos-ui-kit-0.1.0.tgz
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This is a normal npm install: it lands in `node_modules` with the same files,
|
|
61
|
+
the same types and the same exports. Use it while the package is not on the
|
|
62
|
+
registry yet, or to pin an exact build.
|
|
63
|
+
|
|
64
|
+
### From a clone
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
git clone https://github.com/leef-l/macos-ui-kit
|
|
68
|
+
cd macos-ui-kit/packages/macos-ui-kit
|
|
69
|
+
npm install
|
|
70
|
+
npm run build
|
|
71
|
+
|
|
72
|
+
# in your project
|
|
73
|
+
npm install /absolute/path/to/packages/macos-ui-kit
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### No build tooling at all
|
|
77
|
+
|
|
78
|
+
Download `dist/macos-ui-kit.iife.js` and `dist/macos-ui-kit.css` from a release
|
|
79
|
+
(or open the files in `packages/macos-ui-kit/dist/` after building) and use the
|
|
80
|
+
global `MacUI`:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<link rel="stylesheet" href="./macos-ui-kit.css" />
|
|
84
|
+
<script src="./macos-ui-kit.iife.js"></script>
|
|
85
|
+
<script>
|
|
86
|
+
const button = new MacUI.Button({label: "Hello", variant: "primary"});
|
|
87
|
+
document.body.appendChild(button.element);
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The IIFE build needs the `assets/` folder next to the stylesheet, because the
|
|
92
|
+
stylesheet references the Inter font by relative path.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Use
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import {Button, TextField, showToast} from "@leef-l/macos-ui-kit";
|
|
100
|
+
// The stylesheet is separate: the JS bundle contains no CSS.
|
|
101
|
+
import "@leef-l/macos-ui-kit/style.css";
|
|
102
|
+
|
|
103
|
+
const field = new TextField({placeholder: "Your name"});
|
|
104
|
+
document.body.appendChild(field.element);
|
|
105
|
+
|
|
106
|
+
const button = new Button({
|
|
107
|
+
label: "Greet",
|
|
108
|
+
variant: "primary",
|
|
109
|
+
onClick: () => showToast(`Hello ${field.value}`),
|
|
110
|
+
});
|
|
111
|
+
document.body.appendChild(button.element);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every component exposes `element`, which is the node you mount. Construct it,
|
|
115
|
+
append `element`, and keep the instance for the methods.
|
|
116
|
+
|
|
117
|
+
### Whole kit or a single component
|
|
118
|
+
|
|
119
|
+
Both are the same import — take only what you use:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// Whole kit, when you want everything
|
|
123
|
+
import * as ui from "@leef-l/macos-ui-kit";
|
|
124
|
+
|
|
125
|
+
// One component, when you want one
|
|
126
|
+
import {Button} from "@leef-l/macos-ui-kit";
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
There is one entry point, and the ESM build is tree-shakeable: a bundler drops
|
|
130
|
+
every component you do not reference, provided the package's `sideEffects` field
|
|
131
|
+
is respected (Vite, Rollup, webpack and esbuild all do).
|
|
132
|
+
|
|
133
|
+
**Honest caveat:** the JavaScript is tree-shaken, the **stylesheet is not**. CSS
|
|
134
|
+
ships as a single 63 KB file and every component's rules are in it. Splitting it
|
|
135
|
+
per component is not worth the extra requests for a desktop window, but if you
|
|
136
|
+
are shipping to a slow network and use three components, know that you still
|
|
137
|
+
download the rules for thirty.
|
|
138
|
+
|
|
139
|
+
### Theming
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import {themeManager, setDensity} from "@leef-l/macos-ui-kit";
|
|
143
|
+
|
|
144
|
+
themeManager.apply("macos-dark"); // macos-dark | macos-light | hc-dark | hc-light
|
|
145
|
+
setDensity("compact"); // compact | comfortable | touch
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or override tokens in your own stylesheet — components read semantic tokens
|
|
149
|
+
only, so one declaration restyles every component:
|
|
150
|
+
|
|
151
|
+
```css
|
|
152
|
+
:root {
|
|
153
|
+
--mac-accent: #7c5cff;
|
|
154
|
+
--mac-radius-m: 10px;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## API convention
|
|
161
|
+
|
|
162
|
+
Three rules, applied to all 30 components, so a control you have never used
|
|
163
|
+
behaves the way you would guess:
|
|
164
|
+
|
|
165
|
+
| Concern | Rule | Examples |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| **Read state** | property getter | `field.value`, `toggle.checked`, `sheet.isOpen` |
|
|
168
|
+
| **Write state** | `setX()`, always silent | `setValue()`, `setChecked()`, `setSelected()` |
|
|
169
|
+
| **Dismissible overlays** | `open(anchor?)` / `close()` / `isOpen` | `dialog.open()`, `popover.open(btn)` |
|
|
170
|
+
|
|
171
|
+
`setX()` never fires the change callback — only a user interaction does. That
|
|
172
|
+
makes programmatic writes safe to call from a render path.
|
|
173
|
+
|
|
174
|
+
`Toast` is the one exception: it is a notification, not an overlay, so it uses
|
|
175
|
+
`show()` / `hide()`.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Components
|
|
180
|
+
|
|
181
|
+
| Group | Components |
|
|
182
|
+
|---|---|
|
|
183
|
+
| Controls | `Button` `TextField` `TextArea` `NumberField` `Slider` `Progress` `Spinner` `Badge` `Switch` `Checkbox` `SegmentedControl` `Select` `ComboBox` `DatePicker` `Disclosure` `Table` `SplitView` |
|
|
184
|
+
| Layout | `TitleBar` `TrafficLights` `Sidebar` `StatusBar` `Toolbar` `TabView` |
|
|
185
|
+
| Overlays | `Menu` `Tooltip` `Dialog` `Popover` `Sheet` `Toast` |
|
|
186
|
+
| Mobile | `MobileShell` |
|
|
187
|
+
|
|
188
|
+
Every component also exposes `element`, `destroy()` (releases listeners and any
|
|
189
|
+
observer), and `focus()` where focusing makes sense.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Files
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
dist/index.js ESM bundle (import in a bundler)
|
|
197
|
+
dist/macos-ui-kit.iife.js IIFE, global MacUI (<script> tag)
|
|
198
|
+
dist/macos-ui-kit.css one stylesheet
|
|
199
|
+
dist/index.d.ts type declarations
|
|
200
|
+
dist/assets/ Inter font + its OFL licence
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Fonts
|
|
204
|
+
|
|
205
|
+
Apple's SF Pro cannot be redistributed, so the kit ships **Inter** (SIL Open
|
|
206
|
+
Font License 1.1, included in `dist/assets`) as an SF-alike fallback. The font
|
|
207
|
+
stack puts the real system font first on macOS, so Apple users see SF Pro and
|
|
208
|
+
everyone else sees near-identical glyphs.
|
|
209
|
+
|
|
210
|
+
The stylesheet references the font as `./assets/Inter-Medium.ttf`. If your
|
|
211
|
+
bundler relocates CSS assets, copy `dist/assets/` alongside the stylesheet — or
|
|
212
|
+
drop the `@font-face` block and provide your own.
|
|
213
|
+
|
|
214
|
+
## Licence
|
|
215
|
+
|
|
216
|
+
MIT. Bundled Inter typeface: SIL Open Font License 1.1.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for every macOS UI Kit component.
|
|
3
|
+
*
|
|
4
|
+
* It owns two things that raw DOM code gets wrong most often:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Listener lifetime.** Vanilla UI leaks because listeners outlive the
|
|
7
|
+
* elements they touch. `listen` records every binding so `destroy()` can
|
|
8
|
+
* remove all of them in one pass.
|
|
9
|
+
* 2. **A stable root element.** Subclasses build their markup in the
|
|
10
|
+
* constructor; callers only ever see `element`, which keeps the internal DOM
|
|
11
|
+
* structure free to change.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately light DOM: shadow roots would retarget `event.target`, and
|
|
14
|
+
* Wails' window-drag detection reads exactly that property
|
|
15
|
+
* (`internal/runtime/.../drag.ts` uses `event.target`, never `composedPath`).
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class Component<T extends HTMLElement = HTMLElement> {
|
|
18
|
+
/** The root element. Subclasses fill this in before the constructor returns. */
|
|
19
|
+
protected abstract readonly root: T;
|
|
20
|
+
private readonly cleanups;
|
|
21
|
+
private destroyed;
|
|
22
|
+
/** The component's root element, for mounting into the page. */
|
|
23
|
+
get element(): T;
|
|
24
|
+
/** True once `destroy()` has run. */
|
|
25
|
+
get isDestroyed(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Add an event listener whose removal `destroy()` handles.
|
|
28
|
+
*
|
|
29
|
+
* Prefer this over `addEventListener` everywhere: a component that is torn
|
|
30
|
+
* down mid-session (a closed dialog, a re-rendered list) must not leave
|
|
31
|
+
* handlers pointing at detached nodes.
|
|
32
|
+
*/
|
|
33
|
+
protected listen<K extends keyof HTMLElementEventMap>(target: T | HTMLElement | Document | Window, type: K, handler: (event: HTMLElementEventMap[K]) => void, options?: AddEventListenerOptions): void;
|
|
34
|
+
protected listen(target: T | HTMLElement | Document | Window, type: string, handler: EventListenerOrEventListenerObject, options?: AddEventListenerOptions): void;
|
|
35
|
+
/** Register an arbitrary teardown step (timers, observers, subscriptions). */
|
|
36
|
+
protected onDestroy(cleanup: () => void): void;
|
|
37
|
+
/** Reflect a boolean state as a class on the root, skipping no-op writes. */
|
|
38
|
+
protected toggleClass(name: string, on: boolean): void;
|
|
39
|
+
/** Set an attribute only when its value actually changes. */
|
|
40
|
+
protected setAttr(name: string, value: string): void;
|
|
41
|
+
/** Mount the component into a parent, replacing that parent's content. */
|
|
42
|
+
mount(parent?: Element): this;
|
|
43
|
+
/** Remove the component from the document and release everything it holds. */
|
|
44
|
+
remove(): void;
|
|
45
|
+
/** Tear down listeners and timers. Safe to call more than once. */
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Component } from "../component";
|
|
2
|
+
import "./badge.css";
|
|
3
|
+
export type BadgeVariant = "default" | "accent" | "success" | "warning" | "danger";
|
|
4
|
+
export interface BadgeOptions {
|
|
5
|
+
/** Badge text. */
|
|
6
|
+
text: string;
|
|
7
|
+
/** Colour treatment. Default `default` (neutral grey). */
|
|
8
|
+
variant?: BadgeVariant;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A small pill used to tag rows, counts and states.
|
|
12
|
+
*
|
|
13
|
+
* Renders inline, so a badge dropped inside a sidebar row or a paragraph sits
|
|
14
|
+
* on the text baseline rather than breaking that row's layout. The text is
|
|
15
|
+
* written with `textContent` only — a badge is the component most likely to
|
|
16
|
+
* receive a server-supplied string, so it never goes anywhere near HTML.
|
|
17
|
+
*/
|
|
18
|
+
export declare class Badge extends Component<HTMLElement> {
|
|
19
|
+
protected readonly root: HTMLElement;
|
|
20
|
+
private text;
|
|
21
|
+
private variant;
|
|
22
|
+
constructor(options: BadgeOptions);
|
|
23
|
+
/** Replace the badge text. */
|
|
24
|
+
setText(text: string): void;
|
|
25
|
+
/** Swap the colour treatment. */
|
|
26
|
+
setVariant(variant: BadgeVariant): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Component } from "../component";
|
|
2
|
+
import { type Child } from "../dom";
|
|
3
|
+
import "./button.css";
|
|
4
|
+
export type ButtonVariant = "default" | "primary" | "destructive" | "plain";
|
|
5
|
+
export type ButtonSize = "small" | "default" | "large";
|
|
6
|
+
export interface ButtonOptions {
|
|
7
|
+
/** Visible label. Omit for an icon-only button (then set `ariaLabel`). */
|
|
8
|
+
label?: Child;
|
|
9
|
+
/** Leading SVG path data, drawn at the control's icon size. */
|
|
10
|
+
icon?: string;
|
|
11
|
+
variant?: ButtonVariant;
|
|
12
|
+
size?: ButtonSize;
|
|
13
|
+
/** Accessible name; required when there is no visible label. */
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
/** Stretch to the container's width. */
|
|
16
|
+
block?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Native `type`, so a button inside a form does not submit by accident. */
|
|
19
|
+
type?: "button" | "submit" | "reset";
|
|
20
|
+
/** Fires on activation (click, Enter, Space — the browser handles the rest). */
|
|
21
|
+
onClick?: (event: MouseEvent) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A native `<button>` with macOS styling.
|
|
25
|
+
*
|
|
26
|
+
* Built on the real element rather than a `div[role=button]` so keyboard
|
|
27
|
+
* activation, disabled handling and focus order come from the browser.
|
|
28
|
+
*/
|
|
29
|
+
export declare class Button extends Component<HTMLButtonElement> {
|
|
30
|
+
protected readonly root: HTMLButtonElement;
|
|
31
|
+
private readonly labelNode;
|
|
32
|
+
constructor(options?: ButtonOptions);
|
|
33
|
+
/** Replace the visible label text. */
|
|
34
|
+
setLabel(text: string): void;
|
|
35
|
+
/** Enable or disable the button. */
|
|
36
|
+
setDisabled(disabled: boolean): void;
|
|
37
|
+
/** Toggle the primary (blue) treatment at runtime. */
|
|
38
|
+
setPrimary(primary: boolean): void;
|
|
39
|
+
/** Give the caller a way to move focus programmatically. */
|
|
40
|
+
focus(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Component } from "../component";
|
|
2
|
+
import { type Child } from "../dom";
|
|
3
|
+
import "./checkbox.css";
|
|
4
|
+
export interface CheckboxOptions {
|
|
5
|
+
/** Initial ticked state. Default false. */
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
/** Show the dash instead of the tick; it takes precedence over `checked`. */
|
|
8
|
+
indeterminate?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Text rendered next to the box; it also names the control for assistive tech. */
|
|
11
|
+
label?: Child;
|
|
12
|
+
/** Fires with the new state after a user toggle; setters stay silent. */
|
|
13
|
+
onChange?: (checked: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A styled checkbox built around a real `<input type="checkbox">`.
|
|
17
|
+
*
|
|
18
|
+
* The input stays in the document — clipped to a pixel, not `display: none` —
|
|
19
|
+
* so it keeps its place in the tab order, its focus ring and its click target.
|
|
20
|
+
* The drawn box is pure decoration: `:checked` and `:indeterminate` in CSS
|
|
21
|
+
* follow the input, and one native `change` listener mirrors the result to the
|
|
22
|
+
* caller. Space, screen readers and form submission therefore stay native;
|
|
23
|
+
* nothing in this component toggles state behind the browser's back.
|
|
24
|
+
*/
|
|
25
|
+
export declare class Checkbox extends Component<HTMLElement> {
|
|
26
|
+
protected readonly root: HTMLLabelElement;
|
|
27
|
+
private readonly input;
|
|
28
|
+
private readonly onChange?;
|
|
29
|
+
constructor(options?: CheckboxOptions);
|
|
30
|
+
/** The current ticked state. */
|
|
31
|
+
get checked(): boolean;
|
|
32
|
+
/** Set the ticked state; a write that changes nothing is skipped, and `onChange` stays silent. */
|
|
33
|
+
setChecked(checked: boolean): void;
|
|
34
|
+
/** Show the mixed state (a dash instead of the tick). */
|
|
35
|
+
setIndeterminate(indeterminate: boolean): void;
|
|
36
|
+
/** Enable or disable the checkbox, using the native attribute. */
|
|
37
|
+
setDisabled(disabled: boolean): void;
|
|
38
|
+
/** Move keyboard focus onto the native input. */
|
|
39
|
+
focus(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Component } from "../component";
|
|
2
|
+
import "./combobox.css";
|
|
3
|
+
/** One suggestion in the list. */
|
|
4
|
+
export interface ComboBoxOption {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ComboBoxOptions {
|
|
9
|
+
/** Full suggestion list; typing filters it. */
|
|
10
|
+
options: ComboBoxOption[];
|
|
11
|
+
/** Initially selected value; its label fills the field. */
|
|
12
|
+
value?: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Custom rendering for a suggestion row. Returning a `Node` lets the list
|
|
16
|
+
* hold live UI; returning a string renders as text. The list keeps its own
|
|
17
|
+
* ARIA wiring around whatever is returned.
|
|
18
|
+
*/
|
|
19
|
+
renderOption?: (option: ComboBoxOption) => Node | string;
|
|
20
|
+
/** Fires when a suggestion is committed (click, Enter or Tab). */
|
|
21
|
+
onChange?: (value: string) => void;
|
|
22
|
+
/** Fires on every keystroke with the raw text. */
|
|
23
|
+
onInput?: (text: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A macOS combo box: a text field with an editable suggestion list.
|
|
27
|
+
*
|
|
28
|
+
* The list is absolutely positioned inside the component's own box rather than
|
|
29
|
+
* appended to `document.body` like the overlay components. A combo box's
|
|
30
|
+
* suggestions are conceptually part of the field, so staying in the container
|
|
31
|
+
* avoids portal teardown, scroll re-positioning and a `z-index` war with the
|
|
32
|
+
* dialogs above it. The trade-off is that an ancestor with `overflow: hidden`
|
|
33
|
+
* clips the list — acceptable for a control that rarely sits at the edge of a
|
|
34
|
+
* clipped card.
|
|
35
|
+
*
|
|
36
|
+
* Implements the ARIA combobox pattern: the input owns `role="combobox"`,
|
|
37
|
+
* `aria-expanded` and `aria-activedescendant`; the list is a `role="listbox"`
|
|
38
|
+
* of `role="option"` rows. Focus never leaves the input, so typing continues
|
|
39
|
+
* while the arrow keys move the highlight.
|
|
40
|
+
*/
|
|
41
|
+
export declare class ComboBox extends Component<HTMLDivElement> {
|
|
42
|
+
protected readonly root: HTMLDivElement;
|
|
43
|
+
private readonly input;
|
|
44
|
+
private readonly list;
|
|
45
|
+
private readonly onChange?;
|
|
46
|
+
private readonly onInput?;
|
|
47
|
+
private readonly renderOption?;
|
|
48
|
+
private readonly listId;
|
|
49
|
+
private options;
|
|
50
|
+
/** Options currently drawn, i.e. after filtering. */
|
|
51
|
+
private filtered;
|
|
52
|
+
/** Option rows, index-aligned with `filtered`. */
|
|
53
|
+
private rows;
|
|
54
|
+
/** Highlighted row index; -1 means nothing is highlighted. */
|
|
55
|
+
private active;
|
|
56
|
+
/**
|
|
57
|
+
* Value of the committed option, or null once the text is free-form.
|
|
58
|
+
* Kept separately from `input.value`, because the field shows a label while
|
|
59
|
+
* the caller may have handed us a machine value (an id, a slug).
|
|
60
|
+
*/
|
|
61
|
+
private selectedValue;
|
|
62
|
+
constructor(options: ComboBoxOptions);
|
|
63
|
+
/**
|
|
64
|
+
* Show every option.
|
|
65
|
+
*
|
|
66
|
+
* Opening from a click or a focus must not filter by whatever text is
|
|
67
|
+
* already in the field: a field showing "张三" would otherwise open to a
|
|
68
|
+
* single row, which reads as a broken control. Typing is the only thing that
|
|
69
|
+
* narrows the list, and that path filters explicitly.
|
|
70
|
+
*/
|
|
71
|
+
private openWithFullList;
|
|
72
|
+
/**
|
|
73
|
+
* The committed value.
|
|
74
|
+
*
|
|
75
|
+
* The highlight wins (an arrowed-onto row is what the user means), then the
|
|
76
|
+
* value behind the current label, and only then the raw text. Going through
|
|
77
|
+
* `selectedValue` is what keeps a field showing "Apricot" answering with its
|
|
78
|
+
* machine value "b".
|
|
79
|
+
*/
|
|
80
|
+
get value(): string;
|
|
81
|
+
/**
|
|
82
|
+
* Set the text and value programmatically.
|
|
83
|
+
*
|
|
84
|
+
* A value that matches an option shows that option's label, so the field
|
|
85
|
+
* never displays a bare id where a name belongs. Silent, like every setter.
|
|
86
|
+
*/
|
|
87
|
+
setValue(value: string): void;
|
|
88
|
+
/** Replace the suggestion list; the current text is kept and re-filtered. */
|
|
89
|
+
setOptions(options: ComboBoxOption[]): void;
|
|
90
|
+
/** Move keyboard focus into the field and show the full list. */
|
|
91
|
+
focus(): void;
|
|
92
|
+
private optionFor;
|
|
93
|
+
/** Rebuild the visible rows for the given text. */
|
|
94
|
+
private filter;
|
|
95
|
+
private open;
|
|
96
|
+
private close;
|
|
97
|
+
/** Move the highlight by `step` rows, wrapping at both ends. */
|
|
98
|
+
private move;
|
|
99
|
+
/** Mirror `active` into the DOM and the input's ARIA wiring. */
|
|
100
|
+
private syncActive;
|
|
101
|
+
private onKeyDown;
|
|
102
|
+
private onListClick;
|
|
103
|
+
/** Accept an option: fill the field, close the list, report the value. */
|
|
104
|
+
private commit;
|
|
105
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Component } from "../component";
|
|
2
|
+
import "./datepicker.css";
|
|
3
|
+
export interface DatePickerOptions {
|
|
4
|
+
/** Initial date as `YYYY-MM-DD`. */
|
|
5
|
+
value?: string;
|
|
6
|
+
/** Earliest selectable date, inclusive, as `YYYY-MM-DD`. */
|
|
7
|
+
min?: string;
|
|
8
|
+
/** Latest selectable date, inclusive, as `YYYY-MM-DD`. */
|
|
9
|
+
max?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Optional label rendered above the control and linked through `for`/`id`. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Fires with `YYYY-MM-DD` (or "" when cleared) once the value is committed. */
|
|
14
|
+
onChange?: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A macOS date picker wrapping a native `<input type="date">`.
|
|
18
|
+
*
|
|
19
|
+
* The native control is kept, not reimplemented: it brings the platform
|
|
20
|
+
* calendar popup, keyboard editing of each date segment, locale-correct
|
|
21
|
+
* formatting, `min`/`max` enforcement and the iOS wheel picker. A hand-built
|
|
22
|
+
* calendar would have to re-earn all of that and would still not match the OS.
|
|
23
|
+
*
|
|
24
|
+
* What this component adds is purely visual — the macOS field chrome and a
|
|
25
|
+
* trailing calendar icon that is `pointer-events: none`, so the click falls
|
|
26
|
+
* through to the native picker indicator underneath.
|
|
27
|
+
*/
|
|
28
|
+
export declare class DatePicker extends Component<HTMLDivElement> {
|
|
29
|
+
protected readonly root: HTMLDivElement;
|
|
30
|
+
private readonly input;
|
|
31
|
+
private readonly labelNode;
|
|
32
|
+
private readonly onChange?;
|
|
33
|
+
constructor(options?: DatePickerOptions);
|
|
34
|
+
/** The current date as `YYYY-MM-DD`, or "" when empty. */
|
|
35
|
+
get value(): string;
|
|
36
|
+
/** Set the date; an empty string clears it. Silent, like every setter. */
|
|
37
|
+
setValue(value: string): void;
|
|
38
|
+
/** Enable or disable the field, using the native attribute. */
|
|
39
|
+
setDisabled(disabled: boolean): void;
|
|
40
|
+
/** Move keyboard focus into the field. */
|
|
41
|
+
focus(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Component } from "../component";
|
|
2
|
+
import { type Child } from "../dom";
|
|
3
|
+
import "./disclosure.css";
|
|
4
|
+
export interface DisclosureOptions {
|
|
5
|
+
/** Section title shown in the header. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** Collapsible content. */
|
|
8
|
+
content?: Child;
|
|
9
|
+
/** Start expanded. Defaults to false, the macOS convention. */
|
|
10
|
+
open?: boolean;
|
|
11
|
+
/** Optional trailing summary, visible whether open or closed. */
|
|
12
|
+
meta?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onToggle?: (open: boolean) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A collapsible section.
|
|
18
|
+
*
|
|
19
|
+
* Uses `aria-expanded` on the header button and `hidden` on the region, which is
|
|
20
|
+
* the pattern screen readers understand for a disclosure widget: the button
|
|
21
|
+
* announces whether the section is open, and the content is genuinely removed
|
|
22
|
+
* from the accessibility tree when closed rather than merely scrolled away.
|
|
23
|
+
*/
|
|
24
|
+
export declare class Disclosure extends Component<HTMLDivElement> {
|
|
25
|
+
protected readonly root: HTMLDivElement;
|
|
26
|
+
private readonly header;
|
|
27
|
+
private readonly content;
|
|
28
|
+
private readonly metaNode;
|
|
29
|
+
private readonly onToggle?;
|
|
30
|
+
private open;
|
|
31
|
+
constructor(options: DisclosureOptions);
|
|
32
|
+
/** Expand or collapse the section. */
|
|
33
|
+
setOpen(open: boolean, notify?: boolean): void;
|
|
34
|
+
/** Whether the section is currently expanded. */
|
|
35
|
+
get isOpen(): boolean;
|
|
36
|
+
/** Update the trailing summary text. */
|
|
37
|
+
setMeta(meta: string): void;
|
|
38
|
+
/** The region content is appended to after construction. */
|
|
39
|
+
get contentElement(): HTMLDivElement;
|
|
40
|
+
}
|