@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,56 @@
1
+ <custom-element name="ui-badge">
2
+ <script>
3
+ defineProps({
4
+ variant: prop.enum(["default", "secondary", "destructive", "outline"]).default("default"),
5
+ shape: prop.enum(["default", "pill", "round"]).default("default"),
6
+ });
7
+ </script>
8
+
9
+ <style>
10
+ :host {
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ gap: calc(var(--spacing) * 1);
15
+ width: fit-content;
16
+ padding: calc(var(--spacing) * 0.5) calc(var(--spacing) * 2);
17
+ border: 1px solid transparent;
18
+ border-radius: var(--radius-md);
19
+ font-size: var(--text-xs);
20
+ font-weight: var(--font-weight-medium);
21
+ white-space: nowrap;
22
+ background: var(--primary);
23
+ color: var(--primary-foreground);
24
+ }
25
+ :host(:state(attr-variant-is-secondary)) {
26
+ background: var(--secondary);
27
+ color: var(--secondary-foreground);
28
+ }
29
+ :host(:state(attr-variant-is-destructive)) {
30
+ background: var(--destructive);
31
+ color: var(--destructive-foreground);
32
+ }
33
+ :host(:state(attr-variant-is-outline)) {
34
+ background: transparent;
35
+ border-color: var(--border);
36
+ color: var(--foreground);
37
+ }
38
+ :host(:state(attr-shape-is-pill)) {
39
+ border-radius: var(--radius-full);
40
+ }
41
+ :host(:state(attr-shape-is-round)) {
42
+ box-sizing: border-box;
43
+ min-width: calc(var(--spacing) * 5);
44
+ height: calc(var(--spacing) * 5);
45
+ padding: 0 calc(var(--spacing) * 1);
46
+ border-radius: var(--radius-full);
47
+ font-variant-numeric: tabular-nums;
48
+ }
49
+ ::slotted(svg) {
50
+ width: calc(var(--spacing) * 3);
51
+ height: calc(var(--spacing) * 3);
52
+ }
53
+ </style>
54
+
55
+ <slot></slot>
56
+ </custom-element>
@@ -0,0 +1,129 @@
1
+ <custom-element name="ui-breadcrumb">
2
+ <script>
3
+ defineProps({ label: prop.string().default("Breadcrumb") });
4
+ </script>
5
+
6
+ <style>
7
+ :host {
8
+ display: block;
9
+ }
10
+ </style>
11
+
12
+ <nav _aria-label="@label"><slot></slot></nav>
13
+ </custom-element>
14
+
15
+ <custom-element name="ui-breadcrumb-list">
16
+ <style>
17
+ :host {
18
+ display: block;
19
+ }
20
+ ol {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: center;
24
+ gap: calc(var(--spacing) * 1.5);
25
+ margin: 0;
26
+ padding: 0;
27
+ list-style: none;
28
+ font-size: var(--text-sm);
29
+ color: var(--muted-foreground);
30
+ overflow-wrap: anywhere;
31
+ }
32
+ </style>
33
+
34
+ <ol part="list"><slot></slot></ol>
35
+ </custom-element>
36
+
37
+ <custom-element name="ui-breadcrumb-item" role="listitem">
38
+ <style>
39
+ :host {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ gap: calc(var(--spacing) * 1.5);
43
+ }
44
+ </style>
45
+
46
+ <slot></slot>
47
+ </custom-element>
48
+
49
+ <custom-element name="ui-breadcrumb-link">
50
+ <script>
51
+ defineProps({ href: prop.string() });
52
+ </script>
53
+
54
+ <style>
55
+ a {
56
+ color: inherit;
57
+ text-decoration: none;
58
+ border-radius: var(--radius-xs);
59
+ outline: none;
60
+ transition: color var(--default-transition-duration);
61
+ }
62
+ a:hover {
63
+ color: var(--foreground);
64
+ }
65
+ a:focus-visible {
66
+ box-shadow: var(--focus-ring);
67
+ }
68
+ </style>
69
+
70
+ <a part="link" _href="@href"><slot></slot></a>
71
+ </custom-element>
72
+
73
+ <custom-element name="ui-breadcrumb-page">
74
+ <style>
75
+ span {
76
+ font-weight: var(--font-weight-normal);
77
+ color: var(--foreground);
78
+ }
79
+ </style>
80
+
81
+ <span part="page" role="link" aria-disabled="true" aria-current="page"><slot></slot></span>
82
+ </custom-element>
83
+
84
+ <custom-element name="ui-breadcrumb-separator" role="presentation">
85
+ <script>
86
+ internals.ariaHidden = "true";
87
+ </script>
88
+
89
+ <style>
90
+ :host {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ }
94
+ ::slotted(svg),
95
+ svg {
96
+ width: calc(var(--spacing) * 3.5);
97
+ height: calc(var(--spacing) * 3.5);
98
+ }
99
+ </style>
100
+
101
+ <slot><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></slot>
102
+ </custom-element>
103
+
104
+ <custom-element name="ui-breadcrumb-ellipsis">
105
+ <style>
106
+ :host {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ width: calc(var(--spacing) * 9);
111
+ height: calc(var(--spacing) * 9);
112
+ }
113
+ svg {
114
+ width: calc(var(--spacing) * 4);
115
+ height: calc(var(--spacing) * 4);
116
+ }
117
+ span {
118
+ position: absolute;
119
+ width: 1px;
120
+ height: 1px;
121
+ overflow: hidden;
122
+ clip-path: inset(50%);
123
+ white-space: nowrap;
124
+ }
125
+ </style>
126
+
127
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="19" cy="12" r="1.5" /></svg>
128
+ <span part="label"><slot>More</slot></span>
129
+ </custom-element>
@@ -0,0 +1,67 @@
1
+ <custom-element name="ui-bubble">
2
+ <script>
3
+ defineProps({
4
+ variant: prop.enum(["default", "secondary", "muted", "outline", "ghost"]).default("default"),
5
+ align: prop.enum(["start", "end"]).default("start"),
6
+ });
7
+ </script>
8
+
9
+ <style>
10
+ :host {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: calc(var(--spacing) * 1);
14
+ width: fit-content;
15
+ max-width: 80%;
16
+ min-width: 0;
17
+ }
18
+ :host(:state(attr-align-is-end)) {
19
+ align-self: flex-end;
20
+ }
21
+ .content {
22
+ padding: calc(var(--spacing) * 2) calc(var(--spacing) * 3);
23
+ border: 1px solid transparent;
24
+ border-radius: var(--radius-2xl);
25
+ font-size: var(--text-sm);
26
+ overflow-wrap: anywhere;
27
+ background: var(--primary);
28
+ color: var(--primary-foreground);
29
+ }
30
+ :host(:state(attr-variant-is-secondary)) .content {
31
+ background: var(--secondary);
32
+ color: var(--secondary-foreground);
33
+ }
34
+ :host(:state(attr-variant-is-muted)) .content {
35
+ background: var(--muted);
36
+ color: var(--foreground);
37
+ }
38
+ :host(:state(attr-variant-is-outline)) .content {
39
+ border-color: var(--border);
40
+ background: var(--background);
41
+ color: var(--foreground);
42
+ }
43
+ :host(:state(attr-variant-is-ghost)) {
44
+ max-width: 100%;
45
+ }
46
+ :host(:state(attr-variant-is-ghost)) .content {
47
+ padding: 0;
48
+ background: transparent;
49
+ color: var(--foreground);
50
+ }
51
+ </style>
52
+
53
+ <div class="content" part="content"><slot></slot></div>
54
+ <slot name="reactions"></slot>
55
+ </custom-element>
56
+
57
+ <custom-element name="ui-bubble-reactions" slot="reactions">
58
+ <style>
59
+ :host {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: calc(var(--spacing) * 1);
63
+ }
64
+ </style>
65
+
66
+ <slot></slot>
67
+ </custom-element>
@@ -0,0 +1,84 @@
1
+ <custom-element name="ui-button-group" role="group">
2
+ <script>
3
+ defineProps({ orientation: prop.enum(["horizontal", "vertical"]).default("horizontal") });
4
+ </script>
5
+
6
+ <style>
7
+ :host {
8
+ display: inline-flex;
9
+ align-items: stretch;
10
+ width: fit-content;
11
+ }
12
+ :host(:state(attr-orientation-is-vertical)) {
13
+ flex-direction: column;
14
+ }
15
+ :host(:not(:state(attr-orientation-is-vertical))) ::slotted(:not(:first-child)) {
16
+ --ui-radius-start: 0;
17
+ margin-inline-start: -1px;
18
+ }
19
+ :host(:not(:state(attr-orientation-is-vertical))) ::slotted(:not(:last-child)) {
20
+ --ui-radius-end: 0;
21
+ }
22
+ :host(:state(attr-orientation-is-vertical)) ::slotted(:not(:first-child)) {
23
+ --ui-radius-block-start: 0;
24
+ margin-block-start: -1px;
25
+ }
26
+ :host(:state(attr-orientation-is-vertical)) ::slotted(:not(:last-child)) {
27
+ --ui-radius-block-end: 0;
28
+ }
29
+ </style>
30
+
31
+ <slot></slot>
32
+ </custom-element>
33
+
34
+ <custom-element name="ui-button-group-text">
35
+ <style>
36
+ :host {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: calc(var(--spacing) * 2);
40
+ padding: 0 calc(var(--spacing) * 4);
41
+ border: 1px solid var(--border);
42
+ border-start-start-radius: var(--ui-radius-block-start, var(--ui-radius-start, var(--radius-md)));
43
+ border-end-start-radius: var(--ui-radius-block-end, var(--ui-radius-start, var(--radius-md)));
44
+ border-start-end-radius: var(--ui-radius-block-start, var(--ui-radius-end, var(--radius-md)));
45
+ border-end-end-radius: var(--ui-radius-block-end, var(--ui-radius-end, var(--radius-md)));
46
+ background: var(--muted);
47
+ font-size: var(--text-sm);
48
+ font-weight: var(--font-weight-medium);
49
+ white-space: nowrap;
50
+ box-shadow: var(--shadow-xs);
51
+ }
52
+ ::slotted(svg) {
53
+ width: calc(var(--spacing) * 4);
54
+ height: calc(var(--spacing) * 4);
55
+ flex-shrink: 0;
56
+ }
57
+ </style>
58
+
59
+ <slot></slot>
60
+ </custom-element>
61
+
62
+ <custom-element name="ui-button-group-separator" role="separator">
63
+ <script>
64
+ const props = defineProps({ orientation: prop.enum(["vertical", "horizontal"]).default("vertical") });
65
+
66
+ observe(() => {
67
+ internals.ariaOrientation = props.orientation;
68
+ });
69
+ </script>
70
+
71
+ <style>
72
+ :host {
73
+ position: relative;
74
+ z-index: 1;
75
+ align-self: stretch;
76
+ inline-size: 1px;
77
+ background: var(--input);
78
+ }
79
+ :host(:state(attr-orientation-is-horizontal)) {
80
+ inline-size: auto;
81
+ block-size: 1px;
82
+ }
83
+ </style>
84
+ </custom-element>
@@ -0,0 +1,188 @@
1
+ <custom-element name="ui-button" form-associated delegates-focus>
2
+ <script>
3
+ const props = defineProps({
4
+ variant: prop.enum(["default", "destructive", "outline", "secondary", "ghost", "link"]).default("default"),
5
+ size: prop.enum(["default", "xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg"]).default("default"),
6
+ type: prop.enum(["button", "submit", "reset"]).default("button"),
7
+ disabled: prop.boolean().reflect(),
8
+ label: prop.string().attribute("aria-label"),
9
+ description: prop.string().attribute("aria-description"),
10
+ popovertarget: prop.string(),
11
+ popovertargetaction: prop.enum(["toggle", "show", "hide"]),
12
+ commandfor: prop.string(),
13
+ command: prop.string(),
14
+ });
15
+
16
+ const NATIVE_COMMANDS = "commandForElement" in HTMLButtonElement.prototype;
17
+ const COMMAND_METHODS = {
18
+ "show-modal": "showModal",
19
+ close: "close",
20
+ "request-close": "requestClose",
21
+ "toggle-popover": "togglePopover",
22
+ "show-popover": "showPopover",
23
+ "hide-popover": "hidePopover",
24
+ };
25
+ const DIALOG_COMMANDS = new Set(["show-modal", "close", "request-close"]);
26
+ const FORM_ACTIONS = { submit: "requestSubmit", reset: "reset" };
27
+
28
+ function target(id) {
29
+ return id ? host.getRootNode().getElementById(id) : null;
30
+ }
31
+
32
+ function nativeCommand(element) {
33
+ const command = props.command ?? "";
34
+ const customTarget = element && !(element instanceof HTMLDialogElement);
35
+ return customTarget && DIALOG_COMMANDS.has(command) ? `--${command}` : command;
36
+ }
37
+
38
+ function emulateCommand() {
39
+ const element = target(props.commandfor);
40
+ if (NATIVE_COMMANDS || !element || !props.command) return;
41
+ const event = Object.assign(new Event("command", { cancelable: true }), { command: props.command, source: host });
42
+ if (element.dispatchEvent(event)) element[COMMAND_METHODS[props.command]]?.();
43
+ }
44
+
45
+ function activate() {
46
+ if (props.disabled) return;
47
+ emulateCommand();
48
+ const action = FORM_ACTIONS[props.type];
49
+ if (internals.form && action) HTMLFormElement.prototype[action].call(internals.form);
50
+ }
51
+
52
+ observe(() => {
53
+ const button = refs.button;
54
+ if (!button) return;
55
+ button.popoverTargetElement = target(props.popovertarget);
56
+ if (props.popovertargetaction) button.popoverTargetAction = props.popovertargetaction;
57
+ if (!NATIVE_COMMANDS) return;
58
+ const element = target(props.commandfor);
59
+ button.commandForElement = element;
60
+ button.command = nativeCommand(element);
61
+ });
62
+ </script>
63
+
64
+ <style>
65
+ :host {
66
+ display: inline-flex;
67
+ vertical-align: middle;
68
+ }
69
+ :host([hidden]) {
70
+ display: none;
71
+ }
72
+ button {
73
+ all: unset;
74
+ box-sizing: border-box;
75
+ display: inline-flex;
76
+ flex: 1;
77
+ align-items: center;
78
+ justify-content: center;
79
+ gap: calc(var(--spacing) * 2);
80
+ height: calc(var(--spacing) * 9);
81
+ padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
82
+ border: 1px solid transparent;
83
+ border-radius: var(--radius-md);
84
+ border-start-start-radius: var(--ui-radius-block-start, var(--ui-radius-start, var(--radius-md)));
85
+ border-end-start-radius: var(--ui-radius-block-end, var(--ui-radius-start, var(--radius-md)));
86
+ border-start-end-radius: var(--ui-radius-block-start, var(--ui-radius-end, var(--radius-md)));
87
+ border-end-end-radius: var(--ui-radius-block-end, var(--ui-radius-end, var(--radius-md)));
88
+ font: inherit;
89
+ font-size: var(--text-sm);
90
+ line-height: calc(var(--spacing) * 5);
91
+ font-weight: var(--font-weight-medium);
92
+ white-space: nowrap;
93
+ cursor: pointer;
94
+ transition: background-color var(--default-transition-duration), color var(--default-transition-duration), box-shadow var(--default-transition-duration), opacity var(--default-transition-duration);
95
+ background: var(--primary);
96
+ color: var(--primary-foreground);
97
+ }
98
+ /* Mixing toward the text colour darkens a light-mode fill and lightens a dark-mode one, so
99
+ hover never lowers the text contrast. */
100
+ button:hover {
101
+ background: color-mix(in oklab, var(--primary) 88%, var(--foreground));
102
+ }
103
+ button:focus-visible {
104
+ border-color: var(--ring);
105
+ box-shadow: var(--focus-ring);
106
+ }
107
+ button:disabled {
108
+ pointer-events: none;
109
+ opacity: 0.5;
110
+ }
111
+ ::slotted(svg) {
112
+ width: calc(var(--spacing) * 4);
113
+ height: calc(var(--spacing) * 4);
114
+ flex-shrink: 0;
115
+ pointer-events: none;
116
+ }
117
+ :host(:state(attr-variant-is-destructive)) button {
118
+ background: var(--destructive);
119
+ color: var(--destructive-foreground);
120
+ }
121
+ :host(:state(attr-variant-is-destructive)) button:hover {
122
+ background: color-mix(in oklab, var(--destructive) 88%, var(--foreground));
123
+ }
124
+ :host(:state(attr-variant-is-outline)) button {
125
+ border-color: var(--border);
126
+ background: var(--background);
127
+ color: var(--foreground);
128
+ box-shadow: var(--shadow-xs);
129
+ }
130
+ :host(:state(attr-variant-is-outline)) button:hover,
131
+ :host(:state(attr-variant-is-ghost)) button:hover {
132
+ background: var(--accent);
133
+ color: var(--accent-foreground);
134
+ }
135
+ :host(:state(attr-variant-is-secondary)) button {
136
+ background: var(--secondary);
137
+ color: var(--secondary-foreground);
138
+ }
139
+ :host(:state(attr-variant-is-secondary)) button:hover {
140
+ background: color-mix(in oklab, var(--secondary) 80%, transparent);
141
+ }
142
+ :host(:state(attr-variant-is-ghost)) button {
143
+ background: transparent;
144
+ color: var(--foreground);
145
+ }
146
+ :host(:state(attr-variant-is-link)) button {
147
+ background: transparent;
148
+ color: var(--primary);
149
+ text-underline-offset: 4px;
150
+ }
151
+ :host(:state(attr-variant-is-link)) button:hover {
152
+ text-decoration: underline;
153
+ }
154
+ :host(:state(attr-size-is-xs)) button {
155
+ height: calc(var(--spacing) * 6);
156
+ gap: calc(var(--spacing) * 1);
157
+ padding: 0 calc(var(--spacing) * 2);
158
+ font-size: var(--text-xs);
159
+ }
160
+ :host(:state(attr-size-is-sm)) button {
161
+ height: calc(var(--spacing) * 8);
162
+ gap: calc(var(--spacing) * 1.5);
163
+ padding: 0 calc(var(--spacing) * 3);
164
+ }
165
+ :host(:state(attr-size-is-lg)) button {
166
+ height: calc(var(--spacing) * 10);
167
+ padding: 0 calc(var(--spacing) * 6);
168
+ }
169
+ :host(:is(:state(attr-size-is-icon), :state(attr-size-is-icon-xs), :state(attr-size-is-icon-sm), :state(attr-size-is-icon-lg))) button {
170
+ padding: 0;
171
+ width: calc(var(--spacing) * 9);
172
+ }
173
+ :host(:state(attr-size-is-icon-xs)) button {
174
+ width: calc(var(--spacing) * 6);
175
+ height: calc(var(--spacing) * 6);
176
+ }
177
+ :host(:state(attr-size-is-icon-sm)) button {
178
+ width: calc(var(--spacing) * 8);
179
+ height: calc(var(--spacing) * 8);
180
+ }
181
+ :host(:state(attr-size-is-icon-lg)) button {
182
+ width: calc(var(--spacing) * 10);
183
+ height: calc(var(--spacing) * 10);
184
+ }
185
+ </style>
186
+
187
+ <button type="button" ref="button" part="button" _aria-label="@label" _aria-description="@description" _disabled="@disabled" onclick="activate()"><slot></slot></button>
188
+ </custom-element>
@@ -0,0 +1,170 @@
1
+ <import-element from="./button.html"></import-element>
2
+
3
+ <custom-element name="ui-calendar" form-associated>
4
+ <script>
5
+ const props = defineProps({
6
+ value: prop.string().default(""),
7
+ month: prop.string(),
8
+ min: prop.string(),
9
+ max: prop.string(),
10
+ locale: prop.string(),
11
+ });
12
+
13
+ const iso = (date) => date.toISOString().slice(0, 10);
14
+ const parse = (text) => new Date(`${text}T00:00:00Z`);
15
+ const now = new Date();
16
+ const today = iso(new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())));
17
+ const KEYS = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
18
+
19
+ const view = useState({
20
+ shown: (props.month || props.value || today).slice(0, 7),
21
+ focused: props.value || today,
22
+ get title() {
23
+ return new Intl.DateTimeFormat(props.locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(parse(`${this.shown}-01`));
24
+ },
25
+ get weekdays() {
26
+ const sunday = Date.UTC(2023, 0, 1);
27
+ return Array.from({ length: 7 }, (_, day) =>
28
+ new Intl.DateTimeFormat(props.locale, { weekday: "short", timeZone: "UTC" }).format(new Date(sunday + day * 864e5)),
29
+ );
30
+ },
31
+ get weeks() {
32
+ const first = parse(`${this.shown}-01`);
33
+ const start = new Date(first.getTime() - first.getUTCDay() * 864e5);
34
+ const days = Array.from({ length: 42 }, (_, n) => {
35
+ const date = iso(new Date(start.getTime() + n * 864e5));
36
+ return {
37
+ date,
38
+ day: Number(date.slice(8)),
39
+ outside: !date.startsWith(this.shown),
40
+ selected: date === props.value,
41
+ today: date === today,
42
+ disabled: (props.min && date < props.min) || (props.max && date > props.max) || false,
43
+ tabindex: date === this.focused ? 0 : -1,
44
+ };
45
+ });
46
+ return Array.from({ length: 6 }, (_, week) => ({ week, days: days.slice(week * 7, week * 7 + 7) }));
47
+ },
48
+ });
49
+
50
+ function shift(months) {
51
+ const date = parse(`${view.shown}-01`);
52
+ date.setUTCMonth(date.getUTCMonth() + months);
53
+ view.shown = iso(date).slice(0, 7);
54
+ }
55
+
56
+ function previous() {
57
+ shift(-1);
58
+ }
59
+
60
+ function next() {
61
+ shift(1);
62
+ }
63
+
64
+ function pick(day) {
65
+ props.value = day.date;
66
+ view.focused = day.date;
67
+ view.shown = day.date.slice(0, 7);
68
+ dispatch("change");
69
+ }
70
+
71
+ async function keys(day, event) {
72
+ const step = KEYS[event.key];
73
+ if (!step) return;
74
+ event.preventDefault();
75
+ view.focused = iso(new Date(parse(day.date).getTime() + step * 864e5));
76
+ view.shown = view.focused.slice(0, 7);
77
+ await tick();
78
+ refs.grid.querySelector(`[data-date="${view.focused}"]`)?.focus();
79
+ }
80
+ </script>
81
+
82
+ <style>
83
+ :host {
84
+ display: inline-block;
85
+ padding: calc(var(--spacing) * 3);
86
+ border: 1px solid var(--border);
87
+ border-radius: var(--radius-lg);
88
+ font-size: var(--text-sm);
89
+ }
90
+ header {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ margin-bottom: calc(var(--spacing) * 3);
95
+ }
96
+ .title {
97
+ font-weight: var(--font-weight-medium);
98
+ }
99
+ table {
100
+ border-collapse: collapse;
101
+ }
102
+ th {
103
+ width: calc(var(--spacing) * 9);
104
+ padding-bottom: calc(var(--spacing) * 1);
105
+ font-size: var(--text-xs);
106
+ font-weight: var(--font-weight-normal);
107
+ color: var(--muted-foreground);
108
+ }
109
+ td {
110
+ padding: 0;
111
+ }
112
+ button {
113
+ all: unset;
114
+ box-sizing: border-box;
115
+ display: grid;
116
+ place-content: center;
117
+ width: calc(var(--spacing) * 9);
118
+ height: calc(var(--spacing) * 9);
119
+ border-radius: var(--radius-md);
120
+ cursor: pointer;
121
+ }
122
+ button:hover {
123
+ background: var(--accent);
124
+ }
125
+ button:focus-visible {
126
+ box-shadow: var(--focus-ring);
127
+ }
128
+ button[data-outside] {
129
+ color: var(--muted-foreground);
130
+ }
131
+ button[data-today] {
132
+ background: var(--accent);
133
+ }
134
+ button[aria-selected="true"] {
135
+ background: var(--primary);
136
+ color: var(--primary-foreground);
137
+ }
138
+ button:disabled {
139
+ pointer-events: none;
140
+ opacity: 0.4;
141
+ }
142
+ </style>
143
+
144
+ <header part="header">
145
+ <ui-button variant="outline" size="icon-sm" aria-label="Previous month" onclick="previous()">‹</ui-button>
146
+ <span class="title" aria-live="polite"><slot name="#view.title"></slot></span>
147
+ <ui-button variant="outline" size="icon-sm" aria-label="Next month" onclick="next()">›</ui-button>
148
+ </header>
149
+ <table role="grid" ref="grid" part="grid">
150
+ <thead>
151
+ <tr><th repeat="#view.weekdays" as="weekday" scope="col"><slot name="#weekday"></slot></th></tr>
152
+ </thead>
153
+ <tbody>
154
+ <tr repeat="#view.weeks" as="row" key="week">
155
+ <td repeat="#row.days" as="day" key="date">
156
+ <button type="button"
157
+ part="day"
158
+ _data-date="#day.date"
159
+ _data-outside="#day.outside"
160
+ _data-today="#day.today"
161
+ _aria-selected="#day.selected"
162
+ _disabled="#day.disabled"
163
+ _tabindex="#day.tabindex"
164
+ onclick="pick(day)"
165
+ onkeydown="keys(day, event)"><slot name="#day.day"></slot></button>
166
+ </td>
167
+ </tr>
168
+ </tbody>
169
+ </table>
170
+ </custom-element>