@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,292 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, get, Inject, map, render, sig, StyleValue, SVG, uniqueId, watch, XMLNS } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { Action } from "../common/events.js";
|
|
4
|
+
import { THEME } from "../common/theme.js";
|
|
5
|
+
import { DOWN, getSize, getXY, LEFT, RIGHT, UP } from "../common/writing-mode.js";
|
|
6
|
+
import { DialogRole } from "./dialog.js";
|
|
7
|
+
import { LAYER } from "./layer.js";
|
|
8
|
+
import { Popout, PopoutAlignment, PopoutPlacement } from "./popout.js";
|
|
9
|
+
|
|
10
|
+
export type PopoverRole = DialogRole | "menu";
|
|
11
|
+
|
|
12
|
+
export interface PopoverContent {
|
|
13
|
+
(props: {
|
|
14
|
+
popout: Popout;
|
|
15
|
+
}): unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a popover that is initially hidden.
|
|
20
|
+
*/
|
|
21
|
+
export function createPopover(props: {
|
|
22
|
+
/**
|
|
23
|
+
* Defines the direction in which the popover is placed in relation to the anchor.
|
|
24
|
+
*
|
|
25
|
+
* This expression is only evaluated when calculating the popover placement.
|
|
26
|
+
*
|
|
27
|
+
* See {@link PopoutPlacement}
|
|
28
|
+
*
|
|
29
|
+
* @default "block"
|
|
30
|
+
*/
|
|
31
|
+
placement?: Expression<PopoutPlacement | undefined>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Defines which side of the anchor and popover are aligned orthogonally to the placement axis.
|
|
35
|
+
*
|
|
36
|
+
* This expression is only evaluated when calculating the popover placement.
|
|
37
|
+
*
|
|
38
|
+
* See {@link PopoutAlignment}
|
|
39
|
+
*
|
|
40
|
+
* @default "center"
|
|
41
|
+
*/
|
|
42
|
+
alignment?: Expression<PopoutAlignment | undefined>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The content component.
|
|
46
|
+
*/
|
|
47
|
+
content: PopoverContent;
|
|
48
|
+
|
|
49
|
+
inlineSize?: Expression<string | undefined>;
|
|
50
|
+
maxInlineSize?: Expression<string | undefined>;
|
|
51
|
+
blockSize?: Expression<string | undefined>;
|
|
52
|
+
maxBlockSize?: Expression<string | undefined>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* An array of event names that cause the popover to hide automatically when dispatched outside of the current layer stack or the anchor.
|
|
56
|
+
*
|
|
57
|
+
* @default ["resize", "scroll", "mousedown", "touchstart", "focusin", "rvx-ui:passive-action"]
|
|
58
|
+
*/
|
|
59
|
+
foreignEvents?: string[];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The content role.
|
|
63
|
+
*
|
|
64
|
+
* @default "dialog"
|
|
65
|
+
*/
|
|
66
|
+
role?: PopoverRole;
|
|
67
|
+
|
|
68
|
+
id?: string;
|
|
69
|
+
class?: ClassValue;
|
|
70
|
+
style?: StyleValue;
|
|
71
|
+
"aria-label"?: Expression<string | undefined>;
|
|
72
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
73
|
+
"aria-describedby"?: Expression<string | undefined>;
|
|
74
|
+
}): Popout {
|
|
75
|
+
return new Popout({
|
|
76
|
+
placement: map(props.placement, v => v ?? "block"),
|
|
77
|
+
alignment: map(props.alignment, v => v ?? "center"),
|
|
78
|
+
content: ({ popout, onPlacement, placement, setSizeReference }) => {
|
|
79
|
+
const theme = extract(THEME);
|
|
80
|
+
const layer = extract(LAYER)!;
|
|
81
|
+
const spikeTransform = sig("");
|
|
82
|
+
|
|
83
|
+
layer.useHotkey("escape", () => {
|
|
84
|
+
popout.hide();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
onPlacement(args => {
|
|
88
|
+
args.gap = Math.abs(spikeArea.getBoundingClientRect().x - root.getBoundingClientRect().x);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
watch(placement, placement => {
|
|
92
|
+
if (placement) {
|
|
93
|
+
const { anchorRect, placementDir, alignStart } = placement;
|
|
94
|
+
const contentRect = placement.content.getBoundingClientRect();
|
|
95
|
+
const rawOffset = getXY(anchorRect, alignStart) + (getSize(anchorRect, alignStart) / 2) - getXY(contentRect, alignStart);
|
|
96
|
+
const offset = `max(var(--popover-spike-min-offset), min(${rawOffset}px, ${getSize(contentRect, alignStart)}px - var(--popover-spike-min-offset)))`;
|
|
97
|
+
switch (placementDir) {
|
|
98
|
+
case DOWN:
|
|
99
|
+
spikeTransform.value = `translate(${offset}, 0px)`;
|
|
100
|
+
break;
|
|
101
|
+
case UP:
|
|
102
|
+
spikeTransform.value = `translate(${offset}, ${contentRect.height}px) rotate(180deg)`;
|
|
103
|
+
break;
|
|
104
|
+
case RIGHT:
|
|
105
|
+
spikeTransform.value = `translate(0px, ${offset}) rotate(270deg)`;
|
|
106
|
+
break;
|
|
107
|
+
case LEFT:
|
|
108
|
+
spikeTransform.value = `translate(${contentRect.width}px, ${offset}) rotate(90deg)`;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const spikeArea = <div class={theme?.popover_spike_area}>
|
|
115
|
+
<div class={theme?.popover_spike} style={{ transform: spikeTransform }}>
|
|
116
|
+
<Inject key={XMLNS} value={SVG}>
|
|
117
|
+
{() => {
|
|
118
|
+
return <svg viewBox="0 0 16 16" preserveAspectRatio="none">
|
|
119
|
+
<path d="M0,16 L8,0 L16,16 Z" />
|
|
120
|
+
</svg>;
|
|
121
|
+
}}
|
|
122
|
+
</Inject>
|
|
123
|
+
</div>
|
|
124
|
+
</div> as HTMLElement;
|
|
125
|
+
|
|
126
|
+
const content = <div class={[
|
|
127
|
+
theme?.column,
|
|
128
|
+
theme?.column_content,
|
|
129
|
+
theme?.popover_content,
|
|
130
|
+
]}>
|
|
131
|
+
{props.content({ popout })}
|
|
132
|
+
</div> as HTMLElement;
|
|
133
|
+
setSizeReference(content);
|
|
134
|
+
|
|
135
|
+
const root = <div
|
|
136
|
+
tabindex="-1"
|
|
137
|
+
role={map(props.role, v => v ?? "dialog")}
|
|
138
|
+
id={props.id}
|
|
139
|
+
class={[
|
|
140
|
+
theme?.popover,
|
|
141
|
+
props.class,
|
|
142
|
+
]}
|
|
143
|
+
style={[
|
|
144
|
+
props.style,
|
|
145
|
+
{
|
|
146
|
+
"inline-size": props.inlineSize,
|
|
147
|
+
"max-inline-size": props.maxInlineSize,
|
|
148
|
+
"block-size": props.blockSize,
|
|
149
|
+
"max-block-size": props.maxBlockSize,
|
|
150
|
+
},
|
|
151
|
+
]}
|
|
152
|
+
aria-label={props["aria-label"]}
|
|
153
|
+
aria-labelledby={props["aria-labelledby"]}
|
|
154
|
+
aria-describedby={props["aria-describedby"]}
|
|
155
|
+
>
|
|
156
|
+
{spikeArea}
|
|
157
|
+
<div class={theme?.popover_scroll_area}>
|
|
158
|
+
{content}
|
|
159
|
+
</div>
|
|
160
|
+
</div> as HTMLElement;
|
|
161
|
+
layer.useAutoFocusFallback(root);
|
|
162
|
+
return root;
|
|
163
|
+
},
|
|
164
|
+
foreignEvents: props.foreignEvents,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface PopoverAnchorProps {
|
|
169
|
+
action: Action;
|
|
170
|
+
id?: Expression<string | undefined>;
|
|
171
|
+
"aria-label"?: Expression<string | undefined>;
|
|
172
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function Popover(props: {
|
|
176
|
+
/**
|
|
177
|
+
* A function to immediately render the anchor.
|
|
178
|
+
*/
|
|
179
|
+
anchor: (props: PopoverAnchorProps) => unknown;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The anchor id.
|
|
183
|
+
*
|
|
184
|
+
* By default, a unique id is generated and linked to the attached popover.
|
|
185
|
+
*/
|
|
186
|
+
id?: Expression<string | undefined>;
|
|
187
|
+
|
|
188
|
+
/** Class for the popover. */
|
|
189
|
+
class?: ClassValue;
|
|
190
|
+
|
|
191
|
+
/** Style for the popover. */
|
|
192
|
+
style?: StyleValue;
|
|
193
|
+
|
|
194
|
+
/** The popover content component. */
|
|
195
|
+
children: PopoverContent;
|
|
196
|
+
|
|
197
|
+
inlineSize?: Expression<string | undefined>;
|
|
198
|
+
maxInlineSize?: Expression<string | undefined>;
|
|
199
|
+
blockSize?: Expression<string | undefined>;
|
|
200
|
+
maxBlockSize?: Expression<string | undefined>;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Defines the direction in which the popover is placed in relation to the anchor.
|
|
204
|
+
*
|
|
205
|
+
* This expression is only evaluated when calculating the popover placement.
|
|
206
|
+
*
|
|
207
|
+
* See {@link PopoutPlacement}^
|
|
208
|
+
*
|
|
209
|
+
* @default "block"
|
|
210
|
+
*/
|
|
211
|
+
placement?: Expression<PopoutPlacement | undefined>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Defines which side of the anchor and popover are aligned orthogonally to the placement axis.
|
|
215
|
+
*
|
|
216
|
+
* This expression is only evaluated when calculating the popover placement.
|
|
217
|
+
*
|
|
218
|
+
* See {@link PopoutAlignment}
|
|
219
|
+
*
|
|
220
|
+
* @default "center"
|
|
221
|
+
*/
|
|
222
|
+
alignment?: Expression<PopoutAlignment | undefined>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* An array of event names that cause the popover to hide automatically when dispatched outside of the current layer stack or the anchor.
|
|
226
|
+
*
|
|
227
|
+
* @default ["resize", "scroll", "mousedown", "touchstart", "focusin", "rvx-ui:passive-action"]
|
|
228
|
+
*/
|
|
229
|
+
foreignEvents?: string[];
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The popover role.
|
|
233
|
+
*
|
|
234
|
+
* @default "dialog"
|
|
235
|
+
*/
|
|
236
|
+
role?: PopoverRole;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The anchor and popover label.
|
|
240
|
+
*
|
|
241
|
+
* By default, the anchor itself is used a label for the popover.
|
|
242
|
+
*/
|
|
243
|
+
"aria-label"?: Expression<string | undefined>;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The anchor and popover label id.
|
|
247
|
+
*
|
|
248
|
+
* By default, the anchor itself is used a label for the popover.
|
|
249
|
+
*/
|
|
250
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* An optional popover description id.
|
|
254
|
+
*
|
|
255
|
+
* If the popover contains a description, this should be set to it's id.
|
|
256
|
+
*/
|
|
257
|
+
"aria-describedby"?: Expression<string | undefined>;
|
|
258
|
+
}): unknown {
|
|
259
|
+
const defaultId = uniqueId();
|
|
260
|
+
const id = map(props.id, v => v ?? defaultId);
|
|
261
|
+
|
|
262
|
+
const popover = createPopover({
|
|
263
|
+
content: props.children,
|
|
264
|
+
inlineSize: props.inlineSize,
|
|
265
|
+
maxInlineSize: props.maxInlineSize,
|
|
266
|
+
blockSize: props.blockSize,
|
|
267
|
+
maxBlockSize: props.maxBlockSize,
|
|
268
|
+
placement: props.placement,
|
|
269
|
+
alignment: props.alignment,
|
|
270
|
+
foreignEvents: props.foreignEvents,
|
|
271
|
+
role: props.role,
|
|
272
|
+
class: props.class,
|
|
273
|
+
style: props.style,
|
|
274
|
+
|
|
275
|
+
"aria-label": props["aria-label"],
|
|
276
|
+
"aria-labelledby": () => (get(props["aria-label"]) === undefined
|
|
277
|
+
? (get(props["aria-labelledby"]) ?? get(id))
|
|
278
|
+
: undefined),
|
|
279
|
+
"aria-describedby": props["aria-describedby"],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const anchor = render(props.anchor({
|
|
283
|
+
action: event => {
|
|
284
|
+
popover.toggle(anchor, event);
|
|
285
|
+
},
|
|
286
|
+
id,
|
|
287
|
+
"aria-label": props["aria-label"],
|
|
288
|
+
"aria-labelledby": props["aria-labelledby"],
|
|
289
|
+
}));
|
|
290
|
+
|
|
291
|
+
return anchor;
|
|
292
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, For, get, map, optionalString, Signal, string, StyleValue, uniqueId } 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 interface RadioOption<T> {
|
|
9
|
+
value: T;
|
|
10
|
+
label: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RadioButtons<T>(props: {
|
|
14
|
+
value?: Expression<T | undefined>;
|
|
15
|
+
options: Expression<RadioOption<T>[]>;
|
|
16
|
+
|
|
17
|
+
disabled?: Expression<boolean | undefined>;
|
|
18
|
+
|
|
19
|
+
id?: Expression<string | undefined>;
|
|
20
|
+
class?: ClassValue;
|
|
21
|
+
style?: StyleValue;
|
|
22
|
+
autofocus?: Expression<boolean | undefined>;
|
|
23
|
+
"aria-label"?: Expression<string | undefined>;
|
|
24
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
25
|
+
|
|
26
|
+
children?: never;
|
|
27
|
+
}): unknown {
|
|
28
|
+
const group = uniqueId();
|
|
29
|
+
const theme = extract(THEME);
|
|
30
|
+
|
|
31
|
+
const disabled = props.value instanceof Signal
|
|
32
|
+
? () => isPending() || get(props.disabled)
|
|
33
|
+
: true;
|
|
34
|
+
|
|
35
|
+
const validator = props.value instanceof Signal ? validatorFor(props.value) : undefined;
|
|
36
|
+
|
|
37
|
+
return <div
|
|
38
|
+
role="radiogroup"
|
|
39
|
+
id={props.id}
|
|
40
|
+
class={[
|
|
41
|
+
theme?.radio_buttons,
|
|
42
|
+
props.class,
|
|
43
|
+
]}
|
|
44
|
+
style={props.style}
|
|
45
|
+
aria-readonly={string(!(props.options instanceof Signal))}
|
|
46
|
+
aria-invalid={validator ? optionalString(validator.invalid) : undefined}
|
|
47
|
+
aria-errormessage={validator ? validator.errorMessageIds : undefined}
|
|
48
|
+
aria-label={props["aria-label"]}
|
|
49
|
+
aria-labelledby={props["aria-labelledby"]}
|
|
50
|
+
>
|
|
51
|
+
<For each={props.options}>
|
|
52
|
+
{(option, index) => {
|
|
53
|
+
const id = uniqueId();
|
|
54
|
+
|
|
55
|
+
return <label
|
|
56
|
+
for={id}
|
|
57
|
+
class={theme?.radio_button_label}
|
|
58
|
+
>
|
|
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
|
+
<Text class={theme?.radio_button_content}>
|
|
75
|
+
{option.label}
|
|
76
|
+
</Text>
|
|
77
|
+
</label>;
|
|
78
|
+
}}
|
|
79
|
+
</For>
|
|
80
|
+
</div>;
|
|
81
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, get, StyleValue } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { THEME } from "../common/theme.js";
|
|
4
|
+
import { SizeContext } from "../common/types.js";
|
|
5
|
+
|
|
6
|
+
export type RowAlignment = "top" | "center" | "bottom";
|
|
7
|
+
|
|
8
|
+
export function Row(props: {
|
|
9
|
+
size?: Expression<SizeContext | undefined>;
|
|
10
|
+
align?: Expression<RowAlignment | undefined>;
|
|
11
|
+
class?: ClassValue;
|
|
12
|
+
style?: StyleValue;
|
|
13
|
+
children?: unknown;
|
|
14
|
+
}): unknown {
|
|
15
|
+
const theme = extract(THEME);
|
|
16
|
+
return <div
|
|
17
|
+
class={[
|
|
18
|
+
theme?.row,
|
|
19
|
+
() => theme?.[`row_${get(props.size) ?? "content"}`],
|
|
20
|
+
props.class,
|
|
21
|
+
]}
|
|
22
|
+
style={[
|
|
23
|
+
props.style,
|
|
24
|
+
{
|
|
25
|
+
"align-items": () => {
|
|
26
|
+
switch (get(props.align)) {
|
|
27
|
+
case "center": return "center";
|
|
28
|
+
case "bottom": return "end";
|
|
29
|
+
default: return "start";
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
]}
|
|
34
|
+
>
|
|
35
|
+
{props.children}
|
|
36
|
+
</div>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ClassValue, extract, sig, StyleValue, teardown } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { debounceEvent } from "../common/events.js";
|
|
4
|
+
import { THEME } from "../common/theme.js";
|
|
5
|
+
import { axisEquals, DOWN, getBlockStart, getSize, RIGHT, UP, WritingMode } from "../common/writing-mode.js";
|
|
6
|
+
|
|
7
|
+
export function ScrollView(props: {
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
style?: StyleValue;
|
|
10
|
+
children?: unknown;
|
|
11
|
+
}): unknown {
|
|
12
|
+
const theme = extract(THEME);
|
|
13
|
+
const vertical = sig<boolean | undefined>(undefined);
|
|
14
|
+
const scrollbarComp = sig(0);
|
|
15
|
+
const startIndicator = sig(false);
|
|
16
|
+
const endIndicator = sig(false);
|
|
17
|
+
|
|
18
|
+
const content = <div class={theme?.scroll_view_content}>
|
|
19
|
+
{props.children}
|
|
20
|
+
</div> as HTMLElement;
|
|
21
|
+
|
|
22
|
+
const updateIndicators = (blockStart = getBlockStart(getComputedStyle(area).writingMode as WritingMode || "horizontal-tb")) => {
|
|
23
|
+
const areaSize = getSize(area.getBoundingClientRect(), blockStart);
|
|
24
|
+
let start: number;
|
|
25
|
+
let end: number;
|
|
26
|
+
if (blockStart === UP || blockStart === DOWN) {
|
|
27
|
+
start = area.scrollTop;
|
|
28
|
+
end = area.scrollHeight - start - areaSize;
|
|
29
|
+
} else {
|
|
30
|
+
start = area.scrollLeft;
|
|
31
|
+
end = area.scrollWidth - start - areaSize;
|
|
32
|
+
}
|
|
33
|
+
if (blockStart === DOWN || blockStart === RIGHT) {
|
|
34
|
+
const x = start;
|
|
35
|
+
start = end;
|
|
36
|
+
end = x;
|
|
37
|
+
}
|
|
38
|
+
startIndicator.value = start > .5;
|
|
39
|
+
endIndicator.value = end > .5;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const area = <div
|
|
43
|
+
class={theme?.scroll_view_area}
|
|
44
|
+
style={{
|
|
45
|
+
overflow: () => vertical.value ? "hidden auto" : "auto hidden",
|
|
46
|
+
}}
|
|
47
|
+
on:scroll={[debounceEvent(100, () => updateIndicators()), { passive: true }]}
|
|
48
|
+
>
|
|
49
|
+
{content}
|
|
50
|
+
</div> as HTMLElement;
|
|
51
|
+
|
|
52
|
+
const root = <div
|
|
53
|
+
class={[
|
|
54
|
+
props.class,
|
|
55
|
+
theme?.scroll_view,
|
|
56
|
+
]}
|
|
57
|
+
style={[
|
|
58
|
+
props.style,
|
|
59
|
+
{
|
|
60
|
+
"--scrollbar-comp": () => `${scrollbarComp.value}px`,
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
{area}
|
|
65
|
+
<div class={[
|
|
66
|
+
theme?.scroll_view_indicator_start,
|
|
67
|
+
() => startIndicator.value && theme?.scroll_view_indicator_visible,
|
|
68
|
+
]} />
|
|
69
|
+
<div class={[
|
|
70
|
+
theme?.scroll_view_indicator_end,
|
|
71
|
+
() => endIndicator.value && theme?.scroll_view_indicator_visible,
|
|
72
|
+
]} />
|
|
73
|
+
</div> as HTMLElement;
|
|
74
|
+
|
|
75
|
+
const rootObserver = new ResizeObserver(() => {
|
|
76
|
+
const blockStart = getBlockStart(getComputedStyle(root).writingMode as WritingMode || "horizontal-tb");
|
|
77
|
+
vertical.value ??= axisEquals(blockStart, UP);
|
|
78
|
+
updateIndicators(blockStart);
|
|
79
|
+
});
|
|
80
|
+
rootObserver.observe(root);
|
|
81
|
+
teardown(() => rootObserver.disconnect());
|
|
82
|
+
|
|
83
|
+
const contentObserver = new IntersectionObserver(() => {
|
|
84
|
+
const rootRect = root.getBoundingClientRect();
|
|
85
|
+
const contentRect = content.getBoundingClientRect();
|
|
86
|
+
const blockStart = getBlockStart(getComputedStyle(root).writingMode as WritingMode || "horizontal-tb");
|
|
87
|
+
const isVertical = axisEquals(blockStart, UP);
|
|
88
|
+
const dir = isVertical ? RIGHT : UP;
|
|
89
|
+
scrollbarComp.value = Math.max(0, getSize(rootRect, dir) - getSize(contentRect, dir));
|
|
90
|
+
vertical.value ??= isVertical;
|
|
91
|
+
updateIndicators(blockStart);
|
|
92
|
+
}, { root, rootMargin: "0px 0px 0px 0px", threshold: 1 });
|
|
93
|
+
contentObserver.observe(content);
|
|
94
|
+
teardown(() => contentObserver.disconnect());
|
|
95
|
+
|
|
96
|
+
return root;
|
|
97
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, get, optionalString, Signal, StyleValue } from "rvx";
|
|
2
|
+
import { isPending, waitFor } from "rvx/async";
|
|
3
|
+
|
|
4
|
+
import { keyFor } from "../common/events.js";
|
|
5
|
+
import { THEME } from "../common/theme.js";
|
|
6
|
+
import { validatorFor } from "./validation.js";
|
|
7
|
+
|
|
8
|
+
export type TextInputType = "text" | "password";
|
|
9
|
+
export type TextAreaWrap = "hard" | "soft";
|
|
10
|
+
|
|
11
|
+
export function TextInput(props: ({
|
|
12
|
+
/**
|
|
13
|
+
* If true, this component is rendered as a `<textarea>` element.
|
|
14
|
+
*/
|
|
15
|
+
multiline?: false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The input type.
|
|
19
|
+
*
|
|
20
|
+
* @default "text"
|
|
21
|
+
*/
|
|
22
|
+
type?: Expression<TextInputType | undefined>;
|
|
23
|
+
} | {
|
|
24
|
+
/**
|
|
25
|
+
* If true, this component is rendered as a `<textarea>` element.
|
|
26
|
+
*/
|
|
27
|
+
multiline: true;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The number of visible rows if not overwritten via css.
|
|
31
|
+
*
|
|
32
|
+
* If not specified, the browser default is used.
|
|
33
|
+
*/
|
|
34
|
+
rows?: Expression<number | undefined>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Controls how the value is wrapped for form submission.
|
|
38
|
+
*
|
|
39
|
+
* This sets the standard `wrap` attribute.
|
|
40
|
+
*/
|
|
41
|
+
wrap?: Expression<TextAreaWrap | undefined>;
|
|
42
|
+
}) & {
|
|
43
|
+
/**
|
|
44
|
+
* Set when the input is disabled.
|
|
45
|
+
*
|
|
46
|
+
* The input is automatically disabled when there are any {@link isPending pending tasks}.
|
|
47
|
+
*/
|
|
48
|
+
disabled?: Expression<boolean | undefined>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The current text value.
|
|
52
|
+
*
|
|
53
|
+
* If this isn't a signal, the text input is readonly.
|
|
54
|
+
*/
|
|
55
|
+
value: Expression<string>;
|
|
56
|
+
|
|
57
|
+
enterAction?: (event: Event) => void | boolean | Promise<void>;
|
|
58
|
+
|
|
59
|
+
class?: ClassValue;
|
|
60
|
+
style?: StyleValue;
|
|
61
|
+
id?: Expression<string | undefined>;
|
|
62
|
+
autofocus?: Expression<boolean | undefined>;
|
|
63
|
+
spellcheck?: Expression<boolean | undefined>;
|
|
64
|
+
"aria-label"?: Expression<string | undefined>;
|
|
65
|
+
"aria-labelledby"?: Expression<string | undefined>;
|
|
66
|
+
}): unknown {
|
|
67
|
+
const theme = extract(THEME);
|
|
68
|
+
const disabled = () => isPending() || get(props.disabled);
|
|
69
|
+
|
|
70
|
+
const validator = props.value instanceof Signal ? validatorFor(props.value) : undefined;
|
|
71
|
+
|
|
72
|
+
const InputTag = props.multiline ? "textarea" : "input";
|
|
73
|
+
const input = <InputTag
|
|
74
|
+
type={props.multiline ? undefined : (() => get(props.type) ?? "text")}
|
|
75
|
+
rows={props.multiline ? props.rows : undefined}
|
|
76
|
+
wrap={props.multiline ? props.wrap : undefined}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
class={[
|
|
79
|
+
theme?.text_input,
|
|
80
|
+
props.class,
|
|
81
|
+
]}
|
|
82
|
+
style={props.style}
|
|
83
|
+
id={props.id}
|
|
84
|
+
autofocus={props.autofocus}
|
|
85
|
+
spellcheck={optionalString(props.spellcheck)}
|
|
86
|
+
readonly={!(props.value instanceof Signal)}
|
|
87
|
+
|
|
88
|
+
prop:value={props.value}
|
|
89
|
+
on:input={() => {
|
|
90
|
+
if (props.value instanceof Signal) {
|
|
91
|
+
props.value.value = input.value;
|
|
92
|
+
}
|
|
93
|
+
}}
|
|
94
|
+
on:keydown={event => {
|
|
95
|
+
const key = keyFor(event);
|
|
96
|
+
if (key === "enter" && props.enterAction && !disabled()) {
|
|
97
|
+
const result = props.enterAction(event);
|
|
98
|
+
if (result === false) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.stopImmediatePropagation();
|
|
103
|
+
if (result instanceof Promise) {
|
|
104
|
+
waitFor(result);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
|
|
109
|
+
aria-label={props["aria-label"]}
|
|
110
|
+
aria-labelledby={props["aria-labelledby"]}
|
|
111
|
+
|
|
112
|
+
aria-invalid={validator ? optionalString(validator.invalid) : undefined}
|
|
113
|
+
aria-errormessage={validator ? validator.errorMessageIds : undefined}
|
|
114
|
+
/> as HTMLInputElement;
|
|
115
|
+
|
|
116
|
+
return input;
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ClassValue, Expression, extract, StyleValue } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { THEME } from "../common/theme.js";
|
|
4
|
+
|
|
5
|
+
export function Text(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?.text,
|
|
15
|
+
props.class,
|
|
16
|
+
]}
|
|
17
|
+
style={props.style}
|
|
18
|
+
id={props.id}
|
|
19
|
+
>
|
|
20
|
+
{props.children}
|
|
21
|
+
</div>;
|
|
22
|
+
}
|