@realmorg/ui 0.0.0-stage → 2.0.0-alpha.3

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 (66) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +44 -2
  3. package/components/accordion.html +139 -0
  4. package/components/alert-dialog.html +236 -0
  5. package/components/alert.html +83 -0
  6. package/components/aspect-ratio.html +29 -0
  7. package/components/attachment.html +98 -0
  8. package/components/avatar.html +149 -0
  9. package/components/badge.html +62 -0
  10. package/components/breadcrumb.html +159 -0
  11. package/components/bubble.html +77 -0
  12. package/components/button-group.html +98 -0
  13. package/components/button.html +194 -0
  14. package/components/calendar.html +176 -0
  15. package/components/card.html +143 -0
  16. package/components/carousel.html +208 -0
  17. package/components/chart.html +168 -0
  18. package/components/checkbox.html +99 -0
  19. package/components/code-block.html +64 -0
  20. package/components/collapsible.html +90 -0
  21. package/components/combobox.html +237 -0
  22. package/components/command.html +201 -0
  23. package/components/context-menu.html +81 -0
  24. package/components/dialog.html +327 -0
  25. package/components/direction.html +23 -0
  26. package/components/drawer.html +267 -0
  27. package/components/dropdown-menu.html +421 -0
  28. package/components/empty.html +132 -0
  29. package/components/field.html +143 -0
  30. package/components/hover-card.html +111 -0
  31. package/components/input-group.html +91 -0
  32. package/components/input-otp.html +151 -0
  33. package/components/input.html +94 -0
  34. package/components/item.html +208 -0
  35. package/components/kbd.html +45 -0
  36. package/components/label.html +46 -0
  37. package/components/marker.html +41 -0
  38. package/components/menubar.html +137 -0
  39. package/components/message-scroller.html +68 -0
  40. package/components/message.html +95 -0
  41. package/components/navigation-menu.html +233 -0
  42. package/components/pagination.html +160 -0
  43. package/components/popover.html +164 -0
  44. package/components/progress.html +45 -0
  45. package/components/radio-group.html +120 -0
  46. package/components/resizable.html +170 -0
  47. package/components/scroll-area.html +56 -0
  48. package/components/select.html +103 -0
  49. package/components/separator.html +31 -0
  50. package/components/sheet.html +173 -0
  51. package/components/sidebar.html +393 -0
  52. package/components/skeleton.html +36 -0
  53. package/components/slider.html +108 -0
  54. package/components/spinner.html +53 -0
  55. package/components/switch.html +95 -0
  56. package/components/table.html +81 -0
  57. package/components/tabs.html +182 -0
  58. package/components/textarea.html +80 -0
  59. package/components/toaster.html +134 -0
  60. package/components/toggle-group.html +71 -0
  61. package/components/toggle.html +87 -0
  62. package/components/tooltip.html +114 -0
  63. package/dist/theme.css +158 -0
  64. package/dist/ui.html +7923 -0
  65. package/package.json +40 -3
  66. package/theme.css +158 -0
@@ -0,0 +1,237 @@
1
+ <custom-element name="ui-combobox" form-associated delegates-focus>
2
+ <script>
3
+ const props = defineProps({
4
+ value: prop.string().default(""),
5
+ placeholder: prop.string().default("Select…"),
6
+ disabled: prop.boolean(),
7
+ required: prop.boolean(),
8
+ label: prop.string().attribute("aria-label"),
9
+ });
10
+
11
+ const options = useChildren("option", ["value", "label", "disabled"]);
12
+ const combo = useState({
13
+ query: "",
14
+ active: 0,
15
+ open: false,
16
+ get items() {
17
+ return options.map((option) => ({ value: option.value, label: option.label, disabled: option.disabled }));
18
+ },
19
+ get selected() {
20
+ return this.items.find((item) => item.value === props.value);
21
+ },
22
+ get visible() {
23
+ const query = this.query.trim().toLowerCase();
24
+ return this.items
25
+ .filter((item) => !query || item.label.toLowerCase().includes(query))
26
+ .map((item, index) => ({ ...item, id: `option-${index}`, active: index === this.active, chosen: item.value === props.value }));
27
+ },
28
+ get none() {
29
+ return this.visible.length === 0;
30
+ },
31
+ get text() {
32
+ return this.open ? this.query : (this.selected?.label ?? "");
33
+ },
34
+ get activeId() {
35
+ return this.open ? this.visible[this.active]?.id : null;
36
+ },
37
+ });
38
+
39
+ const KEYS = {
40
+ ArrowDown: () => move(1),
41
+ ArrowUp: () => move(-1),
42
+ Enter: () => combo.visible[combo.active] && choose(combo.visible[combo.active]),
43
+ Escape: () => close(),
44
+ };
45
+
46
+ function move(step) {
47
+ openList();
48
+ const count = combo.visible.length;
49
+ if (count) combo.active = (combo.active + step + count) % count;
50
+ }
51
+
52
+ function openList() {
53
+ if (combo.open) return;
54
+ combo.open = true;
55
+ combo.query = "";
56
+ combo.active = Math.max(0, combo.visible.findIndex((item) => item.chosen));
57
+ refs.list.showPopover();
58
+ }
59
+
60
+ function close() {
61
+ combo.open = false;
62
+ if (refs.list.matches(":popover-open")) refs.list.hidePopover();
63
+ }
64
+
65
+ function choose(item) {
66
+ if (item.disabled) return;
67
+ props.value = item.value;
68
+ close();
69
+ dispatch("change");
70
+ }
71
+
72
+ function search(field) {
73
+ openList();
74
+ combo.query = field.value;
75
+ combo.active = 0;
76
+ }
77
+
78
+ function keys(event) {
79
+ const action = KEYS[event.key];
80
+ if (!action || (event.key === "Escape" && !combo.open)) return;
81
+ event.preventDefault();
82
+ action();
83
+ }
84
+
85
+ observe(() => {
86
+ const missing = props.required && !props.value;
87
+ internals.setValidity(missing ? { valueMissing: true } : {}, missing ? "Please select an item." : "", refs.input);
88
+ });
89
+ </script>
90
+
91
+ <style>
92
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
93
+ touch-action does not reach shadow content in iOS Safari. */
94
+ :host,
95
+ * {
96
+ touch-action: manipulation;
97
+ }
98
+ :host {
99
+ display: block;
100
+ min-width: calc(var(--spacing) * 48);
101
+ }
102
+ :host(:state(attr-width)) {
103
+ width: calc(var(--spacing) * var(--attr-width));
104
+ }
105
+ .field {
106
+ display: flex;
107
+ align-items: center;
108
+ height: calc(var(--spacing) * 9);
109
+ box-sizing: border-box;
110
+ border: 1px solid var(--input);
111
+ border-radius: var(--radius-md);
112
+ box-shadow: var(--shadow-xs);
113
+ anchor-name: --combobox;
114
+ }
115
+ .field:focus-within {
116
+ border-color: var(--ring);
117
+ box-shadow: var(--focus-ring);
118
+ }
119
+ input {
120
+ flex: 1;
121
+ min-width: 0;
122
+ height: 100%;
123
+ padding: 0 calc(var(--spacing) * 3);
124
+ border: 0;
125
+ background: transparent;
126
+ color: inherit;
127
+ font: inherit;
128
+ font-size: var(--text-sm);
129
+ outline: none;
130
+ }
131
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
132
+ @media (pointer: coarse) {
133
+ input {
134
+ font-size: var(--text-base);
135
+ }
136
+ }
137
+ .chevron {
138
+ width: calc(var(--spacing) * 4);
139
+ height: calc(var(--spacing) * 4);
140
+ margin-right: calc(var(--spacing) * 2);
141
+ opacity: 0.5;
142
+ }
143
+ [role="listbox"] {
144
+ box-sizing: border-box;
145
+ width: anchor-size(width);
146
+ min-width: calc(var(--spacing) * 48);
147
+ max-height: calc(var(--spacing) * 72);
148
+ margin: calc(var(--spacing) * 1) 0 0;
149
+ padding: calc(var(--spacing) * 1);
150
+ overflow-y: auto;
151
+ border: 1px solid var(--border);
152
+ border-radius: var(--radius-md);
153
+ background: var(--popover);
154
+ color: var(--popover-foreground);
155
+ box-shadow: var(--shadow-md);
156
+ inset: auto;
157
+ position-anchor: --combobox;
158
+ height: fit-content;
159
+ position-area: bottom span-right;
160
+ position-try-fallbacks: flip-block;
161
+ }
162
+ [role="option"] {
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: space-between;
166
+ padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
167
+ border-radius: var(--radius-sm);
168
+ font-size: var(--text-sm);
169
+ cursor: default;
170
+ }
171
+ [role="option"][data-active] {
172
+ background: var(--accent);
173
+ color: var(--accent-foreground);
174
+ }
175
+ [role="option"][aria-disabled="true"] {
176
+ opacity: 0.5;
177
+ }
178
+ .check {
179
+ visibility: hidden;
180
+ }
181
+ [aria-selected="true"] .check {
182
+ visibility: visible;
183
+ }
184
+ .empty {
185
+ margin: 0;
186
+ padding: calc(var(--spacing) * 4);
187
+ text-align: center;
188
+ font-size: var(--text-sm);
189
+ color: var(--muted-foreground);
190
+ }
191
+ .source {
192
+ display: none;
193
+ }
194
+ </style>
195
+
196
+ <div class="field" part="field">
197
+ <input
198
+ ref="input"
199
+ part="input"
200
+ role="combobox"
201
+ aria-autocomplete="list"
202
+ aria-controls="list"
203
+ _aria-expanded="#combo.open"
204
+ _aria-activedescendant="#combo.activeId"
205
+ _aria-label="@label"
206
+ _placeholder="@placeholder"
207
+ _value="#combo.text"
208
+ _disabled="@disabled"
209
+ onfocus="openList()"
210
+ onclick="openList()"
211
+ onblur="close()"
212
+ oninput="search(this)"
213
+ onkeydown="keys(event)">
214
+ <svg aria-hidden="true" class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m7 15 5 5 5-5M7 9l5-5 5 5" /></svg>
215
+ </div>
216
+ <div id="list" ref="list" role="listbox" popover="manual" part="list" onpointerdown="event.preventDefault()">
217
+ <repeat-list of="#combo.visible" as="item" key="value">
218
+ <div
219
+ role="option"
220
+ part="option"
221
+ _id="#item.id"
222
+ _aria-selected="#item.chosen"
223
+ _aria-disabled="#item.disabled"
224
+ _data-active="#item.active"
225
+ onclick="choose(item)">
226
+ <slot name="#item.label"></slot>
227
+ <svg aria-hidden="true" class="check" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5" /></svg>
228
+ </div>
229
+ </repeat-list>
230
+ <p class="empty" part="empty" _hidden="!#combo.none"><slot name="empty">No results found.</slot></p>
231
+ </div>
232
+ <div class="source"><slot></slot></div>
233
+ </custom-element>
234
+
235
+ <custom-element name="ui-combobox-empty" slot="empty">
236
+ <slot></slot>
237
+ </custom-element>
@@ -0,0 +1,201 @@
1
+ <custom-element name="ui-command">
2
+ <script>
3
+ const props = defineProps({
4
+ placeholder: prop.string().default("Type a command or search..."),
5
+ });
6
+
7
+ const options = useChildren("option, optgroup", ["value", "label", "data-shortcut"]);
8
+ const command = useState({
9
+ query: "",
10
+ active: 0,
11
+ get items() {
12
+ return options.flatMap((child) => {
13
+ const grouped = child.localName === "optgroup";
14
+ return [...(grouped ? child.children : [child])].map((option) => ({
15
+ value: option.value,
16
+ label: option.label,
17
+ group: grouped ? child.label : "",
18
+ shortcut: option.dataset.shortcut ?? "",
19
+ }));
20
+ });
21
+ },
22
+ get visible() {
23
+ const query = this.query.trim().toLowerCase();
24
+ return this.items.filter((item) => !query || item.label.toLowerCase().includes(query) || item.value.toLowerCase().includes(query));
25
+ },
26
+ get groups() {
27
+ const groups = [];
28
+ this.visible.forEach((item, index) => {
29
+ let group = groups.find((entry) => entry.name === item.group);
30
+ if (!group) {
31
+ group = { name: item.group, items: [] };
32
+ groups.push(group);
33
+ }
34
+ group.items.push({ ...item, id: `option-${index}`, active: index === this.active });
35
+ });
36
+ return groups;
37
+ },
38
+ get none() {
39
+ return this.visible.length === 0;
40
+ },
41
+ get activeId() {
42
+ return this.visible[this.active] ? `option-${this.active}` : null;
43
+ },
44
+ });
45
+
46
+ const KEYS = {
47
+ ArrowDown: (count) => (command.active = (command.active + 1) % count),
48
+ ArrowUp: (count) => (command.active = (command.active - 1 + count) % count),
49
+ Enter: () => choose(command.visible[command.active]),
50
+ };
51
+
52
+ function search(value) {
53
+ command.query = value;
54
+ command.active = 0;
55
+ }
56
+
57
+ function choose(item) {
58
+ dispatch("select", { value: item.value, label: item.label });
59
+ }
60
+
61
+ function hover(item) {
62
+ command.active = command.visible.findIndex((entry) => entry.value === item.value);
63
+ }
64
+
65
+ function keys(event) {
66
+ const action = KEYS[event.key];
67
+ const count = command.visible.length;
68
+ if (!action) return;
69
+ event.preventDefault();
70
+ if (count) action(count);
71
+ }
72
+ </script>
73
+
74
+ <style>
75
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
76
+ touch-action does not reach shadow content in iOS Safari. */
77
+ :host,
78
+ * {
79
+ touch-action: manipulation;
80
+ }
81
+ :host {
82
+ display: flex;
83
+ flex-direction: column;
84
+ overflow: hidden;
85
+ border: 1px solid var(--border);
86
+ border-radius: var(--radius-lg);
87
+ background: var(--popover);
88
+ color: var(--popover-foreground);
89
+ }
90
+ .search {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: calc(var(--spacing) * 2);
94
+ height: calc(var(--spacing) * 11);
95
+ padding: 0 calc(var(--spacing) * 3);
96
+ border-bottom: 1px solid var(--border);
97
+ }
98
+ .search svg {
99
+ width: calc(var(--spacing) * 4);
100
+ height: calc(var(--spacing) * 4);
101
+ opacity: 0.5;
102
+ }
103
+ input {
104
+ flex: 1;
105
+ border: 0;
106
+ background: transparent;
107
+ color: inherit;
108
+ font: inherit;
109
+ font-size: var(--text-sm);
110
+ outline: none;
111
+ }
112
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
113
+ @media (pointer: coarse) {
114
+ input {
115
+ font-size: var(--text-base);
116
+ }
117
+ }
118
+ input::placeholder {
119
+ color: var(--muted-foreground);
120
+ }
121
+ [role="listbox"] {
122
+ max-height: calc(var(--spacing) * 75);
123
+ overflow-y: auto;
124
+ padding: calc(var(--spacing) * 1);
125
+ }
126
+ .heading {
127
+ padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
128
+ font-size: var(--text-xs);
129
+ font-weight: var(--font-weight-medium);
130
+ color: var(--muted-foreground);
131
+ }
132
+ [role="option"] {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: calc(var(--spacing) * 2);
136
+ padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
137
+ border-radius: var(--radius-sm);
138
+ font-size: var(--text-sm);
139
+ cursor: default;
140
+ }
141
+ [role="option"][aria-selected="true"] {
142
+ background: var(--accent);
143
+ color: var(--accent-foreground);
144
+ }
145
+ .group + .group {
146
+ margin-top: calc(var(--spacing) * 1);
147
+ padding-top: calc(var(--spacing) * 1);
148
+ border-top: 1px solid var(--border);
149
+ }
150
+ .shortcut {
151
+ margin-left: auto;
152
+ font-size: var(--text-xs);
153
+ letter-spacing: 0.1em;
154
+ color: var(--muted-foreground);
155
+ }
156
+ .empty {
157
+ margin: 0;
158
+ padding: calc(var(--spacing) * 6) 0;
159
+ text-align: center;
160
+ font-size: var(--text-sm);
161
+ }
162
+ .source {
163
+ display: none;
164
+ }
165
+ </style>
166
+
167
+ <div class="search" part="search">
168
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
169
+ <input
170
+ ref="input"
171
+ part="input"
172
+ role="combobox"
173
+ aria-expanded="true"
174
+ aria-autocomplete="list"
175
+ aria-controls="list"
176
+ _aria-activedescendant="#command.activeId"
177
+ _placeholder="@placeholder"
178
+ _value="#command.query"
179
+ oninput="search(this.value)"
180
+ onkeydown="keys(event)">
181
+ </div>
182
+ <div id="list" role="listbox" part="list">
183
+ <repeat-list of="#command.groups" as="group" key="name">
184
+ <div role="group" class="group">
185
+ <div class="heading" _hidden="!#group.name"><slot name="#group.name"></slot></div>
186
+ <repeat-list of="#group.items" as="item" key="value">
187
+ <div role="option" part="item" _id="#item.id" _aria-selected="#item.active" onclick="choose(item)" onpointermove="hover(item)">
188
+ <slot name="#item.label"></slot>
189
+ <span class="shortcut" part="shortcut" _hidden="!#item.shortcut"><slot name="#item.shortcut"></slot></span>
190
+ </div>
191
+ </repeat-list>
192
+ </div>
193
+ </repeat-list>
194
+ <p class="empty" part="empty" _hidden="!#command.none"><slot name="empty">No results found.</slot></p>
195
+ </div>
196
+ <div class="source"><slot></slot></div>
197
+ </custom-element>
198
+
199
+ <custom-element name="ui-command-empty" slot="empty">
200
+ <slot></slot>
201
+ </custom-element>
@@ -0,0 +1,81 @@
1
+ <custom-element name="ui-context-menu">
2
+ <script>
3
+ function open(event) {
4
+ event.preventDefault();
5
+ const content = host.querySelector(":scope > ui-context-menu-content");
6
+ content.style.left = `${event.clientX}px`;
7
+ content.style.top = `${event.clientY}px`;
8
+ content.showPopover();
9
+ }
10
+ </script>
11
+
12
+ <style>
13
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
14
+ touch-action does not reach shadow content in iOS Safari. */
15
+ :host,
16
+ * {
17
+ touch-action: manipulation;
18
+ }
19
+ :host {
20
+ display: contents;
21
+ }
22
+ </style>
23
+
24
+ <slot></slot>
25
+ </custom-element>
26
+
27
+ <custom-element name="ui-context-menu-trigger">
28
+ <style>
29
+ :host,
30
+ * {
31
+ touch-action: manipulation;
32
+ }
33
+ :host {
34
+ display: block;
35
+ }
36
+ </style>
37
+
38
+ <div part="trigger" oncontextmenu="open(event)"><slot></slot></div>
39
+ </custom-element>
40
+
41
+ <custom-element name="ui-context-menu-content" role="menu">
42
+ <script>
43
+ const ITEMS = ":is(ui-menu-item, ui-menu-checkbox-item, ui-menu-radio-item):not([disabled])";
44
+
45
+ function opened(event) {
46
+ if (event.newState === "open") host.querySelector(ITEMS)?.focus();
47
+ }
48
+
49
+ function leave(event) {
50
+ if (document.hasFocus() && !host.contains(event.relatedTarget)) host.hidePopover();
51
+ }
52
+
53
+ attrs.popover ??= "auto";
54
+
55
+ listen("toggle", opened);
56
+ listen("focusout", leave);
57
+ listen("select", () => host.hidePopover());
58
+ </script>
59
+
60
+ <style>
61
+ :host,
62
+ * {
63
+ touch-action: manipulation;
64
+ }
65
+ :host {
66
+ position: fixed;
67
+ box-sizing: border-box;
68
+ min-width: calc(var(--spacing) * 32);
69
+ margin: 0;
70
+ padding: calc(var(--spacing) * 1);
71
+ border: 1px solid var(--border);
72
+ border-radius: var(--radius-md);
73
+ background: var(--popover);
74
+ color: var(--popover-foreground);
75
+ box-shadow: var(--shadow-md);
76
+ inset: auto;
77
+ }
78
+ </style>
79
+
80
+ <div part="menu" focusgroup="menu block wrap"><slot></slot></div>
81
+ </custom-element>