@flamingo-stack/openframe-frontend-core 0.0.731 → 0.0.733
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/build/export-map.json +1 -1
- package/dist/{chunk-RAHRDJBP.cjs → chunk-3HPB4H4C.cjs} +4 -4
- package/dist/{chunk-2UNQJVYN.cjs → chunk-3KGICRH7.cjs} +1457 -1187
- package/dist/{chunk-A4HNF4MP.cjs → chunk-3UTPZW36.cjs} +89 -89
- package/dist/{chunk-GID62YEN.cjs → chunk-AAFW4STC.cjs} +3 -3
- package/dist/{chunk-XDBHGWFH.cjs → chunk-ATMBJBOI.cjs} +16 -16
- package/dist/{chunk-YDGBRI3Y.cjs → chunk-AV5533DH.cjs} +15 -15
- package/dist/{chunk-BFHLGEJK.cjs → chunk-BP6R5WDW.cjs} +63 -60
- package/dist/{chunk-CLILWI6O.cjs → chunk-CJVAA7KL.cjs} +32 -32
- package/dist/{chunk-YJFN2W7A.cjs → chunk-DUVOQHQI.cjs} +7 -7
- package/dist/{chunk-E3N24573.cjs → chunk-DUXLZPMJ.cjs} +9 -9
- package/dist/{chunk-TH4N726A.cjs → chunk-E26I4XH6.cjs} +17 -17
- package/dist/{chunk-Y4QRK3CB.cjs → chunk-EMG3T55N.cjs} +7 -7
- package/dist/{chunk-JK75ZZP3.cjs → chunk-EQORT7NK.cjs} +59 -59
- package/dist/chunk-FCGLSWKR.cjs +417 -0
- package/dist/{chunk-MANNIJ42.cjs → chunk-FICJS5OA.cjs} +6 -0
- package/dist/{chunk-QP6QS3YU.cjs → chunk-FNRMDDVN.cjs} +48 -50
- package/dist/{chunk-KLXHCNDC.cjs → chunk-FTTLBMHU.cjs} +3 -3
- package/dist/{chunk-JPZ4Y6X3.cjs → chunk-GB3CHAW7.cjs} +93 -93
- package/dist/{chunk-7DUXR4HL.cjs → chunk-GVTDVS5U.cjs} +3 -3
- package/dist/{chunk-ZV6473Z4.cjs → chunk-HSWHOCZ3.cjs} +20 -20
- package/dist/{chunk-53XZ6MJ5.cjs → chunk-IC4FT3A6.cjs} +28 -28
- package/dist/{chunk-6V4GEE55.cjs → chunk-J7JKPTGO.cjs} +12 -12
- package/dist/{chunk-FKKUQK4C.cjs → chunk-L4ZNR5V7.cjs} +43 -39
- package/dist/{chunk-NYY25425.cjs → chunk-MYBTNOS6.cjs} +2 -2
- package/dist/chunk-NTJE6SM6.cjs +28 -0
- package/dist/{chunk-RBOIGJYF.cjs → chunk-P3R23TOE.cjs} +35 -35
- package/dist/chunk-PX7FY655.cjs +114 -0
- package/dist/{chunk-YUXRIR45.cjs → chunk-SA7LDEVJ.cjs} +98 -50
- package/dist/{chunk-32ENYVFA.cjs → chunk-TIMCHP4X.cjs} +42 -42
- package/dist/{chunk-3SWLL4IM.cjs → chunk-U4T5NLZD.cjs} +8 -8
- package/dist/{chunk-HI7EFAFQ.cjs → chunk-XDWY6HAW.cjs} +28 -28
- package/dist/{chunk-25H2SZH7.cjs → chunk-XY632BCH.cjs} +4 -4
- package/dist/{chunk-HY2KTQSL.cjs → chunk-YIXKH2JO.cjs} +236 -236
- package/dist/{chunk-4YPV4OI4.cjs → chunk-ZBNA6TCW.cjs} +82 -82
- package/dist/{chunk-HLIJ3U2T.cjs → chunk-ZJNYWNCB.cjs} +23 -21
- package/dist/{chunk-HEOD765R.cjs → chunk-ZRYKDN2J.cjs} +32 -379
- package/dist/{cloud-tenants-SC4FNAC2.cjs → cloud-tenants-3GOGR3EN.cjs} +22 -22
- package/dist/components/agent-identity.d.ts +8 -0
- package/dist/components/agent-identity.d.ts.map +1 -1
- package/dist/components/agent-identity.js +13 -1
- package/dist/components/case-studies/index.cjs +35 -35
- package/dist/components/chat/embeddable-chat.d.ts +16 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.js +5 -2
- package/dist/components/chat/index.cjs +16 -16
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/chat/utils/icon-library.js +6 -0
- package/dist/components/contact/index.cjs +23 -23
- package/dist/components/docs/index.cjs +26 -26
- package/dist/components/embeds/index.cjs +23 -23
- package/dist/components/faq/index.cjs +23 -23
- package/dist/components/features/index.cjs +22 -22
- package/dist/components/help-center-pages/index.cjs +138 -138
- package/dist/components/index.cjs +89 -82
- package/dist/components/meeting-scheduler/index.cjs +67 -67
- package/dist/components/navigation/index.cjs +25 -25
- package/dist/components/onboarding-guides/index.cjs +25 -25
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/product-demo/screens/remote-desktop-picture.d.ts.map +1 -1
- package/dist/components/product-demo/screens/remote-desktop-picture.js +2 -1
- package/dist/components/related-content/index.cjs +10 -10
- package/dist/components/tickets/index.cjs +26 -26
- package/dist/components/ui/device-wallpaper.d.ts +7 -0
- package/dist/components/ui/device-wallpaper.d.ts.map +1 -0
- package/dist/components/ui/device-wallpaper.js +5 -0
- package/dist/components/ui/feature-list.d.ts +9 -4
- package/dist/components/ui/feature-list.d.ts.map +1 -1
- package/dist/components/ui/feature-list.js +26 -35
- package/dist/components/ui/index.cjs +28 -22
- package/dist/components/ui/index.d.ts +3 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/components/ui/notification-stack.d.ts +33 -0
- package/dist/components/ui/notification-stack.d.ts.map +1 -0
- package/dist/components/ui/notification-stack.js +78 -0
- package/dist/components/ui/numbered-step.d.ts +29 -0
- package/dist/components/ui/numbered-step.d.ts.map +1 -0
- package/dist/components/ui/numbered-step.js +57 -0
- package/dist/components/ui/phone-frame.d.ts +14 -9
- package/dist/components/ui/phone-frame.d.ts.map +1 -1
- package/dist/components/ui/phone-frame.js +50 -6
- package/dist/components/ui/phone-glyphs/lock-camera-glyph.d.ts +5 -0
- package/dist/components/ui/phone-glyphs/lock-camera-glyph.d.ts.map +1 -0
- package/dist/components/ui/phone-glyphs/lock-camera-glyph.js +14 -0
- package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.d.ts +5 -0
- package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.d.ts.map +1 -0
- package/dist/components/ui/phone-glyphs/lock-flashlight-glyph.js +14 -0
- package/dist/components/ui/phone-glyphs/status-bar-glyphs.d.ts +5 -0
- package/dist/components/ui/phone-glyphs/status-bar-glyphs.d.ts.map +1 -0
- package/dist/components/ui/phone-glyphs/status-bar-glyphs.js +68 -0
- package/dist/components/ui/searchable-select.d.ts +29 -3
- package/dist/components/ui/searchable-select.d.ts.map +1 -1
- package/dist/components/ui/searchable-select.js +101 -45
- package/dist/components/ui/snap-carousel.d.ts +5 -8
- package/dist/components/ui/snap-carousel.d.ts.map +1 -1
- package/dist/components/ui/snap-carousel.js +2 -27
- package/dist/components/ui/tab-navigation.d.ts +8 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.js +30 -20
- package/dist/{devices-7MHSJNXS.cjs → devices-POZXTP7X.cjs} +23 -23
- package/dist/{embeddable-chat-VPIG3F7E.cjs → embeddable-chat-I3Y5GCCR.cjs} +96 -93
- package/dist/hooks/index.cjs +9 -9
- package/dist/hooks/ui/use-autoplay.d.ts +50 -13
- package/dist/hooks/ui/use-autoplay.d.ts.map +1 -1
- package/dist/hooks/ui/use-autoplay.js +76 -13
- package/dist/hooks/ui/use-in-view.d.ts +8 -0
- package/dist/hooks/ui/use-in-view.d.ts.map +1 -1
- package/dist/hooks/ui/use-in-view.js +21 -36
- package/dist/hooks/use-agent-identities.d.ts +6 -0
- package/dist/hooks/use-agent-identities.d.ts.map +1 -1
- package/dist/hooks/use-scenario-player.d.ts +26 -9
- package/dist/hooks/use-scenario-player.d.ts.map +1 -1
- package/dist/hooks/use-scenario-player.js +12 -9
- package/dist/index.cjs +58 -51
- package/dist/{knowledge-4DREGSOZ.cjs → knowledge-AP34IMXN.cjs} +23 -23
- package/dist/{logs-BWXNT2KP.cjs → logs-CEVMIOPW.cjs} +30 -30
- package/dist/{policies-MGFUI23M.cjs → policies-AMBMRCGB.cjs} +26 -26
- package/dist/{remote-session-O6QGXV2N.cjs → remote-session-Z4YZFG5E.cjs} +10 -9
- package/dist/{software-update-7LF7XEYC.cjs → software-update-Z3MWMSHI.cjs} +26 -26
- package/dist/{tickets-board-KNXK747O.cjs → tickets-board-JOSYLOS7.cjs} +13 -13
- package/package.json +1 -1
- package/src/components/agent-identity.tsx +20 -1
- package/src/components/chat/embeddable-chat.tsx +29 -2
- package/src/components/chat/utils/icon-library.tsx +7 -0
- package/src/components/product-demo/screens/remote-desktop-picture.tsx +2 -1
- package/src/components/ui/device-wallpaper.ts +7 -0
- package/src/components/ui/feature-list.tsx +24 -30
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/notification-stack.tsx +133 -0
- package/src/components/ui/numbered-step.tsx +81 -0
- package/src/components/ui/phone-frame.tsx +83 -19
- package/src/components/ui/phone-glyphs/lock-camera-glyph.tsx +14 -0
- package/src/components/ui/phone-glyphs/lock-flashlight-glyph.tsx +14 -0
- package/src/components/ui/phone-glyphs/status-bar-glyphs.tsx +45 -0
- package/src/components/ui/searchable-select.tsx +124 -32
- package/src/components/ui/snap-carousel.tsx +6 -38
- package/src/components/ui/tab-navigation.tsx +49 -21
- package/src/hooks/ui/use-autoplay.ts +144 -27
- package/src/hooks/ui/use-in-view.ts +31 -38
- package/src/hooks/use-agent-identities.ts +6 -0
- package/src/hooks/use-scenario-player.ts +44 -20
- package/dist/chunk-2WXLQ5R5.cjs +0 -99
- package/dist/chunk-BE2WAMG5.cjs +0 -54
- package/dist/chunk-LCTDHLDL.cjs +0 -43
- package/dist/components/ui/playback-toggle.d.ts +0 -24
- package/dist/components/ui/playback-toggle.d.ts.map +0 -1
- package/dist/components/ui/playback-toggle.js +0 -32
- package/src/components/ui/playback-toggle.tsx +0 -53
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The status bar's signal, Wi-Fi and battery of the drawn phone's lock screen, as the design draws it. It belongs to `PhoneFrame` and
|
|
2
|
+
// nothing else, so it is not part of the icon set. It takes the text colour around it.
|
|
3
|
+
|
|
4
|
+
/** The status bar's right side: signal, Wi-Fi and battery, as one drawing. */
|
|
5
|
+
export function StatusBarGlyphs({ className }: { className?: string }) {
|
|
6
|
+
return (
|
|
7
|
+
<svg viewBox="0 0 54.441 9.78982" fill="none" aria-hidden="true" className={className}>
|
|
8
|
+
<path
|
|
9
|
+
d="M6.89491 2.89491C6.89491 2.51412 7.20361 2.20542 7.5844 2.20542H8.27389C8.65469 2.20542 8.96338 2.51412 8.96338 2.89491V8.41084C8.96338 8.79164 8.65469 9.10033 8.27389 9.10033H7.5844C7.20361 9.10033 6.89491 8.79164 6.89491 8.41084V2.89491Z"
|
|
10
|
+
fill="currentColor"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
d="M10.3424 1.51593C10.3424 1.13513 10.6511 0.826438 11.0319 0.826438H11.7213C12.1021 0.826438 12.4108 1.13513 12.4108 1.51593V8.41084C12.4108 8.79164 12.1021 9.10033 11.7213 9.10033H11.0319C10.6511 9.10033 10.3424 8.79164 10.3424 8.41084V1.51593Z"
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="M3.44746 5.30813C3.44746 4.92733 3.75615 4.61864 4.13695 4.61864H4.82644C5.20723 4.61864 5.51593 4.92733 5.51593 5.30813V8.41084C5.51593 8.79164 5.20723 9.10033 4.82644 9.10033H4.13695C3.75615 9.10033 3.44746 8.79164 3.44746 8.41084V5.30813Z"
|
|
18
|
+
fill="currentColor"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
d="M0 7.03186C0 6.65106 0.308696 6.34237 0.689491 6.34237H1.37898C1.75978 6.34237 2.06847 6.65106 2.06847 7.03186V8.41084C2.06847 8.79164 1.75978 9.10033 1.37898 9.10033H0.689491C0.308696 9.10033 0 8.79164 0 8.41084V7.03186Z"
|
|
22
|
+
fill="currentColor"
|
|
23
|
+
/>
|
|
24
|
+
<path
|
|
25
|
+
d="M22.1436 6.97595C23.1225 6.13113 24.5563 6.13095 25.5352 6.97595C25.5844 7.02139 25.6129 7.08596 25.6143 7.15368C25.6156 7.22145 25.5894 7.28687 25.542 7.33435L24.0088 8.91345C23.9639 8.95973 23.9027 8.98571 23.8389 8.98571C23.775 8.98566 23.7138 8.9598 23.669 8.91345L22.1358 7.33435C22.0884 7.28683 22.0621 7.22144 22.0635 7.15368C22.0649 7.08593 22.0943 7.02136 22.1436 6.97595ZM20.0703 5.0863C22.1795 3.08361 25.4465 3.08362 27.5557 5.0863C27.6033 5.13326 27.6302 5.19841 27.6309 5.26599C27.6314 5.33341 27.6052 5.39784 27.5586 5.44567L26.6729 6.35974C26.5815 6.45307 26.4335 6.45548 26.3399 6.36462C25.6473 5.72459 24.7467 5.36952 23.8125 5.3695C22.8788 5.3699 21.9783 5.72488 21.2861 6.36462C21.1925 6.45548 21.0445 6.45307 20.9531 6.35974L20.0674 5.44567C20.0209 5.39792 19.9946 5.33335 19.9951 5.26599C19.9958 5.19843 20.0227 5.13325 20.0703 5.0863ZM18.001 3.19958C21.236 0.0354236 26.3393 0.0353572 29.5742 3.19958C29.6211 3.2466 29.6481 3.31125 29.6485 3.37829C29.6488 3.44529 29.6224 3.50946 29.5762 3.557L28.6895 4.47107C28.5981 4.56485 28.4493 4.56633 28.3565 4.474C27.124 3.27797 25.4887 2.61088 23.7881 2.61071C22.0873 2.61071 20.4515 3.27787 19.2188 4.474C19.126 4.56648 18.9771 4.565 18.8858 4.47107L17.9981 3.557C17.952 3.50946 17.9264 3.44515 17.9268 3.37829C17.9272 3.31125 17.9541 3.24656 18.001 3.19958Z"
|
|
26
|
+
fill="currentColor"
|
|
27
|
+
/>
|
|
28
|
+
<path
|
|
29
|
+
opacity="0.35"
|
|
30
|
+
d="M37.9222 0.363281H50.0277C51.3498 0.363404 52.4211 1.43566 52.4212 2.75781V6.20508C52.4212 7.5273 51.3499 8.59949 50.0277 8.59961H37.9222C36.6 8.59953 35.5277 7.52732 35.5277 6.20508V2.75781C35.5278 1.43564 36.6 0.363363 37.9222 0.363281Z"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="0.727473"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
opacity="0.4"
|
|
36
|
+
d="M53.4749 3.44746V6.35735C54.0603 6.1109 54.441 5.53758 54.441 4.9024C54.441 4.26722 54.0603 3.69391 53.4749 3.44746Z"
|
|
37
|
+
fill="currentColor"
|
|
38
|
+
/>
|
|
39
|
+
<path
|
|
40
|
+
d="M36.5434 2.75797C36.5434 1.99638 37.1608 1.37898 37.9224 1.37898H50.0274C50.789 1.37898 51.4064 1.99637 51.4064 2.75796V6.20542C51.4064 6.96701 50.789 7.5844 50.0274 7.5844H37.9224C37.1608 7.5844 36.5434 6.96701 36.5434 6.20542V2.75797Z"
|
|
41
|
+
fill="currentColor"
|
|
42
|
+
/>
|
|
43
|
+
</svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
4
|
-
import { type ReactNode, useMemo, useState } from 'react';
|
|
4
|
+
import { type ReactNode, useId, useMemo, useState } from 'react';
|
|
5
5
|
import { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';
|
|
6
6
|
import { cn } from '../../utils/cn';
|
|
7
7
|
import { CheckIcon, SearchIcon } from '../icons-v2-generated';
|
|
8
8
|
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
|
|
9
|
+
import { FieldWrapper } from './field-wrapper';
|
|
9
10
|
import { Input } from './input';
|
|
10
11
|
|
|
11
12
|
export interface SearchableSelectOption {
|
|
12
13
|
value: string;
|
|
13
14
|
label: string;
|
|
14
|
-
/** Optional leading visual for the option row (avatar, icon, badge).
|
|
15
|
+
/** Optional leading visual for the option row (avatar, icon, badge). The
|
|
16
|
+
* default trigger leads with the selected option's icon too. */
|
|
15
17
|
icon?: ReactNode;
|
|
18
|
+
/** Extra terms the search matches besides `label` — e.g. the stored key of a
|
|
19
|
+
* glyph (`chart-pie`) whose label reads "Pie chart". */
|
|
20
|
+
keywords?: string[];
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
/** How the dropdown lays its options out — see `SearchableSelectProps.layout`. */
|
|
24
|
+
export type SearchableSelectLayout = 'list' | 'grid';
|
|
25
|
+
|
|
18
26
|
export interface SearchableSelectProps {
|
|
19
27
|
options: SearchableSelectOption[];
|
|
20
28
|
/** Currently selected option value; null/undefined when nothing is selected. */
|
|
@@ -34,7 +42,27 @@ export interface SearchableSelectProps {
|
|
|
34
42
|
* is rendered.
|
|
35
43
|
*/
|
|
36
44
|
trigger?: ReactNode;
|
|
37
|
-
/**
|
|
45
|
+
/** `id` of the default trigger button, which `label` names through `htmlFor`; generated when omitted. */
|
|
46
|
+
id?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Label above the default trigger — the same `FieldWrapper` treatment
|
|
49
|
+
* `SelectTrigger` gives its `label`, so a searchable field lines up with the
|
|
50
|
+
* plain selects beside it. A custom `trigger` owns its own label.
|
|
51
|
+
*/
|
|
52
|
+
label?: string;
|
|
53
|
+
/** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles). */
|
|
54
|
+
labelVariant?: 'default' | 'large';
|
|
55
|
+
/** Validation message under the default trigger; also paints its border red. */
|
|
56
|
+
error?: string;
|
|
57
|
+
/**
|
|
58
|
+
* `'list'` (default) stacks full-width rows. `'grid'` tiles the options —
|
|
59
|
+
* icon above a centered caption, `minmax(7rem, 1fr)` columns — the
|
|
60
|
+
* icon-picker pattern. The grid fills the popover, so size it through
|
|
61
|
+
* `contentClassName` (360px fits three tiles).
|
|
62
|
+
*/
|
|
63
|
+
layout?: SearchableSelectLayout;
|
|
64
|
+
/** Custom option-row renderer; default is icon + label + selected check
|
|
65
|
+
* (icon above the caption in the grid layout). */
|
|
38
66
|
renderOption?: (option: SearchableSelectOption, isSelected: boolean) => ReactNode;
|
|
39
67
|
align?: 'start' | 'center' | 'end';
|
|
40
68
|
/** Classes for the default trigger button. */
|
|
@@ -50,22 +78,37 @@ export interface SearchableSelectProps {
|
|
|
50
78
|
* custom `trigger` for icon-button use cases; the default trigger looks and
|
|
51
79
|
* behaves like `SelectTrigger`.
|
|
52
80
|
*/
|
|
53
|
-
export function SearchableSelect({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
export function SearchableSelect(allProps: SearchableSelectProps) {
|
|
82
|
+
const {
|
|
83
|
+
options,
|
|
84
|
+
value = null,
|
|
85
|
+
onValueChange,
|
|
86
|
+
placeholder = 'Select...',
|
|
87
|
+
searchPlaceholder = 'Search...',
|
|
88
|
+
emptyText = 'No results found',
|
|
89
|
+
loadingText = 'Loading…',
|
|
90
|
+
isLoading = false,
|
|
91
|
+
disabled = false,
|
|
92
|
+
trigger,
|
|
93
|
+
id,
|
|
94
|
+
label,
|
|
95
|
+
labelVariant,
|
|
96
|
+
error,
|
|
97
|
+
layout = 'list',
|
|
98
|
+
renderOption,
|
|
99
|
+
align = 'start',
|
|
100
|
+
className,
|
|
101
|
+
contentClassName,
|
|
102
|
+
} = allProps;
|
|
103
|
+
// Wired for a message even while `error` is undefined — see `FieldWrapper.errorSlot`.
|
|
104
|
+
const errorSlot = 'error' in allProps;
|
|
105
|
+
const isInvalid = !!error;
|
|
106
|
+
const isGrid = layout === 'grid';
|
|
107
|
+
// The label names the trigger through `htmlFor`: clicking it focuses the
|
|
108
|
+
// button and assistive tech reads the field by it. A caller's `id` wins.
|
|
109
|
+
const generatedId = useId();
|
|
110
|
+
const triggerId = id ?? generatedId;
|
|
111
|
+
|
|
69
112
|
const [isOpen, setIsOpen] = useState(false);
|
|
70
113
|
const [search, setSearch] = useState('');
|
|
71
114
|
const keyboardPadding = useKeyboardCollisionPadding();
|
|
@@ -79,7 +122,9 @@ export function SearchableSelect({
|
|
|
79
122
|
const filtered = useMemo(() => {
|
|
80
123
|
const q = search.trim().toLowerCase();
|
|
81
124
|
if (!q) return options;
|
|
82
|
-
return options.filter(
|
|
125
|
+
return options.filter(
|
|
126
|
+
o => o.label.toLowerCase().includes(q) || (o.keywords ?? []).some(k => k.toLowerCase().includes(q)),
|
|
127
|
+
);
|
|
83
128
|
}, [options, search]);
|
|
84
129
|
|
|
85
130
|
const selectedOption = useMemo(
|
|
@@ -94,8 +139,10 @@ export function SearchableSelect({
|
|
|
94
139
|
|
|
95
140
|
const defaultTrigger = (
|
|
96
141
|
<button
|
|
142
|
+
id={triggerId}
|
|
97
143
|
type="button"
|
|
98
144
|
disabled={disabled}
|
|
145
|
+
data-invalid={isInvalid || undefined}
|
|
99
146
|
className={cn(
|
|
100
147
|
// Mirrors SelectTrigger so the closed state is indistinguishable from
|
|
101
148
|
// the regular Select.
|
|
@@ -103,22 +150,45 @@ export function SearchableSelect({
|
|
|
103
150
|
'border-ods-border bg-ods-card text-h4',
|
|
104
151
|
selectedOption ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
105
152
|
'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',
|
|
106
|
-
'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',
|
|
153
|
+
!isInvalid && 'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',
|
|
107
154
|
'group cursor-pointer transition-colors duration-200 disabled:!cursor-not-allowed disabled:bg-ods-bg',
|
|
155
|
+
isInvalid &&
|
|
156
|
+
'border-ods-error enabled:hover:border-ods-error enabled:active:border-ods-error data-[state=open]:border-ods-error',
|
|
108
157
|
className,
|
|
109
158
|
)}
|
|
110
159
|
>
|
|
111
|
-
<span className="
|
|
160
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
161
|
+
{selectedOption?.icon}
|
|
162
|
+
<span className="truncate">{selectedOption ? selectedOption.label : placeholder}</span>
|
|
163
|
+
</span>
|
|
112
164
|
<Chevron02DownIcon
|
|
113
|
-
className=
|
|
165
|
+
className={cn(
|
|
166
|
+
'shrink-0 text-ods-text-secondary transition-all duration-200 group-data-[state=open]:rotate-180',
|
|
167
|
+
isInvalid ? 'text-ods-error' : 'group-data-[state=open]:text-ods-accent',
|
|
168
|
+
)}
|
|
114
169
|
size={24}
|
|
115
170
|
/>
|
|
116
171
|
</button>
|
|
117
172
|
);
|
|
118
173
|
|
|
174
|
+
const triggerElement = <PopoverPrimitive.Trigger asChild>{trigger ?? defaultTrigger}</PopoverPrimitive.Trigger>;
|
|
175
|
+
|
|
176
|
+
// A `role="listbox"` child that is a status line, not an option: in the grid
|
|
177
|
+
// it has to span the row or it would sit in one tile-sized cell.
|
|
178
|
+
const statusClass = cn(
|
|
179
|
+
'px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-ods-text-secondary text-h5',
|
|
180
|
+
isGrid && 'col-span-full',
|
|
181
|
+
);
|
|
182
|
+
|
|
119
183
|
return (
|
|
120
184
|
<PopoverPrimitive.Root open={isOpen} onOpenChange={disabled ? undefined : setIsOpen} modal={false}>
|
|
121
|
-
|
|
185
|
+
{!trigger && (label !== undefined || errorSlot) ? (
|
|
186
|
+
<FieldWrapper label={label} htmlFor={triggerId} labelVariant={labelVariant} error={error} errorSlot={errorSlot}>
|
|
187
|
+
{triggerElement}
|
|
188
|
+
</FieldWrapper>
|
|
189
|
+
) : (
|
|
190
|
+
triggerElement
|
|
191
|
+
)}
|
|
122
192
|
<PopoverPrimitive.Portal>
|
|
123
193
|
<PopoverPrimitive.Content
|
|
124
194
|
align={align}
|
|
@@ -147,15 +217,22 @@ export function SearchableSelect({
|
|
|
147
217
|
className="rounded-none border-0"
|
|
148
218
|
/>
|
|
149
219
|
</div>
|
|
150
|
-
<div
|
|
220
|
+
<div
|
|
221
|
+
className={cn(
|
|
222
|
+
'max-h-80 min-h-0 overflow-y-auto',
|
|
223
|
+
// Tiles grow from 7rem: the popover's own border and a classic
|
|
224
|
+
// always-on scrollbar both eat into the track, so 7.5rem (the
|
|
225
|
+
// Figma cell) dropped a 360px popover to two columns. The column
|
|
226
|
+
// count follows the popover width. Each tile draws its own
|
|
227
|
+
// bottom/right rule, so the grid needs no gap.
|
|
228
|
+
isGrid ? 'grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))]' : 'py-[var(--spacing-system-xs)]',
|
|
229
|
+
)}
|
|
230
|
+
role="listbox"
|
|
231
|
+
>
|
|
151
232
|
{isLoading ? (
|
|
152
|
-
<div className=
|
|
153
|
-
{loadingText}
|
|
154
|
-
</div>
|
|
233
|
+
<div className={statusClass}>{loadingText}</div>
|
|
155
234
|
) : filtered.length === 0 ? (
|
|
156
|
-
<div className=
|
|
157
|
-
{emptyText}
|
|
158
|
-
</div>
|
|
235
|
+
<div className={statusClass}>{emptyText}</div>
|
|
159
236
|
) : (
|
|
160
237
|
filtered.map(opt => {
|
|
161
238
|
const isSelected = value === opt.value;
|
|
@@ -167,13 +244,28 @@ export function SearchableSelect({
|
|
|
167
244
|
aria-selected={isSelected}
|
|
168
245
|
onClick={() => handleSelect(opt.value)}
|
|
169
246
|
className={cn(
|
|
170
|
-
'flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
171
247
|
'transition-colors hover:bg-ods-bg-hover',
|
|
248
|
+
isGrid
|
|
249
|
+
? 'flex min-w-0 flex-col items-center justify-center gap-[var(--spacing-system-xxs)] border-b border-r border-ods-border p-[var(--spacing-system-xs)]'
|
|
250
|
+
: 'flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
172
251
|
isSelected && 'bg-ods-bg-hover',
|
|
173
252
|
)}
|
|
174
253
|
>
|
|
175
254
|
{renderOption ? (
|
|
176
255
|
renderOption(opt, isSelected)
|
|
256
|
+
) : isGrid ? (
|
|
257
|
+
<>
|
|
258
|
+
{opt.icon}
|
|
259
|
+
<span
|
|
260
|
+
className={cn(
|
|
261
|
+
'w-full truncate text-center text-h6',
|
|
262
|
+
isSelected ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
263
|
+
)}
|
|
264
|
+
title={opt.label}
|
|
265
|
+
>
|
|
266
|
+
{opt.label}
|
|
267
|
+
</span>
|
|
268
|
+
</>
|
|
177
269
|
) : (
|
|
178
270
|
<>
|
|
179
271
|
{opt.icon}
|
|
@@ -6,7 +6,6 @@ import { useInView } from '../../hooks/ui/use-in-view';
|
|
|
6
6
|
import { cn } from '../../utils/cn';
|
|
7
7
|
import { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon';
|
|
8
8
|
import { Chevron02RightIcon } from '../icons-v2-generated/arrows/chevron-02-right-icon';
|
|
9
|
-
import { PlaybackToggle } from './playback-toggle';
|
|
10
9
|
|
|
11
10
|
/** The row under the track: the dots, the counter, the pause and the two arrows (the arrows set its height). */
|
|
12
11
|
const SNAP_CAROUSEL_CONTROLS_CLASS = 'mt-3 flex items-center gap-2';
|
|
@@ -28,9 +27,6 @@ export interface SnapCarouselProps<T> {
|
|
|
28
27
|
label: string;
|
|
29
28
|
/** Move to the next slide after this long. `0` turns auto-advance off. Default 5000. */
|
|
30
29
|
autoAdvanceMs?: number;
|
|
31
|
-
/** Names of the stop/start control. */
|
|
32
|
-
playLabel?: string;
|
|
33
|
-
pauseLabel?: string;
|
|
34
30
|
/** Width of one slide. Default `basis-[300px]`. */
|
|
35
31
|
slideClassName?: string;
|
|
36
32
|
prevLabel?: string;
|
|
@@ -67,10 +63,10 @@ function DotFill({ durationMs }: { durationMs: number }) {
|
|
|
67
63
|
*
|
|
68
64
|
* WHEN IT ADVANCES is `useAutoplay`'s rule, the same one the looping demos
|
|
69
65
|
* use: mostly on screen, tab visible, not paused, motion allowed. A swipe or
|
|
70
|
-
* an arrow moves it and it carries on from there after a full interval
|
|
71
|
-
* waits
|
|
72
|
-
*
|
|
73
|
-
*
|
|
66
|
+
* an arrow moves it and it carries on from there after a full interval. It
|
|
67
|
+
* waits while the visitor holds it (`useAutoplay`'s hold: a finger or the
|
|
68
|
+
* mouse button down on it, the keyboard inside it); there is no pause button. While it is auto-playing the active dot fills over the
|
|
69
|
+
* interval, so the wait is visible.
|
|
74
70
|
*/
|
|
75
71
|
export function SnapCarousel<T>({
|
|
76
72
|
items,
|
|
@@ -78,8 +74,6 @@ export function SnapCarousel<T>({
|
|
|
78
74
|
getKey,
|
|
79
75
|
label,
|
|
80
76
|
autoAdvanceMs = 5000,
|
|
81
|
-
playLabel,
|
|
82
|
-
pauseLabel,
|
|
83
77
|
slideClassName = 'basis-[300px]',
|
|
84
78
|
prevLabel = 'Previous',
|
|
85
79
|
nextLabel = 'Next',
|
|
@@ -88,8 +82,6 @@ export function SnapCarousel<T>({
|
|
|
88
82
|
}: SnapCarouselProps<T>) {
|
|
89
83
|
const trackRef = useRef<HTMLDivElement | null>(null);
|
|
90
84
|
const [index, setIndex] = useState(0);
|
|
91
|
-
// Held only while a finger or the mouse button is down on it: the visitor is mid-swipe.
|
|
92
|
-
const [pressed, setPressed] = useState(false);
|
|
93
85
|
const { ref: viewRef, inView } = useInView<HTMLDivElement>({ threshold: 0.5 });
|
|
94
86
|
const auto = useAutoplay(inView);
|
|
95
87
|
const count = items.length;
|
|
@@ -127,24 +119,8 @@ export function SnapCarousel<T>({
|
|
|
127
119
|
setIndex(Math.min(count - 1, Math.round(track.scrollLeft / step)));
|
|
128
120
|
}, [count, slideStep]);
|
|
129
121
|
|
|
130
|
-
// The release is listened for on the WINDOW: a press that ends outside the
|
|
131
|
-
// carousel (a drag that leaves it, a scroll the browser takes over) must
|
|
132
|
-
// still end, or the carousel would wait forever.
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
if (!pressed) return undefined;
|
|
135
|
-
const release = () => setPressed(false);
|
|
136
|
-
window.addEventListener('pointerup', release);
|
|
137
|
-
window.addEventListener('pointercancel', release);
|
|
138
|
-
window.addEventListener('blur', release);
|
|
139
|
-
return () => {
|
|
140
|
-
window.removeEventListener('pointerup', release);
|
|
141
|
-
window.removeEventListener('pointercancel', release);
|
|
142
|
-
window.removeEventListener('blur', release);
|
|
143
|
-
};
|
|
144
|
-
}, [pressed]);
|
|
145
|
-
|
|
146
122
|
const canAutoAdvance = autoAdvanceMs > 0 && count > 1;
|
|
147
|
-
const playing = auto.playing && canAutoAdvance
|
|
123
|
+
const playing = auto.playing && canAutoAdvance;
|
|
148
124
|
|
|
149
125
|
// Restarts on every slide change, so a swipe or an arrow is followed by a full interval.
|
|
150
126
|
useEffect(() => {
|
|
@@ -164,7 +140,7 @@ export function SnapCarousel<T>({
|
|
|
164
140
|
if (count === 0) return null;
|
|
165
141
|
|
|
166
142
|
return (
|
|
167
|
-
<div className={className}
|
|
143
|
+
<div className={className} {...auto.holdProps}>
|
|
168
144
|
<div
|
|
169
145
|
ref={setRefs}
|
|
170
146
|
onScroll={onScroll}
|
|
@@ -212,14 +188,6 @@ export function SnapCarousel<T>({
|
|
|
212
188
|
>
|
|
213
189
|
{index + 1} / {count}
|
|
214
190
|
</span>
|
|
215
|
-
{canAutoAdvance && !auto.reducedMotion && (
|
|
216
|
-
<PlaybackToggle
|
|
217
|
-
paused={auto.paused}
|
|
218
|
-
onChange={auto.setPaused}
|
|
219
|
-
playLabel={playLabel}
|
|
220
|
-
pauseLabel={pauseLabel}
|
|
221
|
-
/>
|
|
222
|
-
)}
|
|
223
191
|
<button
|
|
224
192
|
type="button"
|
|
225
193
|
aria-label={prevLabel}
|
|
@@ -58,10 +58,17 @@ export interface TabItem {
|
|
|
58
58
|
/**
|
|
59
59
|
* 0..1. Set on the ACTIVE tab, the underline stops being a selection marker
|
|
60
60
|
* and becomes a progress line: its width is this fraction of the tab, and it
|
|
61
|
-
* moves there linearly over `progressTransitionMs`.
|
|
61
|
+
* moves there linearly over `progressTransitionMs`. Start it above 0 (the
|
|
62
|
+
* first step of N is 1/N): at 0 the open tab has no underline at all. A timed, auto-advancing
|
|
62
63
|
* strip (a looping demo) uses it; `0` snaps back with no animation.
|
|
63
64
|
*/
|
|
64
65
|
progress?: number;
|
|
66
|
+
/**
|
|
67
|
+
* The tab's own colour (a CSS colour, such as the hex an agent's identity
|
|
68
|
+
* stores): its underline and its icon take it while the tab is open. Omitted,
|
|
69
|
+
* they are the platform's accent.
|
|
70
|
+
*/
|
|
71
|
+
accent?: string;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
export interface TabNavigationUrlSyncOptions {
|
|
@@ -334,7 +341,34 @@ const TabBar = memo(function TabBarImpl({
|
|
|
334
341
|
// A tab that reports `progress` turns the underline into a progress line.
|
|
335
342
|
const activeProgress = tabs.find(tab => tab.id === activeTab)?.progress;
|
|
336
343
|
const hasProgress = typeof activeProgress === 'number';
|
|
337
|
-
|
|
344
|
+
// A progress line always GROWS from the start of its tab. A run that starts
|
|
345
|
+
// over (another tab opened, by the rotation or by choice, or the same tab
|
|
346
|
+
// chosen again: its progress fell back) is first drawn EMPTY, with no
|
|
347
|
+
// transition, and only then let run to its value. Without that the line
|
|
348
|
+
// either slid across from the last tab, shrank back, or appeared already
|
|
349
|
+
// part-way and sat still until the next step.
|
|
350
|
+
const target = hasProgress ? Math.max(0, Math.min(1, activeProgress)) : 1;
|
|
351
|
+
// It starts on no tab at all, so the FIRST paint is a run starting over too:
|
|
352
|
+
// a page load, the rotation and a click all begin the line the same way.
|
|
353
|
+
const [run, setRun] = useState<{ tab: string | null; progress: number }>({ tab: null, progress: 0 });
|
|
354
|
+
const startsOver = hasProgress && (run.tab !== activeTab || target < run.progress);
|
|
355
|
+
// The same run moving forward: follow it (state derived from props during render).
|
|
356
|
+
if (hasProgress && !startsOver && run.progress !== target) setRun({ tab: activeTab, progress: target });
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!startsOver) return undefined;
|
|
359
|
+
// Two frames: the empty line has to be painted before it is let run.
|
|
360
|
+
let second = 0;
|
|
361
|
+
const first = requestAnimationFrame(() => {
|
|
362
|
+
second = requestAnimationFrame(() => setRun({ tab: activeTab, progress: target }));
|
|
363
|
+
});
|
|
364
|
+
return () => {
|
|
365
|
+
cancelAnimationFrame(first);
|
|
366
|
+
cancelAnimationFrame(second);
|
|
367
|
+
};
|
|
368
|
+
}, [startsOver, activeTab, target]);
|
|
369
|
+
const progress = startsOver ? 0 : target;
|
|
370
|
+
// The open tab's own colour, when it has one; else the platform's accent.
|
|
371
|
+
const activeAccent = tabs.find(tab => tab.id === activeTab)?.accent;
|
|
338
372
|
const indicatorStyle: React.CSSProperties = {
|
|
339
373
|
width: `${indicator.width * progress}px`,
|
|
340
374
|
transform: `translateX(${indicator.left}px)`,
|
|
@@ -362,7 +396,7 @@ const TabBar = memo(function TabBarImpl({
|
|
|
362
396
|
`relative` makes this the offsetParent the underline is measured against. */}
|
|
363
397
|
<div
|
|
364
398
|
ref={scrollRef}
|
|
365
|
-
className="scrollbar-hide relative flex h-full items-center justify-start
|
|
399
|
+
className="scrollbar-hide relative flex h-full items-center justify-start overflow-x-auto overflow-y-hidden"
|
|
366
400
|
>
|
|
367
401
|
{tabs.map(tab => {
|
|
368
402
|
const isActive = activeTab === tab.id;
|
|
@@ -375,28 +409,19 @@ const TabBar = memo(function TabBarImpl({
|
|
|
375
409
|
onClick={() => onTabChange(tab.id)}
|
|
376
410
|
aria-label={tab.badge ? `${tab.label} (${tab.badge})` : undefined}
|
|
377
411
|
className={cn(
|
|
378
|
-
'relative flex shrink-0 cursor-pointer items-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
|
|
412
|
+
'group relative flex shrink-0 cursor-pointer items-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
|
|
379
413
|
tabAlign === 'start' ? 'justify-start text-left' : 'justify-center',
|
|
380
414
|
// Named rather than `transition-all`: the only thing that moves
|
|
381
|
-
// here is colour.
|
|
382
|
-
// property each frame, including the background GRADIENT — which
|
|
383
|
-
// never animated anyway, since the inactive state has no
|
|
384
|
-
// background-image to interpolate from.
|
|
415
|
+
// here is colour.
|
|
385
416
|
'border-none bg-transparent outline-none transition-colors duration-200',
|
|
386
417
|
// `outline-none` above drops the browser's own focus ring, so
|
|
387
418
|
// put one back or the strip is un-navigable by keyboard — you
|
|
388
419
|
// can tab through it with nothing to show where you are. Inset,
|
|
389
|
-
// so it stays within the button
|
|
390
|
-
// sitting 4px away.
|
|
420
|
+
// so it stays within the button: the tabs touch.
|
|
391
421
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus',
|
|
392
422
|
stretchTabs && 'flex-1',
|
|
393
423
|
tab.avatar && 'gap-[var(--spacing-system-xsf)]',
|
|
394
|
-
//
|
|
395
|
-
// alpha modifiers (to-ods-accent/10) silently produce no CSS.
|
|
396
|
-
// color-mix() accepts hex, so we derive the 10%-alpha stop from the token.
|
|
397
|
-
isActive
|
|
398
|
-
? 'bg-gradient-to-b from-transparent to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'
|
|
399
|
-
: 'hover:bg-gradient-to-b hover:from-transparent hover:to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]',
|
|
424
|
+
// The open tab is marked by the underline alone: no surface of its own.
|
|
400
425
|
)}
|
|
401
426
|
>
|
|
402
427
|
{tab.avatar ? (
|
|
@@ -409,8 +434,11 @@ const TabBar = memo(function TabBarImpl({
|
|
|
409
434
|
<tab.icon
|
|
410
435
|
className={cn(
|
|
411
436
|
'h-4 w-4 transition-colors content-md:h-6 content-md:w-6',
|
|
412
|
-
isActive
|
|
437
|
+
isActive
|
|
438
|
+
? !tab.accent && 'text-ods-accent'
|
|
439
|
+
: 'text-ods-text-secondary group-hover:text-ods-text-primary',
|
|
413
440
|
)}
|
|
441
|
+
style={isActive && tab.accent ? { color: tab.accent } : undefined}
|
|
414
442
|
/>
|
|
415
443
|
{tab.indicator && <StatusDot indicator={tab.indicator} className="absolute right-0 top-[-3px]" />}
|
|
416
444
|
</div>
|
|
@@ -425,7 +453,7 @@ const TabBar = memo(function TabBarImpl({
|
|
|
425
453
|
<span
|
|
426
454
|
className={cn(
|
|
427
455
|
'whitespace-nowrap transition-colors text-h4',
|
|
428
|
-
isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
456
|
+
isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary group-hover:text-ods-text-primary',
|
|
429
457
|
)}
|
|
430
458
|
>
|
|
431
459
|
{tab.label}
|
|
@@ -471,11 +499,11 @@ const TabBar = memo(function TabBarImpl({
|
|
|
471
499
|
// tests) rather than incidental styling.
|
|
472
500
|
data-testid="tab-navigation-underline"
|
|
473
501
|
className={cn(
|
|
474
|
-
'pointer-events-none absolute bottom-0 left-0
|
|
475
|
-
|
|
502
|
+
'pointer-events-none absolute bottom-0 left-0 h-1 motion-reduce:transition-none',
|
|
503
|
+
!activeAccent && 'bg-ods-accent',
|
|
476
504
|
!hasProgress && shouldAnimateIndicator && 'transition-[transform,width,opacity] duration-200 ease-out',
|
|
477
505
|
)}
|
|
478
|
-
style={indicatorStyle}
|
|
506
|
+
style={activeAccent ? { ...indicatorStyle, backgroundColor: activeAccent } : indicatorStyle}
|
|
479
507
|
/>
|
|
480
508
|
</div>
|
|
481
509
|
|