@yourrealm/bottom-sheet 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +187 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/rolldown-runtime-DODCFbG6.js +13 -0
- package/dist/sheet-BOTt16RW.d.ts +123 -0
- package/dist/sheet-BgvWfCR8.js +1404 -0
- package/dist/styled.css +113 -0
- package/dist/styled.d.ts +65 -0
- package/dist/styled.js +78 -0
- package/dist/styles.css +97 -0
- package/package.json +62 -3
package/dist/styled.css
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @yourrealm/bottom-sheet/styled: the preset look, plus the structural CSS it builds on. Import it once, before
|
|
3
|
+
* Tailwind: @import "@yourrealm/bottom-sheet/styled.css";
|
|
4
|
+
*/
|
|
5
|
+
@import "./styles.css";
|
|
6
|
+
/*
|
|
7
|
+
* The polished preset. Override with the --ybs-* tokens.
|
|
8
|
+
* Light/dark follow the app's `color-scheme` (inherited from <html>), via light-dark(): an app with its own theme
|
|
9
|
+
* switch that sets `color-scheme` gets a matching sheet; `html { color-scheme: light dark }` follows the OS.
|
|
10
|
+
*/
|
|
11
|
+
@layer ybs {
|
|
12
|
+
:root { --ybs-css-styled: 1; }
|
|
13
|
+
[data-ybs-root] {
|
|
14
|
+
--ybs-sheet-bg: light-dark(#fff, #1c1c1e);
|
|
15
|
+
--ybs-sheet-fg: light-dark(#111, #f2f2f7);
|
|
16
|
+
--ybs-muted: light-dark(#6b6b70, #98989f);
|
|
17
|
+
--ybs-handle: light-dark(rgba(60, 60, 67, 0.3), rgba(235, 235, 245, 0.3));
|
|
18
|
+
--ybs-divider: light-dark(rgba(60, 60, 67, 0.12), rgba(235, 235, 245, 0.12));
|
|
19
|
+
--ybs-radius: 14px;
|
|
20
|
+
/* Side padding of the body, matching the header. 0 for edge-to-edge lists that pad their own rows. */
|
|
21
|
+
--ybs-body-padding: 16px;
|
|
22
|
+
--ybs-backdrop: #000;
|
|
23
|
+
--ybs-backdrop-max: 0.4;
|
|
24
|
+
--ybs-max-width: 640px;
|
|
25
|
+
--ybs-content-width: 960px;
|
|
26
|
+
--ybs-float-gap: 8px;
|
|
27
|
+
--ybs-float-radius: 32px;
|
|
28
|
+
/* 1 within the last 10% of travel to full, else 0..: corners square off as a fullscreen sheet reaches the top. */
|
|
29
|
+
--ybs-at-top: clamp(0, (var(--ybs-progress, 0) - 0.9) * 10, 1);
|
|
30
|
+
}
|
|
31
|
+
.ybs-backdrop {
|
|
32
|
+
background: var(--ybs-backdrop);
|
|
33
|
+
opacity: calc(var(--ybs-progress, 0) * var(--ybs-backdrop-max));
|
|
34
|
+
}
|
|
35
|
+
.ybs-bleed { background: var(--ybs-sheet-bg); }
|
|
36
|
+
/* Stacked sheets: the covered sheet recedes (scales back and lifts slightly), like iOS. */
|
|
37
|
+
.ybs-view { transform-origin: 50% 0; transition: none; }
|
|
38
|
+
[data-ybs-root][data-ybs-covered] .ybs-view {
|
|
39
|
+
transform: translateY(calc(var(--ybs-covered, 0) * -6px)) scale(calc(1 - var(--ybs-covered, 0) * 0.06));
|
|
40
|
+
border-radius: var(--ybs-radius);
|
|
41
|
+
}
|
|
42
|
+
[data-ybs-root][data-ybs-covered] .ybs-bleed { opacity: 0; }
|
|
43
|
+
.ybs-sheet {
|
|
44
|
+
background: var(--ybs-sheet-bg);
|
|
45
|
+
color: var(--ybs-sheet-fg);
|
|
46
|
+
border-radius: var(--ybs-radius) var(--ybs-radius) 0 0;
|
|
47
|
+
box-shadow: 0 -1px 0 var(--ybs-divider), 0 -8px 30px rgba(0, 0, 0, 0.08);
|
|
48
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
49
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
50
|
+
max-width: var(--ybs-max-width); margin-inline: auto;
|
|
51
|
+
}
|
|
52
|
+
/* Full width: the surface spans the window; header and body keep their full width (no corner seams) but pad
|
|
53
|
+
their contents into a readable centred column. */
|
|
54
|
+
.ybs-sheet.ybs-sheet-full { max-width: none; }
|
|
55
|
+
.ybs-sheet-full > .ybs-header { padding-inline: max(16px, (100% - var(--ybs-content-width)) / 2); }
|
|
56
|
+
.ybs-sheet-full > .ybs-body { padding-inline: max(var(--ybs-body-padding), (100% - var(--ybs-content-width)) / 2); }
|
|
57
|
+
/* A page sheet sits lower than the receded page's top edge (+10px), so a strip of that page shows above it, like iOS. */
|
|
58
|
+
[data-ybs-root]:has(.ybs-sheet-page):not([data-ybs-fullscreen]) { --ybs-inset: calc(env(safe-area-inset-top, 0px) + 22px); }
|
|
59
|
+
/* A page sheet fills the screen at full even when its content is short (no fit-content). */
|
|
60
|
+
.ybs-sheet.ybs-sheet-page { min-height: calc(var(--ybs-vv-h, 100dvh) - var(--ybs-inset)); }
|
|
61
|
+
/* Header bar: leading | title | trailing, the title centred like an iOS navigation bar. */
|
|
62
|
+
.ybs-bar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; }
|
|
63
|
+
.ybs-bar > .ybs-title { text-align: center; }
|
|
64
|
+
.ybs-leading { justify-self: start; }
|
|
65
|
+
.ybs-trailing { justify-self: end; }
|
|
66
|
+
/* Floating (iOS 26): at lower detents the sheet floats, inset from the sides and the bottom with large corners;
|
|
67
|
+
over the last quarter of the travel to full it attaches (the gap closes, the bottom corners slide off-screen).
|
|
68
|
+
A clip-path, so nothing reflows while the sheet moves. --ybs-visible is the on-screen height of the sheet, so
|
|
69
|
+
100% - visible is the part below the screen edge. */
|
|
70
|
+
.ybs-sheet.ybs-sheet-floating {
|
|
71
|
+
--ybs-float: calc(1 - clamp(0, (var(--ybs-progress, 0) - 0.75) * 4, 1));
|
|
72
|
+
--ybs-g: calc(var(--ybs-float-gap) * var(--ybs-float));
|
|
73
|
+
--ybs-rt: calc(var(--ybs-radius) + (var(--ybs-float-radius) - var(--ybs-radius)) * var(--ybs-float));
|
|
74
|
+
--ybs-rb: calc(var(--ybs-float-radius) * var(--ybs-float));
|
|
75
|
+
clip-path: inset(0 var(--ybs-g) calc((100% - var(--ybs-visible, 0px)) * var(--ybs-float) + var(--ybs-g)) var(--ybs-g)
|
|
76
|
+
round var(--ybs-rt) var(--ybs-rt) var(--ybs-rb) var(--ybs-rb));
|
|
77
|
+
}
|
|
78
|
+
/* Fullscreen: square corners at the top (they would show the page through), header clear of the notch. */
|
|
79
|
+
[data-ybs-fullscreen] .ybs-sheet, [data-ybs-fullscreen] .ybs-header {
|
|
80
|
+
--ybs-r: calc(var(--ybs-radius) * (1 - var(--ybs-at-top)));
|
|
81
|
+
border-radius: var(--ybs-r) var(--ybs-r) 0 0;
|
|
82
|
+
}
|
|
83
|
+
[data-ybs-fullscreen] .ybs-header { padding-top: calc(6px + env(safe-area-inset-top, 0px)); }
|
|
84
|
+
.ybs-header {
|
|
85
|
+
background: var(--ybs-sheet-bg);
|
|
86
|
+
border-radius: var(--ybs-radius) var(--ybs-radius) 0 0;
|
|
87
|
+
padding: 6px 16px 12px;
|
|
88
|
+
border-bottom: 1px solid var(--ybs-divider);
|
|
89
|
+
/* Content scrolls under this sticky header; at fractional positions (device scaling, or a covered
|
|
90
|
+
sheet's scale) a 1px sliver of it can show above. Cover it with the sheet colour. */
|
|
91
|
+
box-shadow: 0 -2px 0 0 var(--ybs-sheet-bg);
|
|
92
|
+
}
|
|
93
|
+
.ybs-handle {
|
|
94
|
+
width: 36px; height: 5px; border-radius: 3px;
|
|
95
|
+
background: var(--ybs-handle);
|
|
96
|
+
margin: 0 auto 10px;
|
|
97
|
+
}
|
|
98
|
+
.ybs-title { margin: 0; font-size: 17px; font-weight: 600; line-height: 1.3; }
|
|
99
|
+
.ybs-description { margin: 4px 0 0; font-size: 14px; color: var(--ybs-muted); }
|
|
100
|
+
.ybs-body { padding: 0 var(--ybs-body-padding) 12px; }
|
|
101
|
+
/* The page behind recedes (Sheet.Root recede + data-ybs-background on the app wrapper), like iOS: it scales back
|
|
102
|
+
from the top of the screen, drops below the status bar and rounds its top corners over black. The wrapper needs
|
|
103
|
+
its own background (html and body turn black). */
|
|
104
|
+
/* !important: our layer loses to the app's own unlayered html/body background, and this must win while receding. */
|
|
105
|
+
html[data-ybs-recede]:has([data-ybs-background]), html[data-ybs-recede]:has([data-ybs-background]) body { background: #000 !important; }
|
|
106
|
+
html[data-ybs-recede] [data-ybs-background] {
|
|
107
|
+
/* Like iOS, a sheet covering half the screen only dims the page; it recedes as the sheet covers the screen. */
|
|
108
|
+
--ybs-r: clamp(0, (var(--ybs-recede, 0) - 0.6) * 2.5, 1);
|
|
109
|
+
transform-origin: 50% var(--ybs-scroll-y, 0px);
|
|
110
|
+
transform: translateY(calc(var(--ybs-r) * (env(safe-area-inset-top, 0px) + 10px))) scale(calc(1 - var(--ybs-r) * 0.07));
|
|
111
|
+
clip-path: inset(var(--ybs-scroll-y, 0px) 0 0 0 round calc(var(--ybs-r) * 12px) calc(var(--ybs-r) * 12px) 0 0);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/styled.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
import { a as OpenChangeDetails, c as PortalProps, d as Trigger, f as TriggerProps, g as Detent, i as InteractionType, l as Root, m as ViewProps, n as ContentProps, o as OpenChangeReason, p as TriggerRenderProps, r as InitialFocus, s as Portal, t as Close, u as RootProps } from "./sheet-BOTt16RW.js";
|
|
3
|
+
import { ComponentPropsWithRef, ReactElement, ReactNode } from "react";
|
|
4
|
+
/** Styled parts: same API as the headless `Sheet`, with the preset look. */
|
|
5
|
+
export declare const Sheet: {
|
|
6
|
+
Root: typeof Root;
|
|
7
|
+
Trigger: typeof Trigger;
|
|
8
|
+
Close: typeof Close;
|
|
9
|
+
Portal: typeof Portal;
|
|
10
|
+
Backdrop: (p: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>) => import("react").JSX.Element;
|
|
11
|
+
View: (p: ViewProps) => import("react").JSX.Element;
|
|
12
|
+
Content: (p: ContentProps) => import("react").JSX.Element;
|
|
13
|
+
Header: (p: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>) => import("react").JSX.Element;
|
|
14
|
+
Handle: (p: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>) => import("react").JSX.Element;
|
|
15
|
+
Title: (p: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>) => import("react").JSX.Element;
|
|
16
|
+
Description: (p: import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>) => import("react").JSX.Element;
|
|
17
|
+
};
|
|
18
|
+
export interface BottomSheetProps extends Omit<RootProps, 'children'> {
|
|
19
|
+
/**
|
|
20
|
+
* What opens the sheet. Content (text, elements) is placed inside our trigger button. A function renders your own
|
|
21
|
+
* button instead: `trigger={(props) => <Button variant="paper" {...props}>Open</Button>}` (it receives `type`,
|
|
22
|
+
* `onClick` and the ARIA state; put className, disabled, aria-label on your button).
|
|
23
|
+
*/
|
|
24
|
+
trigger?: ReactNode | ((props: TriggerRenderProps) => ReactElement);
|
|
25
|
+
title: ReactNode;
|
|
26
|
+
description?: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* - `'sheet'` (default): detents as given, a card-width sheet.
|
|
29
|
+
* - `'page'`: an iOS-style page sheet for a task (compose, edit): opens straight to full,
|
|
30
|
+
* spans the full width (`width` default `'full'`), and the page behind recedes (`recede` default true; mark your
|
|
31
|
+
* app wrapper with `data-ybs-background`). Pair it with `leading` / `trailing` (Cancel / Save).
|
|
32
|
+
*/
|
|
33
|
+
variant?: 'sheet' | 'page';
|
|
34
|
+
/**
|
|
35
|
+
* - `'card'` (default for `sheet`): the sheet is at most 640px wide (`--ybs-max-width`), centred.
|
|
36
|
+
* - `'full'` (default for `page`): the sheet spans the window; its header and body stay in a centred column
|
|
37
|
+
* (`--ybs-content-width`, default 960px).
|
|
38
|
+
*/
|
|
39
|
+
width?: 'card' | 'full';
|
|
40
|
+
/**
|
|
41
|
+
* iOS 26 style: at lower detents the sheet floats (inset from the screen edges, large corners) and attaches as it
|
|
42
|
+
* reaches full. Tokens: --ybs-float-gap (8px), --ybs-float-radius (32px).
|
|
43
|
+
*/
|
|
44
|
+
floating?: boolean;
|
|
45
|
+
/** Header bar slots, iOS navigation-bar style: e.g. `leading={<Sheet.Close>Cancel</Sheet.Close>}`. The title centres between them. */
|
|
46
|
+
leading?: ReactNode;
|
|
47
|
+
trailing?: ReactNode;
|
|
48
|
+
/** Extra content placed in the sticky header under the title (e.g. a search field). */
|
|
49
|
+
headerExtra?: ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* Where focus goes when the sheet opens, like Base UI's: an element ref, `false`, `true` (the sheet), or a function
|
|
52
|
+
* of how it was opened, e.g. `(type) => type !== 'touch' && saveRef.current`. Default: the sheet for a modal sheet,
|
|
53
|
+
* nothing for a non-modal one.
|
|
54
|
+
*/
|
|
55
|
+
initialFocus?: InitialFocus;
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
className?: string;
|
|
58
|
+
contentProps?: ComponentPropsWithRef<'section'>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The simple path:
|
|
62
|
+
* <BottomSheet trigger="Open" title="Settings">…</BottomSheet>
|
|
63
|
+
*/
|
|
64
|
+
export declare function BottomSheet({ trigger, title, description, variant, floating, width, leading, trailing, headerExtra, initialFocus, children, className, contentProps, ...rootProps }: BottomSheetProps): import("react").JSX.Element;
|
|
65
|
+
export type { ContentProps, Detent, InitialFocus, InteractionType, OpenChangeDetails, OpenChangeReason, PortalProps, RootProps, TriggerProps, TriggerRenderProps, ViewProps };
|
package/dist/styled.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { a as Handle, c as Root, d as View, i as Description, l as Title, n as Close, o as Header, p as checkCss, r as Content, s as Portal, t as Backdrop, u as Trigger } from "./sheet-BgvWfCR8.js";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
// src/styled/index.tsx
|
|
6
|
+
const cx = (...c) => c.filter(Boolean).join(" ");
|
|
7
|
+
function withClass(Comp, cls) {
|
|
8
|
+
return (p) => /* @__PURE__ */ jsx(Comp, {
|
|
9
|
+
...p,
|
|
10
|
+
className: cx(cls, p.className)
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Styled parts: same API as the headless `Sheet`, with the preset look. */
|
|
14
|
+
const Sheet = {
|
|
15
|
+
Root,
|
|
16
|
+
Trigger,
|
|
17
|
+
Close,
|
|
18
|
+
Portal,
|
|
19
|
+
Backdrop: withClass(Backdrop, "ybs-backdrop"),
|
|
20
|
+
View: (p) => {
|
|
21
|
+
useEffect(() => checkCss("styled", "styled.css"), []);
|
|
22
|
+
return /* @__PURE__ */ jsx(View, {
|
|
23
|
+
...p,
|
|
24
|
+
className: cx("ybs-view", p.className),
|
|
25
|
+
bleedClassName: cx("ybs-bleed", p.bleedClassName)
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
Content: withClass(Content, "ybs-sheet"),
|
|
29
|
+
Header: withClass(Header, "ybs-header"),
|
|
30
|
+
Handle: withClass(Handle, "ybs-handle"),
|
|
31
|
+
Title: withClass(Title, "ybs-title"),
|
|
32
|
+
Description: withClass(Description, "ybs-description")
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The simple path:
|
|
36
|
+
* <BottomSheet trigger="Open" title="Settings">…</BottomSheet>
|
|
37
|
+
*/
|
|
38
|
+
function BottomSheet({ trigger, title, description, variant = "sheet", floating = false, width = variant === "page" ? "full" : "card", leading, trailing, headerExtra, initialFocus, children, className, contentProps, ...rootProps }) {
|
|
39
|
+
const root = variant === "page" ? {
|
|
40
|
+
recede: true,
|
|
41
|
+
...rootProps
|
|
42
|
+
} : rootProps;
|
|
43
|
+
const bar = leading != null || trailing != null;
|
|
44
|
+
return /* @__PURE__ */ jsxs(Sheet.Root, {
|
|
45
|
+
...root,
|
|
46
|
+
children: [typeof trigger === "function" ? /* @__PURE__ */ jsx(Sheet.Trigger, { render: trigger }) : trigger != null && /* @__PURE__ */ jsx(Sheet.Trigger, {
|
|
47
|
+
className: "ybs-trigger",
|
|
48
|
+
children: trigger
|
|
49
|
+
}), /* @__PURE__ */ jsxs(Sheet.Portal, { children: [root.modal !== false && /* @__PURE__ */ jsx(Sheet.Backdrop, {}), /* @__PURE__ */ jsx(Sheet.View, { children: /* @__PURE__ */ jsxs(Sheet.Content, {
|
|
50
|
+
...contentProps,
|
|
51
|
+
initialFocus,
|
|
52
|
+
className: cx(width === "full" && "ybs-sheet-full", variant === "page" && "ybs-sheet-page", floating && "ybs-sheet-floating", className, contentProps?.className),
|
|
53
|
+
children: [/* @__PURE__ */ jsxs(Sheet.Header, { children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Sheet.Handle, {}),
|
|
55
|
+
bar ? /* @__PURE__ */ jsxs("div", {
|
|
56
|
+
className: "ybs-bar",
|
|
57
|
+
children: [
|
|
58
|
+
/* @__PURE__ */ jsx("div", {
|
|
59
|
+
className: "ybs-leading",
|
|
60
|
+
children: leading
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ jsx(Sheet.Title, { children: title }),
|
|
63
|
+
/* @__PURE__ */ jsx("div", {
|
|
64
|
+
className: "ybs-trailing",
|
|
65
|
+
children: trailing
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
}) : /* @__PURE__ */ jsx(Sheet.Title, { children: title }),
|
|
69
|
+
description != null && /* @__PURE__ */ jsx(Sheet.Description, { children: description }),
|
|
70
|
+
headerExtra
|
|
71
|
+
] }), /* @__PURE__ */ jsx("div", {
|
|
72
|
+
className: "ybs-body",
|
|
73
|
+
children
|
|
74
|
+
})]
|
|
75
|
+
}) })] })]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export { BottomSheet, Sheet };
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @yourrealm/bottom-sheet: structural CSS (the mechanics: overlay, scroller, snapping). Import it once, before
|
|
3
|
+
* Tailwind: @import "@yourrealm/bottom-sheet/styles.css"; (or styled.css, which includes it).
|
|
4
|
+
* Everything is in the `ybs` cascade layer, ordered between Tailwind v4's `base` and `components`, so preflight
|
|
5
|
+
* can't reset it and your utilities and unlayered CSS win. The order statement only counts if this comes first.
|
|
6
|
+
*/
|
|
7
|
+
/*
|
|
8
|
+
* Structural CSS only: the mechanics that make the single-scroller sheet work.
|
|
9
|
+
* No colours, radii or spacing; those belong to the consumer or `/styled`.
|
|
10
|
+
*
|
|
11
|
+
* CSS variables set by the controller on [data-ybs-root]:
|
|
12
|
+
* --ybs-h available height (px)
|
|
13
|
+
* --ybs-vv-top visualViewport.offsetTop (px)
|
|
14
|
+
* --ybs-vv-h visualViewport.height (px)
|
|
15
|
+
* --ybs-progress 0..1, closed -> full
|
|
16
|
+
* --ybs-visible px of the sheet on screen (scrollTop, capped at full)
|
|
17
|
+
* --ybs-bleed 0|1, whether the bleed (sheet colour under the visual viewport) shows
|
|
18
|
+
* Consumer-settable:
|
|
19
|
+
* --ybs-inset gap above the sheet at the full detent (default: safe area + 10px; 0 with fullscreen)
|
|
20
|
+
* --ybs-sheet-bg colour of the bleed, should match the sheet
|
|
21
|
+
*/
|
|
22
|
+
@layer theme, base, ybs, components, utilities;
|
|
23
|
+
@layer ybs {
|
|
24
|
+
/* Marker: tells the library this file is loaded (it logs an error when a sheet opens without it). */
|
|
25
|
+
:root { --ybs-css-core: 1; }
|
|
26
|
+
[data-ybs-root] {
|
|
27
|
+
--ybs-inset: calc(env(safe-area-inset-top, 0px) + 10px);
|
|
28
|
+
position: fixed; inset: 0; z-index: 50; pointer-events: none;
|
|
29
|
+
}
|
|
30
|
+
[data-ybs-root][data-ybs-fullscreen] { --ybs-inset: 0px; }
|
|
31
|
+
/* Fullscreen means the full detent covers the screen, even with short content. */
|
|
32
|
+
[data-ybs-root][data-ybs-fullscreen] [data-ybs-sheet] { min-height: calc(var(--ybs-vv-h, 100dvh) - var(--ybs-inset)); }
|
|
33
|
+
[data-ybs-backdrop] {
|
|
34
|
+
position: fixed; left: 0; right: 0; top: var(--ybs-vv-top, 0px); height: 200lvh;
|
|
35
|
+
pointer-events: auto;
|
|
36
|
+
}
|
|
37
|
+
[data-ybs-bleed] {
|
|
38
|
+
position: fixed; left: 0; right: 0; height: 100lvh;
|
|
39
|
+
top: calc(var(--ybs-vv-top, 0px) + var(--ybs-vv-h, 100dvh));
|
|
40
|
+
background: var(--ybs-sheet-bg, Canvas);
|
|
41
|
+
opacity: var(--ybs-bleed, 0);
|
|
42
|
+
}
|
|
43
|
+
[data-ybs-scroller] {
|
|
44
|
+
position: fixed; left: 0; right: 0;
|
|
45
|
+
top: calc(var(--ybs-vv-top, 0px) + var(--ybs-inset));
|
|
46
|
+
height: calc(var(--ybs-vv-h, 100dvh) - var(--ybs-inset));
|
|
47
|
+
overflow-y: auto;
|
|
48
|
+
overscroll-behavior: contain;
|
|
49
|
+
scroll-snap-type: y mandatory;
|
|
50
|
+
scrollbar-width: none;
|
|
51
|
+
pointer-events: auto;
|
|
52
|
+
}
|
|
53
|
+
[data-ybs-scroller]::-webkit-scrollbar { display: none; }
|
|
54
|
+
[data-ybs-root][data-ybs-drag] [data-ybs-scroller],
|
|
55
|
+
[data-ybs-root][data-ybs-glide] [data-ybs-scroller] { scroll-snap-type: none; }
|
|
56
|
+
[data-ybs-track] { position: relative; }
|
|
57
|
+
[data-ybs-spacer] { height: var(--ybs-h, 100dvh); touch-action: none; }
|
|
58
|
+
[data-ybs-markers] { position: absolute; top: 0; left: 0; width: 1px; height: 0; }
|
|
59
|
+
[data-ybs-marker] {
|
|
60
|
+
position: absolute; left: 0; width: 1px; height: 1px;
|
|
61
|
+
scroll-snap-align: start; pointer-events: none;
|
|
62
|
+
}
|
|
63
|
+
/* scroll-snap-stop: a fling may not skip the full position, in either direction. This is the native
|
|
64
|
+
way to stop momentum at the content top / full detent; the JS guard is only a fallback. (Firefox
|
|
65
|
+
Android: the JS overflow toggle made the next touch get swallowed as a "stop the fling" tap.) */
|
|
66
|
+
[data-ybs-sheet] { position: relative; scroll-snap-align: start; scroll-snap-stop: always; outline: none; }
|
|
67
|
+
[data-ybs-root][data-ybs-fit] [data-ybs-sheet] { scroll-snap-align: end; }
|
|
68
|
+
[data-ybs-header] { position: sticky; top: 0; z-index: 1; }
|
|
69
|
+
/* Content scrolled: a drag on the header moves the whole sheet (JS, "detached drag"), like native, instead of
|
|
70
|
+
scrolling the content back first. */
|
|
71
|
+
[data-ybs-root][data-ybs-scrolled] [data-ybs-drag] { touch-action: none; }
|
|
72
|
+
/* Non-modal: the layer lets taps, clicks and wheel through to the page; only the sheet takes them. A drag on
|
|
73
|
+
the sheet still scrolls the scroller (its scroll container), which is how the sheet moves. */
|
|
74
|
+
[data-ybs-root][data-ybs-modeless] [data-ybs-scroller] { pointer-events: none; }
|
|
75
|
+
[data-ybs-root][data-ybs-modeless] [data-ybs-sheet] { pointer-events: auto; }
|
|
76
|
+
/* iOS Safari won't scroll a pointer-events:none scroller from a touch on its pointer-events:auto child unless
|
|
77
|
+
there is a scroll container under the finger. Make each direct part of the sheet a 1px-wide horizontal
|
|
78
|
+
scroller (overflow-y stays clipped to its own height, so sticky headers keep sticking to the scroller); the
|
|
79
|
+
vertical drag then chains to the sheet scroller. WebKit touch only. */
|
|
80
|
+
@supports (-webkit-touch-callout: none) {
|
|
81
|
+
[data-ybs-root][data-ybs-modeless] [data-ybs-sheet] > * {
|
|
82
|
+
overflow-x: scroll; overflow-y: hidden; overscroll-behavior-x: none; scrollbar-width: none;
|
|
83
|
+
}
|
|
84
|
+
[data-ybs-root][data-ybs-modeless] [data-ybs-sheet] > *::-webkit-scrollbar { display: none; }
|
|
85
|
+
/* 1px of horizontal overflow, measured from the padding box so side padding can't absorb it. */
|
|
86
|
+
[data-ybs-root][data-ybs-modeless] [data-ybs-sheet] > *:not([data-ybs-header]) { position: relative; }
|
|
87
|
+
[data-ybs-root][data-ybs-modeless] [data-ybs-sheet] > *::after {
|
|
88
|
+
content: ''; position: absolute; top: 0; left: 0; width: calc(100% + 1px); height: 1px; pointer-events: none;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/* Stacked: a covered sheet is inert (set by the sheet above), so it can't be scrolled. Don't toggle its
|
|
92
|
+
overflow: Firefox re-snaps on overflow changes. The visual recede lives in /styled. */
|
|
93
|
+
@media (pointer: fine) {
|
|
94
|
+
[data-ybs-drag] { cursor: grab; user-select: none; -webkit-user-select: none; }
|
|
95
|
+
[data-ybs-root][data-ybs-drag="active"] [data-ybs-drag] { cursor: grabbing; }
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yourrealm/bottom-sheet",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native-feeling bottom sheet for the web. One native scroller for sheet travel and content.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/yourrealm/bottom-sheet.git",
|
|
9
|
+
"directory": "packages/bottom-sheet"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://yourrealm.github.io/bottom-sheet/",
|
|
12
|
+
"bugs": "https://github.com/yourrealm/bottom-sheet/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react",
|
|
15
|
+
"bottom-sheet",
|
|
16
|
+
"sheet",
|
|
17
|
+
"drawer",
|
|
18
|
+
"scroll-snap",
|
|
19
|
+
"mobile",
|
|
20
|
+
"headless"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"*.css"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./styled": {
|
|
38
|
+
"types": "./dist/styled.d.ts",
|
|
39
|
+
"import": "./dist/styled.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json",
|
|
42
|
+
"./styles.css": "./dist/styles.css",
|
|
43
|
+
"./styled.css": "./dist/styled.css"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=19",
|
|
47
|
+
"react-dom": ">=19"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "^19.3.0",
|
|
51
|
+
"@types/react-dom": "^19.2.0",
|
|
52
|
+
"react": "^19.3.1",
|
|
53
|
+
"react-dom": "^19.3.1",
|
|
54
|
+
"tsdown": "^0.23.4",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vitest": "^5.0.5"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown && node scripts/build-css.mjs",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"typecheck": "tsc -p .",
|
|
62
|
+
"pack:check": "node scripts/prepack.mjs && pnpm build && pnpm pack; rm -f README.md LICENSE",
|
|
63
|
+
"release": "node scripts/prepack.mjs && pnpm build && pnpm publish --access public; rm -f README.md LICENSE"
|
|
64
|
+
}
|
|
6
65
|
}
|