@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,75 @@
1
+ <custom-element name="ui-input-group" form-associated delegates-focus>
2
+ <script>
3
+ defineProps({
4
+ value: prop.string().default(""),
5
+ type: prop.enum(["text", "email", "password", "number", "search", "tel", "url"]).default("text"),
6
+ placeholder: prop.string(),
7
+ disabled: prop.boolean(),
8
+ required: prop.boolean(),
9
+ label: prop.string().attribute("aria-label"),
10
+ });
11
+ </script>
12
+
13
+ <style>
14
+ :host {
15
+ display: flex;
16
+ align-items: center;
17
+ min-width: 0;
18
+ height: calc(var(--spacing) * 9);
19
+ box-sizing: border-box;
20
+ border: 1px solid var(--input);
21
+ border-radius: var(--radius-md);
22
+ box-shadow: var(--shadow-xs);
23
+ transition: box-shadow var(--default-transition-duration);
24
+ }
25
+ :host(:focus-within) {
26
+ border-color: var(--ring);
27
+ box-shadow: var(--focus-ring);
28
+ }
29
+ input {
30
+ flex: 1;
31
+ min-width: 0;
32
+ height: 100%;
33
+ padding: 0 calc(var(--spacing) * 3);
34
+ border: 0;
35
+ background: transparent;
36
+ color: inherit;
37
+ font: inherit;
38
+ font-size: var(--text-sm);
39
+ outline: none;
40
+ }
41
+ input::placeholder {
42
+ color: var(--muted-foreground);
43
+ }
44
+ </style>
45
+
46
+ <slot></slot>
47
+ <input ref="control" part="input" _type="@type" _value="@value" _placeholder="@placeholder" _disabled="@disabled" _required="@required" _aria-label="@label">
48
+ </custom-element>
49
+
50
+ <custom-element name="ui-input-group-addon">
51
+ <style>
52
+ :host {
53
+ display: flex;
54
+ order: -1;
55
+ align-items: center;
56
+ gap: calc(var(--spacing) * 2);
57
+ height: 100%;
58
+ padding-inline-start: calc(var(--spacing) * 3);
59
+ font-size: var(--text-sm);
60
+ color: var(--muted-foreground);
61
+ cursor: text;
62
+ user-select: none;
63
+ }
64
+ :host(:state(attr-align-is-inline-end)) {
65
+ order: 1;
66
+ padding-inline: 0 calc(var(--spacing) * 3);
67
+ }
68
+ ::slotted(svg) {
69
+ width: calc(var(--spacing) * 4);
70
+ height: calc(var(--spacing) * 4);
71
+ }
72
+ </style>
73
+
74
+ <slot></slot>
75
+ </custom-element>
@@ -0,0 +1,139 @@
1
+ <custom-element name="ui-input-otp" form-associated>
2
+ <script>
3
+ const props = defineProps({
4
+ value: prop.string().default(""),
5
+ length: prop.number().int().min(1).max(12).default(6),
6
+ pattern: prop.string().default("[0-9]"),
7
+ separator: prop.number().int().min(0).default(0),
8
+ disabled: prop.boolean(),
9
+ required: prop.boolean(),
10
+ });
11
+
12
+ const otp = useState({
13
+ get slots() {
14
+ return Array.from({ length: props.length }, (_, index) => ({
15
+ index,
16
+ char: props.value[index] ?? "",
17
+ gap: props.separator > 0 && index > 0 && index % props.separator === 0,
18
+ label: `Character ${index + 1} of ${props.length}`,
19
+ }));
20
+ },
21
+ get complete() {
22
+ return props.value.length === props.length;
23
+ },
24
+ });
25
+
26
+ const KEYS = { ArrowLeft: -1, ArrowRight: 1 };
27
+
28
+ function boxes() {
29
+ return [...refs.group.querySelectorAll("input")];
30
+ }
31
+
32
+ function accept(text) {
33
+ const matcher = new RegExp(`^${props.pattern}$`);
34
+ return [...text].filter((char) => matcher.test(char)).join("");
35
+ }
36
+
37
+ async function write(start, text) {
38
+ const chars = [...props.value.padEnd(start, " ")];
39
+ [...accept(text)].forEach((char, offset) => {
40
+ chars[start + offset] = char;
41
+ });
42
+ props.value = chars.join("").slice(0, props.length).trimEnd();
43
+ dispatch("change");
44
+ if (otp.complete) dispatch("complete", { value: props.value });
45
+ await tick();
46
+ boxes()[Math.min(start + accept(text).length, props.length - 1)]?.focus();
47
+ }
48
+
49
+ function input(slot, field) {
50
+ const text = field.value;
51
+ field.value = slot.char;
52
+ if (text) write(slot.index, text.slice(-1));
53
+ }
54
+
55
+ function paste(slot, event) {
56
+ event.preventDefault();
57
+ write(slot.index, event.clipboardData.getData("text"));
58
+ }
59
+
60
+ function keys(slot, event) {
61
+ const step = KEYS[event.key];
62
+ if (step) boxes()[slot.index + step]?.focus();
63
+ if (event.key !== "Backspace") return;
64
+ event.preventDefault();
65
+ const target = slot.char ? slot.index : slot.index - 1;
66
+ if (target < 0) return;
67
+ props.value = props.value.slice(0, target) + props.value.slice(target + 1);
68
+ boxes()[target]?.focus();
69
+ dispatch("change");
70
+ }
71
+
72
+ observe(() => {
73
+ const missing = props.required && !otp.complete;
74
+ const anchor = refs.group?.querySelectorAll("input")[Math.min(props.value.length, props.length - 1)];
75
+ internals.setValidity(missing ? { valueMissing: true } : {}, missing ? "Enter the full code." : "", anchor);
76
+ });
77
+ </script>
78
+
79
+ <style>
80
+ :host {
81
+ display: inline-flex;
82
+ }
83
+ .group {
84
+ display: flex;
85
+ align-items: center;
86
+ }
87
+ input {
88
+ box-sizing: border-box;
89
+ width: calc(var(--spacing) * 9);
90
+ height: calc(var(--spacing) * 9);
91
+ margin-left: -1px;
92
+ border: 1px solid var(--input);
93
+ background: transparent;
94
+ color: inherit;
95
+ font: inherit;
96
+ font-size: var(--text-sm);
97
+ text-align: center;
98
+ outline: none;
99
+ }
100
+ input:first-child,
101
+ input[data-gap] {
102
+ margin-left: 0;
103
+ border-start-start-radius: var(--radius-md);
104
+ border-end-start-radius: var(--radius-md);
105
+ }
106
+ input:last-child {
107
+ border-start-end-radius: var(--radius-md);
108
+ border-end-end-radius: var(--radius-md);
109
+ }
110
+ input[data-gap] {
111
+ margin-inline-start: calc(var(--spacing) * 6);
112
+ }
113
+ input:focus-visible {
114
+ position: relative;
115
+ border-color: var(--ring);
116
+ box-shadow: var(--focus-ring);
117
+ }
118
+ input:disabled {
119
+ opacity: 0.5;
120
+ }
121
+ </style>
122
+
123
+ <div class="group" ref="group" part="group" role="group">
124
+ <repeat-list of="#otp.slots" as="slot" key="index">
125
+ <input
126
+ part="slot"
127
+ inputmode="numeric"
128
+ autocomplete="one-time-code"
129
+ maxlength="2"
130
+ _value="#slot.char"
131
+ _aria-label="#slot.label"
132
+ _data-gap="#slot.gap"
133
+ _disabled="@disabled"
134
+ oninput="input(slot, this)"
135
+ onpaste="paste(slot, event)"
136
+ onkeydown="keys(slot, event)">
137
+ </repeat-list>
138
+ </div>
139
+ </custom-element>
@@ -0,0 +1,82 @@
1
+ <custom-element name="ui-input" form-associated delegates-focus>
2
+ <script>
3
+ defineProps({
4
+ value: prop.string().default(""),
5
+ type: prop
6
+ .enum(["text", "email", "password", "number", "search", "tel", "url", "date", "time", "datetime-local", "month", "week", "color"])
7
+ .default("text"),
8
+ placeholder: prop.string(),
9
+ autocomplete: prop.string(),
10
+ inputmode: prop.string(),
11
+ pattern: prop.string(),
12
+ min: prop.string(),
13
+ max: prop.string(),
14
+ step: prop.string(),
15
+ minlength: prop.number(),
16
+ maxlength: prop.number(),
17
+ disabled: prop.boolean(),
18
+ readonly: prop.boolean(),
19
+ required: prop.boolean(),
20
+ invalid: prop.boolean(),
21
+ label: prop.string().attribute("aria-label"),
22
+ });
23
+ </script>
24
+
25
+ <style>
26
+ :host {
27
+ display: block;
28
+ min-width: 0;
29
+ }
30
+ input {
31
+ box-sizing: border-box;
32
+ width: 100%;
33
+ height: calc(var(--spacing) * 9);
34
+ padding: calc(var(--spacing) * 1) calc(var(--spacing) * 3);
35
+ border: 1px solid var(--input);
36
+ border-radius: var(--radius-md);
37
+ background: transparent;
38
+ color: inherit;
39
+ font: inherit;
40
+ font-size: var(--text-sm);
41
+ box-shadow: var(--shadow-xs);
42
+ outline: none;
43
+ transition: color var(--default-transition-duration), box-shadow var(--default-transition-duration);
44
+ }
45
+ input::placeholder {
46
+ color: var(--muted-foreground);
47
+ }
48
+ input:focus-visible {
49
+ border-color: var(--ring);
50
+ box-shadow: var(--focus-ring);
51
+ }
52
+ input:disabled {
53
+ cursor: not-allowed;
54
+ opacity: 0.5;
55
+ }
56
+ input[aria-invalid="true"] {
57
+ border-color: var(--destructive);
58
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--destructive) 20%, transparent);
59
+ }
60
+ </style>
61
+
62
+ <input
63
+ ref="control"
64
+ _aria-label="@label"
65
+ part="input"
66
+ _type="@type"
67
+ _value="@value"
68
+ _placeholder="@placeholder"
69
+ _autocomplete="@autocomplete"
70
+ _inputmode="@inputmode"
71
+ _pattern="@pattern"
72
+ _min="@min"
73
+ _max="@max"
74
+ _step="@step"
75
+ _minlength="@minlength"
76
+ _maxlength="@maxlength"
77
+ _disabled="@disabled"
78
+ _readonly="@readonly"
79
+ _required="@required"
80
+ _aria-invalid="@invalid"
81
+ >
82
+ </custom-element>
@@ -0,0 +1,170 @@
1
+ <custom-element name="ui-item">
2
+ <script>
3
+ defineProps({
4
+ variant: prop.enum(["default", "outline", "muted"]).default("default"),
5
+ size: prop.enum(["default", "sm"]).default("default"),
6
+ });
7
+
8
+ onConnected(() => {
9
+ internals.role = host.parentElement?.localName === "ui-item-group" ? "listitem" : null;
10
+ });
11
+ </script>
12
+
13
+ <style>
14
+ :host {
15
+ display: flex;
16
+ flex-wrap: wrap;
17
+ align-items: center;
18
+ gap: calc(var(--spacing) * 4);
19
+ padding: calc(var(--spacing) * 4);
20
+ border: 1px solid transparent;
21
+ border-radius: var(--radius-md);
22
+ font-size: var(--text-sm);
23
+ transition: background-color 0.1s;
24
+ }
25
+ :host(:state(attr-variant-is-outline)) {
26
+ border-color: var(--border);
27
+ }
28
+ :host(:state(attr-variant-is-muted)) {
29
+ background: color-mix(in oklab, var(--muted) 50%, transparent);
30
+ }
31
+ :host(:state(attr-size-is-sm)) {
32
+ gap: calc(var(--spacing) * 2.5);
33
+ padding: calc(var(--spacing) * 3) calc(var(--spacing) * 4);
34
+ }
35
+ </style>
36
+
37
+ <slot></slot>
38
+ </custom-element>
39
+
40
+ <custom-element name="ui-item-group" role="list">
41
+ <style>
42
+ :host {
43
+ display: flex;
44
+ flex-direction: column;
45
+ }
46
+ </style>
47
+
48
+ <slot></slot>
49
+ </custom-element>
50
+
51
+ <custom-element name="ui-item-media">
52
+ <style>
53
+ :host {
54
+ display: flex;
55
+ flex-shrink: 0;
56
+ align-items: center;
57
+ justify-content: center;
58
+ gap: calc(var(--spacing) * 2);
59
+ }
60
+ :host(:state(attr-variant-is-icon)) {
61
+ box-sizing: border-box;
62
+ width: calc(var(--spacing) * 8);
63
+ height: calc(var(--spacing) * 8);
64
+ border: 1px solid var(--border);
65
+ border-radius: var(--radius-sm);
66
+ background: var(--muted);
67
+ }
68
+ :host(:state(attr-variant-is-image)) {
69
+ width: calc(var(--spacing) * 10);
70
+ height: calc(var(--spacing) * 10);
71
+ overflow: hidden;
72
+ border-radius: var(--radius-sm);
73
+ }
74
+ ::slotted(svg) {
75
+ width: calc(var(--spacing) * 4);
76
+ height: calc(var(--spacing) * 4);
77
+ }
78
+ :host(:state(attr-variant-is-image)) ::slotted(img) {
79
+ width: 100%;
80
+ height: 100%;
81
+ object-fit: cover;
82
+ }
83
+ </style>
84
+
85
+ <slot></slot>
86
+ </custom-element>
87
+
88
+ <custom-element name="ui-item-content">
89
+ <style>
90
+ :host {
91
+ display: flex;
92
+ flex: 1;
93
+ flex-direction: column;
94
+ gap: calc(var(--spacing) * 1);
95
+ min-width: 0;
96
+ }
97
+ </style>
98
+
99
+ <slot></slot>
100
+ </custom-element>
101
+
102
+ <custom-element name="ui-item-title">
103
+ <style>
104
+ :host {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: calc(var(--spacing) * 2);
108
+ font-weight: var(--font-weight-medium);
109
+ line-height: 1.25;
110
+ }
111
+ </style>
112
+
113
+ <slot></slot>
114
+ </custom-element>
115
+
116
+ <custom-element name="ui-item-description">
117
+ <style>
118
+ :host {
119
+ display: -webkit-box;
120
+ overflow: hidden;
121
+ -webkit-box-orient: vertical;
122
+ -webkit-line-clamp: 2;
123
+ color: var(--muted-foreground);
124
+ line-height: 1.4;
125
+ text-wrap: balance;
126
+ }
127
+ </style>
128
+
129
+ <slot></slot>
130
+ </custom-element>
131
+
132
+ <custom-element name="ui-item-actions">
133
+ <style>
134
+ :host {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: calc(var(--spacing) * 2);
138
+ }
139
+ </style>
140
+
141
+ <slot></slot>
142
+ </custom-element>
143
+
144
+ <custom-element name="ui-item-header">
145
+ <style>
146
+ :host {
147
+ display: flex;
148
+ flex-basis: 100%;
149
+ align-items: center;
150
+ justify-content: space-between;
151
+ gap: calc(var(--spacing) * 2);
152
+ }
153
+ </style>
154
+
155
+ <slot></slot>
156
+ </custom-element>
157
+
158
+ <custom-element name="ui-item-footer">
159
+ <style>
160
+ :host {
161
+ display: flex;
162
+ flex-basis: 100%;
163
+ align-items: center;
164
+ justify-content: space-between;
165
+ gap: calc(var(--spacing) * 2);
166
+ }
167
+ </style>
168
+
169
+ <slot></slot>
170
+ </custom-element>
@@ -0,0 +1,35 @@
1
+ <custom-element name="ui-kbd">
2
+ <style>
3
+ :host {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ gap: calc(var(--spacing) * 1);
8
+ height: calc(var(--spacing) * 5);
9
+ min-width: calc(var(--spacing) * 5);
10
+ padding: 0 calc(var(--spacing) * 1);
11
+ border-radius: var(--radius-xs);
12
+ background: var(--muted);
13
+ color: var(--muted-foreground);
14
+ font-family: var(--font-sans);
15
+ font-size: var(--text-xs);
16
+ font-weight: var(--font-weight-medium);
17
+ pointer-events: none;
18
+ user-select: none;
19
+ }
20
+ </style>
21
+
22
+ <slot></slot>
23
+ </custom-element>
24
+
25
+ <custom-element name="ui-kbd-group">
26
+ <style>
27
+ :host {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ gap: calc(var(--spacing) * 1);
31
+ }
32
+ </style>
33
+
34
+ <slot></slot>
35
+ </custom-element>
@@ -0,0 +1,40 @@
1
+ <custom-element name="ui-label">
2
+ <script>
3
+ const props = defineProps({ for: prop.string() });
4
+
5
+ const CHECKABLE = new Set(["checkbox", "radio"]);
6
+
7
+ function target() {
8
+ return props.for ? host.getRootNode().getElementById(props.for) : null;
9
+ }
10
+
11
+ function activate() {
12
+ const field = target();
13
+ if (!field) return;
14
+ field.focus();
15
+ if (CHECKABLE.has(field.type) || "checked" in field) (field.shadowRoot?.querySelector('[ref="control"]') ?? field).click();
16
+ }
17
+
18
+ onConnected(() => {
19
+ const field = target();
20
+ if (field && !field.hasAttribute("aria-label")) field.setAttribute("aria-label", host.textContent.trim());
21
+ });
22
+ </script>
23
+
24
+ <style>
25
+ :host {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: calc(var(--spacing) * 2);
29
+ font-size: var(--text-sm);
30
+ font-weight: var(--font-weight-medium);
31
+ line-height: 1;
32
+ user-select: none;
33
+ }
34
+ :host(:state(attr-for)) {
35
+ cursor: pointer;
36
+ }
37
+ </style>
38
+
39
+ <span part="label" onclick="activate()"><slot></slot></span>
40
+ </custom-element>
@@ -0,0 +1,35 @@
1
+ <custom-element name="ui-marker">
2
+ <script>
3
+ defineProps({ variant: prop.enum(["default", "separator", "border"]).default("default") });
4
+ </script>
5
+
6
+ <style>
7
+ :host {
8
+ display: flex;
9
+ align-items: center;
10
+ gap: calc(var(--spacing) * 2);
11
+ width: 100%;
12
+ min-height: calc(var(--spacing) * 4);
13
+ font-size: var(--text-sm);
14
+ color: var(--muted-foreground);
15
+ }
16
+ :host(:state(attr-variant-is-separator))::before,
17
+ :host(:state(attr-variant-is-separator))::after {
18
+ content: "";
19
+ flex: 1;
20
+ height: 1px;
21
+ background: var(--border);
22
+ }
23
+ :host(:state(attr-variant-is-border)) {
24
+ padding-bottom: calc(var(--spacing) * 2);
25
+ border-bottom: 1px solid var(--border);
26
+ }
27
+ ::slotted(svg) {
28
+ width: calc(var(--spacing) * 4);
29
+ height: calc(var(--spacing) * 4);
30
+ flex-shrink: 0;
31
+ }
32
+ </style>
33
+
34
+ <slot></slot>
35
+ </custom-element>
@@ -0,0 +1,116 @@
1
+ <custom-element name="ui-menubar" role="menubar">
2
+ <style>
3
+ :host {
4
+ display: flex;
5
+ align-items: center;
6
+ gap: calc(var(--spacing) * 1);
7
+ width: fit-content;
8
+ height: calc(var(--spacing) * 9);
9
+ padding: calc(var(--spacing) * 1);
10
+ box-sizing: border-box;
11
+ border: 1px solid var(--border);
12
+ border-radius: var(--radius-md);
13
+ background: var(--background);
14
+ box-shadow: var(--shadow-xs);
15
+ }
16
+ div {
17
+ display: contents;
18
+ }
19
+ </style>
20
+
21
+ <div part="bar" focusgroup="menubar inline wrap"><slot></slot></div>
22
+ </custom-element>
23
+
24
+ <custom-element name="ui-menubar-menu">
25
+ <script>
26
+ let wasOpen = false;
27
+
28
+ function press() {
29
+ wasOpen = host.querySelector(":scope > ui-menubar-content").matches(":popover-open");
30
+ }
31
+
32
+ function toggle(trigger) {
33
+ const content = host.querySelector(":scope > ui-menubar-content");
34
+ const open = wasOpen || content.matches(":popover-open");
35
+ wasOpen = false;
36
+ if (open) content.hidePopover();
37
+ else {
38
+ trigger.focus();
39
+ content.showPopover({ source: trigger });
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <style>
45
+ :host {
46
+ display: contents;
47
+ }
48
+ </style>
49
+
50
+ <slot></slot>
51
+ </custom-element>
52
+
53
+ <custom-element name="ui-menubar-trigger">
54
+ <style>
55
+ :host {
56
+ display: inline-flex;
57
+ }
58
+ button {
59
+ all: unset;
60
+ display: inline-flex;
61
+ align-items: center;
62
+ padding: calc(var(--spacing) * 1) calc(var(--spacing) * 2);
63
+ border-radius: var(--radius-sm);
64
+ font-size: var(--text-sm);
65
+ font-weight: var(--font-weight-medium);
66
+ cursor: default;
67
+ user-select: none;
68
+ }
69
+ button:hover,
70
+ button:focus-visible {
71
+ background: var(--accent);
72
+ color: var(--accent-foreground);
73
+ }
74
+ </style>
75
+
76
+ <button type="button" part="trigger" role="menuitem" aria-haspopup="menu" onpointerdown="press()" onclick="toggle(this)"><slot></slot></button>
77
+ </custom-element>
78
+
79
+ <custom-element name="ui-menubar-content" role="menu">
80
+ <script>
81
+ const ITEMS = ":is(ui-menu-item, ui-menu-checkbox-item, ui-menu-radio-item):not([disabled])";
82
+
83
+ function opened(event) {
84
+ if (event.newState === "open") host.querySelector(ITEMS)?.focus();
85
+ }
86
+
87
+ function leave(event) {
88
+ if (document.hasFocus() && !host.contains(event.relatedTarget)) host.hidePopover();
89
+ }
90
+
91
+ attrs.popover ??= "auto";
92
+
93
+ listen("toggle", opened);
94
+ listen("focusout", leave);
95
+ listen("select", () => host.hidePopover());
96
+ </script>
97
+
98
+ <style>
99
+ :host {
100
+ box-sizing: border-box;
101
+ min-width: calc(var(--spacing) * 48);
102
+ margin: calc(var(--spacing) * 2) 0 0 calc(var(--spacing) * -1);
103
+ padding: calc(var(--spacing) * 1);
104
+ border: 1px solid var(--border);
105
+ border-radius: var(--radius-md);
106
+ background: var(--popover);
107
+ color: var(--popover-foreground);
108
+ box-shadow: var(--shadow-md);
109
+ inset: auto;
110
+ position-area: bottom span-right;
111
+ position-try-fallbacks: flip-block, flip-inline;
112
+ }
113
+ </style>
114
+
115
+ <div part="menu" focusgroup="menu block wrap"><slot></slot></div>
116
+ </custom-element>