@rvx/ui 0.1.6
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 +21 -0
- package/dist/common/events.d.ts +72 -0
- package/dist/common/events.js +58 -0
- package/dist/common/events.js.map +1 -0
- package/dist/common/parsers.d.ts +88 -0
- package/dist/common/parsers.js +62 -0
- package/dist/common/parsers.js.map +1 -0
- package/dist/common/theme-test.d.ts +7 -0
- package/dist/common/theme-test.js +14 -0
- package/dist/common/theme-test.js.map +1 -0
- package/dist/common/theme.d.ts +144 -0
- package/dist/common/theme.js +2 -0
- package/dist/common/theme.js.map +1 -0
- package/dist/common/trim.d.ts +12 -0
- package/dist/common/trim.js +16 -0
- package/dist/common/trim.js.map +1 -0
- package/dist/common/types.d.ts +13 -0
- package/dist/common/types.js +10 -0
- package/dist/common/types.js.map +1 -0
- package/dist/common/writing-mode.d.ts +82 -0
- package/dist/common/writing-mode.js +61 -0
- package/dist/common/writing-mode.js.map +1 -0
- package/dist/components/button.d.ts +42 -0
- package/dist/components/button.js +26 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +32 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/collapse-test.d.ts +8 -0
- package/dist/components/collapse-test.js +15 -0
- package/dist/components/collapse-test.js.map +1 -0
- package/dist/components/collapse.d.ts +13 -0
- package/dist/components/collapse.js +44 -0
- package/dist/components/collapse.js.map +1 -0
- package/dist/components/column.d.ts +12 -0
- package/dist/components/column.js +12 -0
- package/dist/components/column.js.map +1 -0
- package/dist/components/control-group.d.ts +7 -0
- package/dist/components/control-group.js +11 -0
- package/dist/components/control-group.js.map +1 -0
- package/dist/components/dialog.d.ts +33 -0
- package/dist/components/dialog.js +67 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-input.d.ts +27 -0
- package/dist/components/dropdown-input.js +31 -0
- package/dist/components/dropdown-input.js.map +1 -0
- package/dist/components/dropdown.d.ts +123 -0
- package/dist/components/dropdown.js +176 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/flex-space.d.ts +4 -0
- package/dist/components/flex-space.js +10 -0
- package/dist/components/flex-space.js.map +1 -0
- package/dist/components/heading.d.ts +9 -0
- package/dist/components/heading.js +14 -0
- package/dist/components/heading.js.map +1 -0
- package/dist/components/label.d.ts +14 -0
- package/dist/components/label.js +15 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/layer.d.ts +81 -0
- package/dist/components/layer.js +164 -0
- package/dist/components/layer.js.map +1 -0
- package/dist/components/link.d.ts +57 -0
- package/dist/components/link.js +26 -0
- package/dist/components/link.js.map +1 -0
- package/dist/components/page.d.ts +9 -0
- package/dist/components/page.js +17 -0
- package/dist/components/page.js.map +1 -0
- package/dist/components/popout.d.ts +134 -0
- package/dist/components/popout.js +259 -0
- package/dist/components/popout.js.map +1 -0
- package/dist/components/popover.d.ts +139 -0
- package/dist/components/popover.js +101 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/radio-buttons.d.ts +17 -0
- package/dist/components/radio-buttons.js +26 -0
- package/dist/components/radio-buttons.js.map +1 -0
- package/dist/components/row.d.ts +10 -0
- package/dist/components/row.js +23 -0
- package/dist/components/row.js.map +1 -0
- package/dist/components/scroll-view.d.ts +6 -0
- package/dist/components/scroll-view.js +72 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/text-input.d.ts +53 -0
- package/dist/components/text-input.js +35 -0
- package/dist/components/text-input.js.map +1 -0
- package/dist/components/text.d.ts +7 -0
- package/dist/components/text.js +11 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/validation.d.ts +109 -0
- package/dist/components/validation.js +151 -0
- package/dist/components/validation.js.map +1 -0
- package/dist/components/value.d.ts +7 -0
- package/dist/components/value.js +11 -0
- package/dist/components/value.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +3 -0
- package/dist/test.js.map +1 -0
- package/dist/theme.module.css +679 -0
- package/dist/theme.module.css.map +1 -0
- package/package.json +29 -0
- package/src/common/events.tsx +130 -0
- package/src/common/parsers.tsx +167 -0
- package/src/common/theme-test.tsx +20 -0
- package/src/common/theme.tsx +165 -0
- package/src/common/trim.tsx +30 -0
- package/src/common/types.tsx +23 -0
- package/src/common/writing-mode.tsx +150 -0
- package/src/components/button.tsx +94 -0
- package/src/components/checkbox.tsx +64 -0
- package/src/components/collapse-test.tsx +23 -0
- package/src/components/collapse.tsx +75 -0
- package/src/components/column.tsx +28 -0
- package/src/components/control-group.tsx +22 -0
- package/src/components/dialog.tsx +137 -0
- package/src/components/dropdown-input.tsx +82 -0
- package/src/components/dropdown.tsx +352 -0
- package/src/components/flex-space.tsx +15 -0
- package/src/components/heading.tsx +23 -0
- package/src/components/label.tsx +37 -0
- package/src/components/layer.tsx +299 -0
- package/src/components/link.tsx +118 -0
- package/src/components/page.tsx +36 -0
- package/src/components/popout.tsx +461 -0
- package/src/components/popover.tsx +292 -0
- package/src/components/radio-buttons.tsx +81 -0
- package/src/components/row.tsx +37 -0
- package/src/components/scroll-view.tsx +97 -0
- package/src/components/text-input.tsx +117 -0
- package/src/components/text.tsx +22 -0
- package/src/components/validation.tsx +272 -0
- package/src/components/value.tsx +22 -0
- package/src/index.tsx +29 -0
- package/src/test.tsx +2 -0
- package/src/theme/base.scss +69 -0
- package/src/theme/common.scss +51 -0
- package/src/theme/components/button.scss +116 -0
- package/src/theme/components/checkbox.scss +25 -0
- package/src/theme/components/collapse.scss +64 -0
- package/src/theme/components/column.scss +28 -0
- package/src/theme/components/control-group.scss +14 -0
- package/src/theme/components/dialog.scss +44 -0
- package/src/theme/components/dropdown.scss +50 -0
- package/src/theme/components/flex-space.scss +6 -0
- package/src/theme/components/heading.scss +39 -0
- package/src/theme/components/label.scss +24 -0
- package/src/theme/components/link.scss +25 -0
- package/src/theme/components/page.scss +22 -0
- package/src/theme/components/popover.scss +58 -0
- package/src/theme/components/radio-buttons.scss +31 -0
- package/src/theme/components/row.scss +17 -0
- package/src/theme/components/scroll-view.scss +51 -0
- package/src/theme/components/text-input.scss +45 -0
- package/src/theme/components/text.scss +12 -0
- package/src/theme/components/validation.scss +15 -0
- package/src/theme/components/value.scss +4 -0
- package/src/theme/theme.scss +22 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* A CSS writing mode property value.
|
|
4
|
+
*/
|
|
5
|
+
export type WritingMode = "horizontal-tb" | "vertical-rl" | "vertical-lr" | "sideways-rl" | "sideways-lr";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A CSS script direction property value.
|
|
9
|
+
*/
|
|
10
|
+
export type ScriptDirection = "ltr" | "rtl";
|
|
11
|
+
|
|
12
|
+
/** The up {@link Direction}. */
|
|
13
|
+
export const UP = 0;
|
|
14
|
+
/** The right {@link Direction}. */
|
|
15
|
+
export const RIGHT = 1;
|
|
16
|
+
/** The down {@link Direction}. */
|
|
17
|
+
export const DOWN = 2;
|
|
18
|
+
/** The left {@link Direction}. */
|
|
19
|
+
export const LEFT = 3;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a direction ({@link UP}, {@link RIGHT}, {@link DOWN}, {@link LEFT}).
|
|
23
|
+
*/
|
|
24
|
+
export type Direction = typeof UP | typeof RIGHT | typeof DOWN | typeof LEFT;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Can be indexed by a {@link Direction} to get the respective css inset property.
|
|
28
|
+
*/
|
|
29
|
+
export const INSET: Record<Direction, "top" | "right" | "bottom" | "left"> = ["top", "right", "bottom", "left"];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Flip a direction.
|
|
33
|
+
*/
|
|
34
|
+
export function flip(dir: Direction): Direction {
|
|
35
|
+
return ((dir + 2) & 3) as Direction;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the specified directions are along the same axis.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* axisEquals(UP, UP) // true
|
|
44
|
+
* axisEquals(UP, DOWN) // true
|
|
45
|
+
* axisEquals(UP, RIGHT) // false
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function axisEquals(a: Direction, b: Direction): boolean {
|
|
49
|
+
return (a & 1) === (b & 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the window size in CSS pixels along the axis of the specified direction.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* getWindowSize(UP) === window.innerHeight // true
|
|
58
|
+
* getWindowSize(DOWN) === window.innerHeight // true
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function getWindowSize(dir: Direction): number {
|
|
62
|
+
return (dir & 1) ? window.innerWidth : window.innerHeight;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface DOMRectSize {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the size in CSS pixels of a DOM rect along the axis of the specified direction.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* getSize(button.getBoundingClientRect(), RIGHT) // 234.5
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function getSize(rect: DOMRectSize, dir: Direction): number {
|
|
79
|
+
return (dir & 1) ? rect.width : rect.height;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DOMRectXY {
|
|
83
|
+
x: number;
|
|
84
|
+
y: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the X/Y coordinate start in CSS pixels of the specified DOM rect along the axis of the specified direction.
|
|
89
|
+
*/
|
|
90
|
+
export function getXY(rect: DOMRectXY, dir: Direction): number {
|
|
91
|
+
return (dir & 1) ? rect.x : rect.y;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the block start direction with respect to the specified writing mode.
|
|
96
|
+
*/
|
|
97
|
+
export function getBlockStart(writingMode: WritingMode): Direction {
|
|
98
|
+
switch (writingMode) {
|
|
99
|
+
case "horizontal-tb":
|
|
100
|
+
return UP;
|
|
101
|
+
|
|
102
|
+
case "vertical-rl":
|
|
103
|
+
case "sideways-rl":
|
|
104
|
+
return RIGHT;
|
|
105
|
+
|
|
106
|
+
case "vertical-lr":
|
|
107
|
+
case "sideways-lr":
|
|
108
|
+
return LEFT;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the inline start direction with respect to the specified writing mode and script direction.
|
|
114
|
+
*/
|
|
115
|
+
export function getInlineStart(writingMode: WritingMode, scriptDir: ScriptDirection): Direction {
|
|
116
|
+
let dir: Direction;
|
|
117
|
+
switch (writingMode) {
|
|
118
|
+
case "horizontal-tb":
|
|
119
|
+
dir = LEFT;
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case "vertical-rl":
|
|
123
|
+
case "vertical-lr":
|
|
124
|
+
case "sideways-rl":
|
|
125
|
+
dir = UP;
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case "sideways-lr":
|
|
129
|
+
dir = DOWN;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (scriptDir !== "ltr") {
|
|
134
|
+
dir = flip(dir);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return dir;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the space in CSS pixels between a DOM rect and the window in the specified direction.
|
|
142
|
+
*/
|
|
143
|
+
export function getWindowSpaceAround(rect: DOMRect, dir: Direction): number {
|
|
144
|
+
switch (dir) {
|
|
145
|
+
case UP: return rect.y;
|
|
146
|
+
case RIGHT: return window.innerWidth - rect.right;
|
|
147
|
+
case DOWN: return window.innerHeight - rect.bottom;
|
|
148
|
+
case LEFT: return rect.x;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, get, optionalString, StyleValue } from "rvx";
|
|
2
|
+
import { isPending } from "rvx/async";
|
|
3
|
+
|
|
4
|
+
import { Action, handleActionEvent, keyFor } from "../common/events.js";
|
|
5
|
+
import { THEME } from "../common/theme.js";
|
|
6
|
+
import { Validator } from "./validation.js";
|
|
7
|
+
|
|
8
|
+
export type ButtonType = "button" | "submit" | "reset" | "menu";
|
|
9
|
+
export type ButtonVariant = "default" | "primary" | "success" | "danger" | "warning" | "input";
|
|
10
|
+
|
|
11
|
+
export function Button(props: {
|
|
12
|
+
/**
|
|
13
|
+
* The button type.
|
|
14
|
+
*
|
|
15
|
+
* @default "button"
|
|
16
|
+
*/
|
|
17
|
+
type?: Expression<ButtonType | undefined>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The theme variant.
|
|
21
|
+
*
|
|
22
|
+
* @default "default"
|
|
23
|
+
*/
|
|
24
|
+
variant?: Expression<ButtonVariant | undefined>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set when the button is disabled.
|
|
28
|
+
*
|
|
29
|
+
* The button is automatically disabled when there are any pending tasks.
|
|
30
|
+
*/
|
|
31
|
+
disabled?: Expression<boolean | undefined>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The action to run when the button is clicked.
|
|
35
|
+
*/
|
|
36
|
+
action?: Action;
|
|
37
|
+
|
|
38
|
+
class?: ClassValue;
|
|
39
|
+
style?: StyleValue;
|
|
40
|
+
id?: Expression<string | undefined>;
|
|
41
|
+
autofocus?: Expression<boolean | undefined>;
|
|
42
|
+
title?: Expression<string | undefined>;
|
|
43
|
+
role?: Expression<string | undefined>;
|
|
44
|
+
"aria-label"?: Expression<string | undefined>;
|
|
45
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
46
|
+
"aria-expanded"?: Expression<boolean | undefined>;
|
|
47
|
+
"aria-haspopup"?: Expression<string | undefined>;
|
|
48
|
+
"aria-controls"?: Expression<string | undefined>;
|
|
49
|
+
validator?: Validator;
|
|
50
|
+
|
|
51
|
+
children?: unknown;
|
|
52
|
+
}): unknown {
|
|
53
|
+
const theme = extract(THEME);
|
|
54
|
+
const disabled = () => isPending() || get(props.disabled);
|
|
55
|
+
|
|
56
|
+
function action(event: Event) {
|
|
57
|
+
if (disabled() || !props.action) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
handleActionEvent(event, props.action);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return <button
|
|
64
|
+
type={() => get(props.type) ?? "button"}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
class={[
|
|
67
|
+
theme?.button,
|
|
68
|
+
() => theme?.[`button_${get(props.variant) ?? "default"}`],
|
|
69
|
+
props.class,
|
|
70
|
+
]}
|
|
71
|
+
style={props.style}
|
|
72
|
+
id={props.id}
|
|
73
|
+
title={props.title}
|
|
74
|
+
role={props.role}
|
|
75
|
+
aria-label={props["aria-label"]}
|
|
76
|
+
aria-labelledby={props["aria-labelledby"]}
|
|
77
|
+
aria-expanded={optionalString(props["aria-expanded"])}
|
|
78
|
+
aria-haspopup={props["aria-haspopup"]}
|
|
79
|
+
aria-controls={props["aria-controls"]}
|
|
80
|
+
aria-invalid={props.validator ? optionalString(props.validator.invalid) : undefined}
|
|
81
|
+
aria-errormessage={props.validator ? props.validator.errorMessageIds : undefined}
|
|
82
|
+
autofocus={props.autofocus}
|
|
83
|
+
|
|
84
|
+
on:click={action}
|
|
85
|
+
on:keydown={event => {
|
|
86
|
+
const key = keyFor(event);
|
|
87
|
+
if (key === "enter" || key === "space") {
|
|
88
|
+
action(event);
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{props.children}
|
|
93
|
+
</button>;
|
|
94
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, get, optionalString, Signal, string, StyleValue, uniqueId, watch } from "rvx";
|
|
2
|
+
import { isPending } from "rvx/async";
|
|
3
|
+
|
|
4
|
+
import { THEME } from "../common/theme.js";
|
|
5
|
+
import { Text } from "./text.js";
|
|
6
|
+
import { validatorFor } from "./validation.js";
|
|
7
|
+
|
|
8
|
+
export function Checkbox(props: {
|
|
9
|
+
checked?: Expression<boolean | undefined>;
|
|
10
|
+
|
|
11
|
+
disabled?: Expression<boolean | undefined>;
|
|
12
|
+
|
|
13
|
+
class?: ClassValue;
|
|
14
|
+
style?: StyleValue;
|
|
15
|
+
autofocus?: Expression<boolean | undefined>;
|
|
16
|
+
children?: unknown;
|
|
17
|
+
}): unknown {
|
|
18
|
+
const id = uniqueId();
|
|
19
|
+
const theme = extract(THEME);
|
|
20
|
+
|
|
21
|
+
const disabled = props.checked instanceof Signal
|
|
22
|
+
? () => isPending() || get(props.disabled)
|
|
23
|
+
: () => true;
|
|
24
|
+
|
|
25
|
+
const validator = props.checked instanceof Signal ? validatorFor(props.checked) : undefined;
|
|
26
|
+
|
|
27
|
+
const input = <input
|
|
28
|
+
id={id}
|
|
29
|
+
type="checkbox"
|
|
30
|
+
class={theme?.checkbox_input}
|
|
31
|
+
on:input={() => {
|
|
32
|
+
if (props.checked instanceof Signal) {
|
|
33
|
+
props.checked.value = input.checked;
|
|
34
|
+
}
|
|
35
|
+
}}
|
|
36
|
+
aria-readonly={string(!(props.checked instanceof Signal))}
|
|
37
|
+
aria-invalid={validator ? optionalString(validator.invalid) : undefined}
|
|
38
|
+
aria-errormessage={validator ? validator.errorMessageIds : undefined}
|
|
39
|
+
autofocus={props.autofocus}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
/> as HTMLInputElement;
|
|
42
|
+
|
|
43
|
+
watch(props.checked, checked => {
|
|
44
|
+
if (checked === undefined) {
|
|
45
|
+
input.indeterminate = true;
|
|
46
|
+
} else {
|
|
47
|
+
input.checked = checked;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return <label
|
|
52
|
+
for={id}
|
|
53
|
+
class={[
|
|
54
|
+
theme?.checkbox_label,
|
|
55
|
+
props.class,
|
|
56
|
+
]}
|
|
57
|
+
style={props.style}
|
|
58
|
+
>
|
|
59
|
+
{input}
|
|
60
|
+
<Text class={theme?.checkbox_content}>
|
|
61
|
+
{props.children}
|
|
62
|
+
</Text>
|
|
63
|
+
</label>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { themeClass } from "../test.js";
|
|
2
|
+
|
|
3
|
+
function assertCollapse(collapse: Element): asserts collapse is HTMLDivElement {
|
|
4
|
+
if (!collapse.matches(`.${themeClass("collapse")}`)) {
|
|
5
|
+
throw new Error("collapse must be a collapse root element.");
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the content element of a collapse.
|
|
11
|
+
*/
|
|
12
|
+
export function getCollapseContent(collapse: Element): HTMLDivElement {
|
|
13
|
+
assertCollapse(collapse);
|
|
14
|
+
return collapse.querySelector<HTMLDivElement>(`.${themeClass("collapse_content")}`)!;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a collapse is visible.
|
|
19
|
+
*/
|
|
20
|
+
export function isCollapseVisible(collapse: Element): boolean {
|
|
21
|
+
assertCollapse(collapse);
|
|
22
|
+
return !collapse.hasAttribute("inert");
|
|
23
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ClassValue, Event, Expression, extract, get, map, optionalString, sig, StyleValue, teardown } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { THEME } from "../common/theme.js";
|
|
4
|
+
import { AriaLive, AriaRelevant } from "../common/types.js";
|
|
5
|
+
|
|
6
|
+
export function Collapse(props: {
|
|
7
|
+
visible?: Expression<boolean | undefined>;
|
|
8
|
+
alert?: Event<[]>;
|
|
9
|
+
children?: unknown;
|
|
10
|
+
class?: ClassValue;
|
|
11
|
+
style?: StyleValue;
|
|
12
|
+
id?: Expression<string | undefined>;
|
|
13
|
+
"aria-live"?: Expression<AriaLive | undefined>;
|
|
14
|
+
"aria-relevant"?: Expression<AriaRelevant | undefined>;
|
|
15
|
+
"aria-atomic"?: Expression<boolean | undefined>;
|
|
16
|
+
}): unknown {
|
|
17
|
+
const theme = extract(THEME);
|
|
18
|
+
const visible = map(props.visible, v => v ?? false);
|
|
19
|
+
const alert = sig(false);
|
|
20
|
+
const size = sig<number | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
const content = <div class={theme?.collapse_content}>
|
|
23
|
+
{props.children}
|
|
24
|
+
</div> as HTMLDivElement;
|
|
25
|
+
|
|
26
|
+
const observer = new ResizeObserver(entries => {
|
|
27
|
+
const entry = entries[entries.length - 1];
|
|
28
|
+
const boxSize = entry?.borderBoxSize[entry.borderBoxSize.length - 1];
|
|
29
|
+
if (boxSize) {
|
|
30
|
+
size.value = boxSize.blockSize;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
observer.observe(content);
|
|
35
|
+
teardown(() => {
|
|
36
|
+
observer.disconnect();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
props.alert?.(() => {
|
|
40
|
+
if (get(visible)) {
|
|
41
|
+
alert.value = false;
|
|
42
|
+
// Force a reflow:
|
|
43
|
+
void root.offsetWidth;
|
|
44
|
+
alert.value = true;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const root = <div
|
|
49
|
+
inert={map(visible, v => !v)}
|
|
50
|
+
class={[
|
|
51
|
+
theme?.collapse,
|
|
52
|
+
() => size.value === undefined ? undefined : theme?.collapse_sized,
|
|
53
|
+
() => alert.value ? theme?.collapse_alert : undefined,
|
|
54
|
+
map(visible, v => v ? theme?.collapse_visible : undefined),
|
|
55
|
+
props.class,
|
|
56
|
+
]}
|
|
57
|
+
style={[
|
|
58
|
+
{
|
|
59
|
+
"--collapse-size": () => size.value === undefined ? undefined : `${size.value}px`,
|
|
60
|
+
},
|
|
61
|
+
props.style,
|
|
62
|
+
]}
|
|
63
|
+
id={props.id}
|
|
64
|
+
aria-live={map(props["aria-live"], v => v ?? "polite")}
|
|
65
|
+
aria-relevant={props["aria-relevant"]}
|
|
66
|
+
aria-atomic={optionalString(props["aria-atomic"])}
|
|
67
|
+
>
|
|
68
|
+
{theme?.collapse_view
|
|
69
|
+
? <div class={theme.collapse_view}>
|
|
70
|
+
{content}
|
|
71
|
+
</div>
|
|
72
|
+
: content}
|
|
73
|
+
</div> as HTMLDivElement;
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, map, StyleValue } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { THEME } from "../common/theme.js";
|
|
4
|
+
import { SizeContext } from "../common/types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A flex column with automatic spacing between it's children.
|
|
8
|
+
*/
|
|
9
|
+
export function Column(props: {
|
|
10
|
+
size?: Expression<SizeContext | undefined>;
|
|
11
|
+
class?: ClassValue;
|
|
12
|
+
style?: StyleValue;
|
|
13
|
+
id?: Expression<string | undefined>;
|
|
14
|
+
children?: unknown;
|
|
15
|
+
}): unknown {
|
|
16
|
+
const theme = extract(THEME);
|
|
17
|
+
return <div
|
|
18
|
+
class={[
|
|
19
|
+
theme?.column,
|
|
20
|
+
map(props.size, size => theme?.[`column_${size ?? "content"}`]),
|
|
21
|
+
props.class,
|
|
22
|
+
]}
|
|
23
|
+
style={props.style}
|
|
24
|
+
id={props.id}
|
|
25
|
+
>
|
|
26
|
+
{props.children}
|
|
27
|
+
</div>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, StyleValue } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { THEME } from "../common/theme.js";
|
|
4
|
+
|
|
5
|
+
export function ControlGroup(props: {
|
|
6
|
+
class?: ClassValue;
|
|
7
|
+
style?: StyleValue;
|
|
8
|
+
id?: Expression<string | undefined>;
|
|
9
|
+
children?: unknown;
|
|
10
|
+
}): unknown {
|
|
11
|
+
const theme = extract(THEME);
|
|
12
|
+
return <div
|
|
13
|
+
class={[
|
|
14
|
+
theme?.control_group,
|
|
15
|
+
props.class,
|
|
16
|
+
]}
|
|
17
|
+
style={props.style}
|
|
18
|
+
id={props.id}
|
|
19
|
+
>
|
|
20
|
+
{props.children}
|
|
21
|
+
</div>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { captureSelf, ClassValue, Expression, extract, map, mount, StyleValue, uniqueId } from "rvx";
|
|
2
|
+
import { TASKS, Tasks } from "rvx/async";
|
|
3
|
+
|
|
4
|
+
import { FlexSpace, Heading, Row, Text, THEME } from "../index.js";
|
|
5
|
+
import { LAYER, Layer } from "./layer.js";
|
|
6
|
+
|
|
7
|
+
export class DialogAbortError extends Error {}
|
|
8
|
+
|
|
9
|
+
export interface Dialog<T> {
|
|
10
|
+
resolve: (value: T) => void;
|
|
11
|
+
reject: (cause?: unknown) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type DialogInit<T> = (dialog: Dialog<T>) => unknown;
|
|
15
|
+
|
|
16
|
+
export interface DialogOptions {
|
|
17
|
+
cancellable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function showDialog<T = void>(init: DialogInit<T>, options?: DialogOptions): Promise<T> {
|
|
21
|
+
return new Promise<T>((resolve, reject) => {
|
|
22
|
+
captureSelf(dispose => {
|
|
23
|
+
mount(
|
|
24
|
+
document.body,
|
|
25
|
+
<Layer modal>
|
|
26
|
+
{ctx => {
|
|
27
|
+
ctx.set(TASKS, new Tasks());
|
|
28
|
+
const dialog: Dialog<T> = {
|
|
29
|
+
resolve(value) {
|
|
30
|
+
dispose();
|
|
31
|
+
resolve(value);
|
|
32
|
+
},
|
|
33
|
+
reject(reason) {
|
|
34
|
+
dispose();
|
|
35
|
+
reject(reason);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
if (options?.cancellable ?? true) {
|
|
39
|
+
extract(LAYER)?.useHotkey("escape", () => {
|
|
40
|
+
dialog.reject(new DialogAbortError());
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return init(dialog);
|
|
44
|
+
}}
|
|
45
|
+
</Layer>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type DialogRole = "dialog" | "alertdialog";
|
|
52
|
+
|
|
53
|
+
export function DialogBody(props: {
|
|
54
|
+
class?: ClassValue;
|
|
55
|
+
style?: StyleValue;
|
|
56
|
+
children?: unknown;
|
|
57
|
+
role?: Expression<DialogRole | undefined>;
|
|
58
|
+
title?: unknown;
|
|
59
|
+
description?: unknown;
|
|
60
|
+
|
|
61
|
+
inlineSize?: Expression<string | undefined>;
|
|
62
|
+
maxInlineSize?: Expression<string | undefined>;
|
|
63
|
+
blockSize?: Expression<string | undefined>;
|
|
64
|
+
maxBlockSize?: Expression<string | undefined>;
|
|
65
|
+
|
|
66
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
67
|
+
"aria-describedby"?: Expression<string | undefined>;
|
|
68
|
+
}): unknown {
|
|
69
|
+
const theme = extract(THEME);
|
|
70
|
+
|
|
71
|
+
let titleId: string | undefined;
|
|
72
|
+
let descriptionId: string | undefined;
|
|
73
|
+
const head: unknown[] = [];
|
|
74
|
+
|
|
75
|
+
if (props.title !== undefined) {
|
|
76
|
+
titleId = uniqueId();
|
|
77
|
+
head.push(<Heading level="2" id={titleId}>{props.title}</Heading>);
|
|
78
|
+
}
|
|
79
|
+
if (props.description !== undefined) {
|
|
80
|
+
descriptionId = uniqueId();
|
|
81
|
+
head.push(<Text id={descriptionId}>{props.description}</Text>);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return <div
|
|
85
|
+
class={[
|
|
86
|
+
theme?.dialog_container,
|
|
87
|
+
props.class,
|
|
88
|
+
]}
|
|
89
|
+
style={props.style}
|
|
90
|
+
role={map(props.role, v => v ?? "dialog")}
|
|
91
|
+
aria-labelledby={map(props["aria-labelledby"], v => v ?? titleId)}
|
|
92
|
+
aria-describedby={map(props["aria-describedby"], v => v ?? descriptionId)}
|
|
93
|
+
>
|
|
94
|
+
<div
|
|
95
|
+
class={[
|
|
96
|
+
theme?.column,
|
|
97
|
+
theme?.column_content,
|
|
98
|
+
theme?.dialog_body,
|
|
99
|
+
]}
|
|
100
|
+
style={{
|
|
101
|
+
"inline-size": props.inlineSize,
|
|
102
|
+
"max-inline-size": props.maxInlineSize,
|
|
103
|
+
"block-size": props.blockSize,
|
|
104
|
+
"max-block-size": props.maxBlockSize,
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{head}
|
|
108
|
+
{props.children}
|
|
109
|
+
</div>
|
|
110
|
+
</div>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function DialogFooter(props: {
|
|
114
|
+
class?: ClassValue;
|
|
115
|
+
style?: StyleValue;
|
|
116
|
+
links?: unknown;
|
|
117
|
+
children?: unknown;
|
|
118
|
+
}): unknown {
|
|
119
|
+
const theme = extract(THEME);
|
|
120
|
+
return <Row
|
|
121
|
+
size="control"
|
|
122
|
+
class={[
|
|
123
|
+
theme?.dialog_footer,
|
|
124
|
+
props.class,
|
|
125
|
+
]}
|
|
126
|
+
style={props.style}
|
|
127
|
+
align="center"
|
|
128
|
+
>
|
|
129
|
+
<Row size="control">
|
|
130
|
+
{props.links}
|
|
131
|
+
</Row>
|
|
132
|
+
<FlexSpace />
|
|
133
|
+
<Row size="control">
|
|
134
|
+
{props.children}
|
|
135
|
+
</Row>
|
|
136
|
+
</Row>;
|
|
137
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ClassValue, Expression, get, map, Signal, StyleValue } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { Button, ButtonVariant } from "./button.js";
|
|
4
|
+
import { Dropdown, DropdownItem } from "./dropdown.js";
|
|
5
|
+
import { PopoutAlignment, PopoutPlacement } from "./popout.js";
|
|
6
|
+
import { validatorFor } from "./validation.js";
|
|
7
|
+
|
|
8
|
+
export interface DropdownValue<T> {
|
|
9
|
+
value: T;
|
|
10
|
+
label: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DropdownInput<T>(props: {
|
|
14
|
+
children?: unknown;
|
|
15
|
+
value: Expression<T>;
|
|
16
|
+
values: Expression<DropdownValue<T>[]>;
|
|
17
|
+
|
|
18
|
+
variant?: ButtonVariant;
|
|
19
|
+
disabled?: Expression<boolean | undefined>;
|
|
20
|
+
|
|
21
|
+
class?: ClassValue;
|
|
22
|
+
style?: StyleValue;
|
|
23
|
+
id?: Expression<string | undefined>;
|
|
24
|
+
autofocus?: Expression<boolean | undefined>;
|
|
25
|
+
title?: Expression<string | undefined>;
|
|
26
|
+
"aria-label"?: Expression<string | undefined>;
|
|
27
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
28
|
+
|
|
29
|
+
dropdownId?: string;
|
|
30
|
+
dropdownClass?: ClassValue;
|
|
31
|
+
dropdownStyle?: StyleValue;
|
|
32
|
+
placement?: Expression<PopoutPlacement | undefined>;
|
|
33
|
+
alignment?: Expression<PopoutAlignment | undefined>;
|
|
34
|
+
foreignEvents?: string[];
|
|
35
|
+
}): unknown {
|
|
36
|
+
const items = new WeakMap<DropdownValue<T>, DropdownItem>();
|
|
37
|
+
return <Dropdown
|
|
38
|
+
anchor={a => <Button
|
|
39
|
+
{...a}
|
|
40
|
+
variant={map(props.variant, v => v ?? "input")}
|
|
41
|
+
disabled={props.value instanceof Signal ? props.disabled : true}
|
|
42
|
+
class={props.class}
|
|
43
|
+
style={props.style}
|
|
44
|
+
id={props.id}
|
|
45
|
+
autofocus={props.autofocus}
|
|
46
|
+
title={props.title}
|
|
47
|
+
role="combobox"
|
|
48
|
+
aria-label={props["aria-label"]}
|
|
49
|
+
aria-labelledby={props["aria-labelledby"]}
|
|
50
|
+
validator={props.value instanceof Signal ? validatorFor(props.value) : undefined}
|
|
51
|
+
>
|
|
52
|
+
{props.children ?? (() => {
|
|
53
|
+
const value = get(props.value);
|
|
54
|
+
return get(props.values).find(v => v.value === value)?.label;
|
|
55
|
+
})}
|
|
56
|
+
</Button>}
|
|
57
|
+
items={() => get(props.values).map<DropdownItem>(value => {
|
|
58
|
+
let item = items.get(value);
|
|
59
|
+
if (item === undefined) {
|
|
60
|
+
item = {
|
|
61
|
+
label: value.label,
|
|
62
|
+
selected: () => value.value === get(props.value),
|
|
63
|
+
action: () => {
|
|
64
|
+
if (props.value instanceof Signal) {
|
|
65
|
+
props.value.value = value.value;
|
|
66
|
+
} else {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
items.set(value, item);
|
|
72
|
+
}
|
|
73
|
+
return item;
|
|
74
|
+
})}
|
|
75
|
+
id={props.dropdownId}
|
|
76
|
+
style={props.dropdownStyle}
|
|
77
|
+
class={props.dropdownClass}
|
|
78
|
+
placement={props.placement}
|
|
79
|
+
alignment={props.alignment}
|
|
80
|
+
foreignEvents={props.foreignEvents}
|
|
81
|
+
/>;
|
|
82
|
+
}
|