@rvx/ui 0.1.13 → 0.1.14
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/dist/common/theme.d.ts +10 -0
- package/dist/components/button.js +3 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/card.d.ts +1 -0
- package/dist/components/card.js +6 -3
- package/dist/components/card.js.map +1 -1
- package/dist/components/checkbox.js +6 -6
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/collapse.d.ts +16 -2
- package/dist/components/collapse.js +85 -4
- package/dist/components/collapse.js.map +1 -1
- package/dist/components/column.d.ts +1 -0
- package/dist/components/column.js +1 -0
- package/dist/components/column.js.map +1 -1
- package/dist/components/dialog.d.ts +1 -2
- package/dist/components/dialog.js +3 -5
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/dropdown-input.js +2 -2
- package/dist/components/dropdown-input.js.map +1 -1
- package/dist/components/dropdown.js +2 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/label.js +1 -1
- package/dist/components/label.js.map +1 -1
- package/dist/components/link.js +2 -1
- package/dist/components/link.js.map +1 -1
- package/dist/components/nav-list.js +2 -1
- package/dist/components/nav-list.js.map +1 -1
- package/dist/components/popout.d.ts +1 -2
- package/dist/components/popout.js +1 -2
- package/dist/components/popout.js.map +1 -1
- package/dist/components/popover.js +1 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/radio-buttons.js +11 -10
- package/dist/components/radio-buttons.js.map +1 -1
- package/dist/components/slider.js +1 -2
- package/dist/components/slider.js.map +1 -1
- package/dist/components/tabs.d.ts +10 -0
- package/dist/components/tabs.js +31 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/text-input.js +5 -4
- package/dist/components/text-input.js.map +1 -1
- package/dist/components/validation-rules.d.ts +11 -0
- package/dist/components/validation-rules.js +37 -0
- package/dist/components/validation-rules.js.map +1 -0
- package/dist/components/validation.d.ts +53 -77
- package/dist/components/validation.js +117 -95
- package/dist/components/validation.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/theme.module.css +75 -0
- package/dist/theme.module.css.map +1 -1
- package/package.json +3 -3
- package/src/common/theme.tsx +11 -0
- package/src/components/button.tsx +3 -2
- package/src/components/card.tsx +10 -5
- package/src/components/checkbox.tsx +6 -6
- package/src/components/collapse.tsx +127 -5
- package/src/components/column.tsx +2 -0
- package/src/components/dialog.tsx +3 -5
- package/src/components/dropdown-input.tsx +2 -2
- package/src/components/dropdown.tsx +2 -2
- package/src/components/label.tsx +1 -3
- package/src/components/link.tsx +2 -1
- package/src/components/nav-list.tsx +2 -1
- package/src/components/popout.tsx +1 -2
- package/src/components/popover.tsx +1 -2
- package/src/components/radio-buttons.tsx +21 -20
- package/src/components/slider.tsx +1 -2
- package/src/components/tabs.tsx +67 -0
- package/src/components/text-input.tsx +5 -4
- package/src/components/validation-rules.tsx +50 -0
- package/src/components/validation.tsx +175 -177
- package/src/index.tsx +2 -3
- package/src/theme/base.scss +7 -0
- package/src/theme/components/card.scss +4 -1
- package/src/theme/components/checkbox.scss +5 -0
- package/src/theme/components/column.scss +5 -0
- package/src/theme/components/radio-buttons.scss +5 -0
- package/src/theme/components/slider.scss +1 -0
- package/src/theme/components/tabs.scss +72 -0
- package/src/theme/theme.scss +1 -0
- package/dist/common/debounce.d.ts +0 -12
- package/dist/common/debounce.js +0 -23
- package/dist/common/debounce.js.map +0 -1
- package/dist/common/parsers.d.ts +0 -88
- package/dist/common/parsers.js +0 -62
- package/dist/common/parsers.js.map +0 -1
- package/dist/common/trim.d.ts +0 -12
- package/dist/common/trim.js +0 -16
- package/dist/common/trim.js.map +0 -1
- package/src/common/debounce.tsx +0 -36
- package/src/common/parsers.tsx +0 -167
- package/src/common/trim.tsx +0 -30
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ClassValue, Expression, get,
|
|
1
|
+
import { ClassValue, Expression, get, Signal, StyleValue, uniqueId, watch } from "rvx";
|
|
2
2
|
import { isPending } from "rvx/async";
|
|
3
|
-
import { uniqueId } from "rvx/id";
|
|
4
3
|
|
|
4
|
+
import { optionalString, string } from "rvx/convert";
|
|
5
5
|
import { THEME } from "../common/theme.js";
|
|
6
6
|
import { Text } from "./text.js";
|
|
7
|
-
import {
|
|
7
|
+
import { closestValidator } from "./validation.js";
|
|
8
8
|
|
|
9
9
|
export function Checkbox(props: {
|
|
10
10
|
checked?: Expression<boolean | undefined>;
|
|
@@ -23,7 +23,7 @@ export function Checkbox(props: {
|
|
|
23
23
|
? () => isPending() || get(props.disabled)
|
|
24
24
|
: () => true;
|
|
25
25
|
|
|
26
|
-
const validator = props.checked instanceof Signal ?
|
|
26
|
+
const validator = props.checked instanceof Signal ? closestValidator(props.checked) : undefined;
|
|
27
27
|
|
|
28
28
|
const input = <input
|
|
29
29
|
id={id}
|
|
@@ -36,7 +36,7 @@ export function Checkbox(props: {
|
|
|
36
36
|
}}
|
|
37
37
|
aria-readonly={string(!(props.checked instanceof Signal))}
|
|
38
38
|
aria-invalid={validator ? optionalString(validator.invalid) : undefined}
|
|
39
|
-
aria-errormessage={validator ? validator.
|
|
39
|
+
aria-errormessage={validator ? validator.messageIds : undefined}
|
|
40
40
|
autofocus={props.autofocus}
|
|
41
41
|
disabled={disabled}
|
|
42
42
|
/> as HTMLInputElement;
|
|
@@ -54,7 +54,7 @@ export function Checkbox(props: {
|
|
|
54
54
|
]}
|
|
55
55
|
style={props.style}
|
|
56
56
|
>
|
|
57
|
-
{input}
|
|
57
|
+
{theme?.checkbox_padding ? <div class={theme.checkbox_padding}>{input}</div> : input}
|
|
58
58
|
<Text class={theme?.checkbox_content}>
|
|
59
59
|
{props.children}
|
|
60
60
|
</Text>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { $, ClassValue, Expression, get, map,
|
|
2
|
-
import {
|
|
1
|
+
import { $, ClassValue, Component, Event, Expression, For, get, map, Signal, StyleValue, teardown, watch } from "rvx";
|
|
2
|
+
import { useMicrotask, useTimeout } from "rvx/async";
|
|
3
|
+
import { optionalString } from "rvx/convert";
|
|
3
4
|
import { THEME } from "../common/theme.js";
|
|
4
5
|
import { AriaLive, AriaRelevant } from "../common/types.js";
|
|
5
6
|
|
|
6
7
|
export function Collapse(props: {
|
|
7
8
|
visible?: Expression<boolean | undefined>;
|
|
9
|
+
fadein?: Expression<boolean | undefined>;
|
|
8
10
|
alert?: Event<[]>;
|
|
9
11
|
children?: unknown;
|
|
10
12
|
class?: ClassValue;
|
|
@@ -15,7 +17,6 @@ export function Collapse(props: {
|
|
|
15
17
|
"aria-atomic"?: Expression<boolean | undefined>;
|
|
16
18
|
}): unknown {
|
|
17
19
|
const theme = THEME.current;
|
|
18
|
-
const visible = map(props.visible, v => v ?? false);
|
|
19
20
|
const alert = $(false);
|
|
20
21
|
const size = $<number | undefined>(undefined);
|
|
21
22
|
|
|
@@ -37,7 +38,7 @@ export function Collapse(props: {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
props.alert?.(() => {
|
|
40
|
-
if (get(visible)) {
|
|
41
|
+
if (get(props.visible) ?? false) {
|
|
41
42
|
alert.value = false;
|
|
42
43
|
// Force a reflow:
|
|
43
44
|
void root.offsetWidth;
|
|
@@ -45,8 +46,27 @@ export function Collapse(props: {
|
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
let visible = props.visible;
|
|
50
|
+
if (props.fadein !== undefined) {
|
|
51
|
+
const visibleSig = visible = $(false);
|
|
52
|
+
watch(props.visible, visible => {
|
|
53
|
+
const fadein = get(props.fadein);
|
|
54
|
+
if (fadein) {
|
|
55
|
+
visibleSig.value = false;
|
|
56
|
+
let handle = requestAnimationFrame(() => {
|
|
57
|
+
handle = requestAnimationFrame(() => {
|
|
58
|
+
visibleSig.value = visible ?? false;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
teardown(() => cancelAnimationFrame(handle));
|
|
62
|
+
} else {
|
|
63
|
+
visibleSig.value = visible ?? false;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
const root = <div
|
|
49
|
-
inert={map(visible, v => !v)}
|
|
69
|
+
inert={map(props.visible, v => !v)}
|
|
50
70
|
class={[
|
|
51
71
|
theme?.collapse,
|
|
52
72
|
() => size.value === undefined ? undefined : theme?.collapse_sized,
|
|
@@ -73,3 +93,105 @@ export function Collapse(props: {
|
|
|
73
93
|
</div> as HTMLDivElement;
|
|
74
94
|
return root;
|
|
75
95
|
}
|
|
96
|
+
|
|
97
|
+
export interface CollapseItem<T> {
|
|
98
|
+
value: T;
|
|
99
|
+
alert?: Event<[]>;
|
|
100
|
+
class?: ClassValue;
|
|
101
|
+
style?: StyleValue;
|
|
102
|
+
id?: Expression<string | undefined>;
|
|
103
|
+
"aria-live"?: Expression<AriaLive | undefined>;
|
|
104
|
+
"aria-relevant"?: Expression<AriaRelevant | undefined>;
|
|
105
|
+
"aria-atomic"?: Expression<boolean | undefined>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function CollapseFor<T>(props: {
|
|
109
|
+
each: Expression<Iterable<CollapseItem<T>>>;
|
|
110
|
+
children: Component<T>;
|
|
111
|
+
}): unknown {
|
|
112
|
+
|
|
113
|
+
interface Entry {
|
|
114
|
+
/** item */
|
|
115
|
+
i: CollapseItem<T>;
|
|
116
|
+
/** visible */
|
|
117
|
+
v: Signal<boolean>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const entries = $<Entry[]>([]);
|
|
121
|
+
const fadein = $(false);
|
|
122
|
+
useMicrotask(() => fadein.value = true);
|
|
123
|
+
|
|
124
|
+
watch(() => {
|
|
125
|
+
const iter = get(props.each);
|
|
126
|
+
return Array.isArray(iter) ? iter : Array.from(iter);
|
|
127
|
+
}, items => {
|
|
128
|
+
entries.update(entries => {
|
|
129
|
+
let itemIndex = 0;
|
|
130
|
+
let entryIndex = 0;
|
|
131
|
+
|
|
132
|
+
function hasRemainingItem(value: T): boolean {
|
|
133
|
+
for (let i = itemIndex + 1; i < items.length; i++) {
|
|
134
|
+
if (Object.is(items[i].value, value)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function spliceRemainingEntry(value: T): Entry | undefined {
|
|
142
|
+
for (let i = entryIndex + 1; i < entries.length; i++) {
|
|
143
|
+
if (Object.is(entries[i].i.value, value)) {
|
|
144
|
+
return entries.splice(i, 1)[0];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
items: while (itemIndex < items.length) {
|
|
150
|
+
const item = items[itemIndex];
|
|
151
|
+
let entry = entries[entryIndex] as Entry | undefined;
|
|
152
|
+
if (entry && Object.is(entry.i.value, item.value)) {
|
|
153
|
+
entry.v.value = true;
|
|
154
|
+
} else if (entry && !hasRemainingItem(entry.i.value)) {
|
|
155
|
+
entry.v.value = false;
|
|
156
|
+
entryIndex++;
|
|
157
|
+
continue items;
|
|
158
|
+
} else if (entry = spliceRemainingEntry(item.value)) {
|
|
159
|
+
entries.splice(entryIndex, 0, entry);
|
|
160
|
+
entry.v.value = true;
|
|
161
|
+
} else {
|
|
162
|
+
entries.splice(entryIndex, 0, { i: item, v: $(true) });
|
|
163
|
+
}
|
|
164
|
+
itemIndex++;
|
|
165
|
+
entryIndex++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
while (entryIndex < entries.length) {
|
|
169
|
+
entries[entryIndex].v.value = false;
|
|
170
|
+
entryIndex++;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
useTimeout(() => {
|
|
175
|
+
const filtered = entries.value.filter(e => e.v.value);
|
|
176
|
+
if (filtered.length < entries.value.length) {
|
|
177
|
+
entries.value = filtered;
|
|
178
|
+
}
|
|
179
|
+
}, 1000);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return <For each={entries}>
|
|
183
|
+
{instance => <Collapse
|
|
184
|
+
visible={instance.v}
|
|
185
|
+
fadein={fadein}
|
|
186
|
+
alert={instance.i.alert}
|
|
187
|
+
class={instance.i.class}
|
|
188
|
+
style={instance.i.style}
|
|
189
|
+
id={instance.i.id}
|
|
190
|
+
aria-live={instance.i["aria-live"]}
|
|
191
|
+
aria-relevant={instance.i["aria-relevant"]}
|
|
192
|
+
aria-atomic={instance.i["aria-atomic"]}
|
|
193
|
+
>
|
|
194
|
+
{props.children(instance.i.value)}
|
|
195
|
+
</Collapse>}
|
|
196
|
+
</For>;
|
|
197
|
+
}
|
|
@@ -11,12 +11,14 @@ export function Column(props: {
|
|
|
11
11
|
style?: StyleValue;
|
|
12
12
|
id?: Expression<string | undefined>;
|
|
13
13
|
children?: unknown;
|
|
14
|
+
padded?: boolean;
|
|
14
15
|
}): unknown {
|
|
15
16
|
const theme = THEME.current;
|
|
16
17
|
return <div
|
|
17
18
|
class={[
|
|
18
19
|
theme?.column,
|
|
19
20
|
map(props.size, size => theme?.[`column_${size ?? "content"}`]),
|
|
21
|
+
map(props.padded, padded => padded ? theme?.column_padded : undefined),
|
|
20
22
|
props.class,
|
|
21
23
|
]}
|
|
22
24
|
style={props.style}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { $, captureSelf, ClassValue, Context,
|
|
2
|
-
import { TASKS, Tasks } from "rvx/async";
|
|
3
|
-
import { Emitter, Event } from "rvx/event";
|
|
4
|
-
import { uniqueId } from "rvx/id";
|
|
1
|
+
import { $, captureSelf, ClassValue, Context, Emitter, Event, Expression, map, render, StyleValue, teardown, uniqueId } from "rvx";
|
|
2
|
+
import { TASKS, Tasks, useMicrotask } from "rvx/async";
|
|
5
3
|
import { FlexSpace, Heading, Row, Text, THEME } from "../index.js";
|
|
6
4
|
import { LAYER, Layer } from "./layer.js";
|
|
7
5
|
|
|
@@ -126,7 +124,7 @@ export function DialogBody(props: {
|
|
|
126
124
|
</div>
|
|
127
125
|
</div> as HTMLElement;
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
useMicrotask(() => {
|
|
130
128
|
if (theme?.dialog_fadein) {
|
|
131
129
|
body.offsetParent;
|
|
132
130
|
body.classList.add(theme.dialog_fadein);
|
|
@@ -2,7 +2,7 @@ import { ClassValue, Expression, get, map, Signal, StyleValue } from "rvx";
|
|
|
2
2
|
import { Button, ButtonVariant } from "./button.js";
|
|
3
3
|
import { Dropdown, DropdownItem } from "./dropdown.js";
|
|
4
4
|
import { PopoutAlignment, PopoutPlacement } from "./popout.js";
|
|
5
|
-
import {
|
|
5
|
+
import { closestValidator } from "./validation.js";
|
|
6
6
|
|
|
7
7
|
export interface DropdownValue<T> {
|
|
8
8
|
value: T;
|
|
@@ -46,7 +46,7 @@ export function DropdownInput<T>(props: {
|
|
|
46
46
|
role="combobox"
|
|
47
47
|
aria-label={props["aria-label"]}
|
|
48
48
|
aria-labelledby={props["aria-labelledby"]}
|
|
49
|
-
validator={props.value instanceof Signal ?
|
|
49
|
+
validator={props.value instanceof Signal ? closestValidator(props.value) : undefined}
|
|
50
50
|
>
|
|
51
51
|
{props.children ?? (() => {
|
|
52
52
|
const value = get(props.value);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $, ClassValue, Expression, For, get, map, memo,
|
|
2
|
-
import {
|
|
1
|
+
import { $, ClassValue, Expression, For, get, map, memo, render, StyleValue, uniqueId, View, watch } from "rvx";
|
|
2
|
+
import { optionalString } from "rvx/convert";
|
|
3
3
|
import { Action, createPassiveActionEvent, handleActionEvent, keyFor, startDelayedHoverOnMouseenter } from "../common/events.js";
|
|
4
4
|
import { THEME } from "../common/theme.js";
|
|
5
5
|
import { LAYER } from "./layer.js";
|
package/src/components/label.tsx
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { ClassValue, Expression, StyleValue } from "rvx";
|
|
2
|
-
|
|
1
|
+
import { ClassValue, Expression, StyleValue, uniqueId } from "rvx";
|
|
3
2
|
import { THEME } from "../common/theme.js";
|
|
4
|
-
import { uniqueId } from "rvx/id";
|
|
5
3
|
|
|
6
4
|
export function Label(props: {
|
|
7
5
|
class?: ClassValue;
|
package/src/components/link.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ClassValue, Expression, get, map,
|
|
1
|
+
import { ClassValue, Expression, get, map, StyleValue } from "rvx";
|
|
2
2
|
import { isPending } from "rvx/async";
|
|
3
|
+
import { optionalString } from "rvx/convert";
|
|
3
4
|
import { Action, handleActionEvent, keyFor } from "../common/events.js";
|
|
4
5
|
import { THEME } from "../common/theme.js";
|
|
5
6
|
import { separated } from "../common/types.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ClassValue, Expression, get, map,
|
|
1
|
+
import { ClassValue, Expression, get, map, StyleValue } from "rvx";
|
|
2
2
|
import { isPending } from "rvx/async";
|
|
3
|
+
import { optionalString } from "rvx/convert";
|
|
3
4
|
import { Action, handleActionEvent, keyFor } from "../common/events.js";
|
|
4
5
|
import { THEME } from "../common/theme.js";
|
|
5
6
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { $, captureSelf, Context, Expression, get, render, teardown, TeardownHook, untrack, View, viewNodes } from "rvx";
|
|
2
|
-
import { Emitter, Event as RvxEvent } from "rvx/event";
|
|
1
|
+
import { $, captureSelf, Context, Emitter, Expression, get, render, Event as RvxEvent, teardown, TeardownHook, untrack, View, viewNodes } from "rvx";
|
|
3
2
|
import { PASSIVE_ACTION_EVENT } from "../common/events.js";
|
|
4
3
|
import { axisEquals, Direction, DOWN, flip, getBlockStart, getInlineStart, getSize, getWindowSize, getWindowSpaceAround, INSET, LEFT, RIGHT, ScriptDirection, UP, WritingMode } from "../common/writing-mode.js";
|
|
5
4
|
import { LAYER, Layer } from "./layer.js";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { $, ClassValue, Expression, get, Inject, map, render, StyleValue, SVG, watch, XMLNS } from "rvx";
|
|
2
|
-
import { uniqueId } from "rvx/id";
|
|
1
|
+
import { $, ClassValue, Expression, get, Inject, map, render, StyleValue, SVG, uniqueId, watch, XMLNS } from "rvx";
|
|
3
2
|
import { Action } from "../common/events.js";
|
|
4
3
|
import { THEME } from "../common/theme.js";
|
|
5
4
|
import { DOWN, getSize, getXY, LEFT, RIGHT, UP } from "../common/writing-mode.js";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ClassValue, Expression, For, get, map,
|
|
1
|
+
import { ClassValue, Expression, For, get, map, Signal, StyleValue, uniqueId } from "rvx";
|
|
2
2
|
import { isPending } from "rvx/async";
|
|
3
|
+
import { optionalString, string } from "rvx/convert";
|
|
3
4
|
import { THEME } from "../common/theme.js";
|
|
4
5
|
import { Text } from "./text.js";
|
|
5
|
-
import {
|
|
6
|
-
import { uniqueId } from "rvx/id";
|
|
6
|
+
import { closestValidator } from "./validation.js";
|
|
7
7
|
|
|
8
8
|
export interface RadioOption<T> {
|
|
9
9
|
value: T;
|
|
@@ -32,7 +32,7 @@ export function RadioButtons<T>(props: {
|
|
|
32
32
|
? () => isPending() || get(props.disabled)
|
|
33
33
|
: true;
|
|
34
34
|
|
|
35
|
-
const validator = props.value instanceof Signal ?
|
|
35
|
+
const validator = props.value instanceof Signal ? closestValidator(props.value) : undefined;
|
|
36
36
|
|
|
37
37
|
return <div
|
|
38
38
|
role="radiogroup"
|
|
@@ -44,33 +44,34 @@ export function RadioButtons<T>(props: {
|
|
|
44
44
|
style={props.style}
|
|
45
45
|
aria-readonly={string(!(props.options instanceof Signal))}
|
|
46
46
|
aria-invalid={validator ? optionalString(validator.invalid) : undefined}
|
|
47
|
-
aria-errormessage={validator ? validator.
|
|
47
|
+
aria-errormessage={validator ? validator.messageIds : undefined}
|
|
48
48
|
aria-label={props["aria-label"]}
|
|
49
49
|
aria-labelledby={props["aria-labelledby"]}
|
|
50
50
|
>
|
|
51
51
|
<For each={props.options}>
|
|
52
52
|
{(option, index) => {
|
|
53
53
|
const id = uniqueId();
|
|
54
|
+
const input = <input
|
|
55
|
+
id={id}
|
|
56
|
+
type="radio"
|
|
57
|
+
class={theme?.radio_button_input}
|
|
58
|
+
name={group}
|
|
59
|
+
value={id}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
autofocus={() => get(props.autofocus) && index() === 0}
|
|
62
|
+
prop:checked={map(props.value, x => x === option.value)}
|
|
63
|
+
on:input={() => {
|
|
64
|
+
if (props.value instanceof Signal) {
|
|
65
|
+
props.value.value = option.value;
|
|
66
|
+
}
|
|
67
|
+
}}
|
|
68
|
+
/>;
|
|
54
69
|
|
|
55
70
|
return <label
|
|
56
71
|
for={id}
|
|
57
72
|
class={theme?.radio_button_label}
|
|
58
73
|
>
|
|
59
|
-
<input
|
|
60
|
-
id={id}
|
|
61
|
-
type="radio"
|
|
62
|
-
class={theme?.radio_button_input}
|
|
63
|
-
name={group}
|
|
64
|
-
value={id}
|
|
65
|
-
disabled={disabled}
|
|
66
|
-
autofocus={() => get(props.autofocus) && index() === 0}
|
|
67
|
-
prop:checked={map(props.value, x => x === option.value)}
|
|
68
|
-
on:input={() => {
|
|
69
|
-
if (props.value instanceof Signal) {
|
|
70
|
-
props.value.value = option.value;
|
|
71
|
-
}
|
|
72
|
-
}}
|
|
73
|
-
/>
|
|
74
|
+
{theme?.radio_button_padding ? <div class={theme.radio_button_padding}>{input}</div> : input}
|
|
74
75
|
<Text class={theme?.radio_button_content}>
|
|
75
76
|
{option.label}
|
|
76
77
|
</Text>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { $, Component, Expression, For, get, map, Show, Signal, uniqueIdFor, watch } from "rvx";
|
|
2
|
+
import { string } from "rvx/convert";
|
|
3
|
+
import { THEME } from "../common/theme.js";
|
|
4
|
+
|
|
5
|
+
export interface Tab {
|
|
6
|
+
label: Component;
|
|
7
|
+
content: Component;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Tabs(props: {
|
|
11
|
+
tabs: Expression<Iterable<Tab>>;
|
|
12
|
+
selected?: Signal<Tab | undefined>;
|
|
13
|
+
padded?: Expression<boolean | undefined>;
|
|
14
|
+
}) {
|
|
15
|
+
const theme = THEME.current;
|
|
16
|
+
const selected = props.selected ?? $(undefined);
|
|
17
|
+
|
|
18
|
+
watch(selected, current => {
|
|
19
|
+
if (current === undefined) {
|
|
20
|
+
for (const tab of get(props.tabs)) {
|
|
21
|
+
selected.value = tab;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return <>
|
|
28
|
+
<div
|
|
29
|
+
role="tablist"
|
|
30
|
+
class={[
|
|
31
|
+
theme?.tab_list,
|
|
32
|
+
map(props.padded, padded => padded ? theme?.tab_list_padded : undefined),
|
|
33
|
+
]}
|
|
34
|
+
>
|
|
35
|
+
<For each={props.tabs}>
|
|
36
|
+
{tab => <button
|
|
37
|
+
role="tab"
|
|
38
|
+
class={[
|
|
39
|
+
theme?.tab_handle,
|
|
40
|
+
() => selected.value === tab ? theme?.tab_handle_current : undefined,
|
|
41
|
+
]}
|
|
42
|
+
aria-selected={string(() => selected.value === tab)}
|
|
43
|
+
aria-controls={uniqueIdFor(tab)}
|
|
44
|
+
on:click={event => {
|
|
45
|
+
event.stopImmediatePropagation();
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
selected.value = tab;
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{tab.label()}
|
|
51
|
+
</button>}
|
|
52
|
+
</For>
|
|
53
|
+
</div>
|
|
54
|
+
<Show when={selected}>
|
|
55
|
+
{tab => <div
|
|
56
|
+
role="tabpanel"
|
|
57
|
+
id={uniqueIdFor(tab)}
|
|
58
|
+
class={[
|
|
59
|
+
theme?.tab_panel,
|
|
60
|
+
map(props.padded, padded => padded ? theme?.tab_panel_padded : undefined),
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{tab.content()}
|
|
64
|
+
</div>}
|
|
65
|
+
</Show>
|
|
66
|
+
</>;
|
|
67
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ClassValue, Expression, get,
|
|
1
|
+
import { ClassValue, Expression, get, Signal, StyleValue } from "rvx";
|
|
2
2
|
import { isPending, waitFor } from "rvx/async";
|
|
3
|
+
import { optionalString } from "rvx/convert";
|
|
3
4
|
import { keyFor } from "../common/events.js";
|
|
4
5
|
import { THEME } from "../common/theme.js";
|
|
5
|
-
import {
|
|
6
|
+
import { closestValidator } from "./validation.js";
|
|
6
7
|
|
|
7
8
|
export type TextInputType = "text" | "password";
|
|
8
9
|
export type TextAreaWrap = "hard" | "soft";
|
|
@@ -66,7 +67,7 @@ export function TextInput(props: ({
|
|
|
66
67
|
const theme = THEME.current;
|
|
67
68
|
const disabled = () => isPending() || get(props.disabled);
|
|
68
69
|
|
|
69
|
-
const validator = props.value instanceof Signal ?
|
|
70
|
+
const validator = props.value instanceof Signal ? closestValidator(props.value) : undefined;
|
|
70
71
|
|
|
71
72
|
const InputTag = props.multiline ? "textarea" : "input";
|
|
72
73
|
const input = <InputTag
|
|
@@ -109,7 +110,7 @@ export function TextInput(props: ({
|
|
|
109
110
|
aria-labelledby={props["aria-labelledby"]}
|
|
110
111
|
|
|
111
112
|
aria-invalid={validator ? optionalString(validator.invalid) : undefined}
|
|
112
|
-
aria-errormessage={validator ? validator.
|
|
113
|
+
aria-errormessage={validator ? validator.messageIds : undefined}
|
|
113
114
|
/> as HTMLInputElement;
|
|
114
115
|
|
|
115
116
|
return input;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { $, Component, Expression, get, Signal, watchUpdates } from "rvx";
|
|
2
|
+
import { Validator } from "./validation.js";
|
|
3
|
+
|
|
4
|
+
export function rule<T>(source: Signal<T>, condition: (value: T) => boolean, message: Component): Signal<T> {
|
|
5
|
+
Validator.get(source).prependRule(() => {
|
|
6
|
+
if (!condition(source.value)) {
|
|
7
|
+
return [message];
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
return source;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IntParserOptions {
|
|
14
|
+
/** The validation message for invalid formats. */
|
|
15
|
+
format: Component;
|
|
16
|
+
/** The validation message for an out of range value. Defaults to the format message. */
|
|
17
|
+
range?: Component;
|
|
18
|
+
min?: Expression<number>;
|
|
19
|
+
max?: Expression<number>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function intParser(source: Signal<number>, options: IntParserOptions): Signal<string> {
|
|
23
|
+
const input = $(String(source.value), source);
|
|
24
|
+
|
|
25
|
+
const messages = $<Component[]>([]);
|
|
26
|
+
Validator.get(source).prependRule(() => messages.value);
|
|
27
|
+
|
|
28
|
+
const min = options.min ?? Number.MIN_SAFE_INTEGER;
|
|
29
|
+
const max = options.max ?? Number.MAX_SAFE_INTEGER;
|
|
30
|
+
|
|
31
|
+
watchUpdates(source, value => {
|
|
32
|
+
input.value = String(value);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
watchUpdates(input, value => {
|
|
36
|
+
if (/^-?\d+$/.test(value)) {
|
|
37
|
+
const num = Number.parseInt(value);
|
|
38
|
+
if (Number.isSafeInteger(num) && num >= get(min) && num <= get(max)) {
|
|
39
|
+
messages.value = [];
|
|
40
|
+
source.value = num;
|
|
41
|
+
} else {
|
|
42
|
+
messages.value = [options.range ?? options.format];
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
messages.value = [options.format];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return input;
|
|
50
|
+
}
|