@bunnix/components 0.9.0 → 0.9.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 (42) hide show
  1. package/@types/index.d.ts +134 -30
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/src/components/AccordionGroup.mjs +2 -1
  5. package/src/components/Badge.mjs +18 -4
  6. package/src/components/Button.mjs +7 -9
  7. package/src/components/Card.mjs +37 -0
  8. package/src/components/Checkbox.mjs +5 -7
  9. package/src/components/CodeBlock.mjs +31 -0
  10. package/src/components/ComboBox.mjs +22 -14
  11. package/src/components/Container.mjs +8 -10
  12. package/src/components/DatePicker.mjs +13 -15
  13. package/src/components/Dialog.mjs +35 -4
  14. package/src/components/DropdownMenu.mjs +16 -14
  15. package/src/components/HStack.mjs +11 -3
  16. package/src/components/Icon.mjs +9 -5
  17. package/src/components/InputField.mjs +12 -4
  18. package/src/components/NavigationBar.mjs +55 -25
  19. package/src/components/PageHeader.mjs +11 -8
  20. package/src/components/PageSection.mjs +20 -10
  21. package/src/components/PopoverMenu.mjs +94 -50
  22. package/src/components/RadioCheckbox.mjs +5 -7
  23. package/src/components/SearchBox.mjs +12 -21
  24. package/src/components/Sidebar.mjs +142 -67
  25. package/src/components/Table.mjs +145 -96
  26. package/src/components/Text.mjs +52 -21
  27. package/src/components/TimePicker.mjs +13 -15
  28. package/src/components/ToastNotification.mjs +16 -13
  29. package/src/components/ToggleSwitch.mjs +5 -7
  30. package/src/components/VStack.mjs +7 -6
  31. package/src/index.mjs +2 -0
  32. package/src/styles/buttons.css +8 -0
  33. package/src/styles/colors.css +8 -0
  34. package/src/styles/controls.css +61 -0
  35. package/src/styles/layout.css +64 -5
  36. package/src/styles/media.css +11 -0
  37. package/src/styles/menu.css +39 -21
  38. package/src/styles/table.css +2 -2
  39. package/src/styles/typography.css +25 -0
  40. package/src/styles/variables.css +3 -0
  41. package/src/utils/iconUtils.mjs +10 -0
  42. package/src/utils/sizeUtils.mjs +87 -0
@@ -1,38 +1,69 @@
1
1
  import Bunnix from "@bunnix/core";
2
+ import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
2
3
  const { span, p, h1, h2, h3, h4 } = Bunnix;
3
4
 
4
- export default function Text({
5
- type = "text",
6
- color = "primary",
7
- design = "regular",
8
- class: className = "",
9
- ...rest
10
- } = {}, children) {
5
+ export default function Text(props = {}, children) {
6
+ const isState = (value) => value && typeof value.map === "function";
7
+
8
+ // Normalize arguments: Text("Value"), Text(State), or Text({ props }, value)
9
+ if (
10
+ props === null ||
11
+ props === undefined ||
12
+ Array.isArray(props) ||
13
+ typeof props === "string" ||
14
+ isState(props)
15
+ ) {
16
+ children = props;
17
+ props = {};
18
+ }
19
+
20
+ const {
21
+ type = "text",
22
+ color = "default",
23
+ design = "regular",
24
+ weight,
25
+ size,
26
+ wrap,
27
+ class: className = "",
28
+ ...rest
29
+ } = props;
30
+
31
+ const normalizeSize = (value) =>
32
+ clampSize(value, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
33
+ const normalizedSize = normalizeSize(size);
34
+ const sizeToken = toSizeToken(normalizedSize);
35
+ const sizeClass = sizeToken ? `text-${sizeToken}` : "";
11
36
  const tagMap = {
12
37
  text: span,
13
38
  paragraph: p,
14
39
  heading1: h1,
15
40
  heading2: h2,
16
41
  heading3: h3,
17
- heading4: h4
42
+ heading4: h4,
18
43
  };
19
44
 
20
45
  const tag = tagMap[type] || span;
21
-
22
- // Color mapping: primary -> text-primary, secondary -> text-secondary, etc.
23
46
  const colorClass = color ? `text-${color}` : "";
24
-
25
- // Design mapping: mono -> text-mono, regular -> ""
26
47
  const designClass = design === "mono" ? "text-mono" : "";
27
-
28
- const isState = className && typeof className.map === "function";
48
+ const weightClass =
49
+ weight === "bold" ? "bold" : weight === "semibold" ? "semibold" : "";
29
50
 
30
- const combinedClass = isState
31
- ? className.map((value) => `${colorClass} ${designClass} ${value}`.trim())
32
- : `${colorClass} ${designClass} ${className}`.trim();
51
+ const isClassState = isState(className);
33
52
 
34
- return tag({
35
- class: combinedClass,
36
- ...rest
37
- }, children);
53
+ const wrapClass =
54
+ wrap === "nowrap" ? "whitespace-nowrap" : wrap === "wrap" ? "" : "";
55
+
56
+ const combinedClass = isClassState
57
+ ? className.map((value) =>
58
+ `${colorClass} ${designClass} ${weightClass} ${sizeClass} ${wrapClass} ${value}`.trim(),
59
+ )
60
+ : `${colorClass} ${designClass} ${weightClass} ${sizeClass} ${wrapClass} ${className}`.trim();
61
+
62
+ return tag(
63
+ {
64
+ class: combinedClass,
65
+ ...rest,
66
+ },
67
+ children,
68
+ );
38
69
  }
@@ -1,4 +1,5 @@
1
1
  import Bunnix, { useRef, useState, useMemo } from "@bunnix/core";
2
+ import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
2
3
  import Icon from "./Icon.mjs";
3
4
  const { div, button, span, hr, input } = Bunnix;
4
5
 
@@ -8,7 +9,7 @@ export default function TimePicker({
8
9
  id,
9
10
  placeholder,
10
11
  variant = "regular",
11
- size = "md",
12
+ size = "regular",
12
13
  class: className = ""
13
14
  } = {}) {
14
15
  const popoverRef = useRef(null);
@@ -96,25 +97,22 @@ export default function TimePicker({
96
97
 
97
98
  const hasValue = isModified.map(m => !!m);
98
99
 
99
- const normalizeSize = (value) => {
100
- if (!value || value === "default" || value === "regular" || value === "md") return "md";
101
- if (value === "sm") return "md";
102
- if (value === "lg" || value === "xl") return value;
103
- return value;
104
- };
100
+ // TimePicker does not support small size (clamps to regular)
101
+ const normalizeSize = (value) => clampSize(value, ["xsmall", "regular", "large", "xlarge"], "regular");
105
102
  const normalizedSize = normalizeSize(size);
103
+ const sizeToken = toSizeToken(normalizedSize);
106
104
  const variantClass = variant === "rounded" ? "rounded-full" : "";
107
- const triggerSizeClass = normalizedSize === "xl"
105
+ const triggerSizeClass = sizeToken === "xl"
108
106
  ? "dropdown-xl"
109
- : normalizedSize === "lg"
107
+ : sizeToken === "lg"
110
108
  ? "dropdown-lg"
111
109
  : "";
112
- const iconSizeValue = normalizedSize === "sm"
113
- ? "sm"
114
- : normalizedSize === "lg"
115
- ? "lg"
116
- : normalizedSize === "xl"
117
- ? "xl"
110
+ const iconSizeValue = normalizedSize === "small"
111
+ ? "small"
112
+ : normalizedSize === "large"
113
+ ? "large"
114
+ : normalizedSize === "xlarge"
115
+ ? "xlarge"
118
116
  : undefined;
119
117
 
120
118
  return div({ class: `timepicker-wrapper ${className}`.trim() }, [
@@ -1,5 +1,6 @@
1
1
  import Bunnix, { useEffect, useRef, useState } from "@bunnix/core";
2
- import Icon from "./Icon.mjs";
2
+ import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
3
+ import { resolveIconClass } from "../utils/iconUtils.mjs";
3
4
  const { div, h4 } = Bunnix;
4
5
 
5
6
  const defaultToast = {
@@ -11,13 +12,9 @@ const defaultToast = {
11
12
 
12
13
  export const toastState = useState(defaultToast);
13
14
 
14
- export const showToast = ({ message, duration = 3, anchor = "topRight", size = "md", icon } = {}) => {
15
- const normalizeSize = (value) => {
16
- if (!value || value === "default" || value === "regular" || value === "md") return "md";
17
- if (value === "sm") return "md";
18
- if (value === "lg" || value === "xl") return value;
19
- return value;
20
- };
15
+ export const showToast = ({ message, duration = 3, anchor = "topRight", size = "regular", icon } = {}) => {
16
+ // ToastNotification does not support small size (clamps to regular)
17
+ const normalizeSize = (value) => clampSize(value, ["xsmall", "regular", "large", "xlarge"], "regular");
21
18
  toastState.set({
22
19
  open: true,
23
20
  message: message ?? "",
@@ -77,13 +74,16 @@ export default function ToastNotification() {
77
74
  const motionClass = value.anchor === "topLeft" || value.anchor === "bottomLeft"
78
75
  ? "slide-in-left"
79
76
  : "slide-in-right";
80
- const sizeClass = value.size === "lg" ? "p-lg" : value.size === "xl" ? "p-xl" : "p-base";
77
+ const sizeToken = toSizeToken(value.size);
78
+ const sizeClass = sizeToken === "xl" ? "p-xl" : sizeToken === "lg" ? "p-lg" : sizeToken === "md" ? "p-base" : "p-sm";
81
79
  return `box-control card shadow bg-base ${sizeClass} w-300 overflow-visible ${motionClass}`.trim();
82
80
  });
83
81
  const textSizeClass = toastState.map((value) => {
84
- if (value.size === "lg") return "text-lg";
85
- if (value.size === "xl") return "text-xl";
86
- return "text-base";
82
+ const sizeToken = toSizeToken(value.size);
83
+ if (sizeToken === "xl") return "text-xl";
84
+ if (sizeToken === "lg") return "text-lg";
85
+ if (sizeToken === "md") return "text-base";
86
+ return "text-sm";
87
87
  });
88
88
 
89
89
  return div({
@@ -95,7 +95,10 @@ export default function ToastNotification() {
95
95
  div({ class: "row-container items-center gap-sm no-margin" }, [
96
96
  div({
97
97
  class: toastState.map((value) =>
98
- value.icon ? `icon ${value.icon} icon-base` : "hidden"
98
+ (() => {
99
+ const resolvedIcon = resolveIconClass(value.icon);
100
+ return resolvedIcon ? `icon ${resolvedIcon} icon-base` : "hidden";
101
+ })()
99
102
  )
100
103
  }),
101
104
  h4({ class: textSizeClass.map(cls => `no-margin ${cls}`.trim()) }, toastState.map((value) => value.message))
@@ -1,4 +1,5 @@
1
1
  import Bunnix from "@bunnix/core";
2
+ import { clampSize, toSizeToken } from "../utils/sizeUtils.mjs";
2
3
  const { label, input, span } = Bunnix;
3
4
 
4
5
  export default function ToggleSwitch({
@@ -8,14 +9,11 @@ export default function ToggleSwitch({
8
9
  class: className = "",
9
10
  ...inputProps
10
11
  }) {
11
- const normalizeSize = (value) => {
12
- if (!value || value === "default" || value === "regular" || value === "md") return "md";
13
- if (value === "sm") return "sm";
14
- if (value === "lg" || value === "xl") return value;
15
- return value;
16
- };
12
+ // ToggleSwitch supports all sizes
13
+ const normalizeSize = (value) => clampSize(value, ["xsmall", "small", "regular", "large", "xlarge"], "regular");
17
14
  const normalizedSize = normalizeSize(size);
18
- const sizeClass = normalizedSize === "lg" ? "switch-lg" : normalizedSize === "xl" ? "switch-xl" : "";
15
+ const sizeToken = toSizeToken(normalizedSize);
16
+ const sizeClass = sizeToken === "xl" ? "switch-xl" : sizeToken === "lg" ? "switch-lg" : "";
19
17
  const change = onChange ?? inputProps.change;
20
18
 
21
19
  return label({ class: `switch-control ${sizeClass} ${className}`.trim() }, [
@@ -1,7 +1,7 @@
1
1
  import Bunnix from "@bunnix/core";
2
2
  const { div } = Bunnix;
3
3
 
4
- export default function VStack(props = {}, children) {
4
+ export default function VStack(props = {}, ...children) {
5
5
  if (props === null || props === undefined || Array.isArray(props) || typeof props !== "object") {
6
6
  children = props;
7
7
  props = {};
@@ -13,14 +13,15 @@ export default function VStack(props = {}, children) {
13
13
  class: className = "",
14
14
  ...rest
15
15
  } = props;
16
- // For VStack, alignment controls the vertical distribution (main axis)
16
+ // For VStack, alignment controls horizontal alignment (cross axis)
17
17
  const alignmentMap = {
18
- leading: "justify-start",
19
- middle: "justify-center",
20
- trailing: "justify-end"
18
+ leading: "items-start",
19
+ middle: "items-center",
20
+ trailing: "items-end"
21
21
  };
22
22
 
23
23
  const gapMap = {
24
+ xsmall: "gap-xs",
24
25
  small: "gap-sm",
25
26
  regular: "gap-md",
26
27
  large: "gap-lg"
@@ -31,5 +32,5 @@ export default function VStack(props = {}, children) {
31
32
  return div({
32
33
  class: combinedClass,
33
34
  ...rest
34
- }, children);
35
+ }, ...children);
35
36
  }
package/src/index.mjs CHANGED
@@ -1,7 +1,9 @@
1
1
  export { default as AccordionGroup } from "./components/AccordionGroup.mjs";
2
2
  export { default as Badge } from "./components/Badge.mjs";
3
3
  export { default as Button } from "./components/Button.mjs";
4
+ export { default as Card } from "./components/Card.mjs";
4
5
  export { default as Checkbox } from "./components/Checkbox.mjs";
6
+ export { default as CodeBlock } from "./components/CodeBlock.mjs";
5
7
  export { default as ComboBox } from "./components/ComboBox.mjs";
6
8
  export { default as Container } from "./components/Container.mjs";
7
9
  export { default as DatePicker } from "./components/DatePicker.mjs";
@@ -42,6 +42,8 @@ button:focus-visible,
42
42
  .btn {
43
43
  background-color: var(--accent-color);
44
44
  color: white;
45
+ --text-color-default: inherit;
46
+ --icon-color-default: currentColor;
45
47
  }
46
48
 
47
49
  .btn:visited {
@@ -61,6 +63,8 @@ button:focus-visible,
61
63
  .btn-flat {
62
64
  background-color: transparent;
63
65
  color: var(--color-primary);
66
+ --text-color-default: inherit;
67
+ --icon-color-default: currentColor;
64
68
  }
65
69
 
66
70
  .btn-flat:visited {
@@ -81,6 +85,8 @@ button:focus-visible,
81
85
  background-color: transparent;
82
86
  border: 1px solid var(--border-color);
83
87
  color: var(--color-primary);
88
+ --text-color-default: inherit;
89
+ --icon-color-default: currentColor;
84
90
  }
85
91
 
86
92
  .btn-outline:visited {
@@ -97,6 +103,8 @@ button:focus-visible,
97
103
  .btn-destructive {
98
104
  background-color: var(--color-destructive-dimmed);
99
105
  color: white;
106
+ --text-color-default: inherit;
107
+ --icon-color-default: currentColor;
100
108
  }
101
109
 
102
110
  .btn-destructive:visited {
@@ -13,6 +13,10 @@ body {
13
13
  }
14
14
 
15
15
  /* Text Color Utilities */
16
+ .text-default {
17
+ color: var(--text-color-default, var(--color-primary));
18
+ }
19
+
16
20
  .text-primary {
17
21
  color: var(--color-primary);
18
22
  }
@@ -58,6 +62,10 @@ body {
58
62
  background-color: var(--background-color) !important;
59
63
  }
60
64
 
65
+ .icon-default {
66
+ background-color: var(--icon-color-default, var(--color-primary)) !important;
67
+ }
68
+
61
69
  .icon-base {
62
70
  background-color: var(--color-primary) !important;
63
71
  }
@@ -51,6 +51,24 @@ select {
51
51
  background-size: 1rem;
52
52
  }
53
53
 
54
+ .combobox {
55
+ position: relative;
56
+ width: 100%;
57
+ }
58
+
59
+ .combobox-select {
60
+ background-image: none !important;
61
+ padding-right: calc(var(--base-padding) * 3) !important;
62
+ }
63
+
64
+ .combobox-chevron {
65
+ position: absolute;
66
+ right: var(--base-padding);
67
+ top: 50%;
68
+ transform: translateY(-50%);
69
+ pointer-events: none;
70
+ }
71
+
54
72
  .input-search {
55
73
  position: relative;
56
74
  width: 100%;
@@ -299,6 +317,49 @@ kbd {
299
317
  font-size: var(--font-size-xs) !important;
300
318
  }
301
319
 
320
+ /* Code Block */
321
+ .codeblock {
322
+ width: 100%;
323
+ }
324
+
325
+ .codeblock-pre {
326
+ margin: 0;
327
+ padding: var(--base-padding);
328
+ border-radius: var(--min-control-radius);
329
+ border: 1px solid var(--border-color);
330
+ background-color: var(--alternate-background-color);
331
+ }
332
+
333
+ .codeblock-pre code {
334
+ display: block;
335
+ font-family: var(--font-mono);
336
+ font-size: var(--font-size);
337
+ line-height: 1.6;
338
+ white-space: pre;
339
+ color: var(--color-primary);
340
+ text-shadow: none;
341
+ }
342
+
343
+ .codeblock-pre.codeblock-wrap code {
344
+ white-space: pre-wrap;
345
+ }
346
+
347
+ .codeblock-pre code.codeblock-code,
348
+ .codeblock-pre code.codeblock-code[class*="language-"] {
349
+ color: var(--color-primary);
350
+ text-shadow: none;
351
+ }
352
+
353
+ .codeblock-pre code.codeblock-code span {
354
+ text-shadow: none;
355
+ }
356
+
357
+ .codeblock-pre code.codeblock-code .token,
358
+ .codeblock-pre code.codeblock-code[class*="language-"] .token {
359
+ background: transparent;
360
+ box-shadow: none;
361
+ }
362
+
302
363
  /* Control Sizes */
303
364
  .input-lg {
304
365
  padding: calc(var(--base-padding) * 0.75) calc(var(--base-padding) * 1.25) !important;
@@ -116,7 +116,7 @@ nav,
116
116
  align-items: center;
117
117
  border: 1px solid var(--border-color);
118
118
  border-radius: var(--min-control-radius);
119
- padding: calc(var(--base-padding) * 0.5);
119
+ padding: var(--base-padding);
120
120
  }
121
121
 
122
122
  .gap-xs {
@@ -153,17 +153,14 @@ nav,
153
153
 
154
154
  .justify-start {
155
155
  justify-content: flex-start !important;
156
- text-align: left;
157
156
  }
158
157
 
159
158
  .justify-center {
160
159
  justify-content: center !important;
161
- text-align: center;
162
160
  }
163
161
 
164
162
  .justify-end {
165
163
  justify-content: flex-end !important;
166
- text-align: right;
167
164
  }
168
165
 
169
166
  .shrink-0 {
@@ -191,6 +188,10 @@ nav,
191
188
  }
192
189
 
193
190
  /* New Utilities for Layout Page */
191
+ .flex-1 {
192
+ flex: 1;
193
+ }
194
+
194
195
  .w-full {
195
196
  width: 100%;
196
197
  }
@@ -207,6 +208,10 @@ nav,
207
208
  min-width: 200px;
208
209
  }
209
210
 
211
+ .w-400 {
212
+ min-width: 400px;
213
+ }
214
+
210
215
  .max-w-100 {
211
216
  max-width: 100px;
212
217
  }
@@ -239,6 +244,14 @@ nav,
239
244
  height: 40px;
240
245
  }
241
246
 
247
+ .w-24 {
248
+ width: 24px;
249
+ }
250
+
251
+ .h-24 {
252
+ height: 24px;
253
+ }
254
+
242
255
  .h-200 {
243
256
  height: 200px;
244
257
  }
@@ -251,6 +264,10 @@ nav,
251
264
  padding: calc(var(--base-padding) * 0.5);
252
265
  }
253
266
 
267
+ .p-md {
268
+ padding: var(--base-padding);
269
+ }
270
+
254
271
  .pt-sm {
255
272
  padding-top: calc(var(--base-padding) * 0.5) !important;
256
273
  }
@@ -285,6 +302,11 @@ nav,
285
302
  padding-bottom: calc(var(--base-gap) * 0.5);
286
303
  }
287
304
 
305
+ .py-sm {
306
+ padding-top: calc(var(--base-gap) * 0.8);
307
+ padding-bottom: calc(var(--base-gap) * 0.8);
308
+ }
309
+
288
310
  .py-md {
289
311
  padding-top: calc(var(--base-gap) * 2);
290
312
  padding-bottom: calc(var(--base-gap) * 2);
@@ -339,6 +361,10 @@ nav,
339
361
  border-radius: 1rem !important;
340
362
  }
341
363
 
364
+ .rounded-circle {
365
+ border-radius: 9999px;
366
+ }
367
+
342
368
  .border-dashed {
343
369
  border: 1px dashed var(--border-color);
344
370
  }
@@ -355,6 +381,22 @@ nav,
355
381
  overflow: auto;
356
382
  }
357
383
 
384
+ .overflow-y-auto {
385
+ overflow-y: auto;
386
+ }
387
+
388
+ .overflow-x-auto {
389
+ overflow-x: auto;
390
+ }
391
+
392
+ .overflow-y-hidden {
393
+ overflow-y: hidden;
394
+ }
395
+
396
+ .overflow-x-hidden {
397
+ overflow-x: hidden;
398
+ }
399
+
358
400
  .select-none {
359
401
  user-select: none;
360
402
  }
@@ -365,6 +407,12 @@ nav,
365
407
  z-index: 10;
366
408
  }
367
409
 
410
+ .sticky-bottom {
411
+ position: sticky;
412
+ bottom: var(--sticky-offset, 0);
413
+ z-index: 10;
414
+ }
415
+
368
416
  .z-99 {
369
417
  z-index: 99 !important;
370
418
  }
@@ -423,7 +471,7 @@ nav,
423
471
  }
424
472
 
425
473
  .dialog-base {
426
- margin: 0;
474
+ margin: auto;
427
475
  padding: 0;
428
476
  border: none;
429
477
  outline: none;
@@ -439,6 +487,17 @@ nav,
439
487
  background-color: rgba(0, 0, 0, 0.4);
440
488
  }
441
489
 
490
+ @media (prefers-color-scheme: dark) {
491
+ .dialog-backdrop::backdrop {
492
+ background-color: rgba(var(--background-color-rgb), 0.4);
493
+ }
494
+ }
495
+
496
+ .dialog-panel {
497
+ width: auto;
498
+ max-width: calc(100vw - (var(--base-padding) * 2));
499
+ }
500
+
442
501
  .overflow-visible {
443
502
  overflow: visible !important;
444
503
  }
@@ -153,3 +153,14 @@
153
153
  border: 1px solid rgba(0,0,0,0.1);
154
154
  flex-shrink: 0;
155
155
  }
156
+
157
+ /* Force raster images to black in light mode and white in dark mode */
158
+ .high-contrast {
159
+ filter: brightness(0);
160
+ }
161
+
162
+ @media (prefers-color-scheme: dark) {
163
+ .high-contrast {
164
+ filter: brightness(0) invert(1);
165
+ }
166
+ }
@@ -10,8 +10,14 @@
10
10
  /* Modern CSS Anchor Positioning (Chrome/Edge) */
11
11
  position-anchor: var(--anchor-id);
12
12
  position: fixed;
13
+
14
+ /* Default positioning (left-aligned, below anchor) */
13
15
  top: anchor(var(--anchor-id) bottom);
14
16
  left: anchor(var(--anchor-id) left);
17
+
18
+ /* Default fallbacks and ordering */
19
+ position-try-fallbacks: --flip-v, --flip-v-right, --flip-h, --flip-vh;
20
+ position-try-order: most-height;
15
21
 
16
22
  /* Fallback for Safari/Firefox:
17
23
  Since .menu-wrapper is relative, this puts it below the button
@@ -19,37 +25,50 @@
19
25
  margin-top: var(--base-gap);
20
26
  }
21
27
 
22
- /* Custom Try States to prevent horizontal shifting */
28
+ /* Custom Try States with consistent spacing */
23
29
  @position-try --flip-v {
24
30
  top: auto;
25
31
  bottom: anchor(var(--anchor-id) top);
26
32
  left: anchor(var(--anchor-id) left);
33
+ margin-top: 0;
34
+ margin-bottom: var(--base-gap);
27
35
  }
28
36
 
29
37
  @position-try --flip-v-right {
30
38
  top: auto;
31
39
  bottom: anchor(var(--anchor-id) top);
32
40
  right: anchor(var(--anchor-id) right);
41
+ margin-top: 0;
42
+ margin-bottom: var(--base-gap);
33
43
  }
34
44
 
35
- [popover].menu-popover {
36
- position-try-fallbacks: --flip-v, --flip-v-right;
45
+ @position-try --flip-h {
46
+ top: anchor(var(--anchor-id) bottom);
47
+ left: auto;
48
+ right: anchor(var(--anchor-id) right);
49
+ margin-top: var(--base-gap);
50
+ margin-bottom: 0;
37
51
  }
38
52
 
39
- /* Alignment Modifiers for Menu */
40
- .menu-anchor-right {
41
- left: auto !important;
42
- right: anchor(var(--anchor-id) right) !important;
53
+ @position-try --flip-vh {
54
+ top: auto;
55
+ bottom: anchor(var(--anchor-id) top);
56
+ left: auto;
57
+ right: anchor(var(--anchor-id) right);
58
+ margin-top: 0;
59
+ margin-bottom: var(--base-gap);
43
60
  }
44
61
 
45
- .menu-anchor-left {
46
- left: anchor(var(--anchor-id) left) !important;
47
- right: auto !important;
62
+ /* Alignment Modifiers - Override default positioning */
63
+ [popover].menu-popover.menu-anchor-right {
64
+ left: auto;
65
+ right: anchor(var(--anchor-id) right);
66
+ position-try-fallbacks: --flip-v-right, --flip-v, --flip-vh, --flip-h;
48
67
  }
49
68
 
50
- [popover].menu-popover {
51
- position-try-fallbacks: --flip-v, --flip-v-right;
52
- position-try-order: most-height;
69
+ [popover].menu-popover.menu-anchor-left {
70
+ /* Uses default left/top positioning from base rule */
71
+ position-try-fallbacks: --flip-v, --flip-v-right, --flip-h, --flip-vh;
53
72
  }
54
73
 
55
74
  [popover].menu-popover.match-anchor {
@@ -69,18 +88,17 @@
69
88
  color: white !important;
70
89
  }
71
90
 
72
- [popover].menu-popover.menu-anchor-right {
73
- position-try-fallbacks: --flip-v-right, --flip-v;
74
- }
75
-
76
- [popover].menu-popover.menu-anchor-left {
77
- position-try-fallbacks: --flip-v, --flip-v-right;
78
- }
79
-
80
91
  [popover]:popover-open {
81
92
  display: flex;
82
93
  }
83
94
 
95
+ /* Ensure menu content fits within viewport and scrolls if needed */
96
+ [popover].menu-popover > .card {
97
+ max-height: calc(100dvh - var(--base-padding) * 4);
98
+ overflow-y: auto;
99
+ overflow-x: hidden;
100
+ }
101
+
84
102
  .menu-wrapper {
85
103
  position: relative;
86
104
  display: inline-block;