@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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/dist/common/events.d.ts +72 -0
  3. package/dist/common/events.js +58 -0
  4. package/dist/common/events.js.map +1 -0
  5. package/dist/common/parsers.d.ts +88 -0
  6. package/dist/common/parsers.js +62 -0
  7. package/dist/common/parsers.js.map +1 -0
  8. package/dist/common/theme-test.d.ts +7 -0
  9. package/dist/common/theme-test.js +14 -0
  10. package/dist/common/theme-test.js.map +1 -0
  11. package/dist/common/theme.d.ts +144 -0
  12. package/dist/common/theme.js +2 -0
  13. package/dist/common/theme.js.map +1 -0
  14. package/dist/common/trim.d.ts +12 -0
  15. package/dist/common/trim.js +16 -0
  16. package/dist/common/trim.js.map +1 -0
  17. package/dist/common/types.d.ts +13 -0
  18. package/dist/common/types.js +10 -0
  19. package/dist/common/types.js.map +1 -0
  20. package/dist/common/writing-mode.d.ts +82 -0
  21. package/dist/common/writing-mode.js +61 -0
  22. package/dist/common/writing-mode.js.map +1 -0
  23. package/dist/components/button.d.ts +42 -0
  24. package/dist/components/button.js +26 -0
  25. package/dist/components/button.js.map +1 -0
  26. package/dist/components/checkbox.d.ts +9 -0
  27. package/dist/components/checkbox.js +32 -0
  28. package/dist/components/checkbox.js.map +1 -0
  29. package/dist/components/collapse-test.d.ts +8 -0
  30. package/dist/components/collapse-test.js +15 -0
  31. package/dist/components/collapse-test.js.map +1 -0
  32. package/dist/components/collapse.d.ts +13 -0
  33. package/dist/components/collapse.js +44 -0
  34. package/dist/components/collapse.js.map +1 -0
  35. package/dist/components/column.d.ts +12 -0
  36. package/dist/components/column.js +12 -0
  37. package/dist/components/column.js.map +1 -0
  38. package/dist/components/control-group.d.ts +7 -0
  39. package/dist/components/control-group.js +11 -0
  40. package/dist/components/control-group.js.map +1 -0
  41. package/dist/components/dialog.d.ts +33 -0
  42. package/dist/components/dialog.js +67 -0
  43. package/dist/components/dialog.js.map +1 -0
  44. package/dist/components/dropdown-input.d.ts +27 -0
  45. package/dist/components/dropdown-input.js +31 -0
  46. package/dist/components/dropdown-input.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +123 -0
  48. package/dist/components/dropdown.js +176 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/flex-space.d.ts +4 -0
  51. package/dist/components/flex-space.js +10 -0
  52. package/dist/components/flex-space.js.map +1 -0
  53. package/dist/components/heading.d.ts +9 -0
  54. package/dist/components/heading.js +14 -0
  55. package/dist/components/heading.js.map +1 -0
  56. package/dist/components/label.d.ts +14 -0
  57. package/dist/components/label.js +15 -0
  58. package/dist/components/label.js.map +1 -0
  59. package/dist/components/layer.d.ts +81 -0
  60. package/dist/components/layer.js +164 -0
  61. package/dist/components/layer.js.map +1 -0
  62. package/dist/components/link.d.ts +57 -0
  63. package/dist/components/link.js +26 -0
  64. package/dist/components/link.js.map +1 -0
  65. package/dist/components/page.d.ts +9 -0
  66. package/dist/components/page.js +17 -0
  67. package/dist/components/page.js.map +1 -0
  68. package/dist/components/popout.d.ts +134 -0
  69. package/dist/components/popout.js +259 -0
  70. package/dist/components/popout.js.map +1 -0
  71. package/dist/components/popover.d.ts +139 -0
  72. package/dist/components/popover.js +101 -0
  73. package/dist/components/popover.js.map +1 -0
  74. package/dist/components/radio-buttons.d.ts +17 -0
  75. package/dist/components/radio-buttons.js +26 -0
  76. package/dist/components/radio-buttons.js.map +1 -0
  77. package/dist/components/row.d.ts +10 -0
  78. package/dist/components/row.js +23 -0
  79. package/dist/components/row.js.map +1 -0
  80. package/dist/components/scroll-view.d.ts +6 -0
  81. package/dist/components/scroll-view.js +72 -0
  82. package/dist/components/scroll-view.js.map +1 -0
  83. package/dist/components/text-input.d.ts +53 -0
  84. package/dist/components/text-input.js +35 -0
  85. package/dist/components/text-input.js.map +1 -0
  86. package/dist/components/text.d.ts +7 -0
  87. package/dist/components/text.js +11 -0
  88. package/dist/components/text.js.map +1 -0
  89. package/dist/components/validation.d.ts +109 -0
  90. package/dist/components/validation.js +151 -0
  91. package/dist/components/validation.js.map +1 -0
  92. package/dist/components/value.d.ts +7 -0
  93. package/dist/components/value.js +11 -0
  94. package/dist/components/value.js.map +1 -0
  95. package/dist/index.d.ts +29 -0
  96. package/dist/index.js +30 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/test.d.ts +2 -0
  99. package/dist/test.js +3 -0
  100. package/dist/test.js.map +1 -0
  101. package/dist/theme.module.css +679 -0
  102. package/dist/theme.module.css.map +1 -0
  103. package/package.json +29 -0
  104. package/src/common/events.tsx +130 -0
  105. package/src/common/parsers.tsx +167 -0
  106. package/src/common/theme-test.tsx +20 -0
  107. package/src/common/theme.tsx +165 -0
  108. package/src/common/trim.tsx +30 -0
  109. package/src/common/types.tsx +23 -0
  110. package/src/common/writing-mode.tsx +150 -0
  111. package/src/components/button.tsx +94 -0
  112. package/src/components/checkbox.tsx +64 -0
  113. package/src/components/collapse-test.tsx +23 -0
  114. package/src/components/collapse.tsx +75 -0
  115. package/src/components/column.tsx +28 -0
  116. package/src/components/control-group.tsx +22 -0
  117. package/src/components/dialog.tsx +137 -0
  118. package/src/components/dropdown-input.tsx +82 -0
  119. package/src/components/dropdown.tsx +352 -0
  120. package/src/components/flex-space.tsx +15 -0
  121. package/src/components/heading.tsx +23 -0
  122. package/src/components/label.tsx +37 -0
  123. package/src/components/layer.tsx +299 -0
  124. package/src/components/link.tsx +118 -0
  125. package/src/components/page.tsx +36 -0
  126. package/src/components/popout.tsx +461 -0
  127. package/src/components/popover.tsx +292 -0
  128. package/src/components/radio-buttons.tsx +81 -0
  129. package/src/components/row.tsx +37 -0
  130. package/src/components/scroll-view.tsx +97 -0
  131. package/src/components/text-input.tsx +117 -0
  132. package/src/components/text.tsx +22 -0
  133. package/src/components/validation.tsx +272 -0
  134. package/src/components/value.tsx +22 -0
  135. package/src/index.tsx +29 -0
  136. package/src/test.tsx +2 -0
  137. package/src/theme/base.scss +69 -0
  138. package/src/theme/common.scss +51 -0
  139. package/src/theme/components/button.scss +116 -0
  140. package/src/theme/components/checkbox.scss +25 -0
  141. package/src/theme/components/collapse.scss +64 -0
  142. package/src/theme/components/column.scss +28 -0
  143. package/src/theme/components/control-group.scss +14 -0
  144. package/src/theme/components/dialog.scss +44 -0
  145. package/src/theme/components/dropdown.scss +50 -0
  146. package/src/theme/components/flex-space.scss +6 -0
  147. package/src/theme/components/heading.scss +39 -0
  148. package/src/theme/components/label.scss +24 -0
  149. package/src/theme/components/link.scss +25 -0
  150. package/src/theme/components/page.scss +22 -0
  151. package/src/theme/components/popover.scss +58 -0
  152. package/src/theme/components/radio-buttons.scss +31 -0
  153. package/src/theme/components/row.scss +17 -0
  154. package/src/theme/components/scroll-view.scss +51 -0
  155. package/src/theme/components/text-input.scss +45 -0
  156. package/src/theme/components/text.scss +12 -0
  157. package/src/theme/components/validation.scss +15 -0
  158. package/src/theme/components/value.scss +4 -0
  159. package/src/theme/theme.scss +22 -0
@@ -0,0 +1,299 @@
1
+ import { Context, ContextKey, DeriveContext, Expression, extract, get, memo, sig, Signal, teardown, uncapture, untrack, watch, wrapContext } from "rvx";
2
+
3
+ import { Action, handleActionEvent, keyFor } from "../common/events.js";
4
+
5
+ interface LayerInstance {
6
+ /** The root nodes of this layer. */
7
+ roots: Node[];
8
+ /** True if this is a modal layer. */
9
+ modal: boolean;
10
+ /** A signal representing if this layer is inert due to a modal layer on top. */
11
+ inert: Signal<boolean>;
12
+ /** An element to use as auto focus fallback. */
13
+ autoFocusFallback: Element | undefined;
14
+ }
15
+
16
+ export const LAYER = Symbol.for("rvx-ui:layer-handle") as ContextKey<LayerHandle>;
17
+
18
+ const LAYERS = sig<LayerInstance[]>([
19
+ {
20
+ roots: [],
21
+ modal: false,
22
+ inert: sig(false),
23
+ autoFocusFallback: undefined,
24
+ },
25
+ ]);
26
+
27
+ uncapture(() => watch(LAYERS, layers => {
28
+ const modal = layers.findLastIndex(l => l.modal);
29
+ for (let i = 0; i < layers.length; i++) {
30
+ layers[i].inert.value = i < modal;
31
+ }
32
+ }));
33
+
34
+ /**
35
+ * Render content inside the root layer.
36
+ */
37
+ export function RootLayer(props: {
38
+ children: (ctx: Context) => unknown;
39
+ }): unknown {
40
+ const layer = LAYERS.value[0];
41
+ const root = <div
42
+ style={{ display: "contents" }}
43
+ inert={layer.inert}
44
+ >
45
+ <DeriveContext>
46
+ {ctx => {
47
+ ctx.set(LAYER, new Handle(layer));
48
+ return props.children(ctx);
49
+ }}
50
+ </DeriveContext>
51
+ </div> as HTMLDivElement;
52
+ layer.roots.push(root);
53
+ teardown(() => {
54
+ const index = layer.roots.indexOf(root);
55
+ if (index >= 0) {
56
+ layer.roots.splice(index, 1);
57
+ }
58
+ });
59
+ return root;
60
+ }
61
+
62
+ /**
63
+ * An input layer that is inert while there are other modal layers on top of it.
64
+ *
65
+ * After creation, the first element with the "autofocus" attribute inside this layer is focused.
66
+ *
67
+ * When disposed, focus is moved back to the previously focused element.
68
+ */
69
+ export function Layer(props: {
70
+ children: (ctx: Context) => unknown;
71
+
72
+ /**
73
+ * If true, all layers below this one are marked as inert until the current context is disposed.
74
+ */
75
+ modal?: boolean;
76
+
77
+ /**
78
+ * If false, the layer doesn't affect other layers but is marked as inert. Default is true.
79
+ */
80
+ enabled?: Expression<boolean | undefined>;
81
+ }): unknown {
82
+ const layer: LayerInstance = {
83
+ roots: [],
84
+ modal: props.modal ?? false,
85
+ inert: sig(false),
86
+ autoFocusFallback: undefined,
87
+ };
88
+
89
+ const enabled = memo(() => Boolean(get(props.enabled) ?? true));
90
+ watch(enabled, enable => {
91
+ if (!enable) {
92
+ return;
93
+ }
94
+
95
+ LAYERS.update(layers => {
96
+ layers.push(layer);
97
+ });
98
+
99
+ const previous = document.activeElement;
100
+ if (previous && previous !== document.body) {
101
+ (previous as HTMLElement).blur?.();
102
+ }
103
+
104
+ queueMicrotask(() => {
105
+ const layers = LAYERS.value;
106
+ if (layer === layers[layers.length - 1] && root.isConnected) {
107
+ let active = document.activeElement;
108
+ if (active && (root === active || root.contains(active))) {
109
+ return;
110
+ }
111
+
112
+ const autoFocus = root.querySelector("[autofocus]");
113
+ if (autoFocus) {
114
+ (autoFocus as HTMLElement).focus?.();
115
+ active = document.activeElement;
116
+ if (active === autoFocus) {
117
+ return;
118
+ }
119
+ }
120
+
121
+ (layer.autoFocusFallback as HTMLElement | undefined)?.focus?.();
122
+ }
123
+ });
124
+
125
+ teardown(() => {
126
+ let next: LayerInstance | undefined = undefined;
127
+ LAYERS.update(layers => {
128
+ const index = layers.lastIndexOf(layer);
129
+ if (index >= 0) {
130
+ layers.splice(index, 1);
131
+ next = layers[index - 1];
132
+ }
133
+ });
134
+
135
+ queueMicrotask(() => {
136
+ const layers = LAYERS.value;
137
+ if (next === layers[layers.length - 1] && previous?.isConnected && previous !== document.body) {
138
+ (previous as HTMLElement).focus?.();
139
+ }
140
+ });
141
+ });
142
+ });
143
+
144
+ const root = <div
145
+ style={{ display: "contents" }}
146
+ inert={() => layer.inert.value || !enabled()}
147
+ >
148
+ <DeriveContext>
149
+ {ctx => {
150
+ ctx.set(LAYER, new Handle(layer));
151
+ return props.children(ctx);
152
+ }}
153
+ </DeriveContext>
154
+ </div> as HTMLElement;
155
+ layer.roots.push(root);
156
+ return root;
157
+ }
158
+
159
+ export interface LayerHandle {
160
+ /**
161
+ * Reactively check if this layer is inert.
162
+ */
163
+ get inert(): boolean;
164
+
165
+ /**
166
+ * Reactively check if this is the top layer.
167
+ */
168
+ get top(): boolean;
169
+
170
+ /**
171
+ * Add a global event listener that is only called when this is the top layer.
172
+ *
173
+ * The current context is available in the event listener.
174
+ *
175
+ * The event listener is removed when the current context is disposed.
176
+ *
177
+ * @param type The event type.
178
+ * @param listener The event listener.
179
+ * @param options Event listener options. See {@link window.addEventListener}.
180
+ */
181
+ useEvent<K extends keyof WindowEventMap>(type: K, listener: (event: WindowEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
182
+ useEvent(type: string, listener: (event: Event) => void, options?: boolean | AddEventListenerOptions): void;
183
+
184
+ /**
185
+ * Shorthand for adding a global "keydown" event listener using {@link useEvent} and {@link keyFor}.
186
+ */
187
+ useHotkey(key: string, action: Action): void;
188
+
189
+ /**
190
+ * Check if this layer contains the specified node.
191
+ *
192
+ * @param node The node to check.
193
+ */
194
+ contains(node: Node): boolean;
195
+
196
+ /**
197
+ * Check if this layer or any layers in top contain the specified node.
198
+ *
199
+ * @param node The node to check.
200
+ * @param includeModals If false (default), any modal layers on top or layers above that are ignored.
201
+ */
202
+ stackContains(node: Node, includeModals?: boolean): boolean;
203
+
204
+ /**
205
+ * Try focusing the specified element after the layer is created (in the next microtask), if no element with the `autofocus` attribute has been found or successfully focused.
206
+ *
207
+ * @param element The element to use as fallback.
208
+ */
209
+ useAutoFocusFallback(element: Element): void;
210
+ }
211
+
212
+ /**
213
+ * Reactively check if the layer in the current context (or the root layer if there is none) is inert.
214
+ */
215
+ export function isInertLayer(): boolean {
216
+ return extract(LAYER)?.inert ?? untrack(() => LAYERS.value[0]).inert.value;
217
+ }
218
+
219
+ /**
220
+ * Reactively check if the layer in the current context (or the root layer if there is none) is the top layer.
221
+ */
222
+ export function isTopLayer(): boolean {
223
+ return extract(LAYER)?.top ?? LAYERS.value.length === 1;
224
+ }
225
+
226
+ function instanceContains(instance: LayerInstance, node: Node): boolean {
227
+ const roots = instance.roots;
228
+ for (let i = 0; i < roots.length; i++) {
229
+ const root = roots[i];
230
+ if (root === node || root.contains(node)) {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+
237
+ class Handle implements LayerHandle {
238
+ #instance: LayerInstance;
239
+
240
+ constructor(instance: LayerInstance) {
241
+ this.#instance = instance;
242
+ }
243
+
244
+ get inert(): boolean {
245
+ return this.#instance.inert.value;
246
+ }
247
+
248
+ get top(): boolean {
249
+ const layers = LAYERS.value;
250
+ return layers[layers.length - 1] === this.#instance;
251
+ }
252
+
253
+ useEvent<K extends keyof WindowEventMap>(type: K, listener: (event: WindowEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
254
+ useEvent(type: string, listener: (event: Event) => void, options?: boolean | AddEventListenerOptions): void;
255
+ useEvent(type: string, listener: (event: Event) => void, options?: boolean | AddEventListenerOptions): void {
256
+ const wrapper = wrapContext((event: Event): void => {
257
+ if (this.top) {
258
+ listener(event);
259
+ }
260
+ });
261
+ window.addEventListener(type, wrapper, options);
262
+ teardown(() => {
263
+ window.removeEventListener(type, wrapper, options);
264
+ });
265
+ }
266
+
267
+ useHotkey(key: string, action: Action): void {
268
+ this.useEvent("keydown", event => {
269
+ if (keyFor(event) === key) {
270
+ handleActionEvent(event, action);
271
+ }
272
+ });
273
+ }
274
+
275
+ contains(node: Node): boolean {
276
+ return instanceContains(this.#instance, node);
277
+ }
278
+
279
+ stackContains(node: Node, includeModals = false): boolean {
280
+ const layers = untrack(() => LAYERS.value);
281
+ let i = layers.indexOf(this.#instance);
282
+ if (i < 0) {
283
+ return this.contains(node);
284
+ }
285
+ for (;;) {
286
+ if (instanceContains(layers[i], node)) {
287
+ return true;
288
+ }
289
+ i++;
290
+ if (i >= layers.length || (!includeModals && layers[i].modal)) {
291
+ return false;
292
+ }
293
+ }
294
+ }
295
+
296
+ useAutoFocusFallback(element: Element | undefined): void {
297
+ this.#instance.autoFocusFallback = element;
298
+ }
299
+ }
@@ -0,0 +1,118 @@
1
+ import { ClassValue, Expression, extract, get, map, optionalString, StyleValue } from "rvx";
2
+ import { isPending } from "rvx/async";
3
+
4
+ import { Action, handleActionEvent, keyFor } from "../common/events.js";
5
+ import { THEME } from "../common/theme.js";
6
+ import { separated } from "../common/types.js";
7
+
8
+ 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";
9
+
10
+ /**
11
+ * Possible values of the `"rel"` attribute that are applicable to `<a>` elements.
12
+ */
13
+ export type LinkAnchorRel = "alternate" | "author" | "bookmark" | "external" | "help" | "license" | "me" | "next" | "nofollow" | "noopener" | "noreferrer" | "opener" | "prev" | "privacy-policy" | "search" | "tag" | "terms-of-service";
14
+
15
+ export type LinkAnchorTarget = "_self" | "_blank" | "_parent" | "_top" | "_unfencedTop";
16
+
17
+ export function Link(props: {
18
+ /**
19
+ * Set when the link is disabled.
20
+ *
21
+ * The link is automatically disabled when there are any pending tasks.
22
+ */
23
+ disabled?: Expression<boolean | undefined>;
24
+
25
+ /**
26
+ * The action to run when the link is clicked.
27
+ */
28
+ action?: Action;
29
+
30
+ /**
31
+ * Causes the browser to treat the linked url as a download when true or a filename.
32
+ */
33
+ download?: Expression<string | boolean | undefined>;
34
+
35
+ /**
36
+ * The url this link points to.
37
+ */
38
+ href?: Expression<string | undefined>;
39
+
40
+ /**
41
+ * Hints the human language of the linked url.
42
+ */
43
+ hreflang?: Expression<string | undefined>;
44
+
45
+ /**
46
+ * The link target.
47
+ */
48
+ target?: Expression<LinkAnchorTarget | string | undefined>;
49
+
50
+ /**
51
+ * How much of the referrer to send when following the link.
52
+ *
53
+ * @default "no-referrer"
54
+ */
55
+ referrerpolicy?: Expression<LinkReferrerPolicy | undefined>;
56
+
57
+ /**
58
+ * The link type.
59
+ *
60
+ * @default "noreferrer"
61
+ */
62
+ rel?: Expression<LinkAnchorRel | LinkAnchorRel[] | undefined>;
63
+
64
+ class?: ClassValue;
65
+ style?: StyleValue;
66
+ id?: Expression<string | undefined>;
67
+ title?: Expression<string | undefined>;
68
+ autofocus?: Expression<boolean | undefined>;
69
+ "aria-expanded"?: Expression<boolean | undefined>;
70
+ "aria-label"?: Expression<string | undefined>;
71
+ "aria-labelledby"?: Expression<string | undefined>;
72
+
73
+ children?: unknown;
74
+ }): unknown {
75
+ const theme = extract(THEME);
76
+ const disabled = () => isPending() || get(props.disabled);
77
+
78
+ function action(event: Event) {
79
+ if (disabled() || !props.action) {
80
+ return;
81
+ }
82
+ handleActionEvent(event, props.action);
83
+ }
84
+
85
+ return <a
86
+ disabled={disabled}
87
+ class={[
88
+ theme?.link,
89
+ props.class,
90
+ ]}
91
+ style={props.style}
92
+ id={props.id}
93
+ aria-expanded={optionalString(props["aria-expanded"])}
94
+ aria-label={props["aria-label"]}
95
+ aria-labelledby={props["aria-labelledby"]}
96
+ title={props.title}
97
+ autofocus={props.autofocus}
98
+ role={props.action === undefined ? undefined : "button"}
99
+ tabindex="0"
100
+
101
+ download={props.download}
102
+ href={props.href}
103
+ hreflang={props.hreflang}
104
+ target={props.target}
105
+ referrerpolicy={map(props.referrerpolicy, v => v ?? "no-referrer")}
106
+ rel={separated(map(props.rel, v => v ?? "noreferrer"), " ")}
107
+
108
+ on:click={action}
109
+ on:keydown={event => {
110
+ const key = keyFor(event);
111
+ if (key === "enter" || key === "space") {
112
+ action(event);
113
+ }
114
+ }}
115
+ >
116
+ {props.children}
117
+ </a>;
118
+ }
@@ -0,0 +1,36 @@
1
+ import { ClassValue, Expression, extract, StyleValue } from "rvx";
2
+
3
+ import { THEME } from "../common/theme.js";
4
+ import { Column } from "./column.js";
5
+
6
+ export function Page(props: {
7
+ inlineSize?: Expression<string | undefined>;
8
+ role?: Expression<string | undefined>;
9
+ id?: Expression<string | undefined>;
10
+ class?: ClassValue;
11
+ style?: StyleValue;
12
+ children?: unknown;
13
+ }): unknown {
14
+ const theme = extract(THEME);
15
+ return <div
16
+ role={props.role}
17
+ id={props.id}
18
+ class={[
19
+ props.class,
20
+ theme?.page,
21
+ ]}
22
+ style={[
23
+ props.style,
24
+ {
25
+ "--page-inline-size": props.inlineSize,
26
+ },
27
+ ]}
28
+ >
29
+ <div class={theme?.page_scrollbar_comp} />
30
+ <div class={theme?.page_content_col}>
31
+ <Column class={theme?.page_content}>
32
+ {props.children}
33
+ </Column>
34
+ </div>
35
+ </div>;
36
+ }