@expressive/ui 0.0.0 → 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/README.md +47 -1
- package/dist/button/base.d.ts +63 -0
- package/dist/button/base.js +90 -0
- package/dist/button/base.js.map +1 -0
- package/dist/button/button.d.ts +56 -0
- package/dist/button/button.js +138 -0
- package/dist/button/button.js.map +1 -0
- package/dist/calendar/base.d.ts +112 -0
- package/dist/calendar/base.js +235 -0
- package/dist/calendar/base.js.map +1 -0
- package/dist/calendar/calendar.d.ts +78 -0
- package/dist/calendar/calendar.js +145 -0
- package/dist/calendar/calendar.js.map +1 -0
- package/dist/calendar/date.js +56 -0
- package/dist/calendar/date.js.map +1 -0
- package/dist/dialog/base.d.ts +74 -0
- package/dist/dialog/base.js +143 -0
- package/dist/dialog/base.js.map +1 -0
- package/dist/dialog/dialog.d.ts +69 -0
- package/dist/dialog/dialog.js +108 -0
- package/dist/dialog/dialog.js.map +1 -0
- package/dist/dom.d.ts +16 -0
- package/dist/dom.js +17 -0
- package/dist/dropzone/base.d.ts +77 -0
- package/dist/dropzone/base.js +159 -0
- package/dist/dropzone/base.js.map +1 -0
- package/dist/dropzone/dropzone.d.ts +38 -0
- package/dist/dropzone/dropzone.js +92 -0
- package/dist/dropzone/dropzone.js.map +1 -0
- package/dist/form/base.d.ts +157 -0
- package/dist/form/base.js +243 -0
- package/dist/form/base.js.map +1 -0
- package/dist/form/form.d.ts +123 -0
- package/dist/form/form.js +179 -0
- package/dist/form/form.js.map +1 -0
- package/dist/icons/caret.d.ts +8 -0
- package/dist/icons/caret.js +16 -0
- package/dist/icons/caret.js.map +1 -0
- package/dist/icons/check.d.ts +8 -0
- package/dist/icons/check.js +16 -0
- package/dist/icons/check.js.map +1 -0
- package/dist/icons/cross.d.ts +8 -0
- package/dist/icons/cross.js +16 -0
- package/dist/icons/cross.js.map +1 -0
- package/dist/icons/icon.js +16 -0
- package/dist/icons/icon.js.map +1 -0
- package/dist/icons/spinner.d.ts +8 -0
- package/dist/icons/spinner.js +27 -0
- package/dist/icons/spinner.js.map +1 -0
- package/dist/icons/upload.d.ts +8 -0
- package/dist/icons/upload.js +16 -0
- package/dist/icons/upload.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/layout/base.d.ts +88 -0
- package/dist/layout/base.js +158 -0
- package/dist/layout/base.js.map +1 -0
- package/dist/layout/layout.d.ts +53 -0
- package/dist/layout/layout.js +80 -0
- package/dist/layout/layout.js.map +1 -0
- package/dist/piece.js +13 -0
- package/dist/piece.js.map +1 -0
- package/dist/popover/base.d.ts +94 -0
- package/dist/popover/base.js +176 -0
- package/dist/popover/base.js.map +1 -0
- package/dist/popover/place.d.ts +12 -0
- package/dist/popover/place.js +32 -0
- package/dist/popover/place.js.map +1 -0
- package/dist/popover/popover.d.ts +52 -0
- package/dist/popover/popover.js +104 -0
- package/dist/popover/popover.js.map +1 -0
- package/dist/select/base.d.ts +196 -0
- package/dist/select/base.js +409 -0
- package/dist/select/base.js.map +1 -0
- package/dist/select/select.d.ts +119 -0
- package/dist/select/select.js +219 -0
- package/dist/select/select.js.map +1 -0
- package/dist/table/base.d.ts +96 -0
- package/dist/table/base.js +154 -0
- package/dist/table/base.js.map +1 -0
- package/dist/table/column.d.ts +53 -0
- package/dist/table/column.js +29 -0
- package/dist/table/column.js.map +1 -0
- package/dist/table/table.d.ts +41 -0
- package/dist/table/table.js +101 -0
- package/dist/table/table.js.map +1 -0
- package/package.json +68 -3
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
1
|
# @expressive/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Components for [Expressive](https://github.com/gabeklein/expressive) apps - every piece a function component you can swap by prop or subclass.
|
|
4
|
+
|
|
5
|
+
`Button`, `Calendar`, `Dialog`, `Dropzone`, `Form` (`Field`, `Submit`), `Layout`, `Popover`, `Select`, `Table` (`Column`).
|
|
6
|
+
|
|
7
|
+
Two entries export the same names:
|
|
8
|
+
|
|
9
|
+
- `@expressive/ui/dom` - styled for `@expressive/dom`, plus the icons `Caret`, `Check`, `Cross`, `Spinner`, `Upload`.
|
|
10
|
+
- `@expressive/ui` - the same components without a look: state, keyboard, focus and ARIA, rendered through pieces that throw until a subclass provides them. No `@expressive/dom` import, so any mvc host can skin them.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { Dialog } from '@expressive/ui/dom';
|
|
14
|
+
|
|
15
|
+
const Tall = (props) => <Dialog.Panel _tall {...props} />;
|
|
16
|
+
|
|
17
|
+
<Dialog Panel={Tall} />;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A skin of your own spreads the base's `*Props` getters onto its elements and stays accessible:
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { Select } from '@expressive/ui';
|
|
24
|
+
|
|
25
|
+
class Fruit extends Select {
|
|
26
|
+
Option: Select.Item = ({ option, index }) => {
|
|
27
|
+
const { active, props } = Select.get().optionProps(option, index);
|
|
28
|
+
|
|
29
|
+
return <li {...props} _active={active}>{option.label}</li>;
|
|
30
|
+
};
|
|
31
|
+
// Root, Control, Values, Value, Input, Toggle, Menu likewise
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @expressive/ui @expressive/mvc @expressive/dom
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`@expressive/dom` is needed only for `/dom`. Scope and status in [FEATURE.md](FEATURE.md).
|
|
40
|
+
|
|
41
|
+
## Develop
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun install
|
|
45
|
+
bun run test # typecheck + coverage (100% gate)
|
|
46
|
+
bun run build
|
|
47
|
+
bun run dev # examples site with live source, http://localhost:3200
|
|
48
|
+
bun run e2e # Playwright specs beside each example
|
|
49
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Component, Model } from "@expressive/mvc";
|
|
2
|
+
|
|
3
|
+
//#region src/button/base.d.ts
|
|
4
|
+
declare namespace Button {
|
|
5
|
+
type Variant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
6
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
7
|
+
type Status = 'idle' | 'pending' | 'done' | 'error';
|
|
8
|
+
type Wrap = (props: {
|
|
9
|
+
children?: Component.Node;
|
|
10
|
+
}) => Component.Node;
|
|
11
|
+
type Mark = (props: {
|
|
12
|
+
status: Status;
|
|
13
|
+
}) => Component.Node;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Button behavior without a look: press handling, async status, debounce and ARIA, assembled from pieces.
|
|
17
|
+
*
|
|
18
|
+
* `Root` and `Label` are placeholders that throw when rendered - a skin assigns them. `Status` renders nothing
|
|
19
|
+
* unless assigned. `Root` spreads `rootProps.props` onto its element to stay accessible.
|
|
20
|
+
*/
|
|
21
|
+
declare class Button extends Component {
|
|
22
|
+
onClick?: (event: MouseEvent) => unknown;
|
|
23
|
+
onError?: (error: Error) => void;
|
|
24
|
+
type: 'button' | 'submit' | 'reset';
|
|
25
|
+
variant: Button.Variant;
|
|
26
|
+
size: Button.Size;
|
|
27
|
+
disabled: boolean;
|
|
28
|
+
/** Show as pending regardless of `onClick` - for work started elsewhere. */
|
|
29
|
+
pending: boolean;
|
|
30
|
+
/** Milliseconds after an accepted press during which presses are dropped. */
|
|
31
|
+
debounce: number;
|
|
32
|
+
/** Milliseconds a done or error status shows before returning to idle. */
|
|
33
|
+
settle: number;
|
|
34
|
+
status: Button.Status;
|
|
35
|
+
error?: Error;
|
|
36
|
+
Root: Button.Wrap;
|
|
37
|
+
Label: Button.Wrap;
|
|
38
|
+
Status: Button.Mark;
|
|
39
|
+
constructor(...args: Model.Args);
|
|
40
|
+
/** Whether a press would be ignored right now. */
|
|
41
|
+
get busy(): boolean;
|
|
42
|
+
/** For the button element, with `busy` and the shown `status` for styling. */
|
|
43
|
+
get rootProps(): {
|
|
44
|
+
busy: boolean;
|
|
45
|
+
status: Button.Status;
|
|
46
|
+
props: {
|
|
47
|
+
readonly type: "button" | "submit" | "reset";
|
|
48
|
+
readonly disabled: boolean;
|
|
49
|
+
readonly 'aria-busy': boolean;
|
|
50
|
+
readonly 'aria-disabled': true | undefined;
|
|
51
|
+
readonly 'data-status': Button.Status;
|
|
52
|
+
readonly onClick: (event: MouseEvent) => void;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
press(event: MouseEvent): void;
|
|
56
|
+
private track;
|
|
57
|
+
render(props: {
|
|
58
|
+
children?: Component.Node;
|
|
59
|
+
}): Component.Node;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { Button };
|
|
63
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { missing, none } from "../piece.js";
|
|
2
|
+
import { Component } from "@expressive/mvc";
|
|
3
|
+
import { jsx, jsxs } from "@expressive/mvc/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/button/base.tsx
|
|
6
|
+
const COOLDOWN = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
const SETTLE = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* Button behavior without a look: press handling, async status, debounce and ARIA, assembled from pieces.
|
|
10
|
+
*
|
|
11
|
+
* `Root` and `Label` are placeholders that throw when rendered - a skin assigns them. `Status` renders nothing
|
|
12
|
+
* unless assigned. `Root` spreads `rootProps.props` onto its element to stay accessible.
|
|
13
|
+
*/
|
|
14
|
+
var Button = class extends Component {
|
|
15
|
+
onClick = void 0;
|
|
16
|
+
onError = void 0;
|
|
17
|
+
type = "button";
|
|
18
|
+
variant = "primary";
|
|
19
|
+
size = "md";
|
|
20
|
+
disabled = false;
|
|
21
|
+
/** Show as pending regardless of `onClick` - for work started elsewhere. */
|
|
22
|
+
pending = false;
|
|
23
|
+
/** Milliseconds after an accepted press during which presses are dropped. */
|
|
24
|
+
debounce = 0;
|
|
25
|
+
/** Milliseconds a done or error status shows before returning to idle. */
|
|
26
|
+
settle = 1500;
|
|
27
|
+
status = "idle";
|
|
28
|
+
error = void 0;
|
|
29
|
+
Root = missing("Button.Root");
|
|
30
|
+
Label = missing("Button.Label");
|
|
31
|
+
Status = none;
|
|
32
|
+
constructor(...args) {
|
|
33
|
+
super(...args, () => {
|
|
34
|
+
this.get(null, () => clearTimeout(SETTLE.get(this)));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Whether a press would be ignored right now. */
|
|
38
|
+
get busy() {
|
|
39
|
+
return this.pending || this.status == "pending";
|
|
40
|
+
}
|
|
41
|
+
/** For the button element, with `busy` and the shown `status` for styling. */
|
|
42
|
+
get rootProps() {
|
|
43
|
+
const { type, disabled, busy, status, pending } = this;
|
|
44
|
+
const shown = pending ? "pending" : status;
|
|
45
|
+
return {
|
|
46
|
+
busy,
|
|
47
|
+
status: shown,
|
|
48
|
+
props: {
|
|
49
|
+
type,
|
|
50
|
+
disabled,
|
|
51
|
+
"aria-busy": busy,
|
|
52
|
+
"aria-disabled": busy || void 0,
|
|
53
|
+
"data-status": shown,
|
|
54
|
+
onClick: this.press
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
press(event) {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (this.disabled || this.busy || now < (COOLDOWN.get(this) || 0)) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.debounce) COOLDOWN.set(this, now + this.debounce);
|
|
65
|
+
const result = this.onClick && this.onClick(event);
|
|
66
|
+
if (result instanceof Promise) this.track(result);
|
|
67
|
+
}
|
|
68
|
+
async track(result) {
|
|
69
|
+
clearTimeout(SETTLE.get(this));
|
|
70
|
+
this.status = "pending";
|
|
71
|
+
this.error = void 0;
|
|
72
|
+
try {
|
|
73
|
+
await result;
|
|
74
|
+
this.status = "done";
|
|
75
|
+
} catch (error) {
|
|
76
|
+
this.error = error instanceof Error ? error : new Error(String(error));
|
|
77
|
+
this.status = "error";
|
|
78
|
+
if (this.onError) this.onError(this.error);
|
|
79
|
+
}
|
|
80
|
+
SETTLE.set(this, setTimeout(() => this.status = "idle", this.settle));
|
|
81
|
+
}
|
|
82
|
+
render(props) {
|
|
83
|
+
const { status } = this.rootProps;
|
|
84
|
+
return /* @__PURE__ */ jsxs(this.Root, { children: [/* @__PURE__ */ jsx(this.Label, { children: props.children }), status != "idle" && /* @__PURE__ */ jsx(this.Status, { status })] });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { Button };
|
|
90
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","names":[],"sources":["../../src/button/base.tsx"],"sourcesContent":["import { Component, type Model } from '@expressive/mvc';\n\nimport { missing, none } from '../piece';\n\ndeclare namespace Button {\n type Variant = 'primary' | 'secondary' | 'ghost' | 'danger';\n type Size = 'sm' | 'md' | 'lg';\n type Status = 'idle' | 'pending' | 'done' | 'error';\n\n type Wrap = (props: { children?: Component.Node }) => Component.Node;\n type Mark = (props: { status: Status }) => Component.Node;\n}\n\nconst COOLDOWN = new WeakMap<Button, number>();\n\nconst SETTLE = new WeakMap<Button, ReturnType<typeof setTimeout>>();\n\n/**\n * Button behavior without a look: press handling, async status, debounce and ARIA, assembled from pieces.\n *\n * `Root` and `Label` are placeholders that throw when rendered - a skin assigns them. `Status` renders nothing\n * unless assigned. `Root` spreads `rootProps.props` onto its element to stay accessible.\n */\nclass Button extends Component {\n onClick?: (event: MouseEvent) => unknown = undefined;\n onError?: (error: Error) => void = undefined;\n\n type: 'button' | 'submit' | 'reset' = 'button';\n variant: Button.Variant = 'primary';\n size: Button.Size = 'md';\n disabled = false;\n\n /** Show as pending regardless of `onClick` - for work started elsewhere. */\n pending = false;\n\n /** Milliseconds after an accepted press during which presses are dropped. */\n debounce = 0;\n\n /** Milliseconds a done or error status shows before returning to idle. */\n settle = 1500;\n\n status: Button.Status = 'idle';\n error?: Error = undefined;\n\n Root: Button.Wrap = missing('Button.Root');\n Label: Button.Wrap = missing('Button.Label');\n Status: Button.Mark = none;\n\n constructor(...args: Model.Args) {\n super(...args, () => {\n this.get(null, () => clearTimeout(SETTLE.get(this)));\n });\n }\n\n /** Whether a press would be ignored right now. */\n get busy(): boolean {\n return this.pending || this.status == 'pending';\n }\n\n /** For the button element, with `busy` and the shown `status` for styling. */\n get rootProps() {\n const { type, disabled, busy, status, pending } = this;\n const shown: Button.Status = pending ? 'pending' : status;\n\n return {\n busy,\n status: shown,\n props: {\n type,\n disabled,\n 'aria-busy': busy,\n 'aria-disabled': busy || undefined,\n 'data-status': shown,\n onClick: this.press\n } as const\n };\n }\n\n press(event: MouseEvent) {\n const now = Date.now();\n\n if (this.disabled || this.busy || now < (COOLDOWN.get(this) || 0)) {\n event.preventDefault();\n return;\n }\n\n if (this.debounce) COOLDOWN.set(this, now + this.debounce);\n\n const result = this.onClick && this.onClick(event);\n\n if (result instanceof Promise) this.track(result);\n }\n\n private async track(result: Promise<unknown>) {\n clearTimeout(SETTLE.get(this));\n this.status = 'pending';\n this.error = undefined;\n\n try {\n await result;\n this.status = 'done';\n } catch (error) {\n this.error = error instanceof Error ? error : new Error(String(error));\n this.status = 'error';\n\n if (this.onError) this.onError(this.error);\n }\n\n SETTLE.set(this, setTimeout(() => (this.status = 'idle'), this.settle));\n }\n\n render(props: { children?: Component.Node }): Component.Node {\n const { status } = this.rootProps;\n\n return (\n <this.Root>\n <this.Label>{props.children}</this.Label>\n {status != 'idle' && <this.Status status={status} />}\n </this.Root>\n );\n }\n}\n\nexport { Button };\n"],"mappings":";;;;;AAaA,MAAM,2BAAW,IAAI,SAAyB;AAE9C,MAAM,yBAAS,IAAI,SAAgD;;;;;;;AAQnE,IAAM,SAAN,cAAqB,UAAU;CAC7B,UAA2C;CAC3C,UAAmC;CAEnC,OAAsC;CACtC,UAA0B;CAC1B,OAAoB;CACpB,WAAW;;CAGX,UAAU;;CAGV,WAAW;;CAGX,SAAS;CAET,SAAwB;CACxB,QAAgB;CAEhB,OAAoB,QAAQ,cAAc;CAC1C,QAAqB,QAAQ,eAAe;CAC5C,SAAsB;CAEtB,YAAY,GAAG,MAAkB;AAC/B,QAAM,GAAG,YAAY;AACnB,QAAK,IAAI,YAAY,aAAa,OAAO,IAAI,KAAK,CAAC,CAAC;IACpD;;;CAIJ,IAAI,OAAgB;AAClB,SAAO,KAAK,WAAW,KAAK,UAAU;;;CAIxC,IAAI,YAAY;EACd,MAAM,EAAE,MAAM,UAAU,MAAM,QAAQ,YAAY;EAClD,MAAM,QAAuB,UAAU,YAAY;AAEnD,SAAO;GACL;GACA,QAAQ;GACR,OAAO;IACL;IACA;IACA,aAAa;IACb,iBAAiB,QAAQ;IACzB,eAAe;IACf,SAAS,KAAK;IACf;GACF;;CAGH,MAAM,OAAmB;EACvB,MAAM,MAAM,KAAK,KAAK;AAEtB,MAAI,KAAK,YAAY,KAAK,QAAQ,OAAO,SAAS,IAAI,KAAK,IAAI,IAAI;AACjE,SAAM,gBAAgB;AACtB;;AAGF,MAAI,KAAK,SAAU,UAAS,IAAI,MAAM,MAAM,KAAK,SAAS;EAE1D,MAAM,SAAS,KAAK,WAAW,KAAK,QAAQ,MAAM;AAElD,MAAI,kBAAkB,QAAS,MAAK,MAAM,OAAO;;CAGnD,MAAc,MAAM,QAA0B;AAC5C,eAAa,OAAO,IAAI,KAAK,CAAC;AAC9B,OAAK,SAAS;AACd,OAAK,QAAQ;AAEb,MAAI;AACF,SAAM;AACN,QAAK,SAAS;WACP,OAAO;AACd,QAAK,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AACtE,QAAK,SAAS;AAEd,OAAI,KAAK,QAAS,MAAK,QAAQ,KAAK,MAAM;;AAG5C,SAAO,IAAI,MAAM,iBAAkB,KAAK,SAAS,QAAS,KAAK,OAAO,CAAC;;CAGzE,OAAO,OAAsD;EAC3D,MAAM,EAAE,WAAW,KAAK;AAExB,SACE,qBAAC,KAAK,mBACJ,oBAAC,KAAK,mBAAO,MAAM,WAAsB,EACxC,UAAU,UAAU,oBAAC,KAAK,UAAe,SAAU,IAC1C"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Button as Button$1 } from "./base.js";
|
|
2
|
+
import { Component } from "@expressive/mvc";
|
|
3
|
+
|
|
4
|
+
//#region src/button/button.d.ts
|
|
5
|
+
declare namespace Button {
|
|
6
|
+
type Variant = Button$1.Variant;
|
|
7
|
+
type Size = Button$1.Size;
|
|
8
|
+
type Status = Button$1.Status;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Button with variants, sizes and a status for async work.
|
|
12
|
+
*
|
|
13
|
+
* When `onClick` returns a promise the button shows a spinner and ignores presses until it settles, then shows
|
|
14
|
+
* done or error for `settle` milliseconds. A rejection is kept on `error` and passed to `onError`. `debounce`
|
|
15
|
+
* drops presses that follow an accepted one within that many milliseconds. `data-status` on the element mirrors
|
|
16
|
+
* the status. Pieces - `Root`, `Label`, `Status` - are function components reading `Button.get()`, each styled on
|
|
17
|
+
* its own; pass one as a prop or assign it in a subclass to replace it. Stock pieces are statics.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* <Button variant="secondary" onClick={() => this.save()}>Save</Button>
|
|
21
|
+
*/
|
|
22
|
+
declare class Button extends Button$1 {
|
|
23
|
+
static Root: ({
|
|
24
|
+
children
|
|
25
|
+
}: {
|
|
26
|
+
children?: Component.Node;
|
|
27
|
+
}) => Component.Node;
|
|
28
|
+
static Label: ({
|
|
29
|
+
children
|
|
30
|
+
}: {
|
|
31
|
+
children?: Component.Node;
|
|
32
|
+
}) => Component.Node;
|
|
33
|
+
static Status: ({
|
|
34
|
+
status
|
|
35
|
+
}: {
|
|
36
|
+
status: Button.Status;
|
|
37
|
+
}) => Component.Node;
|
|
38
|
+
Root: ({
|
|
39
|
+
children
|
|
40
|
+
}: {
|
|
41
|
+
children?: Component.Node;
|
|
42
|
+
}) => Component.Node;
|
|
43
|
+
Label: ({
|
|
44
|
+
children
|
|
45
|
+
}: {
|
|
46
|
+
children?: Component.Node;
|
|
47
|
+
}) => Component.Node;
|
|
48
|
+
Status: ({
|
|
49
|
+
status
|
|
50
|
+
}: {
|
|
51
|
+
status: Button.Status;
|
|
52
|
+
}) => Component.Node;
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { Button };
|
|
56
|
+
//# sourceMappingURL=button.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Button as Button$1 } from "./base.js";
|
|
2
|
+
import { Check } from "../icons/check.js";
|
|
3
|
+
import { Cross } from "../icons/cross.js";
|
|
4
|
+
import { Spinner } from "../icons/spinner.js";
|
|
5
|
+
import { style } from "@expressive/dom";
|
|
6
|
+
import { jsx } from "@expressive/dom/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/button/button.tsx
|
|
9
|
+
const Root = ({ children }) => {
|
|
10
|
+
const { variant, size, rootProps } = Button$1.get();
|
|
11
|
+
const { busy, status, props } = rootProps;
|
|
12
|
+
return /* @__PURE__ */ jsx("button", {
|
|
13
|
+
...props,
|
|
14
|
+
_secondary: variant == "secondary",
|
|
15
|
+
_ghost: variant == "ghost",
|
|
16
|
+
_danger: variant == "danger",
|
|
17
|
+
_sm: size == "sm",
|
|
18
|
+
_lg: size == "lg",
|
|
19
|
+
_busy: busy,
|
|
20
|
+
_failed: status == "error",
|
|
21
|
+
children
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
style(Root, {
|
|
25
|
+
position: "relative",
|
|
26
|
+
display: "inline-flex",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
justifyContent: "center",
|
|
29
|
+
gap: "8px",
|
|
30
|
+
height: "36px",
|
|
31
|
+
padding: "0 16px",
|
|
32
|
+
border: "1px solid transparent",
|
|
33
|
+
borderRadius: "6px",
|
|
34
|
+
background: "var(--ui-accent, #2684ff)",
|
|
35
|
+
color: "#fff",
|
|
36
|
+
font: "inherit",
|
|
37
|
+
fontWeight: "500",
|
|
38
|
+
whiteSpace: "nowrap",
|
|
39
|
+
cursor: "pointer",
|
|
40
|
+
userSelect: "none",
|
|
41
|
+
transition: "filter 100ms, background 100ms, transform 50ms",
|
|
42
|
+
":hover": { filter: "brightness(1.08)" },
|
|
43
|
+
":active": { transform: "scale(0.98)" },
|
|
44
|
+
":focus-visible": {
|
|
45
|
+
outline: "2px solid var(--ui-accent, #2684ff)",
|
|
46
|
+
outlineOffset: "2px"
|
|
47
|
+
},
|
|
48
|
+
":disabled": {
|
|
49
|
+
opacity: "0.5",
|
|
50
|
+
cursor: "not-allowed",
|
|
51
|
+
filter: "none",
|
|
52
|
+
transform: "none"
|
|
53
|
+
},
|
|
54
|
+
_secondary: {
|
|
55
|
+
background: "var(--ui-surface, #fff)",
|
|
56
|
+
borderColor: "var(--ui-line, #ccc)",
|
|
57
|
+
color: "inherit",
|
|
58
|
+
":hover": {
|
|
59
|
+
filter: "none",
|
|
60
|
+
background: "var(--ui-highlight, #deebff)"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
_ghost: {
|
|
64
|
+
background: "none",
|
|
65
|
+
color: "inherit",
|
|
66
|
+
":hover": {
|
|
67
|
+
filter: "none",
|
|
68
|
+
background: "var(--ui-highlight, #deebff)"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
_danger: { background: "var(--ui-danger, #e5484d)" },
|
|
72
|
+
_sm: {
|
|
73
|
+
height: "30px",
|
|
74
|
+
padding: "0 10px",
|
|
75
|
+
fontSize: "13px"
|
|
76
|
+
},
|
|
77
|
+
_lg: {
|
|
78
|
+
height: "44px",
|
|
79
|
+
padding: "0 22px",
|
|
80
|
+
fontSize: "16px"
|
|
81
|
+
},
|
|
82
|
+
_busy: {
|
|
83
|
+
cursor: "progress",
|
|
84
|
+
":active": { transform: "none" }
|
|
85
|
+
},
|
|
86
|
+
_failed: {
|
|
87
|
+
background: "var(--ui-danger, #e5484d)",
|
|
88
|
+
borderColor: "var(--ui-danger, #e5484d)",
|
|
89
|
+
color: "#fff"
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const Label = ({ children }) => {
|
|
93
|
+
const { status } = Button$1.get().rootProps;
|
|
94
|
+
return /* @__PURE__ */ jsx("span", {
|
|
95
|
+
_marked: status != "idle",
|
|
96
|
+
children
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
style(Label, {
|
|
100
|
+
transition: "opacity 100ms",
|
|
101
|
+
_marked: { opacity: "0" }
|
|
102
|
+
});
|
|
103
|
+
const Status = ({ status }) => {
|
|
104
|
+
return /* @__PURE__ */ jsx("span", {
|
|
105
|
+
"aria-hidden": "true",
|
|
106
|
+
children: status == "pending" ? /* @__PURE__ */ jsx(Spinner, {}) : status == "done" ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Cross, {})
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
style(Status, {
|
|
110
|
+
position: "absolute",
|
|
111
|
+
inset: "0",
|
|
112
|
+
display: "grid",
|
|
113
|
+
placeItems: "center"
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* Button with variants, sizes and a status for async work.
|
|
117
|
+
*
|
|
118
|
+
* When `onClick` returns a promise the button shows a spinner and ignores presses until it settles, then shows
|
|
119
|
+
* done or error for `settle` milliseconds. A rejection is kept on `error` and passed to `onError`. `debounce`
|
|
120
|
+
* drops presses that follow an accepted one within that many milliseconds. `data-status` on the element mirrors
|
|
121
|
+
* the status. Pieces - `Root`, `Label`, `Status` - are function components reading `Button.get()`, each styled on
|
|
122
|
+
* its own; pass one as a prop or assign it in a subclass to replace it. Stock pieces are statics.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* <Button variant="secondary" onClick={() => this.save()}>Save</Button>
|
|
126
|
+
*/
|
|
127
|
+
var Button = class extends Button$1 {
|
|
128
|
+
static Root = Root;
|
|
129
|
+
static Label = Label;
|
|
130
|
+
static Status = Status;
|
|
131
|
+
Root = Root;
|
|
132
|
+
Label = Label;
|
|
133
|
+
Status = Status;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
export { Button };
|
|
138
|
+
//# sourceMappingURL=button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"button.js","names":["Base.Button"],"sources":["../../src/button/button.tsx"],"sourcesContent":["/** @jsxImportSource @expressive/dom */\nimport { Component } from '@expressive/mvc';\nimport { style } from '@expressive/dom';\n\nimport { Check, Cross, Spinner } from '../icons';\nimport * as Base from './base';\n\ndeclare namespace Button {\n type Variant = Base.Button.Variant;\n type Size = Base.Button.Size;\n type Status = Base.Button.Status;\n}\n\nconst Root = ({ children }: { children?: Component.Node }): Component.Node => {\n const { variant, size, rootProps } = Base.Button.get();\n const { busy, status, props } = rootProps;\n\n return (\n <button\n {...props}\n _secondary={variant == 'secondary'}\n _ghost={variant == 'ghost'}\n _danger={variant == 'danger'}\n _sm={size == 'sm'}\n _lg={size == 'lg'}\n _busy={busy}\n _failed={status == 'error'}>\n {children}\n </button>\n );\n};\n\nstyle(Root, {\n position: 'relative',\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n gap: '8px',\n height: '36px',\n padding: '0 16px',\n border: '1px solid transparent',\n borderRadius: '6px',\n background: 'var(--ui-accent, #2684ff)',\n color: '#fff',\n font: 'inherit',\n fontWeight: '500',\n whiteSpace: 'nowrap',\n cursor: 'pointer',\n userSelect: 'none',\n transition: 'filter 100ms, background 100ms, transform 50ms',\n ':hover': {\n filter: 'brightness(1.08)'\n },\n ':active': {\n transform: 'scale(0.98)'\n },\n ':focus-visible': {\n outline: '2px solid var(--ui-accent, #2684ff)',\n outlineOffset: '2px'\n },\n ':disabled': {\n opacity: '0.5',\n cursor: 'not-allowed',\n filter: 'none',\n transform: 'none'\n },\n _secondary: {\n background: 'var(--ui-surface, #fff)',\n borderColor: 'var(--ui-line, #ccc)',\n color: 'inherit',\n ':hover': {\n filter: 'none',\n background: 'var(--ui-highlight, #deebff)'\n }\n },\n _ghost: {\n background: 'none',\n color: 'inherit',\n ':hover': {\n filter: 'none',\n background: 'var(--ui-highlight, #deebff)'\n }\n },\n _danger: {\n background: 'var(--ui-danger, #e5484d)'\n },\n _sm: {\n height: '30px',\n padding: '0 10px',\n fontSize: '13px'\n },\n _lg: {\n height: '44px',\n padding: '0 22px',\n fontSize: '16px'\n },\n _busy: {\n cursor: 'progress',\n ':active': {\n transform: 'none'\n }\n },\n _failed: {\n background: 'var(--ui-danger, #e5484d)',\n borderColor: 'var(--ui-danger, #e5484d)',\n color: '#fff'\n }\n});\n\nconst Label = ({ children }: { children?: Component.Node }): Component.Node => {\n const { status } = Base.Button.get().rootProps;\n\n return <span _marked={status != 'idle'}>{children}</span>;\n};\n\nstyle(Label, {\n transition: 'opacity 100ms',\n _marked: {\n opacity: '0'\n }\n});\n\nconst Status = ({ status }: { status: Button.Status }): Component.Node => {\n return (\n <span aria-hidden=\"true\">\n {status == 'pending' ? <Spinner /> : status == 'done' ? <Check /> : <Cross />}\n </span>\n );\n};\n\nstyle(Status, {\n position: 'absolute',\n inset: '0',\n display: 'grid',\n placeItems: 'center'\n});\n\n/**\n * Button with variants, sizes and a status for async work.\n *\n * When `onClick` returns a promise the button shows a spinner and ignores presses until it settles, then shows\n * done or error for `settle` milliseconds. A rejection is kept on `error` and passed to `onError`. `debounce`\n * drops presses that follow an accepted one within that many milliseconds. `data-status` on the element mirrors\n * the status. Pieces - `Root`, `Label`, `Status` - are function components reading `Button.get()`, each styled on\n * its own; pass one as a prop or assign it in a subclass to replace it. Stock pieces are statics.\n *\n * @example\n * <Button variant=\"secondary\" onClick={() => this.save()}>Save</Button>\n */\nclass Button extends Base.Button {\n static Root = Root;\n static Label = Label;\n static Status = Status;\n\n Root = Root;\n Label = Label;\n Status = Status;\n}\n\nexport { Button, Label, Root, Status };\n"],"mappings":";;;;;;;;AAaA,MAAM,QAAQ,EAAE,eAA8D;CAC5E,MAAM,EAAE,SAAS,MAAM,uBAA0B,KAAK;CACtD,MAAM,EAAE,MAAM,QAAQ,UAAU;AAEhC,QACE,oBAAC;EACC,GAAI;EACJ,YAAY,WAAW;EACvB,QAAQ,WAAW;EACnB,SAAS,WAAW;EACpB,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,OAAO;EACP,SAAS,UAAU;EAClB;GACM;;AAIb,MAAM,MAAM;CACV,UAAU;CACV,SAAS;CACT,YAAY;CACZ,gBAAgB;CAChB,KAAK;CACL,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,cAAc;CACd,YAAY;CACZ,OAAO;CACP,MAAM;CACN,YAAY;CACZ,YAAY;CACZ,QAAQ;CACR,YAAY;CACZ,YAAY;CACZ,UAAU,EACR,QAAQ,oBACT;CACD,WAAW,EACT,WAAW,eACZ;CACD,kBAAkB;EAChB,SAAS;EACT,eAAe;EAChB;CACD,aAAa;EACX,SAAS;EACT,QAAQ;EACR,QAAQ;EACR,WAAW;EACZ;CACD,YAAY;EACV,YAAY;EACZ,aAAa;EACb,OAAO;EACP,UAAU;GACR,QAAQ;GACR,YAAY;GACb;EACF;CACD,QAAQ;EACN,YAAY;EACZ,OAAO;EACP,UAAU;GACR,QAAQ;GACR,YAAY;GACb;EACF;CACD,SAAS,EACP,YAAY,6BACb;CACD,KAAK;EACH,QAAQ;EACR,SAAS;EACT,UAAU;EACX;CACD,KAAK;EACH,QAAQ;EACR,SAAS;EACT,UAAU;EACX;CACD,OAAO;EACL,QAAQ;EACR,WAAW,EACT,WAAW,QACZ;EACF;CACD,SAAS;EACP,YAAY;EACZ,aAAa;EACb,OAAO;EACR;CACF,CAAC;AAEF,MAAM,SAAS,EAAE,eAA8D;CAC7E,MAAM,EAAE,oBAAuB,KAAK,CAAC;AAErC,QAAO,oBAAC;EAAK,SAAS,UAAU;EAAS;GAAgB;;AAG3D,MAAM,OAAO;CACX,YAAY;CACZ,SAAS,EACP,SAAS,KACV;CACF,CAAC;AAEF,MAAM,UAAU,EAAE,aAAwD;AACxE,QACE,oBAAC;EAAK,eAAY;YACf,UAAU,YAAY,oBAAC,YAAU,GAAG,UAAU,SAAS,oBAAC,UAAQ,GAAG,oBAAC,UAAQ;GACxE;;AAIX,MAAM,QAAQ;CACZ,UAAU;CACV,OAAO;CACP,SAAS;CACT,YAAY;CACb,CAAC;;;;;;;;;;;;;AAcF,IAAM,SAAN,cAAqBA,SAAY;CAC/B,OAAO,OAAO;CACd,OAAO,QAAQ;CACf,OAAO,SAAS;CAEhB,OAAO;CACP,QAAQ;CACR,SAAS"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Component } from "@expressive/mvc";
|
|
2
|
+
|
|
3
|
+
//#region src/calendar/base.d.ts
|
|
4
|
+
declare namespace Calendar {
|
|
5
|
+
interface Day {
|
|
6
|
+
date: Date;
|
|
7
|
+
display: number;
|
|
8
|
+
isCurrentMonth: boolean;
|
|
9
|
+
isToday: boolean;
|
|
10
|
+
isPast: boolean;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
/** Holds keyboard focus within the grid. */
|
|
13
|
+
focused: boolean;
|
|
14
|
+
/** `false` when out of range. */
|
|
15
|
+
select: false | (() => void);
|
|
16
|
+
}
|
|
17
|
+
interface Option {
|
|
18
|
+
value: number;
|
|
19
|
+
label: string;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** A boundary date, or `true` for "today". */
|
|
23
|
+
type Bound = Date | boolean | undefined;
|
|
24
|
+
type Key = Pick<KeyboardEvent, 'key' | 'shiftKey' | 'preventDefault'>;
|
|
25
|
+
type Wrap = (props: {
|
|
26
|
+
children: Component.Node;
|
|
27
|
+
}) => Component.Node;
|
|
28
|
+
type Each = (props: {
|
|
29
|
+
day: Day;
|
|
30
|
+
}) => Component.Node;
|
|
31
|
+
type Label = (props: {
|
|
32
|
+
name: string;
|
|
33
|
+
}) => Component.Node;
|
|
34
|
+
type Part = () => Component.Node;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Month grid date picker behavior without a look: bounds, keyboard navigation, roving focus and ARIA, assembled
|
|
38
|
+
* from pieces.
|
|
39
|
+
*
|
|
40
|
+
* Every piece is a placeholder that throws when rendered - a skin assigns each one. Pieces spread the matching
|
|
41
|
+
* `*Props` getter onto their element to stay accessible. ARIA a skin writes itself: `role="row"` on `Row`,
|
|
42
|
+
* `role="columnheader"` on `Weekday`, and `aria-live="polite"` around the month title in `Header`.
|
|
43
|
+
*/
|
|
44
|
+
declare class Calendar extends Component {
|
|
45
|
+
static months: string[];
|
|
46
|
+
static weekdays: string[];
|
|
47
|
+
value?: Date;
|
|
48
|
+
onChange?: (date: Date) => void;
|
|
49
|
+
/** The day holding keyboard focus; its month is the one shown. Starts at `value` or today, within `min`/`max`. */
|
|
50
|
+
focus: Date;
|
|
51
|
+
min: Calendar.Bound;
|
|
52
|
+
max: Calendar.Bound;
|
|
53
|
+
Root: Calendar.Wrap;
|
|
54
|
+
Header: Calendar.Part;
|
|
55
|
+
Grid: Calendar.Wrap;
|
|
56
|
+
Row: Calendar.Wrap;
|
|
57
|
+
Weekday: Calendar.Label;
|
|
58
|
+
Day: Calendar.Each;
|
|
59
|
+
/** Month and year shown, e.g. "January 2024". */
|
|
60
|
+
get title(): string;
|
|
61
|
+
/** `false` when the step would cross a bound. */
|
|
62
|
+
get backMonth(): false | (() => void);
|
|
63
|
+
get nextMonth(): false | (() => void);
|
|
64
|
+
/** Whole weeks covering the focused month. */
|
|
65
|
+
get days(): Calendar.Day[];
|
|
66
|
+
get months(): Calendar.Option[];
|
|
67
|
+
get years(): Calendar.Option[];
|
|
68
|
+
get backProps(): {
|
|
69
|
+
readonly type: "button";
|
|
70
|
+
readonly 'aria-label': "Previous month";
|
|
71
|
+
readonly disabled: boolean;
|
|
72
|
+
readonly onClick: (() => void) | undefined;
|
|
73
|
+
};
|
|
74
|
+
get nextProps(): {
|
|
75
|
+
readonly type: "button";
|
|
76
|
+
readonly 'aria-label': "Next month";
|
|
77
|
+
readonly disabled: boolean;
|
|
78
|
+
readonly onClick: (() => void) | undefined;
|
|
79
|
+
};
|
|
80
|
+
get gridProps(): {
|
|
81
|
+
readonly role: "grid";
|
|
82
|
+
readonly 'aria-label': string;
|
|
83
|
+
readonly onKeyDown: (event: Calendar.Key) => void;
|
|
84
|
+
};
|
|
85
|
+
/** For a day's element, with `outside`, `today` and `selected` for styling. Binds `claim` on the focused day. */
|
|
86
|
+
get dayProps(): (day: Calendar.Day) => {
|
|
87
|
+
outside: boolean;
|
|
88
|
+
today: boolean;
|
|
89
|
+
selected: boolean;
|
|
90
|
+
props: {
|
|
91
|
+
readonly type: "button";
|
|
92
|
+
readonly role: "gridcell";
|
|
93
|
+
readonly 'aria-label': string;
|
|
94
|
+
readonly 'aria-selected': boolean;
|
|
95
|
+
readonly 'aria-current': "date" | undefined;
|
|
96
|
+
readonly tabIndex: 0 | -1;
|
|
97
|
+
readonly ref: ((element: HTMLElement | null) => void) | undefined;
|
|
98
|
+
readonly disabled: boolean;
|
|
99
|
+
readonly onClick: (() => void) | undefined;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
select(date: Date): void;
|
|
103
|
+
/** Move focus to `date`, clamped to `min`/`max`. */
|
|
104
|
+
move(date: Date): void;
|
|
105
|
+
keyDown(event: Calendar.Key): void;
|
|
106
|
+
/** Ref for the focused day; moves focus to it after keyboard navigation. `dayProps` binds it. */
|
|
107
|
+
claim(element: HTMLElement | null): void;
|
|
108
|
+
render(): Component.Node;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { Calendar };
|
|
112
|
+
//# sourceMappingURL=base.d.ts.map
|