@passui/react 0.0.0-stage → 0.1.15
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 +83 -2
- package/package.json +96 -5
- package/src/appearance/index.ts +2 -0
- package/src/appearance/platform.tsx +123 -0
- package/src/appearance/theme.tsx +535 -0
- package/src/components/ambient/ambient.docs.tsx +68 -0
- package/src/components/ambient/ambient.tsx +43 -0
- package/src/components/ambient/examples/ambient-images.tsx +30 -0
- package/src/components/ambient/examples/ambient-now-playing.tsx +55 -0
- package/src/components/ambient/examples/ambient-showcase.tsx +33 -0
- package/src/components/ambient/examples/ambient-usage.tsx +19 -0
- package/src/components/ambient/index.ts +1 -0
- package/src/components/button/button-styles.ts +60 -0
- package/src/components/button/button.docs.tsx +116 -0
- package/src/components/button/button.tsx +91 -0
- package/src/components/button/examples/button-disabled.tsx +19 -0
- package/src/components/button/examples/button-icon.tsx +39 -0
- package/src/components/button/examples/button-link.tsx +12 -0
- package/src/components/button/examples/button-pending.tsx +27 -0
- package/src/components/button/examples/button-showcase.tsx +115 -0
- package/src/components/button/examples/button-sizes.tsx +12 -0
- package/src/components/button/examples/button-usage.tsx +12 -0
- package/src/components/button/examples/button-variants.tsx +15 -0
- package/src/components/button/examples/button-with-icon.tsx +32 -0
- package/src/components/button/examples/icon-button-player.tsx +38 -0
- package/src/components/button/examples/icon-button-showcase.tsx +83 -0
- package/src/components/button/examples/icon-button-usage.tsx +11 -0
- package/src/components/button/examples/icon-button-variants.tsx +39 -0
- package/src/components/button/icon-button.docs.tsx +68 -0
- package/src/components/button/icon-button.tsx +73 -0
- package/src/components/button/pending-spinner.tsx +14 -0
- package/src/components/button/use-pending-indicator.ts +27 -0
- package/src/components/chip/chip.docs.tsx +37 -0
- package/src/components/chip/chip.tsx +35 -0
- package/src/components/chip/examples/chip-in-rows.tsx +19 -0
- package/src/components/chip/examples/chip-showcase.tsx +51 -0
- package/src/components/chip/examples/chip-tones.tsx +13 -0
- package/src/components/chip/examples/chip-usage.tsx +5 -0
- package/src/components/compact-date-picker/calendar-month.ts +131 -0
- package/src/components/compact-date-picker/compact-date-picker.docs.tsx +81 -0
- package/src/components/compact-date-picker/compact-date-picker.tsx +438 -0
- package/src/components/compact-date-picker/examples/compact-date-picker-showcase.tsx +35 -0
- package/src/components/compact-date-picker/examples/compact-date-picker-usage.tsx +22 -0
- package/src/components/compact-time-picker/compact-time-picker.docs.tsx +80 -0
- package/src/components/compact-time-picker/compact-time-picker.tsx +432 -0
- package/src/components/compact-time-picker/examples/compact-time-picker-showcase.tsx +31 -0
- package/src/components/compact-time-picker/examples/compact-time-picker-usage.tsx +21 -0
- package/src/components/compact-time-picker/time-value.ts +67 -0
- package/src/components/content-view/content-view.docs.tsx +61 -0
- package/src/components/content-view/content-view.tsx +197 -0
- package/src/components/content-view/examples/content-view-header-action.tsx +33 -0
- package/src/components/content-view/examples/content-view-showcase.tsx +65 -0
- package/src/components/content-view/examples/content-view-stats.tsx +35 -0
- package/src/components/content-view/examples/content-view-usage.tsx +14 -0
- package/src/components/content-view/index.ts +1 -0
- package/src/components/context-menu/conceal-menu-source.ts +33 -0
- package/src/components/context-menu/context-menu-host.tsx +445 -0
- package/src/components/context-menu/context-menu-store.ts +165 -0
- package/src/components/context-menu/context-menu.docs.tsx +92 -0
- package/src/components/context-menu/examples/context-menu-checked.tsx +37 -0
- package/src/components/context-menu/examples/context-menu-list.tsx +85 -0
- package/src/components/context-menu/examples/context-menu-showcase.tsx +56 -0
- package/src/components/context-menu/examples/context-menu-toolbar.tsx +63 -0
- package/src/components/context-menu/examples/context-menu-usage.tsx +24 -0
- package/src/components/context-menu/menu-position.ts +55 -0
- package/src/components/context-menu/menu-reveal.ts +75 -0
- package/src/components/context-menu/use-context-menu.ts +90 -0
- package/src/components/conversation/conversation.docs.tsx +88 -0
- package/src/components/conversation/conversation.tsx +279 -0
- package/src/components/conversation/examples/conversation-composer.tsx +36 -0
- package/src/components/conversation/examples/conversation-days.tsx +48 -0
- package/src/components/conversation/examples/conversation-showcase.tsx +71 -0
- package/src/components/conversation/examples/conversation-usage.tsx +28 -0
- package/src/components/conversation/index.ts +1 -0
- package/src/components/date-picker/date-picker.docs.tsx +88 -0
- package/src/components/date-picker/date-picker.tsx +103 -0
- package/src/components/date-picker/date-value.ts +37 -0
- package/src/components/date-picker/examples/date-picker-locales.tsx +41 -0
- package/src/components/date-picker/examples/date-picker-past-only.tsx +29 -0
- package/src/components/date-picker/examples/date-picker-showcase.tsx +49 -0
- package/src/components/date-picker/examples/date-picker-usage.tsx +22 -0
- package/src/components/dialog/dialog-host.tsx +156 -0
- package/src/components/dialog/dialog-store.ts +56 -0
- package/src/components/dialog/dialog.docs.tsx +114 -0
- package/src/components/dialog/dialog.tsx +200 -0
- package/src/components/dialog/examples/dialog-confirm.tsx +24 -0
- package/src/components/dialog/examples/dialog-form.tsx +46 -0
- package/src/components/dialog/examples/dialog-inline.tsx +48 -0
- package/src/components/dialog/examples/dialog-showcase.tsx +53 -0
- package/src/components/dialog/examples/dialog-usage.tsx +31 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/labeled-content/examples/labeled-content-descriptions.tsx +37 -0
- package/src/components/labeled-content/examples/labeled-content-showcase.tsx +54 -0
- package/src/components/labeled-content/examples/labeled-content-usage.tsx +22 -0
- package/src/components/labeled-content/examples/labeled-content-values.tsx +26 -0
- package/src/components/labeled-content/examples/labeled-content-wide.tsx +46 -0
- package/src/components/labeled-content/labeled-content.docs.tsx +69 -0
- package/src/components/labeled-content/labeled-content.tsx +66 -0
- package/src/components/locale-switcher/examples/locale-switcher-native-names.tsx +30 -0
- package/src/components/locale-switcher/examples/locale-switcher-showcase.tsx +46 -0
- package/src/components/locale-switcher/examples/locale-switcher-translated-page.tsx +35 -0
- package/src/components/locale-switcher/examples/locale-switcher-usage.tsx +20 -0
- package/src/components/locale-switcher/index.ts +1 -0
- package/src/components/locale-switcher/locale-switcher.docs.tsx +91 -0
- package/src/components/locale-switcher/locale-switcher.tsx +252 -0
- package/src/components/picker/examples/picker-details.tsx +44 -0
- package/src/components/picker/examples/picker-searchable.tsx +48 -0
- package/src/components/picker/examples/picker-showcase.tsx +66 -0
- package/src/components/picker/examples/picker-usage.tsx +20 -0
- package/src/components/picker/picker.docs.tsx +68 -0
- package/src/components/picker/picker.tsx +493 -0
- package/src/components/popover/examples/popover-help.tsx +57 -0
- package/src/components/popover/examples/popover-menu.tsx +51 -0
- package/src/components/popover/examples/popover-showcase.tsx +61 -0
- package/src/components/popover/examples/popover-usage.tsx +31 -0
- package/src/components/popover/popover.docs.tsx +84 -0
- package/src/components/popover/popover.tsx +138 -0
- package/src/components/profile-image/examples/profile-image-fallbacks.tsx +29 -0
- package/src/components/profile-image/examples/profile-image-group.tsx +34 -0
- package/src/components/profile-image/examples/profile-image-showcase.tsx +30 -0
- package/src/components/profile-image/examples/profile-image-usage.tsx +14 -0
- package/src/components/profile-image/profile-image.docs.tsx +81 -0
- package/src/components/profile-image/profile-image.tsx +80 -0
- package/src/components/search-field/examples/search-field-filter.tsx +53 -0
- package/src/components/search-field/examples/search-field-showcase.tsx +56 -0
- package/src/components/search-field/examples/search-field-toolbar.tsx +27 -0
- package/src/components/search-field/examples/search-field-usage.tsx +18 -0
- package/src/components/search-field/search-field.docs.tsx +58 -0
- package/src/components/search-field/search-field.tsx +29 -0
- package/src/components/search-list/examples/search-list-dropdown.tsx +53 -0
- package/src/components/search-list/examples/search-list-groups.tsx +64 -0
- package/src/components/search-list/examples/search-list-showcase.tsx +67 -0
- package/src/components/search-list/examples/search-list-usage.tsx +27 -0
- package/src/components/search-list/search-list.docs.tsx +87 -0
- package/src/components/search-list/search-list.tsx +265 -0
- package/src/components/section/examples/section-actions.tsx +35 -0
- package/src/components/section/examples/section-groups.tsx +41 -0
- package/src/components/section/examples/section-showcase.tsx +63 -0
- package/src/components/section/examples/section-usage.tsx +19 -0
- package/src/components/section/section.docs.tsx +51 -0
- package/src/components/section/section.tsx +41 -0
- package/src/components/secure-field/examples/secure-field-new-password.tsx +56 -0
- package/src/components/secure-field/examples/secure-field-reveal.tsx +20 -0
- package/src/components/secure-field/examples/secure-field-showcase.tsx +62 -0
- package/src/components/secure-field/examples/secure-field-usage.tsx +19 -0
- package/src/components/secure-field/secure-field.docs.tsx +72 -0
- package/src/components/secure-field/secure-field.tsx +10 -0
- package/src/components/segmented-control/examples/segmented-control-overflow.tsx +28 -0
- package/src/components/segmented-control/examples/segmented-control-showcase.tsx +59 -0
- package/src/components/segmented-control/examples/segmented-control-usage.tsx +20 -0
- package/src/components/segmented-control/examples/segmented-control-views.tsx +32 -0
- package/src/components/segmented-control/segmented-control.docs.tsx +61 -0
- package/src/components/segmented-control/segmented-control.tsx +122 -0
- package/src/components/sheet/examples/sheet-choices.tsx +58 -0
- package/src/components/sheet/examples/sheet-share.tsx +51 -0
- package/src/components/sheet/examples/sheet-showcase.tsx +74 -0
- package/src/components/sheet/examples/sheet-usage.tsx +32 -0
- package/src/components/sheet/modal-environment.ts +113 -0
- package/src/components/sheet/sheet-portal.tsx +89 -0
- package/src/components/sheet/sheet.docs.tsx +75 -0
- package/src/components/sheet/sheet.tsx +112 -0
- package/src/components/sheet/use-sheet-focus.ts +130 -0
- package/src/components/sidebar/examples/sidebar-groups.tsx +133 -0
- package/src/components/sidebar/examples/sidebar-showcase.tsx +123 -0
- package/src/components/sidebar/examples/sidebar-unread.tsx +66 -0
- package/src/components/sidebar/examples/sidebar-usage.tsx +52 -0
- package/src/components/sidebar/sidebar.docs.tsx +135 -0
- package/src/components/sidebar/sidebar.tsx +69 -0
- package/src/components/stat-tile/examples/stat-tile-filters.tsx +32 -0
- package/src/components/stat-tile/examples/stat-tile-grid.tsx +21 -0
- package/src/components/stat-tile/examples/stat-tile-showcase.tsx +52 -0
- package/src/components/stat-tile/examples/stat-tile-usage.tsx +13 -0
- package/src/components/stat-tile/index.ts +1 -0
- package/src/components/stat-tile/stat-tile.docs.tsx +60 -0
- package/src/components/stat-tile/stat-tile.tsx +68 -0
- package/src/components/status-indicator/examples/status-indicator-live.tsx +34 -0
- package/src/components/status-indicator/examples/status-indicator-showcase.tsx +39 -0
- package/src/components/status-indicator/examples/status-indicator-states.tsx +17 -0
- package/src/components/status-indicator/examples/status-indicator-usage.tsx +9 -0
- package/src/components/status-indicator/status-indicator.docs.tsx +43 -0
- package/src/components/status-indicator/status-indicator.tsx +35 -0
- package/src/components/swatch-picker/examples/swatch-picker-accents.tsx +42 -0
- package/src/components/swatch-picker/examples/swatch-picker-labels.tsx +45 -0
- package/src/components/swatch-picker/examples/swatch-picker-showcase.tsx +70 -0
- package/src/components/swatch-picker/examples/swatch-picker-usage.tsx +39 -0
- package/src/components/swatch-picker/swatch-picker.docs.tsx +60 -0
- package/src/components/swatch-picker/swatch-picker.tsx +96 -0
- package/src/components/tab-bar/examples/tab-bar-five-tabs.tsx +66 -0
- package/src/components/tab-bar/examples/tab-bar-minimize.tsx +71 -0
- package/src/components/tab-bar/examples/tab-bar-showcase.tsx +82 -0
- package/src/components/tab-bar/examples/tab-bar-usage.tsx +54 -0
- package/src/components/tab-bar/tab-bar.docs.tsx +136 -0
- package/src/components/tab-bar/tab-bar.tsx +148 -0
- package/src/components/tab-view/examples/tab-view-keyboard.tsx +39 -0
- package/src/components/tab-view/examples/tab-view-settings.tsx +56 -0
- package/src/components/tab-view/examples/tab-view-showcase.tsx +53 -0
- package/src/components/tab-view/examples/tab-view-usage.tsx +23 -0
- package/src/components/tab-view/tab-view.docs.tsx +77 -0
- package/src/components/tab-view/tab-view.tsx +130 -0
- package/src/components/table/examples/table-empty.tsx +53 -0
- package/src/components/table/examples/table-invoices.tsx +102 -0
- package/src/components/table/examples/table-selection.tsx +150 -0
- package/src/components/table/examples/table-showcase.tsx +115 -0
- package/src/components/table/examples/table-usage.tsx +67 -0
- package/src/components/table/examples/table-wide.tsx +75 -0
- package/src/components/table/table.docs.tsx +131 -0
- package/src/components/table/table.tsx +149 -0
- package/src/components/text-editor/examples/text-editor-comment.tsx +48 -0
- package/src/components/text-editor/examples/text-editor-count.tsx +38 -0
- package/src/components/text-editor/examples/text-editor-showcase.tsx +58 -0
- package/src/components/text-editor/examples/text-editor-usage.tsx +25 -0
- package/src/components/text-editor/text-editor.docs.tsx +61 -0
- package/src/components/text-editor/text-editor.tsx +19 -0
- package/src/components/text-field/examples/text-field-icons.tsx +72 -0
- package/src/components/text-field/examples/text-field-showcase.tsx +50 -0
- package/src/components/text-field/examples/text-field-usage.tsx +18 -0
- package/src/components/text-field/examples/text-field-validation.tsx +27 -0
- package/src/components/text-field/text-field.docs.tsx +118 -0
- package/src/components/text-field/text-field.tsx +135 -0
- package/src/components/toast/examples/toast-action.tsx +32 -0
- package/src/components/toast/examples/toast-promise.tsx +45 -0
- package/src/components/toast/examples/toast-showcase.tsx +49 -0
- package/src/components/toast/examples/toast-usage.tsx +13 -0
- package/src/components/toast/toast-store.ts +166 -0
- package/src/components/toast/toast.docs.tsx +79 -0
- package/src/components/toast/toaster.tsx +374 -0
- package/src/components/toggle/examples/toggle-checkboxes.tsx +43 -0
- package/src/components/toggle/examples/toggle-settings.tsx +31 -0
- package/src/components/toggle/examples/toggle-showcase.tsx +59 -0
- package/src/components/toggle/examples/toggle-usage.tsx +13 -0
- package/src/components/toggle/toggle.docs.tsx +69 -0
- package/src/components/toggle/toggle.tsx +85 -0
- package/src/components/tooltip/examples/tooltip-icon-buttons.tsx +27 -0
- package/src/components/tooltip/examples/tooltip-shortcuts.tsx +38 -0
- package/src/components/tooltip/examples/tooltip-showcase.tsx +44 -0
- package/src/components/tooltip/examples/tooltip-usage.tsx +13 -0
- package/src/components/tooltip/tooltip-host.tsx +156 -0
- package/src/components/tooltip/tooltip.docs.tsx +56 -0
- package/src/components/view-controller/examples/view-controller-action.tsx +27 -0
- package/src/components/view-controller/examples/view-controller-settings.tsx +31 -0
- package/src/components/view-controller/examples/view-controller-showcase.tsx +45 -0
- package/src/components/view-controller/examples/view-controller-usage.tsx +19 -0
- package/src/components/view-controller/index.ts +1 -0
- package/src/components/view-controller/view-controller.docs.tsx +71 -0
- package/src/components/view-controller/view-controller.tsx +81 -0
- package/src/docs/index.ts +3 -0
- package/src/docs/registry.ts +113 -0
- package/src/docs/snippet.ts +17 -0
- package/src/docs/types.ts +180 -0
- package/src/hooks/scroll/minimize-decision.ts +13 -0
- package/src/hooks/use-anchored-placement.ts +43 -0
- package/src/hooks/use-dismiss.ts +58 -0
- package/src/hooks/use-media-query.ts +106 -0
- package/src/hooks/use-minimize-on-scroll.ts +92 -0
- package/src/hooks/use-visual-viewport.ts +76 -0
- package/src/hooks/viewport/types.ts +19 -0
- package/src/hooks/viewport/viewport-metrics.ts +25 -0
- package/src/icons/icons.tsx +298 -0
- package/src/index.ts +78 -0
- package/src/lib/anchored-placement.ts +92 -0
- package/src/lib/class-names.ts +21 -0
- package/src/lib/date.ts +96 -0
- package/src/lib/floating-layers.ts +28 -0
- package/src/lib/haptics.ts +25 -0
- package/src/lib/language.ts +21 -0
- package/src/lib/screen-edges.ts +45 -0
- package/src/lib/shapes.ts +70 -0
- package/src/motion/transitions.ts +30 -0
- package/src/styles/base.css +1 -0
- package/src/styles/styles.css +3 -0
- package/src/styles/theme.css +1 -0
- package/src/styles/typeset.css +1 -0
- package/src/styles/utilities.css +1 -0
- package/src/tokens.ts +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useContextMenu } from "@passui/react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
const orders = {
|
|
5
|
+
recent: "Recently edited",
|
|
6
|
+
title: "Title",
|
|
7
|
+
created: "Date created",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Order = keyof typeof orders
|
|
11
|
+
|
|
12
|
+
export function ContextMenuChecked() {
|
|
13
|
+
const [order, setOrder] = useState<Order>("recent")
|
|
14
|
+
const menu = useContextMenu(() => [
|
|
15
|
+
{ heading: "Sort by" },
|
|
16
|
+
...(Object.keys(orders) as Order[]).map((key) => ({
|
|
17
|
+
label: orders[key],
|
|
18
|
+
checked: key === order,
|
|
19
|
+
onSelect: () => setOrder(key),
|
|
20
|
+
})),
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
{...menu}
|
|
26
|
+
className="flex w-64 select-none flex-col items-center gap-1 rounded-4xl bg-elevated p-6 text-center shadow-sm ring-1 ring-separator/60"
|
|
27
|
+
>
|
|
28
|
+
<span className="text-subheadline font-medium text-label">
|
|
29
|
+
Notes
|
|
30
|
+
</span>
|
|
31
|
+
<span className="text-footnote text-label-secondary">
|
|
32
|
+
Sorted by {orders[order].toLowerCase()}.
|
|
33
|
+
Right-click to change.
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { toast, useContextMenu } from "@passui/react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
type Note = { id: string; title: string; pinned: boolean }
|
|
5
|
+
|
|
6
|
+
function NoteRow({
|
|
7
|
+
note,
|
|
8
|
+
onPin,
|
|
9
|
+
onDelete,
|
|
10
|
+
}: {
|
|
11
|
+
note: Note
|
|
12
|
+
onPin: () => void
|
|
13
|
+
onDelete: () => void
|
|
14
|
+
}) {
|
|
15
|
+
const menu = useContextMenu(() => [
|
|
16
|
+
{ heading: note.title },
|
|
17
|
+
{
|
|
18
|
+
label: note.pinned ? "Unpin" : "Pin",
|
|
19
|
+
onSelect: onPin,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: "Duplicate",
|
|
23
|
+
onSelect: () => toast(`Duplicated ${note.title}`),
|
|
24
|
+
},
|
|
25
|
+
"divider",
|
|
26
|
+
{
|
|
27
|
+
label: "Delete",
|
|
28
|
+
destructive: true,
|
|
29
|
+
onSelect: onDelete,
|
|
30
|
+
},
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<li
|
|
35
|
+
{...menu}
|
|
36
|
+
className="flex select-none items-center justify-between rounded-2xl px-3 py-2.5 text-subheadline text-label hover:bg-fill/60"
|
|
37
|
+
>
|
|
38
|
+
{note.title}
|
|
39
|
+
{note.pinned && (
|
|
40
|
+
<span className="text-caption text-accent-text">
|
|
41
|
+
Pinned
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
</li>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ContextMenuList() {
|
|
49
|
+
const [notes, setNotes] = useState<Note[]>([
|
|
50
|
+
{ id: "1", title: "Trip to Kyoto", pinned: true },
|
|
51
|
+
{ id: "2", title: "Groceries", pinned: false },
|
|
52
|
+
{ id: "3", title: "Book club picks", pinned: false },
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<ul className="m-0 flex w-full max-w-xs list-none flex-col p-0">
|
|
57
|
+
{notes.map((note) => (
|
|
58
|
+
<NoteRow
|
|
59
|
+
key={note.id}
|
|
60
|
+
note={note}
|
|
61
|
+
onPin={() =>
|
|
62
|
+
setNotes((current) =>
|
|
63
|
+
current.map((item) =>
|
|
64
|
+
item.id === note.id
|
|
65
|
+
? {
|
|
66
|
+
...item,
|
|
67
|
+
pinned:
|
|
68
|
+
!item.pinned,
|
|
69
|
+
}
|
|
70
|
+
: item
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
onDelete={() =>
|
|
75
|
+
setNotes((current) =>
|
|
76
|
+
current.filter(
|
|
77
|
+
(item) => item.id !== note.id
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { toast, useContextMenu } from "@passui/react"
|
|
2
|
+
|
|
3
|
+
const photos = [
|
|
4
|
+
{ id: "aiko", src: "/avatars/aiko.svg" },
|
|
5
|
+
{ id: "kenji", src: "/avatars/kenji.svg" },
|
|
6
|
+
{ id: "mira", src: "/avatars/mira.svg" },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
function Photo({ id, src }: { id: string; src: string }) {
|
|
10
|
+
const menu = useContextMenu(() => [
|
|
11
|
+
{
|
|
12
|
+
label: "Copy",
|
|
13
|
+
onSelect: () => toast("Photo copied"),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "Share",
|
|
17
|
+
onSelect: () => toast("Share sheet"),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "Add to favourites",
|
|
21
|
+
onSelect: () =>
|
|
22
|
+
toast.success("Added to favourites"),
|
|
23
|
+
},
|
|
24
|
+
"divider",
|
|
25
|
+
{
|
|
26
|
+
label: "Delete",
|
|
27
|
+
destructive: true,
|
|
28
|
+
onSelect: () => toast("Photo deleted"),
|
|
29
|
+
},
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<img
|
|
34
|
+
{...menu}
|
|
35
|
+
src={src}
|
|
36
|
+
alt={`${id} portrait`}
|
|
37
|
+
className="aspect-square w-full select-none rounded-3xl object-cover"
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ContextMenuShowcase() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex w-full max-w-sm flex-col gap-3">
|
|
45
|
+
<div className="grid grid-cols-3 gap-2">
|
|
46
|
+
{photos.map((photo) => (
|
|
47
|
+
<Photo key={photo.id} {...photo} />
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
<p className="text-center text-footnote text-label-secondary">
|
|
51
|
+
Right-click a photo, or press and hold on a
|
|
52
|
+
touch screen.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useRef } from "react"
|
|
2
|
+
import { IconButton } from "../../button/icon-button"
|
|
3
|
+
import {
|
|
4
|
+
MoreIcon,
|
|
5
|
+
ShareIcon,
|
|
6
|
+
StarIcon,
|
|
7
|
+
} from "../../../icons/icons"
|
|
8
|
+
import { openMenu } from "../context-menu-store"
|
|
9
|
+
import { toast } from "../../toast/toast-store"
|
|
10
|
+
|
|
11
|
+
export function ContextMenuToolbar() {
|
|
12
|
+
const toolbar = useRef<HTMLDivElement>(null)
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
ref={toolbar}
|
|
16
|
+
className="flex items-center gap-1"
|
|
17
|
+
>
|
|
18
|
+
<IconButton
|
|
19
|
+
label="Favourite"
|
|
20
|
+
variant="ghost"
|
|
21
|
+
size="icon-xs"
|
|
22
|
+
>
|
|
23
|
+
<StarIcon size={18} />
|
|
24
|
+
</IconButton>
|
|
25
|
+
<IconButton
|
|
26
|
+
label="Share"
|
|
27
|
+
variant="ghost"
|
|
28
|
+
size="icon-xs"
|
|
29
|
+
>
|
|
30
|
+
<ShareIcon size={18} />
|
|
31
|
+
</IconButton>
|
|
32
|
+
<IconButton
|
|
33
|
+
label="More"
|
|
34
|
+
variant="ghost"
|
|
35
|
+
size="icon-xs"
|
|
36
|
+
onClick={(event) =>
|
|
37
|
+
openMenu(
|
|
38
|
+
event.currentTarget,
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
label: "Duplicate",
|
|
42
|
+
onSelect: () =>
|
|
43
|
+
toast("Duplicated"),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "Move",
|
|
47
|
+
onSelect: () =>
|
|
48
|
+
toast("Choose a folder"),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
{
|
|
52
|
+
edge: "trailing",
|
|
53
|
+
toolbar:
|
|
54
|
+
toolbar.current ?? undefined,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<MoreIcon size={18} />
|
|
60
|
+
</IconButton>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { toast, useContextMenu } from "@passui/react"
|
|
2
|
+
|
|
3
|
+
export function ContextMenuUsage() {
|
|
4
|
+
// render one <ContextMenuHost /> near the root of your app
|
|
5
|
+
const menu = useContextMenu(() => [
|
|
6
|
+
{
|
|
7
|
+
label: "Copy link",
|
|
8
|
+
onSelect: () => toast("Link copied"),
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
label: "Open in new tab",
|
|
12
|
+
onSelect: () => toast("Opened"),
|
|
13
|
+
},
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
{...menu}
|
|
19
|
+
className="select-none rounded-3xl bg-fill/60 px-6 py-4 text-callout text-label"
|
|
20
|
+
>
|
|
21
|
+
Right-click this card
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { OpenMenu } from "./context-menu-store"
|
|
2
|
+
import type { MenuRect } from "./menu-reveal"
|
|
3
|
+
|
|
4
|
+
type MenuAnchor = Pick<
|
|
5
|
+
OpenMenu,
|
|
6
|
+
"x" | "y" | "alignX" | "alignY"
|
|
7
|
+
>
|
|
8
|
+
type MenuSize = Pick<MenuRect, "width" | "height">
|
|
9
|
+
type MenuBounds = {
|
|
10
|
+
left: number
|
|
11
|
+
top: number
|
|
12
|
+
right: number
|
|
13
|
+
bottom: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fit within the current visible viewport, including a keyboard or Safari pan. */
|
|
17
|
+
export function menuPosition(
|
|
18
|
+
menu: MenuAnchor,
|
|
19
|
+
size: MenuSize,
|
|
20
|
+
bounds: MenuBounds
|
|
21
|
+
) {
|
|
22
|
+
const { width, height } = size
|
|
23
|
+
const { left, top, right, bottom } = bounds
|
|
24
|
+
const fitsAfterX = menu.x + width <= right
|
|
25
|
+
const fitsBeforeX = menu.x - width >= left
|
|
26
|
+
const endX =
|
|
27
|
+
menu.alignX === "end"
|
|
28
|
+
? fitsBeforeX || !fitsAfterX
|
|
29
|
+
: !fitsAfterX
|
|
30
|
+
const fitsBelow = menu.y + height <= bottom
|
|
31
|
+
const fitsAbove = menu.y - height >= top
|
|
32
|
+
const endY =
|
|
33
|
+
menu.alignY === "bottom"
|
|
34
|
+
? fitsAbove || !fitsBelow
|
|
35
|
+
: !fitsBelow
|
|
36
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
37
|
+
Math.min(Math.max(value, min), Math.max(min, max))
|
|
38
|
+
return {
|
|
39
|
+
left: clamp(
|
|
40
|
+
endX ? menu.x - width : menu.x,
|
|
41
|
+
left,
|
|
42
|
+
right - width
|
|
43
|
+
),
|
|
44
|
+
top: clamp(
|
|
45
|
+
endY ? menu.y - height : menu.y,
|
|
46
|
+
top,
|
|
47
|
+
bottom - height
|
|
48
|
+
),
|
|
49
|
+
originX: endX ? 1 : 0,
|
|
50
|
+
originY: endY ? 1 : 0,
|
|
51
|
+
maxHeight: Math.max(0, bottom - top),
|
|
52
|
+
width,
|
|
53
|
+
height: Math.min(height, Math.max(0, bottom - top)),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type MenuRect = {
|
|
2
|
+
left: number
|
|
3
|
+
top: number
|
|
4
|
+
width: number
|
|
5
|
+
height: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The closed surface occupies its trigger, even when the final menu flips at an edge. */
|
|
9
|
+
export function menuReveal(
|
|
10
|
+
source: MenuRect | undefined,
|
|
11
|
+
target: MenuRect
|
|
12
|
+
) {
|
|
13
|
+
if (
|
|
14
|
+
!source ||
|
|
15
|
+
source.width <= 0 ||
|
|
16
|
+
source.height <= 0 ||
|
|
17
|
+
target.width <= 0 ||
|
|
18
|
+
target.height <= 0
|
|
19
|
+
)
|
|
20
|
+
return {
|
|
21
|
+
x: 0,
|
|
22
|
+
y: 0,
|
|
23
|
+
scaleX: 0.96,
|
|
24
|
+
scaleY: 0.96,
|
|
25
|
+
opacity: 0,
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
x:
|
|
29
|
+
source.left +
|
|
30
|
+
source.width / 2 -
|
|
31
|
+
target.left -
|
|
32
|
+
target.width / 2,
|
|
33
|
+
y:
|
|
34
|
+
source.top +
|
|
35
|
+
source.height / 2 -
|
|
36
|
+
target.top -
|
|
37
|
+
target.height / 2,
|
|
38
|
+
scaleX: source.width / target.width,
|
|
39
|
+
scaleY: source.height / target.height,
|
|
40
|
+
opacity: 1,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Stretch across the action group before opening vertically; reverse on dismissal. */
|
|
45
|
+
export function toolbarMenuReveal(
|
|
46
|
+
source: MenuRect,
|
|
47
|
+
toolbar: MenuRect,
|
|
48
|
+
target: MenuRect
|
|
49
|
+
) {
|
|
50
|
+
const start = menuReveal(source, target)
|
|
51
|
+
const stretched = menuReveal(
|
|
52
|
+
{
|
|
53
|
+
...toolbar,
|
|
54
|
+
top: source.top,
|
|
55
|
+
height: source.height,
|
|
56
|
+
},
|
|
57
|
+
target
|
|
58
|
+
)
|
|
59
|
+
return {
|
|
60
|
+
enter: {
|
|
61
|
+
x: [start.x, stretched.x, 0],
|
|
62
|
+
y: [start.y, stretched.y, 0],
|
|
63
|
+
scaleX: [start.scaleX, stretched.scaleX, 1],
|
|
64
|
+
scaleY: [start.scaleY, stretched.scaleY, 1],
|
|
65
|
+
opacity: 1,
|
|
66
|
+
},
|
|
67
|
+
exit: {
|
|
68
|
+
x: [0, stretched.x, start.x],
|
|
69
|
+
y: [0, stretched.y, start.y],
|
|
70
|
+
scaleX: [1, stretched.scaleX, start.scaleX],
|
|
71
|
+
scaleY: [1, stretched.scaleY, start.scaleY],
|
|
72
|
+
opacity: 1,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MouseEvent,
|
|
3
|
+
type PointerEvent,
|
|
4
|
+
useRef,
|
|
5
|
+
} from "react"
|
|
6
|
+
import { haptic } from "../../lib/haptics"
|
|
7
|
+
import {
|
|
8
|
+
type ContextMenuItem,
|
|
9
|
+
openContextMenu,
|
|
10
|
+
} from "./context-menu-store"
|
|
11
|
+
|
|
12
|
+
const LONG_PRESS_MS = 480
|
|
13
|
+
const MOVE_TOLERANCE = 10
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Right-click on desktop, long-press on touch screens: both open the same
|
|
17
|
+
* menu of actions. Spread the returned handlers onto the element.
|
|
18
|
+
*/
|
|
19
|
+
export function useContextMenu(
|
|
20
|
+
items: () => ContextMenuItem[]
|
|
21
|
+
) {
|
|
22
|
+
const press = useRef<{
|
|
23
|
+
x: number
|
|
24
|
+
y: number
|
|
25
|
+
timer: ReturnType<typeof setTimeout>
|
|
26
|
+
} | null>(null)
|
|
27
|
+
const suppressClick = useRef(false)
|
|
28
|
+
|
|
29
|
+
const cancel = () => {
|
|
30
|
+
if (press.current) clearTimeout(press.current.timer)
|
|
31
|
+
press.current = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
onContextMenu(event: MouseEvent) {
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
cancel()
|
|
38
|
+
openContextMenu(
|
|
39
|
+
event.clientX,
|
|
40
|
+
event.clientY,
|
|
41
|
+
items(),
|
|
42
|
+
false,
|
|
43
|
+
event.currentTarget
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
onPointerDown(event: PointerEvent) {
|
|
47
|
+
if (event.pointerType !== "touch") return
|
|
48
|
+
const { clientX: x, clientY: y } = event
|
|
49
|
+
const anchor = event.currentTarget
|
|
50
|
+
cancel()
|
|
51
|
+
press.current = {
|
|
52
|
+
x,
|
|
53
|
+
y,
|
|
54
|
+
timer: setTimeout(() => {
|
|
55
|
+
press.current = null
|
|
56
|
+
suppressClick.current = true
|
|
57
|
+
haptic("medium")
|
|
58
|
+
openContextMenu(
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
items(),
|
|
62
|
+
true,
|
|
63
|
+
anchor
|
|
64
|
+
)
|
|
65
|
+
}, LONG_PRESS_MS),
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onPointerMove(event: PointerEvent) {
|
|
69
|
+
const start = press.current
|
|
70
|
+
if (
|
|
71
|
+
start &&
|
|
72
|
+
Math.hypot(
|
|
73
|
+
event.clientX - start.x,
|
|
74
|
+
event.clientY - start.y
|
|
75
|
+
) > MOVE_TOLERANCE
|
|
76
|
+
) {
|
|
77
|
+
cancel()
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
onPointerUp: cancel,
|
|
81
|
+
onPointerCancel: cancel,
|
|
82
|
+
// The tap that ends a long-press shouldn't also open the row.
|
|
83
|
+
onClickCapture(event: MouseEvent) {
|
|
84
|
+
if (!suppressClick.current) return
|
|
85
|
+
suppressClick.current = false
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
event.stopPropagation()
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ConversationDays } from "./examples/conversation-days"
|
|
2
|
+
import { CustomerSupportIcon } from "@hugeicons/core-free-icons"
|
|
3
|
+
import { HugeiconsIcon } from "@hugeicons/react"
|
|
4
|
+
import { defineDocs } from "../../docs/types"
|
|
5
|
+
import { Conversation } from "./conversation"
|
|
6
|
+
import { ConversationComposer } from "./examples/conversation-composer"
|
|
7
|
+
import { ConversationShowcase } from "./examples/conversation-showcase"
|
|
8
|
+
import { ConversationUsage } from "./examples/conversation-usage"
|
|
9
|
+
|
|
10
|
+
// Conversation is two parts; the playground shows Messages, with the
|
|
11
|
+
// other side's name and avatar switched on and off.
|
|
12
|
+
type DemoProps = {
|
|
13
|
+
author: string
|
|
14
|
+
avatar: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const minutesAgo = (minutes: number) =>
|
|
18
|
+
new Date(Date.now() - minutes * 60_000).toISOString()
|
|
19
|
+
|
|
20
|
+
export const conversationDocs = defineDocs<DemoProps>()({
|
|
21
|
+
slug: "conversation",
|
|
22
|
+
name: "Conversation",
|
|
23
|
+
importPath: "@passui/react/conversation",
|
|
24
|
+
controls: {
|
|
25
|
+
author: { kind: "text", default: "Support" },
|
|
26
|
+
avatar: { kind: "boolean", default: true },
|
|
27
|
+
},
|
|
28
|
+
render: ({ author, avatar }) => {
|
|
29
|
+
const them = {
|
|
30
|
+
mine: false,
|
|
31
|
+
author: author || undefined,
|
|
32
|
+
avatar: avatar ? (
|
|
33
|
+
<span className="flex size-7 items-center justify-center rounded-full bg-accent-soft text-accent-text">
|
|
34
|
+
<HugeiconsIcon
|
|
35
|
+
icon={CustomerSupportIcon}
|
|
36
|
+
size={16}
|
|
37
|
+
/>
|
|
38
|
+
</span>
|
|
39
|
+
) : undefined,
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<div className="w-full max-w-sm">
|
|
43
|
+
<Conversation.Messages
|
|
44
|
+
messages={[
|
|
45
|
+
{
|
|
46
|
+
id: "1",
|
|
47
|
+
mine: true,
|
|
48
|
+
body: "I can't sign in on my new phone.",
|
|
49
|
+
createdAt: minutesAgo(9),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "2",
|
|
53
|
+
...them,
|
|
54
|
+
body: "Sorry about that! Did you set up a passkey on your old one?",
|
|
55
|
+
createdAt: minutesAgo(7),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "3",
|
|
59
|
+
...them,
|
|
60
|
+
body: "If so, you can use it from your new phone by scanning the code.",
|
|
61
|
+
createdAt: minutesAgo(7),
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
childrenSnippet: ({ author, avatar }) => ({
|
|
69
|
+
imports: avatar
|
|
70
|
+
? [
|
|
71
|
+
'import { CustomerSupportIcon } from "@hugeicons/core-free-icons"',
|
|
72
|
+
'import { HugeiconsIcon } from "@hugeicons/react"',
|
|
73
|
+
]
|
|
74
|
+
: [],
|
|
75
|
+
element: "Conversation.Messages",
|
|
76
|
+
omit: ["author", "avatar"],
|
|
77
|
+
expressionProps: {
|
|
78
|
+
messages: `[\n { id: "1", mine: true, body: "Hi!", createdAt: "2026-10-14T09:00:00Z" },\n { id: "2", mine: false, body: "Hello, how can we help?"${author ? `, author: ${JSON.stringify(author)}` : ""}${avatar ? ", avatar: <HugeiconsIcon icon={CustomerSupportIcon} size={16} />" : ""}, createdAt: "2026-10-14T09:01:00Z" },\n ]`,
|
|
79
|
+
},
|
|
80
|
+
children: "",
|
|
81
|
+
}),
|
|
82
|
+
usage: ConversationUsage,
|
|
83
|
+
showcase: ConversationShowcase,
|
|
84
|
+
examples: [
|
|
85
|
+
{ id: "composer", component: ConversationComposer },
|
|
86
|
+
{ id: "days", component: ConversationDays },
|
|
87
|
+
],
|
|
88
|
+
})
|