@rvx/ui 0.1.12 → 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.
Files changed (101) hide show
  1. package/dist/common/coupling.d.ts +2 -0
  2. package/dist/common/coupling.js +18 -0
  3. package/dist/common/coupling.js.map +1 -0
  4. package/dist/common/theme.d.ts +14 -0
  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 -2
  19. package/dist/components/dialog.js +3 -5
  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 -1
  28. package/dist/components/link.js.map +1 -1
  29. package/dist/components/nav-list.d.ts +26 -0
  30. package/dist/components/nav-list.js +37 -0
  31. package/dist/components/nav-list.js.map +1 -0
  32. package/dist/components/popout.d.ts +1 -2
  33. package/dist/components/popout.js +1 -2
  34. package/dist/components/popout.js.map +1 -1
  35. package/dist/components/popover.js +1 -2
  36. package/dist/components/popover.js.map +1 -1
  37. package/dist/components/radio-buttons.js +11 -10
  38. package/dist/components/radio-buttons.js.map +1 -1
  39. package/dist/components/slider.d.ts +11 -0
  40. package/dist/components/slider.js +24 -0
  41. package/dist/components/slider.js.map +1 -0
  42. package/dist/components/tabs.d.ts +10 -0
  43. package/dist/components/tabs.js +31 -0
  44. package/dist/components/tabs.js.map +1 -0
  45. package/dist/components/text-input.js +5 -4
  46. package/dist/components/text-input.js.map +1 -1
  47. package/dist/components/validation-rules.d.ts +11 -0
  48. package/dist/components/validation-rules.js +37 -0
  49. package/dist/components/validation-rules.js.map +1 -0
  50. package/dist/components/validation.d.ts +53 -77
  51. package/dist/components/validation.js +117 -95
  52. package/dist/components/validation.js.map +1 -1
  53. package/dist/index.d.ts +5 -3
  54. package/dist/index.js +5 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/theme.module.css +144 -0
  57. package/dist/theme.module.css.map +1 -1
  58. package/package.json +3 -3
  59. package/src/common/coupling.tsx +18 -0
  60. package/src/common/theme.tsx +17 -0
  61. package/src/components/button.tsx +3 -2
  62. package/src/components/card.tsx +10 -5
  63. package/src/components/checkbox.tsx +6 -6
  64. package/src/components/collapse.tsx +127 -5
  65. package/src/components/column.tsx +2 -0
  66. package/src/components/dialog.tsx +3 -5
  67. package/src/components/dropdown-input.tsx +2 -2
  68. package/src/components/dropdown.tsx +2 -2
  69. package/src/components/label.tsx +1 -3
  70. package/src/components/link.tsx +2 -1
  71. package/src/components/nav-list.tsx +81 -0
  72. package/src/components/popout.tsx +1 -2
  73. package/src/components/popover.tsx +1 -2
  74. package/src/components/radio-buttons.tsx +21 -20
  75. package/src/components/slider.tsx +55 -0
  76. package/src/components/tabs.tsx +67 -0
  77. package/src/components/text-input.tsx +5 -4
  78. package/src/components/validation-rules.tsx +50 -0
  79. package/src/components/validation.tsx +175 -177
  80. package/src/index.tsx +5 -3
  81. package/src/theme/base.scss +7 -0
  82. package/src/theme/components/card.scss +4 -1
  83. package/src/theme/components/checkbox.scss +5 -0
  84. package/src/theme/components/column.scss +5 -0
  85. package/src/theme/components/nav-list.scss +82 -0
  86. package/src/theme/components/radio-buttons.scss +5 -0
  87. package/src/theme/components/slider.scss +15 -0
  88. package/src/theme/components/tabs.scss +72 -0
  89. package/src/theme/theme.scss +3 -0
  90. package/dist/common/debounce.d.ts +0 -12
  91. package/dist/common/debounce.js +0 -23
  92. package/dist/common/debounce.js.map +0 -1
  93. package/dist/common/parsers.d.ts +0 -88
  94. package/dist/common/parsers.js +0 -62
  95. package/dist/common/parsers.js.map +0 -1
  96. package/dist/common/trim.d.ts +0 -12
  97. package/dist/common/trim.js +0 -16
  98. package/dist/common/trim.js.map +0 -1
  99. package/src/common/debounce.tsx +0 -36
  100. package/src/common/parsers.tsx +0 -167
  101. package/src/common/trim.tsx +0 -30
@@ -33,6 +33,7 @@ export interface Theme {
33
33
  button_text?: string;
34
34
 
35
35
  card?: string;
36
+ card_raw?: string;
36
37
  card_content?: string;
37
38
  card_default?: string;
38
39
  card_info?: string;
@@ -41,6 +42,7 @@ export interface Theme {
41
42
  card_danger?: string;
42
43
 
43
44
  checkbox_label?: string;
45
+ checkbox_padding?: string;
44
46
  checkbox_input?: string;
45
47
  checkbox_content?: string;
46
48
 
@@ -52,6 +54,7 @@ export interface Theme {
52
54
  collapse_content?: string;
53
55
 
54
56
  column?: string;
57
+ column_padded?: string;
55
58
  column_content?: string;
56
59
  column_control?: string;
57
60
 
@@ -78,6 +81,10 @@ export interface Theme {
78
81
 
79
82
  link?: string;
80
83
 
84
+ nav_list?: string;
85
+ nav_list_item?: string;
86
+ nav_list_item_current?: string;
87
+
81
88
  page?: string;
82
89
  page_scrollbar_comp?: string;
83
90
  page_content_col?: string;
@@ -91,6 +98,7 @@ export interface Theme {
91
98
 
92
99
  radio_buttons?: string;
93
100
  radio_button_label?: string;
101
+ radio_button_padding?: string;
94
102
  radio_button_input?: string;
95
103
  radio_button_content?: string;
96
104
 
@@ -105,6 +113,15 @@ export interface Theme {
105
113
  scroll_view_indicator_end?: string;
106
114
  scroll_view_indicator_visible?: string;
107
115
 
116
+ slider_host?: string;
117
+
118
+ tab_handle?: string;
119
+ tab_handle_current?: string;
120
+ tab_list?: string;
121
+ tab_list_padded?: string;
122
+ tab_panel?: string;
123
+ tab_panel_padded?: string;
124
+
108
125
  text_input?: string;
109
126
 
110
127
  text?: string;
@@ -1,5 +1,6 @@
1
- import { ClassValue, Expression, get, optionalString, StyleValue } from "rvx";
1
+ import { ClassValue, Expression, get, 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 { Validator } from "./validation.js";
@@ -77,7 +78,7 @@ export function Button(props: {
77
78
  aria-haspopup={props["aria-haspopup"]}
78
79
  aria-controls={props["aria-controls"]}
79
80
  aria-invalid={props.validator ? optionalString(props.validator.invalid) : undefined}
80
- aria-errormessage={props.validator ? props.validator.errorMessageIds : undefined}
81
+ aria-errormessage={props.validator ? props.validator.messageIds : undefined}
81
82
  autofocus={props.autofocus}
82
83
 
83
84
  on:click={action}
@@ -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
 
@@ -126,7 +124,7 @@ export function DialogBody(props: {
126
124
  </div>
127
125
  </div> as HTMLElement;
128
126
 
129
- created(() => {
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 { 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,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
  import { separated } from "../common/types.js";
@@ -0,0 +1,81 @@
1
+ import { ClassValue, Expression, get, map, StyleValue } from "rvx";
2
+ import { isPending } from "rvx/async";
3
+ import { optionalString } from "rvx/convert";
4
+ import { Action, handleActionEvent, keyFor } from "../common/events.js";
5
+ import { THEME } from "../common/theme.js";
6
+
7
+ export function NavList(props: {
8
+ /**
9
+ * The element role or `false` to disable.
10
+ *
11
+ * @default "navigation"
12
+ */
13
+ role?: Expression<string | false | undefined>;
14
+
15
+ class?: ClassValue;
16
+ style?: StyleValue;
17
+ id?: Expression<string | undefined>;
18
+ children?: unknown;
19
+ }): unknown {
20
+ const theme = THEME.current;
21
+ return <div
22
+ class={[
23
+ theme?.nav_list,
24
+ props.class,
25
+ ]}
26
+ style={[
27
+ { "--nav-list-depth": String(0) },
28
+ props.style,
29
+ ]}
30
+ id={props.id}
31
+ role={map(props.role, role => role ?? "navigation")}
32
+ >
33
+ {props.children}
34
+ </div>;
35
+ }
36
+
37
+ export function NavListButton(props: {
38
+ /**
39
+ * True if this is the currently selected item.
40
+ */
41
+ current?: Expression<boolean | undefined>;
42
+
43
+ disabled?: Expression<boolean | undefined>;
44
+ class?: ClassValue;
45
+ style?: StyleValue;
46
+ id?: Expression<string | undefined>;
47
+ action?: Action;
48
+ children?: unknown;
49
+ }): unknown {
50
+ const theme = THEME.current;
51
+ const disabled = () => isPending() || get(props.disabled);
52
+
53
+ function action(event: Event) {
54
+ if (disabled() || !props.action) {
55
+ return;
56
+ }
57
+ handleActionEvent(event, props.action);
58
+ }
59
+
60
+ return <button
61
+ type="button"
62
+ disabled={disabled}
63
+ class={[
64
+ theme?.nav_list_item,
65
+ map(props.current, current => current && theme?.nav_list_item_current),
66
+ props.class,
67
+ ]}
68
+ style={props.style}
69
+ id={props.id}
70
+ on:click={action}
71
+ on:keydown={event => {
72
+ const key = keyFor(event);
73
+ if (key === "enter" || key === "space") {
74
+ action(event);
75
+ }
76
+ }}
77
+ aria-current={optionalString(props.current)}
78
+ >
79
+ {props.children}
80
+ </button>;
81
+ }
@@ -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>
@@ -0,0 +1,55 @@
1
+ import { Expression, map, Show, Signal, uniqueId } from "rvx";
2
+ import { THEME } from "../common/theme.js";
3
+
4
+ export function Slider(props: {
5
+ id?: Expression<string | undefined>;
6
+ value?: Expression<number | undefined>;
7
+ min?: Expression<number | undefined>;
8
+ max?: Expression<number | undefined>;
9
+ step?: Expression<number | "any" | undefined>;
10
+ markers?: Expression<number[] | undefined>;
11
+ children?: unknown;
12
+ }) {
13
+ const theme = THEME.current;
14
+ const markerId = uniqueId();
15
+
16
+ const input = <input
17
+ id={props.id}
18
+ type="range"
19
+ min={map(props.min, min => min ?? 0)}
20
+ max={map(props.max, max => max ?? 100)}
21
+ step={map(props.step, step => step ?? 1)}
22
+ prop:value={map(props.value, value => value ?? "0")}
23
+ on:input={() => {
24
+ if (props.value instanceof Signal) {
25
+ props.value.value = Number(input.value);
26
+ }
27
+ }}
28
+ list={map(props.markers, markers => markers ? markerId : undefined)}
29
+ /> as HTMLInputElement;
30
+
31
+ return <div
32
+ class={theme?.slider_host}
33
+ >
34
+ {input}
35
+
36
+ <Show when={props.markers}>
37
+ {markers => <datalist id={markerId}>
38
+ {markers.map(marker => <option value={marker} />)}
39
+ </datalist>}
40
+ </Show>
41
+
42
+ {props.children}
43
+ </div>;
44
+ }
45
+
46
+ export function sliderMarkers(min: number, max: number, step: number): number[] {
47
+ if (min > max) {
48
+ [min, max] = [max, min];
49
+ }
50
+ const markers: number[] = [];
51
+ for (let i = min; i <= max; i += step) {
52
+ markers.push(i > max ? max : i);
53
+ }
54
+ return markers;
55
+ }