@hogoshi/ui 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/components/chrome-veil.tsx +20 -0
- package/src/components/context-menu.tsx +145 -37
- package/src/components/dialog.tsx +10 -2
- package/src/components/image-viewer.tsx +1 -1
- package/src/components/navbar.tsx +5 -3
- package/src/components/otp.tsx +11 -1
- package/src/components/sidebar.tsx +16 -12
- package/src/index.ts +1 -0
- package/src/styles.css +28 -0
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "../lib/cn";
|
|
2
|
+
|
|
3
|
+
const sides = {
|
|
4
|
+
top: "top-0 h-[calc(var(--safe-top)+2rem)] bg-gradient-to-b [mask-image:linear-gradient(to_bottom,black_30%,transparent)]",
|
|
5
|
+
bottom: "bottom-0 h-[var(--bottombar-space)] bg-gradient-to-t [mask-image:linear-gradient(to_top,black_30%,transparent)]",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function ChromeVeil({ side, fixed = false, className }: { side: "top" | "bottom"; fixed?: boolean; className?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
aria-hidden
|
|
12
|
+
className={cn(
|
|
13
|
+
"from-bg pointer-events-none inset-x-0 to-transparent backdrop-blur-xl",
|
|
14
|
+
fixed ? "fixed z-[calc(var(--z-sticky)-1)]" : "absolute",
|
|
15
|
+
sides[side],
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
|
|
4
4
|
import { ClipboardIcon, Copy01Icon, ScissorIcon, TextIcon } from "@hugeicons-pro/core-stroke-rounded";
|
|
5
|
-
import type
|
|
5
|
+
import { useEffect, useState, type ComponentProps, type ReactNode } from "react";
|
|
6
6
|
import { cn } from "../lib/cn";
|
|
7
7
|
import type { AppIcon } from "./icon";
|
|
8
8
|
import { Icon } from "./icon";
|
|
@@ -16,6 +16,7 @@ export interface ContextMenuEntry {
|
|
|
16
16
|
icon?: AppIcon;
|
|
17
17
|
onSelect?: () => void;
|
|
18
18
|
destructive?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
19
20
|
items?: ContextMenuEntry[];
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -23,55 +24,141 @@ function selectionText() {
|
|
|
23
24
|
return window.getSelection()?.toString() ?? "";
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
type Field = HTMLInputElement | HTMLTextAreaElement;
|
|
28
|
+
|
|
29
|
+
type SavedEdit = {
|
|
30
|
+
field: Field | null;
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
text: string;
|
|
34
|
+
value: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const emptyEdit: SavedEdit = { field: null, start: 0, end: 0, text: "", value: "" };
|
|
38
|
+
let saved: SavedEdit = emptyEdit;
|
|
39
|
+
let expanded: SavedEdit = emptyEdit;
|
|
40
|
+
let collapsedAt = 0;
|
|
41
|
+
let pasteText = "";
|
|
42
|
+
const pasteListeners = new Set<(ready: boolean) => void>();
|
|
43
|
+
|
|
44
|
+
function isField(node: EventTarget | null): node is Field {
|
|
45
|
+
return node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function publishPaste(text: string) {
|
|
49
|
+
pasteText = text;
|
|
50
|
+
pasteListeners.forEach((listener) => listener(Boolean(text)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rememberField(field: Field, start: number, end: number) {
|
|
54
|
+
const next: SavedEdit = {
|
|
55
|
+
field,
|
|
56
|
+
start,
|
|
57
|
+
end,
|
|
58
|
+
text: field.value.slice(Math.min(start, end), Math.max(start, end)),
|
|
59
|
+
value: field.value,
|
|
60
|
+
};
|
|
61
|
+
saved = next;
|
|
62
|
+
if (start !== end) expanded = next;
|
|
63
|
+
else collapsedAt = performance.now();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function captureEditTarget() {
|
|
67
|
+
const active = document.activeElement;
|
|
68
|
+
if (isField(active)) {
|
|
69
|
+
const start = active.selectionStart ?? 0;
|
|
70
|
+
const end = active.selectionEnd ?? 0;
|
|
71
|
+
const clearedByClick =
|
|
72
|
+
start === end &&
|
|
73
|
+
expanded.field === active &&
|
|
74
|
+
expanded.start !== expanded.end &&
|
|
75
|
+
performance.now() - collapsedAt < 300;
|
|
76
|
+
if (clearedByClick) {
|
|
77
|
+
active.setSelectionRange(expanded.start, expanded.end);
|
|
78
|
+
saved = { ...expanded, value: active.value };
|
|
79
|
+
} else {
|
|
80
|
+
rememberField(active, start, end);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const text = selectionText();
|
|
84
|
+
saved = text ? { ...emptyEdit, text } : { ...emptyEdit };
|
|
85
|
+
}
|
|
86
|
+
void navigator.clipboard.readText().then(publishPaste).catch(() => publishPaste(""));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof document !== "undefined") {
|
|
90
|
+
document.addEventListener(
|
|
91
|
+
"selectionchange",
|
|
92
|
+
() => {
|
|
93
|
+
const active = document.activeElement;
|
|
94
|
+
if (!isField(active)) return;
|
|
95
|
+
rememberField(active, active.selectionStart ?? 0, active.selectionEnd ?? 0);
|
|
96
|
+
},
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
document.addEventListener("contextmenu", captureEditTarget, true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function restoreField() {
|
|
103
|
+
const field = saved.field;
|
|
104
|
+
if (!field || !field.isConnected) return null;
|
|
105
|
+
field.focus({ preventScroll: true });
|
|
106
|
+
field.setSelectionRange(saved.start, saved.end);
|
|
107
|
+
return field;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeField(next: string, cursor: number) {
|
|
111
|
+
const field = restoreField();
|
|
112
|
+
if (!field) return;
|
|
113
|
+
field.setRangeText(next, saved.start, saved.end, "end");
|
|
114
|
+
field.setSelectionRange(cursor, cursor);
|
|
115
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
116
|
+
}
|
|
117
|
+
|
|
26
118
|
function cutSelection() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
119
|
+
if (!saved.text || !saved.field) return;
|
|
120
|
+
void navigator.clipboard.writeText(saved.text);
|
|
121
|
+
writeField("", saved.start);
|
|
30
122
|
}
|
|
31
123
|
|
|
32
124
|
function copySelection() {
|
|
33
|
-
|
|
34
|
-
|
|
125
|
+
if (!saved.text) return;
|
|
126
|
+
void navigator.clipboard.writeText(saved.text);
|
|
127
|
+
restoreField();
|
|
35
128
|
}
|
|
36
129
|
|
|
37
130
|
async function pasteSelection() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return;
|
|
131
|
+
let text = pasteText;
|
|
132
|
+
if (!text) {
|
|
133
|
+
try {
|
|
134
|
+
text = await navigator.clipboard.readText();
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
46
138
|
}
|
|
47
|
-
|
|
139
|
+
if (!text || !saved.field) return;
|
|
140
|
+
writeField(text, saved.start + text.length);
|
|
48
141
|
}
|
|
49
142
|
|
|
50
143
|
function selectAll() {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
const selection = window.getSelection();
|
|
57
|
-
const node = selection?.anchorNode?.parentElement;
|
|
58
|
-
if (!node || !selection) return;
|
|
59
|
-
const range = document.createRange();
|
|
60
|
-
range.selectNodeContents(node);
|
|
61
|
-
selection.removeAllRanges();
|
|
62
|
-
selection.addRange(range);
|
|
144
|
+
const field = saved.field;
|
|
145
|
+
if (!field || !field.value) return;
|
|
146
|
+
field.focus({ preventScroll: true });
|
|
147
|
+
field.setSelectionRange(0, field.value.length);
|
|
63
148
|
}
|
|
64
149
|
|
|
65
|
-
function editEntries(): ContextMenuEntry[] {
|
|
150
|
+
function editEntries(canPaste: boolean): ContextMenuEntry[] {
|
|
151
|
+
const canEdit = Boolean(saved.text);
|
|
152
|
+
const canSelect = Boolean(saved.field?.value);
|
|
66
153
|
return [
|
|
67
154
|
{
|
|
68
155
|
id: "edit",
|
|
69
156
|
label: "Edit",
|
|
70
157
|
items: [
|
|
71
|
-
{ id: "cut", label: "Cut", icon: ScissorIcon, onSelect: cutSelection },
|
|
72
|
-
{ id: "copy", label: "Copy", icon: Copy01Icon, onSelect: copySelection },
|
|
73
|
-
{ id: "paste", label: "Paste", icon: ClipboardIcon, onSelect: () => void pasteSelection() },
|
|
74
|
-
{ id: "select-all", label: "Select all", icon: TextIcon, onSelect: selectAll },
|
|
158
|
+
{ id: "cut", label: "Cut", icon: ScissorIcon, onSelect: cutSelection, disabled: !canEdit || !saved.field },
|
|
159
|
+
{ id: "copy", label: "Copy", icon: Copy01Icon, onSelect: copySelection, disabled: !canEdit },
|
|
160
|
+
{ id: "paste", label: "Paste", icon: ClipboardIcon, onSelect: () => void pasteSelection(), disabled: !canPaste || !saved.field },
|
|
161
|
+
{ id: "select-all", label: "Select all", icon: TextIcon, onSelect: selectAll, disabled: !canSelect },
|
|
75
162
|
],
|
|
76
163
|
},
|
|
77
164
|
];
|
|
@@ -80,16 +167,36 @@ function editEntries(): ContextMenuEntry[] {
|
|
|
80
167
|
export const ContextMenuRoot = BaseContextMenu.Root;
|
|
81
168
|
export const ContextMenuTrigger = BaseContextMenu.Trigger;
|
|
82
169
|
|
|
83
|
-
export function ContextMenuContent({
|
|
170
|
+
export function ContextMenuContent({
|
|
171
|
+
edit = true,
|
|
172
|
+
className,
|
|
173
|
+
children,
|
|
174
|
+
...props
|
|
175
|
+
}: ComponentProps<typeof BaseContextMenu.Popup> & { edit?: boolean }) {
|
|
176
|
+
const [canPaste, setCanPaste] = useState(Boolean(pasteText));
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!edit) return;
|
|
179
|
+
const listener = (ready: boolean) => setCanPaste(ready);
|
|
180
|
+
pasteListeners.add(listener);
|
|
181
|
+
setCanPaste(Boolean(pasteText));
|
|
182
|
+
return () => {
|
|
183
|
+
pasteListeners.delete(listener);
|
|
184
|
+
};
|
|
185
|
+
}, [edit]);
|
|
186
|
+
|
|
84
187
|
return (
|
|
85
188
|
<BaseContextMenu.Portal>
|
|
86
189
|
<BaseContextMenu.Positioner className="z-[var(--z-popover)]">
|
|
87
|
-
<BaseContextMenu.Popup
|
|
190
|
+
<BaseContextMenu.Popup
|
|
191
|
+
className={cn(popupClass, className)}
|
|
192
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
193
|
+
{...props}
|
|
194
|
+
>
|
|
88
195
|
<Glide selector={HIGHLIGHTED} />
|
|
89
|
-
<EntryList items={editEntries()} />
|
|
196
|
+
{edit ? <EntryList items={editEntries(canPaste)} /> : null}
|
|
90
197
|
{children ? (
|
|
91
198
|
<>
|
|
92
|
-
<Separator className="my-1" />
|
|
199
|
+
{edit ? <Separator className="my-1" /> : null}
|
|
93
200
|
{children}
|
|
94
201
|
</>
|
|
95
202
|
) : null}
|
|
@@ -113,8 +220,9 @@ function EntryList({ items }: { items: ContextMenuEntry[] }) {
|
|
|
113
220
|
) : (
|
|
114
221
|
<ContextMenuItem
|
|
115
222
|
key={item.id}
|
|
223
|
+
disabled={item.disabled}
|
|
116
224
|
className={item.destructive ? "text-danger data-[highlighted]:text-danger" : undefined}
|
|
117
|
-
onClick={item.onSelect}
|
|
225
|
+
onClick={item.disabled ? undefined : item.onSelect}
|
|
118
226
|
>
|
|
119
227
|
{item.icon ? <Icon icon={item.icon} variant="stroke" size={16} /> : null}
|
|
120
228
|
{item.label}
|
|
@@ -13,10 +13,18 @@ export const DialogClose = BaseDialog.Close;
|
|
|
13
13
|
export const DialogTitle = BaseDialog.Title;
|
|
14
14
|
export const DialogDescription = BaseDialog.Description;
|
|
15
15
|
|
|
16
|
-
export function DialogContent({
|
|
16
|
+
export function DialogContent({
|
|
17
|
+
className,
|
|
18
|
+
children,
|
|
19
|
+
backdropProps,
|
|
20
|
+
...props
|
|
21
|
+
}: ComponentProps<typeof BaseDialog.Popup> & { backdropProps?: ComponentProps<typeof BaseDialog.Backdrop> }) {
|
|
17
22
|
return (
|
|
18
23
|
<BaseDialog.Portal>
|
|
19
|
-
<BaseDialog.Backdrop
|
|
24
|
+
<BaseDialog.Backdrop
|
|
25
|
+
className={cn("fixed inset-0 z-[var(--z-overlay)] bg-transparent", backdropMotion)}
|
|
26
|
+
{...backdropProps}
|
|
27
|
+
/>
|
|
20
28
|
<BaseDialog.Popup
|
|
21
29
|
className={cn(
|
|
22
30
|
"border-border bg-bg-elevated/70 fixed top-1/2 left-1/2 z-[var(--z-modal)] grid max-h-[calc(100dvh-max(1rem,var(--safe-top))-max(1rem,var(--safe-bottom)))] w-[min(32rem,calc(100vw-max(1rem,var(--safe-left))-max(1rem,var(--safe-right))))] max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-[var(--radius-lg)] border p-6 shadow-[var(--shadow-lg)] backdrop-blur-xl outline-none",
|
|
@@ -204,7 +204,7 @@ export function ImageViewer({
|
|
|
204
204
|
}}
|
|
205
205
|
>
|
|
206
206
|
<BaseDialog.Title className="sr-only">{image.alt ?? fileName(image)}</BaseDialog.Title>
|
|
207
|
-
<div className="flex shrink-0 items-center gap-2 px-3 pt-[calc(0.75rem+var(--safe-top))] pr-[calc(0.75rem+var(--safe-right))] pb-3 pl-[
|
|
207
|
+
<div className="flex shrink-0 items-center gap-2 px-3 pt-[max(3.25rem,calc(0.75rem+var(--safe-top)))] pr-[calc(0.75rem+var(--safe-right))] pb-3 pl-[max(1rem,var(--safe-left))]">
|
|
208
208
|
<p className="text-fg-muted min-w-0 flex-1 truncate text-sm">
|
|
209
209
|
{count > 1 ? <span className="text-fg mr-2 tabular-nums">{index + 1} / {count}</span> : null}
|
|
210
210
|
{image.alt ?? fileName(image)}
|
|
@@ -5,6 +5,7 @@ import { NavigationMenu } from "@base-ui/react/navigation-menu";
|
|
|
5
5
|
import { ArrowDown01Icon } from "@hugeicons-pro/core-stroke-rounded";
|
|
6
6
|
import { useEffect, useState, type ComponentProps, type ReactNode } from "react";
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
|
+
import { ChromeVeil } from "./chrome-veil";
|
|
8
9
|
import { Glide, glideActive } from "./glide";
|
|
9
10
|
import type { AppIcon } from "./icon";
|
|
10
11
|
import { Icon } from "./icon";
|
|
@@ -31,12 +32,13 @@ export function TopBar({ contained = false, children, className }: { contained?:
|
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<header className={cn("pointer-events-none inset-x-0 top-0 z-[var(--z-sticky)]", contained ? "absolute" : "fixed", className)}>
|
|
35
|
+
<ChromeVeil side="top" />
|
|
34
36
|
<div
|
|
35
37
|
className={cn(
|
|
36
|
-
"pointer-events-auto relative mx-auto flex h-12 items-center border transition-[width,
|
|
38
|
+
"pointer-events-auto relative mx-auto flex h-12 translate-y-[var(--topbar-offset)] items-center border transition-[width,border-radius,background-color,border-color,box-shadow] duration-[calc(var(--motion-duration)*4)] ease-[var(--motion-ease)]",
|
|
37
39
|
compact
|
|
38
|
-
? "border-border bg-bg-elevated w-[min(40rem,calc(100%-max(0.75rem,var(--safe-left))-max(0.75rem,var(--safe-right))))]
|
|
39
|
-
: "
|
|
40
|
+
? "border-border bg-bg-elevated w-[min(40rem,calc(100%-max(0.75rem,var(--safe-left))-max(0.75rem,var(--safe-right))))] rounded-full px-2 shadow-[var(--shadow-md)]"
|
|
41
|
+
: "w-full border-transparent bg-transparent pl-[max(0.75rem,var(--safe-left))] pr-[max(0.75rem,var(--safe-right))]",
|
|
40
42
|
)}
|
|
41
43
|
>
|
|
42
44
|
<Navbar className="relative h-full w-full bg-transparent px-1 shadow-none backdrop-blur-none">{children}</Navbar>
|
package/src/components/otp.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { OTPField } from "@base-ui/react/otp-field";
|
|
4
|
-
import { useRef, useState, type ComponentProps } from "react";
|
|
4
|
+
import { useEffect, useRef, useState, type ComponentProps } from "react";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
|
|
7
7
|
const slotClass =
|
|
@@ -51,6 +51,7 @@ function OtpSlot({ text }: { text: string }) {
|
|
|
51
51
|
type OtpProps = Omit<ComponentProps<typeof OTPField.Root>, "length" | "mask"> & {
|
|
52
52
|
length?: number;
|
|
53
53
|
mask?: boolean;
|
|
54
|
+
autoFocus?: boolean;
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
export function Otp({
|
|
@@ -61,12 +62,21 @@ export function Otp({
|
|
|
61
62
|
defaultValue = "",
|
|
62
63
|
onValueChange,
|
|
63
64
|
onValueComplete,
|
|
65
|
+
autoFocus,
|
|
64
66
|
...props
|
|
65
67
|
}: OtpProps) {
|
|
66
68
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
67
69
|
const [inner, setInner] = useState(defaultValue);
|
|
68
70
|
const current = value ?? inner;
|
|
69
71
|
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!autoFocus) return;
|
|
74
|
+
const id = requestAnimationFrame(() => {
|
|
75
|
+
rootRef.current?.querySelector<HTMLInputElement>("input")?.focus();
|
|
76
|
+
});
|
|
77
|
+
return () => cancelAnimationFrame(id);
|
|
78
|
+
}, [autoFocus]);
|
|
79
|
+
|
|
70
80
|
return (
|
|
71
81
|
<OTPField.Root
|
|
72
82
|
ref={rootRef}
|
|
@@ -4,6 +4,7 @@ import { Button as BaseButton } from "@base-ui/react/button";
|
|
|
4
4
|
import { SidebarLeftIcon } from "@hugeicons-pro/core-stroke-rounded";
|
|
5
5
|
import { createContext, useContext, useEffect, useMemo, useState, type ComponentProps, type ReactNode } from "react";
|
|
6
6
|
import { cn } from "../lib/cn";
|
|
7
|
+
import { ChromeVeil } from "./chrome-veil";
|
|
7
8
|
import type { AppIcon, IconVariant } from "./icon";
|
|
8
9
|
import { Glide, glideActive } from "./glide";
|
|
9
10
|
import { Icon } from "./icon";
|
|
@@ -152,18 +153,21 @@ export function SidebarItem({
|
|
|
152
153
|
|
|
153
154
|
export function BottomBar({ className, children, ...props }: ComponentProps<"nav">) {
|
|
154
155
|
return (
|
|
155
|
-
|
|
156
|
-
className=
|
|
157
|
-
|
|
158
|
-
className
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
<>
|
|
157
|
+
<ChromeVeil side="bottom" fixed className="md:hidden" />
|
|
158
|
+
<nav
|
|
159
|
+
className={cn(
|
|
160
|
+
"bg-bg-elevated/70 border-border fixed right-[max(0.75rem,var(--safe-right))] bottom-[var(--bottombar-offset)] left-[max(0.75rem,var(--safe-left))] z-[var(--z-sticky)] rounded-[var(--radius-lg)] border p-1 shadow-[var(--shadow-md)] backdrop-blur-xl md:hidden",
|
|
161
|
+
className,
|
|
162
|
+
)}
|
|
163
|
+
{...props}
|
|
164
|
+
>
|
|
165
|
+
<div className="relative flex items-stretch justify-around">
|
|
166
|
+
<Glide selector={ACTIVE} className="bg-accent-muted/60 rounded-[var(--radius-md)]" />
|
|
167
|
+
{children}
|
|
168
|
+
</div>
|
|
169
|
+
</nav>
|
|
170
|
+
</>
|
|
167
171
|
);
|
|
168
172
|
}
|
|
169
173
|
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ export { Progress } from "./components/progress";
|
|
|
42
42
|
export { Logo } from "./components/logo";
|
|
43
43
|
export { LOGO_PATH, LOGO_VIEWBOX, logoSvg } from "./components/logo-mark";
|
|
44
44
|
export { Navbar, NavbarItem, NavbarMenu, NavbarMenuLink, NavbarMenus, TopBar } from "./components/navbar";
|
|
45
|
+
export { ChromeVeil } from "./components/chrome-veil";
|
|
45
46
|
export { Glide, glideActive } from "./components/glide";
|
|
46
47
|
export type { GlideProps } from "./components/glide";
|
|
47
48
|
export { Calendar } from "./components/calendar";
|
package/src/styles.css
CHANGED
|
@@ -77,6 +77,34 @@ body {
|
|
|
77
77
|
--safe-right: env(safe-area-inset-right, 0px);
|
|
78
78
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
79
79
|
--safe-left: env(safe-area-inset-left, 0px);
|
|
80
|
+
--app-height: 100dvh;
|
|
81
|
+
--topbar-offset: calc(var(--safe-top) + 0.75rem);
|
|
82
|
+
--topbar-space: calc(var(--topbar-offset) + 3.75rem);
|
|
83
|
+
--bottombar-offset: max(0.5rem, var(--safe-bottom));
|
|
84
|
+
--bottombar-space: calc(var(--bottombar-offset) + 4.25rem);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (display-mode: standalone) {
|
|
88
|
+
:root {
|
|
89
|
+
--app-height: 100vh;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
html,
|
|
94
|
+
body {
|
|
95
|
+
min-height: var(--app-height);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@utility h-app {
|
|
99
|
+
height: var(--app-height);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@utility min-h-app {
|
|
103
|
+
min-height: var(--app-height);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@utility max-h-app {
|
|
107
|
+
max-height: var(--app-height);
|
|
80
108
|
}
|
|
81
109
|
|
|
82
110
|
html {
|