@ceebee/ui 0.5.0 → 0.5.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.
package/dist/client.js CHANGED
@@ -456,6 +456,7 @@ function AutoComplete({
456
456
  const [loading, setLoading] = useState(false);
457
457
  const [failed, setFailed] = useState(false);
458
458
  const [chosen, setChosen] = useState(null);
459
+ const [open, setOpen] = useState(false);
459
460
  const available = async ? loaded : items ?? [];
460
461
  const optionFor = (candidate) => {
461
462
  if (candidate == null) return null;
@@ -497,6 +498,17 @@ function AutoComplete({
497
498
  asked.current += 1;
498
499
  inFlight.current?.abort();
499
500
  }, []);
501
+ useEffect(() => {
502
+ if (!open) return void 0;
503
+ const onScroll = (event) => {
504
+ const target = event.target;
505
+ if (target === document || target === document.documentElement || target === document.body) {
506
+ setOpen(false);
507
+ }
508
+ };
509
+ window.addEventListener("scroll", onScroll, { capture: true, passive: true });
510
+ return () => window.removeEventListener("scroll", onScroll, { capture: true });
511
+ }, [open]);
500
512
  return /* @__PURE__ */ jsxs(
501
513
  Combobox.Root,
502
514
  {
@@ -510,6 +522,9 @@ function AutoComplete({
510
522
  setChosen(option);
511
523
  onValueChange?.(option?.value ?? null);
512
524
  },
525
+ modal: true,
526
+ open,
527
+ onOpenChange: setOpen,
513
528
  isItemEqualToValue: (a, b) => a?.value === b?.value,
514
529
  itemToStringLabel: (item) => typeof item === "string" ? item : item.label,
515
530
  disabled,
package/dist/styles.css CHANGED
@@ -547,11 +547,13 @@ html, body { background: var(--cb-bg); color: var(--cb-fg); }
547
547
  not line up with the thing it was a list for. The tick moves to the trailing
548
548
  edge — `order`, so the reading order is untouched — and the row's inline
549
549
  padding matches the field's. */
550
+ /* Same shape and the same reasoning as the Select's popup — see select.css. */
550
551
  .cb-autocomplete__popup {
551
552
  box-sizing: border-box;
552
- width: var(--anchor-width, auto);
553
+ width: max-content;
553
554
  min-width: var(--anchor-width, 12rem);
554
- max-height: 18rem;
555
+ max-width: var(--available-width, 100vw);
556
+ max-height: min(18rem, var(--available-height, 18rem));
555
557
  overflow-y: auto;
556
558
  padding: var(--cb-space-1);
557
559
  background: var(--cb-surface-raised);
@@ -1180,11 +1182,26 @@ html, body { background: var(--cb-bg); color: var(--cb-fg); }
1180
1182
  .cb-select[data-disabled] { opacity: 0.55; cursor: not-allowed; }
1181
1183
  .cb-select__icon { display: inline-flex; color: var(--cb-fg-subtle); }
1182
1184
 
1185
+ /* As wide as its longest option, and never wider than the screen.
1186
+ *
1187
+ * Pinned to the anchor's width, a popup dropping from a field that is half a
1188
+ * filter row is 175px on a phone — and the options in it are place names.
1189
+ * "Kabupaten Tangerang Selatan (5)" wrapped to three lines there, the one below
1190
+ * it to two, and the list read as a cramped column of broken phrases rather
1191
+ * than as a set of choices.
1192
+ *
1193
+ * So the anchor's width becomes the floor rather than the value: still never
1194
+ * narrower than the field it belongs to, because a popup narrower than its
1195
+ * field reads as belonging to something else. `--available-width` is the room
1196
+ * the positioner measured between the anchor and the edge of the screen, so
1197
+ * the ceiling moves with the viewport instead of being a number written here.
1198
+ */
1183
1199
  .cb-select__popup {
1184
1200
  box-sizing: border-box;
1185
- width: var(--anchor-width, auto);
1201
+ width: max-content;
1186
1202
  min-width: var(--anchor-width, 10rem);
1187
- max-height: 18rem;
1203
+ max-width: var(--available-width, 100vw);
1204
+ max-height: min(18rem, var(--available-height, 18rem));
1188
1205
  overflow-y: auto;
1189
1206
  background: var(--cb-surface-raised);
1190
1207
  border: var(--cb-border-width) solid var(--cb-border);
@@ -1762,7 +1779,17 @@ html, body { background: var(--cb-bg); color: var(--cb-fg); }
1762
1779
 
1763
1780
  .cb-drawer__title { margin: 0; font-family: var(--cb-font-sans); font-size: var(--cb-text-lg); font-weight: var(--cb-weight-semibold); }
1764
1781
  .cb-drawer__description { margin: 0; font-family: var(--cb-font-sans); font-size: var(--cb-text-sm); color: var(--cb-fg-muted); }
1765
- .cb-drawer__body { min-height: 0; flex: 1; overflow-y: auto; font-family: var(--cb-font-sans); font-size: var(--cb-text-sm); }
1782
+ /* Same scrollbar as the Modal's body, same answer see modal.css. */
1783
+ .cb-drawer__body {
1784
+ min-height: 0;
1785
+ flex: 1;
1786
+ overflow-y: auto;
1787
+ scrollbar-gutter: stable;
1788
+ margin-inline: calc(-1 * var(--cb-space-5));
1789
+ padding-inline: var(--cb-space-5);
1790
+ font-family: var(--cb-font-sans);
1791
+ font-size: var(--cb-text-sm);
1792
+ }
1766
1793
  .cb-drawer__footer { display: flex; justify-content: flex-end; gap: var(--cb-space-2); }
1767
1794
 
1768
1795
  /* overlay/modal.css */
@@ -1818,7 +1845,27 @@ html, body { background: var(--cb-bg); color: var(--cb-fg); }
1818
1845
 
1819
1846
  .cb-modal__title { margin: 0; font-family: var(--cb-font-sans); font-size: var(--cb-text-lg); font-weight: var(--cb-weight-semibold); }
1820
1847
  .cb-modal__description { margin: 0; font-family: var(--cb-font-sans); font-size: var(--cb-text-sm); color: var(--cb-fg-muted); }
1821
- .cb-modal__body { min-height: 0; overflow-y: auto; font-family: var(--cb-font-sans); font-size: var(--cb-text-sm); }
1848
+ /* The body is the only scrolling part of the dialog, and a scroller draws its
1849
+ bar at its own inline edge — which, with the body running to the content edge,
1850
+ is the right edge of every field inside it. Platforms differ in how that looks
1851
+ and neither way is acceptable: macOS and Android float an overlay bar over the
1852
+ field, Windows reserves real width and squeezes it.
1853
+
1854
+ So both halves are needed. `scrollbar-gutter: stable` keeps the reserved bar
1855
+ out of the content box — always, not only while the content overflows, so a
1856
+ field appearing does not shift the rest sideways. The bleed-and-pad puts a
1857
+ space-5 margin between the content and the edge where an overlay bar is drawn,
1858
+ and hands the width back to the dialog so the padding costs nothing. It also
1859
+ stops a focused field's ring being clipped at the scroller's edge. */
1860
+ .cb-modal__body {
1861
+ min-height: 0;
1862
+ overflow-y: auto;
1863
+ scrollbar-gutter: stable;
1864
+ margin-inline: calc(-1 * var(--cb-space-5));
1865
+ padding-inline: var(--cb-space-5);
1866
+ font-family: var(--cb-font-sans);
1867
+ font-size: var(--cb-text-sm);
1868
+ }
1822
1869
  .cb-modal__footer { flex: none; display: flex; justify-content: flex-end; gap: var(--cb-space-2); margin-top: var(--cb-space-2); }
1823
1870
 
1824
1871
  /* overlay/popover.css */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceebee/ui",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Ceebee's design system: tokens, themed primitives, motion, and onboarding.",
5
5
  "license": "MIT",
6
6
  "repository": {