@salemcobalt/ui 0.2.0-rc.22 → 0.2.0-rc.24

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/README.md CHANGED
@@ -33,7 +33,7 @@ Styles ship as one stylesheet inside `@layer sc`, so an app's unlayered CSS alwa
33
33
  | `Card` | `variant` outlined · elevated · filled · plain (a quiet row: no surface, a hover wash without border or shadow, selected is `accent.wash` with a 1px `accent.border`); `padding` none · sm · md · lg; `interactive`; `selected` (visual only: pair with `aria-pressed`/`aria-selected`/`aria-current`; also turns a Score inside solid); `as` or `render` to make it an `a`/`button`/`li`. |
34
34
  | `Stack`, `Inline` | `gap` from the spacing scale (`none` … `4xl`); `align`; `justify`; `as`. `Inline` wraps unless `wrap={false}`. |
35
35
  | `Kbd` | One key per `Kbd`; `Kbd.Group label="Command K"` groups a shortcut with a `+` (or `separator="space"`). |
36
- | `Dialog` | `Root` (`open`, `defaultOpen`, `onOpenChange`, `dismissible`, `onRequestClose(reason)`: Esc, backdrop and every `Close` ask instead of closing; close by setting `open`), `Trigger`, `Content` (`size` sm · md · lg, every size clamps to the viewport minus a 16px gutter; `variant="sheet"` becomes a bottom sheet below 640px with a scrolling `Body`; `variant="fullscreen"` covers the viewport, inset 16px from 900px, with a scrolling `Body`), `Title`, `Description`, `Body`, `Actions`, `Close`. Menus, selects and tooltips opened inside render above it. |
36
+ | `Dialog` | `Root` (`open`, `defaultOpen`, `onOpenChange`, `dismissible`, `onRequestClose(reason)`: Esc, backdrop and every `Close` ask instead of closing; close by setting `open`), `Trigger`, `Content` (`size` sm · md · lg, every size clamps to the viewport minus a 16px gutter; `variant="sheet"` becomes a bottom sheet below 640px with a scrolling `Body` and swipe-down-to-dismiss from its handle or header (top 64px, outside controls and `Body`; closes past 30% of its height or on a flick, otherwise springs back; the handle is a `Close` named "Dismiss"; `dismissOnSwipe={false}` where an accidental close loses work, also off when the Root is not `dismissible`); `variant="fullscreen"` covers the viewport, inset 16px from 900px, with a scrolling `Body`), `Title`, `Description`, `Body`, `Actions`, `Close`. Menus, selects and tooltips opened inside render above it. |
37
37
  | `FullScreenFlow` | A multi-step takeover on `Dialog variant="fullscreen"` (docs/adr/0006). `Root` (`open` default `true`, required `onRequestClose(reason)` for Esc and the ×, `onNext`/`onBack` for Enter and the arrow keys outside controls, `decoration`), `Header` (`title` names the dialog, `logo`, `compactTitle` below 760px, `status`, `closeLabel`; children: `Progress`), `Progress` (`chapters` `{ label, steps }[]`, `step` counted from 1, `label`; current chapter `aria-current="step"`), `Body`, `Rail` (from 1000px, `footer`) › `RailGroup` (`label`) › `RailItem` (`current`, `done`, `disabled`, `adornment`), `Viewport` (`direction` forward · back; one keyed `Step`), `Step` (`title` an `<h2>` that takes focus, `eyebrow`, `description`, `announcement` for the live region), `Footer` (`back`, `hint`, children; 44px buttons). The app owns the step state; the flow owns motion, focus and announcements. |
38
38
  | `Toolbar` | `Root` (required `aria-label`, `wrap`, `gap`), `Group` (`aria-label`, `gap`), `Separator`, `Spacer`, `Button` (`render={<IconButton … />}` or `render={<Menu.Trigger render={…} />}` so the control joins the roving focus: Tab enters, arrows move, Tab leaves), `Link` (an anchor in the same navigation; `render={<IconButton label render={<a href />} />}` for an icon link). |
39
39
  | `Banner` | `tone` info (default) · success · warning · danger · neutral; `icon` (or `false`); `action` slot; `dismissible` + `onDismiss` (`open` to control); warning and danger are `role="alert"`, the rest `role="status"`. |
package/dist/index.d.ts CHANGED
@@ -738,13 +738,21 @@ interface DialogContentProps extends Omit<Dialog$1.Popup.Props, 'className'> {
738
738
  * the Root when leaving needs a confirmation.
739
739
  */
740
740
  variant?: DialogVariant | undefined;
741
+ /**
742
+ * `sheet` only. Below 640px a downward drag on the handle or the header (the top 64px, outside controls
743
+ * and `Dialog.Body`) moves the sheet; letting go past 30% of its height, or flicking down, closes it the
744
+ * way a press on the handle does (the handle is a `Dialog.Close` named "Dismiss"). Set `false` where an
745
+ * accidental close loses work; the handle then stays as decoration only. Off whenever the Root is not
746
+ * `dismissible`. Default `true`.
747
+ */
748
+ dismissOnSwipe?: boolean | undefined;
741
749
  className?: string | undefined;
742
750
  }
743
751
  /**
744
752
  * Portal, backdrop and the popup. Focus moves inside on open and returns to the trigger on close.
745
753
  * Menus, selects and tooltips opened from inside render above it (`--sc-z-dropdown` > `--sc-z-dialog`).
746
754
  */
747
- declare function Content$2({ size, variant, className, children, ...rest }: DialogContentProps): react.JSX.Element;
755
+ declare function Content$2({ size, variant, dismissOnSwipe, className, children, ...rest }: DialogContentProps): react.JSX.Element;
748
756
  interface DialogTitleProps extends Omit<Dialog$1.Title.Props, 'className'> {
749
757
  className?: string | undefined;
750
758
  }
package/dist/index.js CHANGED
@@ -167,6 +167,141 @@ var Kbd = Object.assign(KbdBase, { Group: KbdGroup });
167
167
 
168
168
  // src/components/Dialog/Dialog.tsx
169
169
  import { Dialog as BaseDialog } from "@base-ui/react/dialog";
170
+ import { createContext, useCallback, useContext } from "react";
171
+
172
+ // src/components/Dialog/sheetSwipe.ts
173
+ var SHEET_SWIPE = {
174
+ /** Height in px, from the sheet's top edge, where a drag may start: the handle and the header. */
175
+ zone: 64,
176
+ /** Movement in px before a press becomes a drag, so taps stay taps. */
177
+ slop: 6,
178
+ /** Fraction of the sheet's height past which a release dismisses. */
179
+ distance: 0.3,
180
+ /** Release speed in px/ms: this fast downward dismisses at any distance, this fast upward keeps it open. */
181
+ velocity: 0.5,
182
+ /** Only samples this recent (ms) before the release count toward its speed. */
183
+ window: 100
184
+ };
185
+ function dragIntent(dx, dy, slop = SHEET_SWIPE.slop) {
186
+ if (Math.abs(dx) < slop && Math.abs(dy) < slop) return "pending";
187
+ return dy > 0 && dy >= Math.abs(dx) ? "drag" : "cancel";
188
+ }
189
+ function releaseVelocity(samples, window2 = SHEET_SWIPE.window) {
190
+ const last = samples[samples.length - 1];
191
+ if (!last) return 0;
192
+ const first = samples.find((s) => last.t - s.t <= window2) ?? last;
193
+ const dt = last.t - first.t;
194
+ return dt > 0 ? (last.y - first.y) / dt : 0;
195
+ }
196
+ function shouldDismissSheet({ offset, height, velocity }) {
197
+ if (offset <= 0 || height <= 0) return false;
198
+ if (velocity >= SHEET_SWIPE.velocity) return true;
199
+ if (velocity <= -SHEET_SWIPE.velocity) return false;
200
+ return offset >= height * SHEET_SWIPE.distance;
201
+ }
202
+ var IGNORE = 'a,button,input,select,textarea,label,[role="button"],[contenteditable]';
203
+ function attachSheetSwipe(handle) {
204
+ const popup = handle.parentElement;
205
+ if (!popup) return () => {
206
+ };
207
+ let press = null;
208
+ let dragging = false;
209
+ let height = 0;
210
+ let samples = [];
211
+ let suppressClick = false;
212
+ const startsDrag = (e) => {
213
+ if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return false;
214
+ if (handle.getClientRects().length === 0) return false;
215
+ const target = e.target instanceof Element ? e.target : null;
216
+ if (!target || handle.contains(target)) return true;
217
+ if (target.closest(IGNORE) || target.closest(".sc-dialog__body")) return false;
218
+ return e.clientY - popup.getBoundingClientRect().top <= SHEET_SWIPE.zone;
219
+ };
220
+ const settle = () => {
221
+ delete popup.dataset.swiping;
222
+ popup.style.removeProperty("transform");
223
+ };
224
+ const onDown = (e) => {
225
+ if (press || !startsDrag(e)) return;
226
+ press = { id: e.pointerId, x: e.clientX, y: e.clientY };
227
+ samples = [{ t: e.timeStamp, y: e.clientY }];
228
+ };
229
+ const onMove = (e) => {
230
+ if (!press || e.pointerId !== press.id) return;
231
+ const dy = e.clientY - press.y;
232
+ if (!dragging) {
233
+ const intent = dragIntent(e.clientX - press.x, dy);
234
+ if (intent === "pending") return;
235
+ if (intent === "cancel") {
236
+ press = null;
237
+ return;
238
+ }
239
+ dragging = true;
240
+ height = popup.getBoundingClientRect().height;
241
+ popup.dataset.swiping = "";
242
+ popup.setPointerCapture?.(e.pointerId);
243
+ window.getSelection()?.removeAllRanges();
244
+ }
245
+ e.preventDefault();
246
+ samples.push({ t: e.timeStamp, y: e.clientY });
247
+ if (samples.length > 32) samples = samples.slice(-16);
248
+ popup.style.transform = `translateY(${Math.max(0, dy)}px)`;
249
+ };
250
+ const end = (e, cancelled) => {
251
+ if (!press || e.pointerId !== press.id) return;
252
+ const startY = press.y;
253
+ const wasDragging = dragging;
254
+ press = null;
255
+ dragging = false;
256
+ if (!wasDragging) return;
257
+ window.setTimeout(() => {
258
+ suppressClick = false;
259
+ }, 0);
260
+ if (cancelled) {
261
+ suppressClick = true;
262
+ settle();
263
+ return;
264
+ }
265
+ samples.push({ t: e.timeStamp, y: e.clientY });
266
+ const release = {
267
+ offset: Math.max(0, e.clientY - startY),
268
+ height,
269
+ velocity: releaseVelocity(samples)
270
+ };
271
+ if (shouldDismissSheet(release)) {
272
+ delete popup.dataset.swiping;
273
+ handle.click();
274
+ suppressClick = true;
275
+ window.requestAnimationFrame(() => {
276
+ if (popup.isConnected && popup.hasAttribute("data-open")) settle();
277
+ });
278
+ } else {
279
+ suppressClick = true;
280
+ settle();
281
+ }
282
+ };
283
+ const onUp = (e) => end(e, false);
284
+ const onCancel = (e) => end(e, true);
285
+ const onClick = (e) => {
286
+ if (!suppressClick) return;
287
+ suppressClick = false;
288
+ e.preventDefault();
289
+ e.stopPropagation();
290
+ };
291
+ popup.addEventListener("pointerdown", onDown);
292
+ popup.addEventListener("pointermove", onMove);
293
+ popup.addEventListener("pointerup", onUp);
294
+ popup.addEventListener("pointercancel", onCancel);
295
+ popup.addEventListener("click", onClick, true);
296
+ return () => {
297
+ popup.removeEventListener("pointerdown", onDown);
298
+ popup.removeEventListener("pointermove", onMove);
299
+ popup.removeEventListener("pointerup", onUp);
300
+ popup.removeEventListener("pointercancel", onCancel);
301
+ popup.removeEventListener("click", onClick, true);
302
+ settle();
303
+ };
304
+ }
170
305
 
171
306
  // src/components/Icon/Icon.tsx
172
307
  import { jsx as jsx4 } from "react/jsx-runtime";
@@ -447,6 +582,7 @@ var REQUESTABLE = /* @__PURE__ */ new Set([
447
582
  "outside-press",
448
583
  "close-press"
449
584
  ]);
585
+ var DismissibleContext = createContext(true);
450
586
  function Root({
451
587
  dismissible = true,
452
588
  onOpenChange,
@@ -473,27 +609,49 @@ function Root({
473
609
  }
474
610
  onOpenChange?.(next);
475
611
  },
476
- children
612
+ children: /* @__PURE__ */ jsx6(DismissibleContext.Provider, { value: dismissible, children })
477
613
  }
478
614
  );
479
615
  }
480
616
  var Trigger = BaseDialog.Trigger;
481
- function Content({ size = "md", variant = "dialog", className, children, ...rest }) {
617
+ function Content({
618
+ size = "md",
619
+ variant = "dialog",
620
+ dismissOnSwipe = true,
621
+ className,
622
+ children,
623
+ ...rest
624
+ }) {
625
+ const dismissible = useContext(DismissibleContext);
626
+ const isSheet = variant === "sheet";
627
+ const swipe = isSheet && dismissOnSwipe && dismissible;
482
628
  return /* @__PURE__ */ jsxs5(BaseDialog.Portal, { children: [
483
629
  /* @__PURE__ */ jsx6(BaseDialog.Backdrop, { className: "sc-dialog__backdrop" }),
484
- /* @__PURE__ */ jsx6(
630
+ /* @__PURE__ */ jsxs5(
485
631
  BaseDialog.Popup,
486
632
  {
487
633
  className: cx("sc-dialog__popup", className),
488
634
  "data-size": size,
489
635
  "data-variant": variant,
636
+ "data-swipe-dismiss": swipe ? "" : void 0,
490
637
  "aria-modal": "true",
491
638
  ...rest,
492
- children
639
+ children: [
640
+ isSheet && /* @__PURE__ */ jsx6(SheetHandle, { swipe }),
641
+ children
642
+ ]
493
643
  }
494
644
  )
495
645
  ] });
496
646
  }
647
+ function SheetHandle({ swipe }) {
648
+ const attach = useCallback(
649
+ (node) => node ? attachSheetSwipe(node) : void 0,
650
+ []
651
+ );
652
+ if (!swipe) return /* @__PURE__ */ jsx6("div", { className: "sc-dialog__handle", "aria-hidden": "true" });
653
+ return /* @__PURE__ */ jsx6(BaseDialog.Close, { ref: attach, className: "sc-dialog__handle", "aria-label": "Dismiss" });
654
+ }
497
655
  function Title({ className, ...rest }) {
498
656
  return /* @__PURE__ */ jsx6(BaseDialog.Title, { className: cx("sc-dialog__title", className), ...rest });
499
657
  }
@@ -1402,14 +1560,14 @@ function Text({
1402
1560
  // src/components/Table/Table.tsx
1403
1561
  import { Checkbox as BaseCheckbox2 } from "@base-ui/react/checkbox";
1404
1562
  import {
1405
- createContext,
1406
- useContext
1563
+ createContext as createContext2,
1564
+ useContext as useContext2
1407
1565
  } from "react";
1408
1566
  import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
1409
- var SelectionContext = createContext(null);
1410
- var RowIdContext = createContext(void 0);
1567
+ var SelectionContext = createContext2(null);
1568
+ var RowIdContext = createContext2(void 0);
1411
1569
  function useSelection(part) {
1412
- const selection = useContext(SelectionContext);
1570
+ const selection = useContext2(SelectionContext);
1413
1571
  if (!selection) throw new Error(`${part} needs Table.Root selection={useRowSelection(ids)}`);
1414
1572
  return selection;
1415
1573
  }
@@ -1498,7 +1656,7 @@ function Row({
1498
1656
  onMouseDown,
1499
1657
  ...rest
1500
1658
  }) {
1501
- const selection = useContext(SelectionContext);
1659
+ const selection = useContext2(SelectionContext);
1502
1660
  const selectable = selection !== null && selectionId !== void 0;
1503
1661
  const isSelected = selected ?? (selectable && selection.isSelected(selectionId));
1504
1662
  const row = /* @__PURE__ */ jsx21(
@@ -1571,7 +1729,7 @@ function SelectionHeader({
1571
1729
  }
1572
1730
  function SelectionCell({ label, className, onClick, onClickCapture, ...rest }) {
1573
1731
  const selection = useSelection("Table.SelectionCell");
1574
- const id = useContext(RowIdContext);
1732
+ const id = useContext2(RowIdContext);
1575
1733
  if (id === void 0) throw new Error("Table.SelectionCell goes inside a Table.Row with selectionId");
1576
1734
  return /* @__PURE__ */ jsx21(
1577
1735
  "td",
@@ -2255,9 +2413,9 @@ function fileTypeLabel(file) {
2255
2413
  }
2256
2414
 
2257
2415
  // src/utils/useMediaQuery.ts
2258
- import { useCallback, useSyncExternalStore } from "react";
2416
+ import { useCallback as useCallback2, useSyncExternalStore } from "react";
2259
2417
  function useMediaQuery(query) {
2260
- const subscribe = useCallback(
2418
+ const subscribe = useCallback2(
2261
2419
  (onChange) => {
2262
2420
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {
2263
2421
  };
@@ -3190,9 +3348,9 @@ function BroomIcon({ sweeping = false, size = "lg", title, className, ...rest })
3190
3348
  // src/components/FullScreenFlow/FullScreenFlow.tsx
3191
3349
  import {
3192
3350
  Children as Children2,
3193
- createContext as createContext2,
3194
- useCallback as useCallback2,
3195
- useContext as useContext2,
3351
+ createContext as createContext3,
3352
+ useCallback as useCallback3,
3353
+ useContext as useContext3,
3196
3354
  useEffect as useEffect6,
3197
3355
  useId as useId5,
3198
3356
  useMemo as useMemo3,
@@ -3200,9 +3358,9 @@ import {
3200
3358
  useState as useState8
3201
3359
  } from "react";
3202
3360
  import { jsx as jsx34, jsxs as jsxs29 } from "react/jsx-runtime";
3203
- var FlowContext = createContext2({ announce: () => {
3361
+ var FlowContext = createContext3({ announce: () => {
3204
3362
  } });
3205
- var FrameContext = createContext2(void 0);
3363
+ var FrameContext = createContext3(void 0);
3206
3364
  var ENTER_IGNORES = 'textarea, button, a[href], summary, select, [contenteditable], [role="combobox"], [data-flow-keys="local"]';
3207
3365
  var ARROW_IGNORES = 'input, textarea, select, button, a[href], summary, [contenteditable], [role="radiogroup"], [role="slider"], [role="listbox"], [role="tablist"], [role="menu"], [role="grid"], [role="combobox"], [data-flow-keys="local"]';
3208
3366
  function Root7({
@@ -3217,7 +3375,7 @@ function Root7({
3217
3375
  const popupRef = useRef6(null);
3218
3376
  const [message, setMessage] = useState8("");
3219
3377
  const timer = useRef6(void 0);
3220
- const announce = useCallback2((next) => {
3378
+ const announce = useCallback3((next) => {
3221
3379
  setMessage("");
3222
3380
  clearTimeout(timer.current);
3223
3381
  timer.current = setTimeout(() => setMessage(next), 30);
@@ -3403,7 +3561,7 @@ function Viewport2({ direction = "forward", className, children, ...rest }) {
3403
3561
  } else if (state.current.element !== element) {
3404
3562
  setState((s) => ({ ...s, current: { ...s.current, element } }));
3405
3563
  }
3406
- const finishExit = useCallback2((leftKey) => {
3564
+ const finishExit = useCallback3((leftKey) => {
3407
3565
  setState((s) => s.leaving?.key === leftKey ? { ...s, leaving: null } : s);
3408
3566
  }, []);
3409
3567
  const ref = useRef6(null);
@@ -3472,8 +3630,8 @@ function Step({
3472
3630
  }) {
3473
3631
  const id = useId5();
3474
3632
  const titleRef = useRef6(null);
3475
- const phase = useContext2(FrameContext);
3476
- const { announce } = useContext2(FlowContext);
3633
+ const phase = useContext3(FrameContext);
3634
+ const { announce } = useContext3(FlowContext);
3477
3635
  const message = announcement ?? (typeof title === "string" ? title : void 0);
3478
3636
  const latest = useRef6({ message, announce });
3479
3637
  useEffect6(() => {