@flamingo-stack/openframe-frontend-core 0.0.535-1762.4618.1 → 0.0.535
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/{chunk-GF5A23J3.cjs → chunk-2P4J2YLI.cjs} +98 -98
- package/dist/{chunk-GF5A23J3.cjs.map → chunk-2P4J2YLI.cjs.map} +1 -1
- package/dist/{chunk-GS4HJA2G.js → chunk-4A736Z6S.js} +74 -20
- package/dist/chunk-4A736Z6S.js.map +1 -0
- package/dist/{chunk-GBWS62LR.js → chunk-4DTUUM5V.js} +45 -25
- package/dist/{chunk-GBWS62LR.js.map → chunk-4DTUUM5V.js.map} +1 -1
- package/dist/{chunk-JW4BTXVH.cjs → chunk-4HQFV4PK.cjs} +14 -14
- package/dist/{chunk-JW4BTXVH.cjs.map → chunk-4HQFV4PK.cjs.map} +1 -1
- package/dist/{chunk-GYONSC3U.cjs → chunk-4QYUNDS3.cjs} +3 -3
- package/dist/{chunk-GYONSC3U.cjs.map → chunk-4QYUNDS3.cjs.map} +1 -1
- package/dist/{chunk-T743Z3KN.cjs → chunk-5OLF6WXF.cjs} +40 -40
- package/dist/{chunk-T743Z3KN.cjs.map → chunk-5OLF6WXF.cjs.map} +1 -1
- package/dist/{chunk-SXLEPNY2.cjs → chunk-5RJUHUCI.cjs} +106 -52
- package/dist/chunk-5RJUHUCI.cjs.map +1 -0
- package/dist/{chunk-ZUVBTDLG.cjs → chunk-7MHYRBBQ.cjs} +5 -5
- package/dist/{chunk-ZUVBTDLG.cjs.map → chunk-7MHYRBBQ.cjs.map} +1 -1
- package/dist/{chunk-T4GNFVT3.cjs → chunk-E3YB55RR.cjs} +173 -153
- package/dist/chunk-E3YB55RR.cjs.map +1 -0
- package/dist/{chunk-JWWZFEFI.js → chunk-EHPQKXXW.js} +3 -3
- package/dist/{chunk-JZMQQBUC.cjs → chunk-GV4FDDFV.cjs} +18 -18
- package/dist/{chunk-JZMQQBUC.cjs.map → chunk-GV4FDDFV.cjs.map} +1 -1
- package/dist/{chunk-XPKCOZQE.js → chunk-JVO5CYMK.js} +8 -8
- package/dist/{chunk-6OZWDOP5.js → chunk-K6566PEB.js} +3 -3
- package/dist/{chunk-WXRQYGE5.js → chunk-KLFGP6PR.js} +2 -2
- package/dist/{chunk-7TQDW3PE.js → chunk-LIQJAC6Z.js} +2 -2
- package/dist/{chunk-GS4R5UQ7.js → chunk-LIX5OUHH.js} +2 -2
- package/dist/{chunk-RCBRCZUF.js → chunk-LQPHB2YF.js} +2 -2
- package/dist/{chunk-5OPVJBHQ.js → chunk-MZVFZMQ6.js} +6 -6
- package/dist/{chunk-URBYMQ4Z.js → chunk-N3AEJTCK.js} +5 -5
- package/dist/{chunk-DRJC7TTW.js → chunk-NB6BP6JB.js} +3 -3
- package/dist/{chunk-DK6IYFLD.cjs → chunk-P65EZEBW.cjs} +12 -12
- package/dist/{chunk-DK6IYFLD.cjs.map → chunk-P65EZEBW.cjs.map} +1 -1
- package/dist/{chunk-JWJQHURJ.js → chunk-PLGITXT7.js} +2 -2
- package/dist/{chunk-HSRHPF32.cjs → chunk-PSBL3S2B.cjs} +7 -7
- package/dist/{chunk-HSRHPF32.cjs.map → chunk-PSBL3S2B.cjs.map} +1 -1
- package/dist/{chunk-A5EA3YO7.js → chunk-QUVIT4JP.js} +6 -6
- package/dist/{chunk-GRGFMXNO.cjs → chunk-RO67OOKJ.cjs} +40 -40
- package/dist/{chunk-GRGFMXNO.cjs.map → chunk-RO67OOKJ.cjs.map} +1 -1
- package/dist/{chunk-2AMYXNQ2.cjs → chunk-TS2EHMEX.cjs} +29 -29
- package/dist/{chunk-2AMYXNQ2.cjs.map → chunk-TS2EHMEX.cjs.map} +1 -1
- package/dist/{chunk-WL3PP4D6.cjs → chunk-U2Q7GEX2.cjs} +32 -32
- package/dist/{chunk-WL3PP4D6.cjs.map → chunk-U2Q7GEX2.cjs.map} +1 -1
- package/dist/{chunk-QHOVACZK.js → chunk-VPDTDKO5.js} +2 -2
- package/dist/{chunk-ENOER2QA.cjs → chunk-Y55XQFJK.cjs} +75 -75
- package/dist/{chunk-ENOER2QA.cjs.map → chunk-Y55XQFJK.cjs.map} +1 -1
- package/dist/{chunk-HNZDQ4SH.js → chunk-Y72MXG5N.js} +4 -4
- package/dist/{chunk-CMIHA6TT.cjs → chunk-ZKKEKG73.cjs} +3 -3
- package/dist/{chunk-CMIHA6TT.cjs.map → chunk-ZKKEKG73.cjs.map} +1 -1
- package/dist/{chunk-TOWXHZF5.cjs → chunk-ZR5CJTNR.cjs} +4 -4
- package/dist/{chunk-TOWXHZF5.cjs.map → chunk-ZR5CJTNR.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +24 -24
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +138 -138
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +40 -40
- package/dist/components/meeting-scheduler/index.js +4 -4
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/search-input.d.ts.map +1 -1
- package/dist/components/ui/searchable-select.d.ts.map +1 -1
- package/dist/components/ui/tag-select-dropdown.d.ts.map +1 -1
- package/dist/components/ui/tags-manager.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-keyboard-collision-padding.d.ts +2 -0
- package/dist/hooks/ui/use-keyboard-collision-padding.d.ts.map +1 -0
- package/dist/index.cjs +5 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -2
- package/package.json +1 -1
- package/src/components/popover.tsx +22 -14
- package/src/components/ui/autocomplete.tsx +9 -2
- package/src/components/ui/search-input.tsx +9 -2
- package/src/components/ui/searchable-select.tsx +9 -2
- package/src/components/ui/tag-select-dropdown.tsx +8 -2
- package/src/components/ui/tags-manager.tsx +9 -2
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/use-keyboard-collision-padding.ts +101 -0
- package/dist/chunk-GS4HJA2G.js.map +0 -1
- package/dist/chunk-SXLEPNY2.cjs.map +0 -1
- package/dist/chunk-T4GNFVT3.cjs.map +0 -1
- /package/dist/{chunk-JWWZFEFI.js.map → chunk-EHPQKXXW.js.map} +0 -0
- /package/dist/{chunk-XPKCOZQE.js.map → chunk-JVO5CYMK.js.map} +0 -0
- /package/dist/{chunk-6OZWDOP5.js.map → chunk-K6566PEB.js.map} +0 -0
- /package/dist/{chunk-WXRQYGE5.js.map → chunk-KLFGP6PR.js.map} +0 -0
- /package/dist/{chunk-7TQDW3PE.js.map → chunk-LIQJAC6Z.js.map} +0 -0
- /package/dist/{chunk-GS4R5UQ7.js.map → chunk-LIX5OUHH.js.map} +0 -0
- /package/dist/{chunk-RCBRCZUF.js.map → chunk-LQPHB2YF.js.map} +0 -0
- /package/dist/{chunk-5OPVJBHQ.js.map → chunk-MZVFZMQ6.js.map} +0 -0
- /package/dist/{chunk-URBYMQ4Z.js.map → chunk-N3AEJTCK.js.map} +0 -0
- /package/dist/{chunk-DRJC7TTW.js.map → chunk-NB6BP6JB.js.map} +0 -0
- /package/dist/{chunk-JWJQHURJ.js.map → chunk-PLGITXT7.js.map} +0 -0
- /package/dist/{chunk-A5EA3YO7.js.map → chunk-QUVIT4JP.js.map} +0 -0
- /package/dist/{chunk-QHOVACZK.js.map → chunk-VPDTDKO5.js.map} +0 -0
- /package/dist/{chunk-HNZDQ4SH.js.map → chunk-Y72MXG5N.js.map} +0 -0
|
@@ -27,6 +27,7 @@ import { Loader2 } from "lucide-react"
|
|
|
27
27
|
import { cn } from "../../utils/cn"
|
|
28
28
|
import { TruncateText } from "./truncate-text"
|
|
29
29
|
import { useAutoLimitTags } from "../../hooks/ui/use-auto-limit-tags"
|
|
30
|
+
import { useKeyboardCollisionPadding } from "../../hooks/ui/use-keyboard-collision-padding"
|
|
30
31
|
import { FieldWrapper } from "./field-wrapper"
|
|
31
32
|
import { HiddenTagsPopup } from "./hidden-tags-popup"
|
|
32
33
|
import { Tag } from "./tag"
|
|
@@ -190,6 +191,7 @@ function AutocompleteInner<T = string>(
|
|
|
190
191
|
|
|
191
192
|
const [isOpen, setIsOpen] = useState(false)
|
|
192
193
|
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
194
|
+
const keyboardPadding = useKeyboardCollisionPadding()
|
|
193
195
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
194
196
|
const hiddenTagsPopupRef = useRef<HTMLDivElement>(null)
|
|
195
197
|
|
|
@@ -561,12 +563,17 @@ function AutocompleteInner<T = string>(
|
|
|
561
563
|
className={cn(
|
|
562
564
|
"z-50 w-[var(--radix-popover-trigger-width)] mt-1",
|
|
563
565
|
"bg-ods-card border border-ods-border rounded-[4px]",
|
|
566
|
+
"flex flex-col overflow-hidden max-h-[var(--radix-popper-available-height)]",
|
|
564
567
|
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
565
568
|
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
566
569
|
dropdownClassName
|
|
567
570
|
)}
|
|
568
571
|
sideOffset={4}
|
|
569
572
|
align="start"
|
|
573
|
+
// The anchor is the text field that raises the software keyboard, so
|
|
574
|
+
// this list is always positioned with the keyboard up — see
|
|
575
|
+
// useKeyboardCollisionPadding.
|
|
576
|
+
collisionPadding={{ bottom: keyboardPadding }}
|
|
570
577
|
onOpenAutoFocus={(e) => {
|
|
571
578
|
e.preventDefault()
|
|
572
579
|
autoLimitTags.inputRef.current?.focus()
|
|
@@ -578,8 +585,8 @@ function AutocompleteInner<T = string>(
|
|
|
578
585
|
}
|
|
579
586
|
}}
|
|
580
587
|
>
|
|
581
|
-
<ScrollAreaPrimitive.Root className="overflow-hidden">
|
|
582
|
-
<ScrollAreaPrimitive.Viewport className="max-h-[240px] w-full [&>div]:!block">
|
|
588
|
+
<ScrollAreaPrimitive.Root className="flex min-h-0 flex-col overflow-hidden">
|
|
589
|
+
<ScrollAreaPrimitive.Viewport className="min-h-0 max-h-[240px] w-full [&>div]:!block">
|
|
583
590
|
<div role="listbox">
|
|
584
591
|
{loading ? (
|
|
585
592
|
<div className="px-3 py-2 text-ods-text-secondary text-h6">
|
|
@@ -6,6 +6,7 @@ import * as React from "react"
|
|
|
6
6
|
import { cn } from "../../utils/cn"
|
|
7
7
|
import { useDebounce } from "../../hooks/ui/use-debounce"
|
|
8
8
|
import { useAutoLimitTags } from "../../hooks/ui/use-auto-limit-tags"
|
|
9
|
+
import { useKeyboardCollisionPadding } from "../../hooks/ui/use-keyboard-collision-padding"
|
|
9
10
|
import { SearchIcon } from "../icons-v2-generated"
|
|
10
11
|
import { XmarkCircleIcon } from "../icons-v2-generated/signs-and-symbols/xmark-circle-icon"
|
|
11
12
|
import { Tag } from "./tag"
|
|
@@ -168,6 +169,7 @@ export function SearchInput({
|
|
|
168
169
|
// ---- Popover state ----
|
|
169
170
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
170
171
|
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
|
|
172
|
+
const keyboardPadding = useKeyboardCollisionPadding()
|
|
171
173
|
|
|
172
174
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
173
175
|
|
|
@@ -499,6 +501,7 @@ export function SearchInput({
|
|
|
499
501
|
className={cn(
|
|
500
502
|
"z-50 w-[var(--radix-popover-trigger-width)] mt-1",
|
|
501
503
|
"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden shadow-lg",
|
|
504
|
+
"flex flex-col max-h-[var(--radix-popper-available-height)]",
|
|
502
505
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
503
506
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
504
507
|
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
@@ -507,6 +510,10 @@ export function SearchInput({
|
|
|
507
510
|
)}
|
|
508
511
|
sideOffset={4}
|
|
509
512
|
align="start"
|
|
513
|
+
// The anchor is the search field that raises the software keyboard,
|
|
514
|
+
// so these suggestions are always positioned with the keyboard up —
|
|
515
|
+
// see useKeyboardCollisionPadding.
|
|
516
|
+
collisionPadding={{ bottom: keyboardPadding }}
|
|
510
517
|
onOpenAutoFocus={(e) => {
|
|
511
518
|
e.preventDefault()
|
|
512
519
|
inputRef.current?.focus()
|
|
@@ -517,8 +524,8 @@ export function SearchInput({
|
|
|
517
524
|
}
|
|
518
525
|
}}
|
|
519
526
|
>
|
|
520
|
-
<ScrollAreaPrimitive.Root className="overflow-hidden">
|
|
521
|
-
<ScrollAreaPrimitive.Viewport className="max-h-[320px] w-full">
|
|
527
|
+
<ScrollAreaPrimitive.Root className="flex min-h-0 flex-col overflow-hidden">
|
|
528
|
+
<ScrollAreaPrimitive.Viewport className="min-h-0 max-h-[320px] w-full">
|
|
522
529
|
<div role="listbox">
|
|
523
530
|
{renderDropdownContent()}
|
|
524
531
|
</div>
|
|
@@ -5,6 +5,7 @@ import * as React from 'react'
|
|
|
5
5
|
import { CheckIcon, SearchIcon } from '../icons-v2-generated'
|
|
6
6
|
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'
|
|
7
7
|
import { cn } from '../../utils/cn'
|
|
8
|
+
import { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding'
|
|
8
9
|
import { Input } from './input'
|
|
9
10
|
|
|
10
11
|
export interface SearchableSelectOption {
|
|
@@ -67,6 +68,7 @@ export function SearchableSelect({
|
|
|
67
68
|
}: SearchableSelectProps) {
|
|
68
69
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
69
70
|
const [search, setSearch] = React.useState('')
|
|
71
|
+
const keyboardPadding = useKeyboardCollisionPadding()
|
|
70
72
|
|
|
71
73
|
React.useEffect(() => {
|
|
72
74
|
if (!isOpen) setSearch('')
|
|
@@ -119,8 +121,13 @@ export function SearchableSelect({
|
|
|
119
121
|
<PopoverPrimitive.Content
|
|
120
122
|
align={align}
|
|
121
123
|
sideOffset={6}
|
|
124
|
+
// The search field below raises the software keyboard the moment this
|
|
125
|
+
// opens — see useKeyboardCollisionPadding for why neither the collision
|
|
126
|
+
// viewport nor the available height knows about it otherwise.
|
|
127
|
+
collisionPadding={{ bottom: keyboardPadding }}
|
|
122
128
|
className={cn(
|
|
123
129
|
'z-50 min-w-[var(--radix-popover-trigger-width)] bg-ods-card border border-ods-border rounded-md shadow-lg overflow-hidden',
|
|
130
|
+
'flex flex-col max-h-[var(--radix-popper-available-height)]',
|
|
124
131
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
125
132
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
126
133
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
@@ -128,7 +135,7 @@ export function SearchableSelect({
|
|
|
128
135
|
contentClassName,
|
|
129
136
|
)}
|
|
130
137
|
>
|
|
131
|
-
<div className="border-b border-ods-border">
|
|
138
|
+
<div className="shrink-0 border-b border-ods-border">
|
|
132
139
|
<Input
|
|
133
140
|
autoFocus
|
|
134
141
|
value={search}
|
|
@@ -138,7 +145,7 @@ export function SearchableSelect({
|
|
|
138
145
|
className="rounded-none border-0"
|
|
139
146
|
/>
|
|
140
147
|
</div>
|
|
141
|
-
<div className="max-h-80 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
148
|
+
<div className="min-h-0 max-h-80 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
142
149
|
{isLoading ? (
|
|
143
150
|
<div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary">
|
|
144
151
|
{loadingText}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
4
4
|
import * as React from 'react'
|
|
5
5
|
import { cn } from '../../utils/cn'
|
|
6
|
+
import { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding'
|
|
6
7
|
import { CheckIcon, PlusIcon, SearchIcon, TrashIcon } from '../icons-v2-generated'
|
|
7
8
|
import { Button } from './button'
|
|
8
9
|
import { Input } from './input'
|
|
@@ -59,6 +60,7 @@ export function TagSelectDropdown({
|
|
|
59
60
|
}: TagSelectDropdownProps) {
|
|
60
61
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
61
62
|
const [search, setSearch] = React.useState('')
|
|
63
|
+
const keyboardPadding = useKeyboardCollisionPadding()
|
|
62
64
|
|
|
63
65
|
React.useEffect(() => {
|
|
64
66
|
if (!isOpen) setSearch('')
|
|
@@ -110,15 +112,19 @@ export function TagSelectDropdown({
|
|
|
110
112
|
<PopoverPrimitive.Content
|
|
111
113
|
align={align}
|
|
112
114
|
sideOffset={6}
|
|
115
|
+
// The search field below raises the software keyboard the moment this
|
|
116
|
+
// opens — see useKeyboardCollisionPadding.
|
|
117
|
+
collisionPadding={{ bottom: keyboardPadding }}
|
|
113
118
|
className={cn(
|
|
114
119
|
'z-50 w-72 bg-ods-card border border-ods-border rounded-[6px] shadow-lg overflow-hidden',
|
|
120
|
+
'flex flex-col max-h-[var(--radix-popper-available-height)]',
|
|
115
121
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
116
122
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
117
123
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
118
124
|
'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',
|
|
119
125
|
)}
|
|
120
126
|
>
|
|
121
|
-
<div className="border-b border-ods-border">
|
|
127
|
+
<div className="shrink-0 border-b border-ods-border">
|
|
122
128
|
<Input
|
|
123
129
|
autoFocus
|
|
124
130
|
value={search}
|
|
@@ -134,7 +140,7 @@ export function TagSelectDropdown({
|
|
|
134
140
|
className="rounded-none border-0"
|
|
135
141
|
/>
|
|
136
142
|
</div>
|
|
137
|
-
<div className="max-h-72 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
143
|
+
<div className="min-h-0 max-h-72 overflow-y-auto py-[var(--spacing-system-xs)]" role="listbox">
|
|
138
144
|
{ordered.map(opt => {
|
|
139
145
|
const isSelected = selectedIds.includes(opt.id)
|
|
140
146
|
return (
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
5
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
6
6
|
import { cn } from "../../utils/cn";
|
|
7
|
+
import { useKeyboardCollisionPadding } from "../../hooks/ui/use-keyboard-collision-padding";
|
|
7
8
|
import {
|
|
8
9
|
CheckIcon,
|
|
9
10
|
PencilIcon,
|
|
@@ -70,6 +71,7 @@ export function TagsManager({
|
|
|
70
71
|
}: TagsManagerProps) {
|
|
71
72
|
const [open, setOpen] = React.useState(false);
|
|
72
73
|
const [search, setSearch] = React.useState("");
|
|
74
|
+
const keyboardPadding = useKeyboardCollisionPadding();
|
|
73
75
|
const [editingId, setEditingId] = React.useState<string | null>(null);
|
|
74
76
|
const [editingName, setEditingName] = React.useState("");
|
|
75
77
|
const editInputRef = React.useRef<HTMLInputElement>(null);
|
|
@@ -242,6 +244,7 @@ export function TagsManager({
|
|
|
242
244
|
className={cn(
|
|
243
245
|
"z-50 w-[var(--radix-popover-trigger-width)] mt-1",
|
|
244
246
|
"bg-ods-card border border-ods-border rounded",
|
|
247
|
+
"flex flex-col overflow-hidden max-h-[var(--radix-popper-available-height)]",
|
|
245
248
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
246
249
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
247
250
|
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
@@ -249,6 +252,10 @@ export function TagsManager({
|
|
|
249
252
|
)}
|
|
250
253
|
sideOffset={4}
|
|
251
254
|
align="start"
|
|
255
|
+
// The anchor is the tag field that raises the software keyboard, so
|
|
256
|
+
// this list is always positioned with the keyboard up — see
|
|
257
|
+
// useKeyboardCollisionPadding.
|
|
258
|
+
collisionPadding={{ bottom: keyboardPadding }}
|
|
252
259
|
onOpenAutoFocus={(e) => {
|
|
253
260
|
e.preventDefault();
|
|
254
261
|
inputRef.current?.focus();
|
|
@@ -259,8 +266,8 @@ export function TagsManager({
|
|
|
259
266
|
}
|
|
260
267
|
}}
|
|
261
268
|
>
|
|
262
|
-
<ScrollAreaPrimitive.Root className="overflow-hidden">
|
|
263
|
-
<ScrollAreaPrimitive.Viewport className="max-h-60 w-full">
|
|
269
|
+
<ScrollAreaPrimitive.Root className="flex min-h-0 flex-col overflow-hidden">
|
|
270
|
+
<ScrollAreaPrimitive.Viewport className="min-h-0 max-h-60 w-full">
|
|
264
271
|
<div role="listbox">
|
|
265
272
|
{filtered.map((tag) => {
|
|
266
273
|
const isSelected = selectedIds.includes(tag.id);
|
package/src/hooks/ui/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './use-horizontal-scrollbar'
|
|
|
8
8
|
export * from './use-image-edge-color'
|
|
9
9
|
export * from './use-is-truncated'
|
|
10
10
|
export * from './use-isomorphic-layout-effect'
|
|
11
|
+
export * from './use-keyboard-collision-padding'
|
|
11
12
|
export * from './use-local-storage'
|
|
12
13
|
export * from './use-marquee-engine'
|
|
13
14
|
export * from './use-media-query'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extra `collisionPadding.bottom` (px) a Radix popper needs to clear the
|
|
7
|
+
* software keyboard. 0 on desktop and anywhere the keyboard is down.
|
|
8
|
+
*
|
|
9
|
+
* Radix positions every popper with floating-ui at `strategy: 'fixed'`, and
|
|
10
|
+
* floating-ui takes its collision viewport from `window.visualViewport`.
|
|
11
|
+
* Neither mobile shell shrinks the box a keyboard opens into: iOS runs
|
|
12
|
+
* Capacitor's Keyboard plugin at `resize: 'none'`, which leaves the WKWebView
|
|
13
|
+
* frame alone, and Android's window is edge-to-edge, where the IME is only a
|
|
14
|
+
* WindowInsets type. So `flip`/`shift`/`size` all believe the screen still
|
|
15
|
+
* reaches the bottom, and a dropdown whose own search field just raised the
|
|
16
|
+
* keyboard is placed straight behind it — worst inside modals, which sit near
|
|
17
|
+
* the bottom of the screen by design.
|
|
18
|
+
*
|
|
19
|
+
* The shell publishes the keyboard height as `--of-keyboard-inset` on <html>
|
|
20
|
+
* (openframe-frontend's `keyboard-inset.ts`; unset everywhere else, hence the
|
|
21
|
+
* 0 default). Handing the part floating-ui does not already know about back as
|
|
22
|
+
* collision padding is what makes the top of the keyboard the bottom edge of
|
|
23
|
+
* the screen.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* const keyboardPadding = useKeyboardCollisionPadding()
|
|
27
|
+
* <PopoverPrimitive.Content collisionPadding={{ bottom: keyboardPadding }} />
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Pair it with `max-h-[var(--radix-popper-available-height)]` on the content:
|
|
31
|
+
* padding alone flips a dropdown above its trigger, but a list taller than what
|
|
32
|
+
* is left above the keyboard would then spill back over it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const KEYBOARD_INSET_VAR = '--of-keyboard-inset'
|
|
36
|
+
|
|
37
|
+
function measure(): number {
|
|
38
|
+
if (typeof document === 'undefined') return 0
|
|
39
|
+
const inset = Number.parseFloat(
|
|
40
|
+
getComputedStyle(document.documentElement).getPropertyValue(KEYBOARD_INSET_VAR),
|
|
41
|
+
)
|
|
42
|
+
// Also catches the NaN of an unset variable.
|
|
43
|
+
if (!(inset > 0)) return 0
|
|
44
|
+
// How far floating-ui's own viewport bottom sits below the top of the
|
|
45
|
+
// keyboard. iOS reports the keyboard on the visual viewport and Android does
|
|
46
|
+
// not; measuring the difference rather than branching on the platform keeps
|
|
47
|
+
// both correct and can never reserve the same keyboard twice.
|
|
48
|
+
const layoutHeight = document.documentElement.clientHeight
|
|
49
|
+
const viewport = window.visualViewport
|
|
50
|
+
const viewportBottom = viewport ? viewport.offsetTop + viewport.height : layoutHeight
|
|
51
|
+
return Math.max(0, Math.round(viewportBottom - (layoutHeight - inset)))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// One document-wide subscription for every popper on the page: the keyboard is
|
|
55
|
+
// a single global, and each open dropdown re-reading it would mean another
|
|
56
|
+
// MutationObserver plus a forced style resolve per keystroke-driven re-render.
|
|
57
|
+
const listeners = new Set<() => void>()
|
|
58
|
+
let padding = 0
|
|
59
|
+
let teardown: (() => void) | null = null
|
|
60
|
+
|
|
61
|
+
function sync(): void {
|
|
62
|
+
const next = measure()
|
|
63
|
+
if (next === padding) return
|
|
64
|
+
padding = next
|
|
65
|
+
for (const notify of listeners) notify()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function subscribe(onStoreChange: () => void): () => void {
|
|
69
|
+
listeners.add(onStoreChange)
|
|
70
|
+
if (listeners.size === 1) {
|
|
71
|
+
padding = measure()
|
|
72
|
+
// Two independent signals. The inset lands on <html>'s inline style, so the
|
|
73
|
+
// attribute is the only notification for it; the visual viewport moves on
|
|
74
|
+
// its own schedule (iOS reports the keyboard there, and a pinch-zoom shifts
|
|
75
|
+
// it with no keyboard involved at all) and changes what is left to reserve.
|
|
76
|
+
const observer = new MutationObserver(sync)
|
|
77
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] })
|
|
78
|
+
const viewport = window.visualViewport
|
|
79
|
+
viewport?.addEventListener('resize', sync)
|
|
80
|
+
viewport?.addEventListener('scroll', sync)
|
|
81
|
+
teardown = () => {
|
|
82
|
+
observer.disconnect()
|
|
83
|
+
viewport?.removeEventListener('resize', sync)
|
|
84
|
+
viewport?.removeEventListener('scroll', sync)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return () => {
|
|
88
|
+
listeners.delete(onStoreChange)
|
|
89
|
+
if (listeners.size > 0) return
|
|
90
|
+
teardown?.()
|
|
91
|
+
teardown = null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function useKeyboardCollisionPadding(): number {
|
|
96
|
+
return useSyncExternalStore(
|
|
97
|
+
subscribe,
|
|
98
|
+
() => padding,
|
|
99
|
+
() => 0,
|
|
100
|
+
)
|
|
101
|
+
}
|