@rvx/ui 0.1.13 → 0.1.15

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.
Files changed (99) hide show
  1. package/dist/common/theme.d.ts +10 -0
  2. package/dist/common/types.d.ts +0 -10
  3. package/dist/common/types.js +1 -9
  4. package/dist/common/types.js.map +1 -1
  5. package/dist/components/button.js +3 -2
  6. package/dist/components/button.js.map +1 -1
  7. package/dist/components/card.d.ts +1 -0
  8. package/dist/components/card.js +6 -3
  9. package/dist/components/card.js.map +1 -1
  10. package/dist/components/checkbox.js +6 -6
  11. package/dist/components/checkbox.js.map +1 -1
  12. package/dist/components/collapse.d.ts +16 -2
  13. package/dist/components/collapse.js +85 -4
  14. package/dist/components/collapse.js.map +1 -1
  15. package/dist/components/column.d.ts +1 -0
  16. package/dist/components/column.js +1 -0
  17. package/dist/components/column.js.map +1 -1
  18. package/dist/components/dialog.d.ts +1 -4
  19. package/dist/components/dialog.js +11 -12
  20. package/dist/components/dialog.js.map +1 -1
  21. package/dist/components/dropdown-input.js +2 -2
  22. package/dist/components/dropdown-input.js.map +1 -1
  23. package/dist/components/dropdown.js +2 -2
  24. package/dist/components/dropdown.js.map +1 -1
  25. package/dist/components/label.js +1 -1
  26. package/dist/components/label.js.map +1 -1
  27. package/dist/components/link.js +2 -2
  28. package/dist/components/link.js.map +1 -1
  29. package/dist/components/nav-list.js +2 -1
  30. package/dist/components/nav-list.js.map +1 -1
  31. package/dist/components/popout.d.ts +1 -2
  32. package/dist/components/popout.js +1 -2
  33. package/dist/components/popout.js.map +1 -1
  34. package/dist/components/popover.js +1 -2
  35. package/dist/components/popover.js.map +1 -1
  36. package/dist/components/radio-buttons.js +11 -10
  37. package/dist/components/radio-buttons.js.map +1 -1
  38. package/dist/components/slider.js +1 -2
  39. package/dist/components/slider.js.map +1 -1
  40. package/dist/components/tabs.d.ts +10 -0
  41. package/dist/components/tabs.js +31 -0
  42. package/dist/components/tabs.js.map +1 -0
  43. package/dist/components/text-input.js +5 -4
  44. package/dist/components/text-input.js.map +1 -1
  45. package/dist/components/validation-rules.d.ts +11 -0
  46. package/dist/components/validation-rules.js +37 -0
  47. package/dist/components/validation-rules.js.map +1 -0
  48. package/dist/components/validation.d.ts +53 -77
  49. package/dist/components/validation.js +117 -95
  50. package/dist/components/validation.js.map +1 -1
  51. package/dist/index.d.ts +2 -3
  52. package/dist/index.js +2 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/theme.module.css +77 -2
  55. package/dist/theme.module.css.map +1 -1
  56. package/package.json +3 -3
  57. package/src/common/theme.tsx +11 -0
  58. package/src/common/types.tsx +0 -18
  59. package/src/components/button.tsx +3 -2
  60. package/src/components/card.tsx +10 -5
  61. package/src/components/checkbox.tsx +6 -6
  62. package/src/components/collapse.tsx +127 -5
  63. package/src/components/column.tsx +2 -0
  64. package/src/components/dialog.tsx +10 -14
  65. package/src/components/dropdown-input.tsx +2 -2
  66. package/src/components/dropdown.tsx +2 -2
  67. package/src/components/label.tsx +1 -3
  68. package/src/components/link.tsx +2 -2
  69. package/src/components/nav-list.tsx +2 -1
  70. package/src/components/popout.tsx +1 -2
  71. package/src/components/popover.tsx +1 -2
  72. package/src/components/radio-buttons.tsx +21 -20
  73. package/src/components/slider.tsx +1 -2
  74. package/src/components/tabs.tsx +67 -0
  75. package/src/components/text-input.tsx +5 -4
  76. package/src/components/validation-rules.tsx +50 -0
  77. package/src/components/validation.tsx +175 -177
  78. package/src/index.tsx +2 -3
  79. package/src/theme/base.scss +7 -0
  80. package/src/theme/components/card.scss +4 -1
  81. package/src/theme/components/checkbox.scss +5 -0
  82. package/src/theme/components/column.scss +5 -0
  83. package/src/theme/components/dialog.scss +2 -2
  84. package/src/theme/components/radio-buttons.scss +5 -0
  85. package/src/theme/components/slider.scss +1 -0
  86. package/src/theme/components/tabs.scss +72 -0
  87. package/src/theme/theme.scss +1 -0
  88. package/dist/common/debounce.d.ts +0 -12
  89. package/dist/common/debounce.js +0 -23
  90. package/dist/common/debounce.js.map +0 -1
  91. package/dist/common/parsers.d.ts +0 -88
  92. package/dist/common/parsers.js +0 -62
  93. package/dist/common/parsers.js.map +0 -1
  94. package/dist/common/trim.d.ts +0 -12
  95. package/dist/common/trim.js +0 -16
  96. package/dist/common/trim.js.map +0 -1
  97. package/src/common/debounce.tsx +0 -36
  98. package/src/common/parsers.tsx +0 -167
  99. package/src/common/trim.tsx +0 -30
@@ -1,4 +1,4 @@
1
- import { Expression, get } from "rvx";
1
+ import { Expression, map } from "rvx";
2
2
  import { THEME } from "../common/theme.js";
3
3
  import { Column } from "./column.js";
4
4
 
@@ -6,17 +6,22 @@ export type CardVariant = "default" | "info" | "success" | "warning" | "danger";
6
6
 
7
7
  export function Card(props: {
8
8
  variant?: Expression<CardVariant | undefined>;
9
+ raw?: boolean;
9
10
  children?: unknown;
10
11
  }): unknown {
11
12
  const theme = THEME.current;
12
13
  return <div
13
14
  class={[
14
15
  theme?.card,
15
- () => theme?.[`card_${get(props.variant) ?? "default"}`],
16
+ map(props.variant, variant => theme?.[`card_${variant ?? "default"}`]),
17
+ map(props.raw, unpadded => unpadded ? theme?.card_raw : undefined),
16
18
  ]}
17
19
  >
18
- <Column class={theme?.card_content}>
19
- {props.children}
20
- </Column>
20
+ {props.raw
21
+ ? props.children
22
+ : <Column class={theme?.card_content}>
23
+ {props.children}
24
+ </Column>
25
+ }
21
26
  </div>;
22
27
  }
@@ -1,10 +1,10 @@
1
- import { ClassValue, Expression, get, optionalString, Signal, string, StyleValue, watch } from "rvx";
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 { validatorFor } from "./validation.js";
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 ? validatorFor(props.checked) : undefined;
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.errorMessageIds : undefined}
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, optionalString, StyleValue, teardown } from "rvx";
2
- import { Event } from "rvx/event";
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, created, Expression, map, render, StyleValue, teardown } from "rvx";
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
 
@@ -76,9 +74,7 @@ export function DialogBody(props: {
76
74
  description?: unknown;
77
75
 
78
76
  inlineSize?: Expression<string | undefined>;
79
- maxInlineSize?: Expression<string | undefined>;
80
77
  blockSize?: Expression<string | undefined>;
81
- maxBlockSize?: Expression<string | undefined>;
82
78
 
83
79
  "aria-labelledby"?: Expression<string | undefined>;
84
80
  "aria-describedby"?: Expression<string | undefined>;
@@ -103,7 +99,13 @@ export function DialogBody(props: {
103
99
  theme?.dialog_container,
104
100
  props.class,
105
101
  ]}
106
- style={props.style}
102
+ style={[
103
+ props.style,
104
+ {
105
+ "--dialog-inline-size": map(props.inlineSize, v => v ?? "auto"),
106
+ "--dialog-block-size": map(props.blockSize, v => v ?? "auto"),
107
+ },
108
+ ]}
107
109
  role={map(props.role, v => v ?? "dialog")}
108
110
  aria-labelledby={map(props["aria-labelledby"], v => v ?? titleId)}
109
111
  aria-describedby={map(props["aria-describedby"], v => v ?? descriptionId)}
@@ -114,19 +116,13 @@ export function DialogBody(props: {
114
116
  theme?.column_content,
115
117
  theme?.dialog_body,
116
118
  ]}
117
- style={{
118
- "inline-size": props.inlineSize,
119
- "max-inline-size": props.maxInlineSize,
120
- "block-size": props.blockSize,
121
- "max-block-size": props.maxBlockSize,
122
- }}
123
119
  >
124
120
  {head}
125
121
  {props.children}
126
122
  </div>
127
123
  </div> as HTMLElement;
128
124
 
129
- created(() => {
125
+ useMicrotask(() => {
130
126
  if (theme?.dialog_fadein) {
131
127
  body.offsetParent;
132
128
  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 { validatorFor } from "./validation.js";
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 ? validatorFor(props.value) : undefined}
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, optionalString, render, StyleValue, View, watch } from "rvx";
2
- import { uniqueId } from "rvx/id";
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";
@@ -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;
@@ -1,8 +1,8 @@
1
- import { ClassValue, Expression, get, map, optionalString, StyleValue } from "rvx";
1
+ import { ClassValue, Expression, get, map, StyleValue } from "rvx";
2
2
  import { isPending } from "rvx/async";
3
+ import { optionalString, separated } from "rvx/convert";
3
4
  import { Action, handleActionEvent, keyFor } from "../common/events.js";
4
5
  import { THEME } from "../common/theme.js";
5
- import { separated } from "../common/types.js";
6
6
 
7
7
  export type LinkReferrerPolicy = "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "same-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url";
8
8
 
@@ -1,5 +1,6 @@
1
- import { ClassValue, Expression, get, map, optionalString, StyleValue } from "rvx";
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, optionalString, Signal, string, StyleValue } from "rvx";
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 { validatorFor } from "./validation.js";
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 ? validatorFor(props.value) : undefined;
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.errorMessageIds : undefined}
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>
@@ -1,5 +1,4 @@
1
- import { Expression, map, Show, Signal } from "rvx";
2
- import { uniqueId } from "rvx/id";
1
+ import { Expression, map, Show, Signal, uniqueId } from "rvx";
3
2
  import { THEME } from "../common/theme.js";
4
3
 
5
4
  export function Slider(props: {
@@ -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, optionalString, Signal, StyleValue } from "rvx";
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 { validatorFor } from "./validation.js";
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 ? validatorFor(props.value) : undefined;
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.errorMessageIds : undefined}
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
+ }