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

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 +121 -0
  4. package/components/alert-dialog.html +196 -0
  5. package/components/alert.html +69 -0
  6. package/components/aspect-ratio.html +23 -0
  7. package/components/attachment.html +84 -0
  8. package/components/avatar.html +127 -0
  9. package/components/badge.html +56 -0
  10. package/components/breadcrumb.html +129 -0
  11. package/components/bubble.html +67 -0
  12. package/components/button-group.html +84 -0
  13. package/components/button.html +188 -0
  14. package/components/calendar.html +170 -0
  15. package/components/card.html +113 -0
  16. package/components/carousel.html +186 -0
  17. package/components/chart.html +162 -0
  18. package/components/checkbox.html +93 -0
  19. package/components/code-block.html +58 -0
  20. package/components/collapsible.html +76 -0
  21. package/components/combobox.html +224 -0
  22. package/components/command.html +189 -0
  23. package/components/context-menu.html +67 -0
  24. package/components/dialog.html +281 -0
  25. package/components/direction.html +17 -0
  26. package/components/drawer.html +224 -0
  27. package/components/dropdown-menu.html +372 -0
  28. package/components/empty.html +106 -0
  29. package/components/field.html +117 -0
  30. package/components/hover-card.html +94 -0
  31. package/components/input-group.html +75 -0
  32. package/components/input-otp.html +139 -0
  33. package/components/input.html +82 -0
  34. package/components/item.html +170 -0
  35. package/components/kbd.html +35 -0
  36. package/components/label.html +40 -0
  37. package/components/marker.html +35 -0
  38. package/components/menubar.html +116 -0
  39. package/components/message-scroller.html +62 -0
  40. package/components/message.html +77 -0
  41. package/components/navigation-menu.html +204 -0
  42. package/components/pagination.html +154 -0
  43. package/components/popover.html +133 -0
  44. package/components/progress.html +39 -0
  45. package/components/radio-group.html +114 -0
  46. package/components/resizable.html +156 -0
  47. package/components/scroll-area.html +50 -0
  48. package/components/select.html +91 -0
  49. package/components/separator.html +25 -0
  50. package/components/sheet.html +143 -0
  51. package/components/sidebar.html +343 -0
  52. package/components/skeleton.html +30 -0
  53. package/components/slider.html +81 -0
  54. package/components/spinner.html +47 -0
  55. package/components/switch.html +89 -0
  56. package/components/table.html +75 -0
  57. package/components/tabs.html +164 -0
  58. package/components/textarea.html +68 -0
  59. package/components/toaster.html +128 -0
  60. package/components/toggle-group.html +65 -0
  61. package/components/toggle.html +81 -0
  62. package/components/tooltip.html +97 -0
  63. package/dist/theme.css +145 -0
  64. package/dist/ui.html +6944 -0
  65. package/package.json +40 -3
  66. package/theme.css +145 -0
@@ -0,0 +1,224 @@
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
+ :host {
93
+ display: block;
94
+ min-width: calc(var(--spacing) * 48);
95
+ }
96
+ :host(:state(attr-width)) {
97
+ width: calc(var(--spacing) * var(--attr-width));
98
+ }
99
+ .field {
100
+ display: flex;
101
+ align-items: center;
102
+ height: calc(var(--spacing) * 9);
103
+ box-sizing: border-box;
104
+ border: 1px solid var(--input);
105
+ border-radius: var(--radius-md);
106
+ box-shadow: var(--shadow-xs);
107
+ anchor-name: --combobox;
108
+ }
109
+ .field:focus-within {
110
+ border-color: var(--ring);
111
+ box-shadow: var(--focus-ring);
112
+ }
113
+ input {
114
+ flex: 1;
115
+ min-width: 0;
116
+ height: 100%;
117
+ padding: 0 calc(var(--spacing) * 3);
118
+ border: 0;
119
+ background: transparent;
120
+ color: inherit;
121
+ font: inherit;
122
+ font-size: var(--text-sm);
123
+ outline: none;
124
+ }
125
+ .chevron {
126
+ width: calc(var(--spacing) * 4);
127
+ height: calc(var(--spacing) * 4);
128
+ margin-right: calc(var(--spacing) * 2);
129
+ opacity: 0.5;
130
+ }
131
+ [role="listbox"] {
132
+ box-sizing: border-box;
133
+ width: anchor-size(width);
134
+ min-width: calc(var(--spacing) * 48);
135
+ max-height: calc(var(--spacing) * 72);
136
+ margin: calc(var(--spacing) * 1) 0 0;
137
+ padding: calc(var(--spacing) * 1);
138
+ overflow-y: auto;
139
+ border: 1px solid var(--border);
140
+ border-radius: var(--radius-md);
141
+ background: var(--popover);
142
+ color: var(--popover-foreground);
143
+ box-shadow: var(--shadow-md);
144
+ inset: auto;
145
+ position-anchor: --combobox;
146
+ position-area: bottom span-right;
147
+ position-try-fallbacks: flip-block;
148
+ }
149
+ [role="option"] {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
154
+ border-radius: var(--radius-sm);
155
+ font-size: var(--text-sm);
156
+ cursor: default;
157
+ }
158
+ [role="option"][data-active] {
159
+ background: var(--accent);
160
+ color: var(--accent-foreground);
161
+ }
162
+ [role="option"][aria-disabled="true"] {
163
+ opacity: 0.5;
164
+ }
165
+ .check {
166
+ visibility: hidden;
167
+ }
168
+ [aria-selected="true"] .check {
169
+ visibility: visible;
170
+ }
171
+ .empty {
172
+ margin: 0;
173
+ padding: calc(var(--spacing) * 4);
174
+ text-align: center;
175
+ font-size: var(--text-sm);
176
+ color: var(--muted-foreground);
177
+ }
178
+ .source {
179
+ display: none;
180
+ }
181
+ </style>
182
+
183
+ <div class="field" part="field">
184
+ <input
185
+ ref="input"
186
+ part="input"
187
+ role="combobox"
188
+ aria-autocomplete="list"
189
+ aria-controls="list"
190
+ _aria-expanded="#combo.open"
191
+ _aria-activedescendant="#combo.activeId"
192
+ _aria-label="@label"
193
+ _placeholder="@placeholder"
194
+ _value="#combo.text"
195
+ _disabled="@disabled"
196
+ onfocus="openList()"
197
+ onclick="openList()"
198
+ onblur="close()"
199
+ oninput="search(this)"
200
+ onkeydown="keys(event)">
201
+ <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>
202
+ </div>
203
+ <div id="list" ref="list" role="listbox" popover="manual" part="list" onpointerdown="event.preventDefault()">
204
+ <repeat-list of="#combo.visible" as="item" key="value">
205
+ <div
206
+ role="option"
207
+ part="option"
208
+ _id="#item.id"
209
+ _aria-selected="#item.chosen"
210
+ _aria-disabled="#item.disabled"
211
+ _data-active="#item.active"
212
+ onclick="choose(item)">
213
+ <slot name="#item.label"></slot>
214
+ <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>
215
+ </div>
216
+ </repeat-list>
217
+ <p class="empty" part="empty" _hidden="!#combo.none"><slot name="empty">No results found.</slot></p>
218
+ </div>
219
+ <div class="source"><slot></slot></div>
220
+ </custom-element>
221
+
222
+ <custom-element name="ui-combobox-empty" slot="empty">
223
+ <slot></slot>
224
+ </custom-element>
@@ -0,0 +1,189 @@
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
+ :host {
76
+ display: flex;
77
+ flex-direction: column;
78
+ overflow: hidden;
79
+ border: 1px solid var(--border);
80
+ border-radius: var(--radius-lg);
81
+ background: var(--popover);
82
+ color: var(--popover-foreground);
83
+ }
84
+ .search {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: calc(var(--spacing) * 2);
88
+ height: calc(var(--spacing) * 11);
89
+ padding: 0 calc(var(--spacing) * 3);
90
+ border-bottom: 1px solid var(--border);
91
+ }
92
+ .search svg {
93
+ width: calc(var(--spacing) * 4);
94
+ height: calc(var(--spacing) * 4);
95
+ opacity: 0.5;
96
+ }
97
+ input {
98
+ flex: 1;
99
+ border: 0;
100
+ background: transparent;
101
+ color: inherit;
102
+ font: inherit;
103
+ font-size: var(--text-sm);
104
+ outline: none;
105
+ }
106
+ input::placeholder {
107
+ color: var(--muted-foreground);
108
+ }
109
+ [role="listbox"] {
110
+ max-height: calc(var(--spacing) * 75);
111
+ overflow-y: auto;
112
+ padding: calc(var(--spacing) * 1);
113
+ }
114
+ .heading {
115
+ padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
116
+ font-size: var(--text-xs);
117
+ font-weight: var(--font-weight-medium);
118
+ color: var(--muted-foreground);
119
+ }
120
+ [role="option"] {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: calc(var(--spacing) * 2);
124
+ padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
125
+ border-radius: var(--radius-sm);
126
+ font-size: var(--text-sm);
127
+ cursor: default;
128
+ }
129
+ [role="option"][aria-selected="true"] {
130
+ background: var(--accent);
131
+ color: var(--accent-foreground);
132
+ }
133
+ .group + .group {
134
+ margin-top: calc(var(--spacing) * 1);
135
+ padding-top: calc(var(--spacing) * 1);
136
+ border-top: 1px solid var(--border);
137
+ }
138
+ .shortcut {
139
+ margin-left: auto;
140
+ font-size: var(--text-xs);
141
+ letter-spacing: 0.1em;
142
+ color: var(--muted-foreground);
143
+ }
144
+ .empty {
145
+ margin: 0;
146
+ padding: calc(var(--spacing) * 6) 0;
147
+ text-align: center;
148
+ font-size: var(--text-sm);
149
+ }
150
+ .source {
151
+ display: none;
152
+ }
153
+ </style>
154
+
155
+ <div class="search" part="search">
156
+ <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>
157
+ <input
158
+ ref="input"
159
+ part="input"
160
+ role="combobox"
161
+ aria-expanded="true"
162
+ aria-autocomplete="list"
163
+ aria-controls="list"
164
+ _aria-activedescendant="#command.activeId"
165
+ _placeholder="@placeholder"
166
+ _value="#command.query"
167
+ oninput="search(this.value)"
168
+ onkeydown="keys(event)">
169
+ </div>
170
+ <div id="list" role="listbox" part="list">
171
+ <repeat-list of="#command.groups" as="group" key="name">
172
+ <div role="group" class="group">
173
+ <div class="heading" _hidden="!#group.name"><slot name="#group.name"></slot></div>
174
+ <repeat-list of="#group.items" as="item" key="value">
175
+ <div role="option" part="item" _id="#item.id" _aria-selected="#item.active" onclick="choose(item)" onpointermove="hover(item)">
176
+ <slot name="#item.label"></slot>
177
+ <span class="shortcut" part="shortcut" _hidden="!#item.shortcut"><slot name="#item.shortcut"></slot></span>
178
+ </div>
179
+ </repeat-list>
180
+ </div>
181
+ </repeat-list>
182
+ <p class="empty" part="empty" _hidden="!#command.none"><slot name="empty">No results found.</slot></p>
183
+ </div>
184
+ <div class="source"><slot></slot></div>
185
+ </custom-element>
186
+
187
+ <custom-element name="ui-command-empty" slot="empty">
188
+ <slot></slot>
189
+ </custom-element>
@@ -0,0 +1,67 @@
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
+ :host {
14
+ display: contents;
15
+ }
16
+ </style>
17
+
18
+ <slot></slot>
19
+ </custom-element>
20
+
21
+ <custom-element name="ui-context-menu-trigger">
22
+ <style>
23
+ :host {
24
+ display: block;
25
+ }
26
+ </style>
27
+
28
+ <div part="trigger" oncontextmenu="open(event)"><slot></slot></div>
29
+ </custom-element>
30
+
31
+ <custom-element name="ui-context-menu-content" role="menu">
32
+ <script>
33
+ const ITEMS = ":is(ui-menu-item, ui-menu-checkbox-item, ui-menu-radio-item):not([disabled])";
34
+
35
+ function opened(event) {
36
+ if (event.newState === "open") host.querySelector(ITEMS)?.focus();
37
+ }
38
+
39
+ function leave(event) {
40
+ if (document.hasFocus() && !host.contains(event.relatedTarget)) host.hidePopover();
41
+ }
42
+
43
+ attrs.popover ??= "auto";
44
+
45
+ listen("toggle", opened);
46
+ listen("focusout", leave);
47
+ listen("select", () => host.hidePopover());
48
+ </script>
49
+
50
+ <style>
51
+ :host {
52
+ position: fixed;
53
+ box-sizing: border-box;
54
+ min-width: calc(var(--spacing) * 32);
55
+ margin: 0;
56
+ padding: calc(var(--spacing) * 1);
57
+ border: 1px solid var(--border);
58
+ border-radius: var(--radius-md);
59
+ background: var(--popover);
60
+ color: var(--popover-foreground);
61
+ box-shadow: var(--shadow-md);
62
+ inset: auto;
63
+ }
64
+ </style>
65
+
66
+ <div part="menu" focusgroup="menu block wrap"><slot></slot></div>
67
+ </custom-element>