@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,92 @@
|
|
|
1
|
+
import { screenEdges } from "./screen-edges"
|
|
2
|
+
|
|
3
|
+
export interface AnchoredPlacement {
|
|
4
|
+
left: number
|
|
5
|
+
width: number
|
|
6
|
+
top?: number
|
|
7
|
+
bottom?: number
|
|
8
|
+
/** Opens upward, so motion should grow from the bottom edge. */
|
|
9
|
+
above: boolean
|
|
10
|
+
/**
|
|
11
|
+
* Set when the chosen side has less room than the panel needs: the
|
|
12
|
+
* panel scrolls inside this height instead of crossing the edge.
|
|
13
|
+
*/
|
|
14
|
+
maxHeight?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The measurements placement decides on, so the math stays testable. */
|
|
18
|
+
export interface PlacementFrame {
|
|
19
|
+
anchor: { top: number; bottom: number; left: number }
|
|
20
|
+
viewport: { width: number; height: number }
|
|
21
|
+
edges: {
|
|
22
|
+
top: number
|
|
23
|
+
right: number
|
|
24
|
+
bottom: number
|
|
25
|
+
left: number
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Places a panel of `width` under its anchor, or above it when there's
|
|
31
|
+
* less than `needed` height below and more room above, kept inside the
|
|
32
|
+
* screen's safe edges.
|
|
33
|
+
*/
|
|
34
|
+
export function placeInViewport(
|
|
35
|
+
{ anchor, viewport, edges }: PlacementFrame,
|
|
36
|
+
width: number,
|
|
37
|
+
needed: number,
|
|
38
|
+
gap = 6
|
|
39
|
+
): AnchoredPlacement {
|
|
40
|
+
const fitted = Math.min(
|
|
41
|
+
width,
|
|
42
|
+
viewport.width - edges.left - edges.right
|
|
43
|
+
)
|
|
44
|
+
const left = Math.min(
|
|
45
|
+
Math.max(edges.left, anchor.left),
|
|
46
|
+
viewport.width - fitted - edges.right
|
|
47
|
+
)
|
|
48
|
+
const roomBelow =
|
|
49
|
+
viewport.height - anchor.bottom - gap - edges.bottom
|
|
50
|
+
const roomAbove = anchor.top - gap - edges.top
|
|
51
|
+
const above = roomBelow < needed && roomAbove > roomBelow
|
|
52
|
+
const room = above ? roomAbove : roomBelow
|
|
53
|
+
const placement: AnchoredPlacement = {
|
|
54
|
+
left,
|
|
55
|
+
width: fitted,
|
|
56
|
+
above,
|
|
57
|
+
...(above
|
|
58
|
+
? { bottom: viewport.height - anchor.top + gap }
|
|
59
|
+
: { top: anchor.bottom + gap }),
|
|
60
|
+
}
|
|
61
|
+
if (room < needed) placement.maxHeight = room
|
|
62
|
+
return placement
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `placeInViewport` from live DOM measurements. */
|
|
66
|
+
export function placeAnchored(
|
|
67
|
+
anchor: HTMLElement,
|
|
68
|
+
width: number,
|
|
69
|
+
needed: number,
|
|
70
|
+
gap = 6
|
|
71
|
+
): AnchoredPlacement {
|
|
72
|
+
const rect = anchor.getBoundingClientRect()
|
|
73
|
+
const viewport = window.visualViewport
|
|
74
|
+
return placeInViewport(
|
|
75
|
+
{
|
|
76
|
+
anchor: {
|
|
77
|
+
top: rect.top,
|
|
78
|
+
bottom: rect.bottom,
|
|
79
|
+
left: rect.left,
|
|
80
|
+
},
|
|
81
|
+
viewport: {
|
|
82
|
+
width: viewport?.width ?? window.innerWidth,
|
|
83
|
+
height:
|
|
84
|
+
viewport?.height ?? window.innerHeight,
|
|
85
|
+
},
|
|
86
|
+
edges: screenEdges(),
|
|
87
|
+
},
|
|
88
|
+
width,
|
|
89
|
+
needed,
|
|
90
|
+
gap
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { clsx } from "clsx"
|
|
2
|
+
import type { ClassValue } from "clsx"
|
|
3
|
+
import { extendTailwindMerge } from "tailwind-merge"
|
|
4
|
+
import { textStyles } from "@passui/tokens/typography"
|
|
5
|
+
|
|
6
|
+
const textStyleClasses = Object.keys(textStyles).map(
|
|
7
|
+
(name) =>
|
|
8
|
+
`text-${name.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const twMerge = extendTailwindMerge({
|
|
12
|
+
extend: {
|
|
13
|
+
classGroups: {
|
|
14
|
+
"font-size": textStyleClasses,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export function cn(...inputs: ClassValue[]) {
|
|
20
|
+
return twMerge(clsx(inputs))
|
|
21
|
+
}
|
package/src/lib/date.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Dates through the platform's own Intl, no date library in the bundle.
|
|
2
|
+
// en-US on purpose: the server renders first, so a viewer-specific locale
|
|
3
|
+
// would make server and browser disagree on hydration.
|
|
4
|
+
const LOCALE = "en-US"
|
|
5
|
+
|
|
6
|
+
const PRESETS = {
|
|
7
|
+
/** Aug 30, 2026 */
|
|
8
|
+
PP: { dateStyle: "medium" },
|
|
9
|
+
/** August 30, 2026 */
|
|
10
|
+
PPP: { dateStyle: "long" },
|
|
11
|
+
/** Aug 30, 2026, 2:15 PM */
|
|
12
|
+
PPp: { dateStyle: "medium", timeStyle: "short" },
|
|
13
|
+
/** August 30, 2026 at 2:15 PM */
|
|
14
|
+
PPPp: { dateStyle: "long", timeStyle: "short" },
|
|
15
|
+
/** Aug 30 */
|
|
16
|
+
"MMM d": { month: "short", day: "numeric" },
|
|
17
|
+
/** Aug 30, 14:15 */
|
|
18
|
+
"MMM d, HH:mm": {
|
|
19
|
+
month: "short",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
hour: "2-digit",
|
|
22
|
+
minute: "2-digit",
|
|
23
|
+
hourCycle: "h23",
|
|
24
|
+
},
|
|
25
|
+
/** Aug 30, 14:15:09 */
|
|
26
|
+
"MMM d, HH:mm:ss": {
|
|
27
|
+
month: "short",
|
|
28
|
+
day: "numeric",
|
|
29
|
+
hour: "2-digit",
|
|
30
|
+
minute: "2-digit",
|
|
31
|
+
second: "2-digit",
|
|
32
|
+
hourCycle: "h23",
|
|
33
|
+
},
|
|
34
|
+
} satisfies Record<string, Intl.DateTimeFormatOptions>
|
|
35
|
+
|
|
36
|
+
export type DatePreset = keyof typeof PRESETS
|
|
37
|
+
|
|
38
|
+
const formatters = new Map<string, Intl.DateTimeFormat>()
|
|
39
|
+
|
|
40
|
+
function formatter(
|
|
41
|
+
key: string,
|
|
42
|
+
options: Intl.DateTimeFormatOptions
|
|
43
|
+
) {
|
|
44
|
+
let cached = formatters.get(key)
|
|
45
|
+
if (!cached) {
|
|
46
|
+
cached = new Intl.DateTimeFormat(LOCALE, options)
|
|
47
|
+
formatters.set(key, cached)
|
|
48
|
+
}
|
|
49
|
+
return cached
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A stored UTC timestamp shown as its UTC wall-clock time, whatever the viewer's timezone. */
|
|
53
|
+
export function formatUtc(
|
|
54
|
+
date: Date | string | number,
|
|
55
|
+
preset: DatePreset
|
|
56
|
+
) {
|
|
57
|
+
return formatter(`utc:${preset}`, {
|
|
58
|
+
...PRESETS[preset],
|
|
59
|
+
timeZone: "UTC",
|
|
60
|
+
}).format(new Date(date))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The viewer's local time of day, like 2:15 PM. */
|
|
64
|
+
export function formatTime(date: Date | string | number) {
|
|
65
|
+
return formatter("local:time", {
|
|
66
|
+
timeStyle: "short",
|
|
67
|
+
}).format(new Date(date))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
|
|
71
|
+
["year", 31_536_000],
|
|
72
|
+
["month", 2_592_000],
|
|
73
|
+
["week", 604_800],
|
|
74
|
+
["day", 86_400],
|
|
75
|
+
["hour", 3_600],
|
|
76
|
+
["minute", 60],
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
let relative: Intl.RelativeTimeFormat | undefined
|
|
80
|
+
|
|
81
|
+
/** "5 minutes ago", "in 30 days": the largest whole unit that fits. */
|
|
82
|
+
export function fromNow(date: Date | string | number) {
|
|
83
|
+
relative ??= new Intl.RelativeTimeFormat(LOCALE, {
|
|
84
|
+
numeric: "auto",
|
|
85
|
+
})
|
|
86
|
+
const seconds =
|
|
87
|
+
(new Date(date).getTime() - Date.now()) / 1000
|
|
88
|
+
for (const [unit, size] of UNITS) {
|
|
89
|
+
if (Math.abs(seconds) >= size)
|
|
90
|
+
return relative.format(
|
|
91
|
+
Math.round(seconds / size),
|
|
92
|
+
unit
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return relative.format(0, "second")
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const FLOATING_LAYER = "data-floating-layer"
|
|
2
|
+
|
|
3
|
+
const anchors = new WeakMap<Element, Element>()
|
|
4
|
+
|
|
5
|
+
/** Portals retain the ownership of the control that opened them. */
|
|
6
|
+
export function registerFloatingLayer(
|
|
7
|
+
layer: Element,
|
|
8
|
+
anchor: Element
|
|
9
|
+
): () => void {
|
|
10
|
+
anchors.set(layer, anchor)
|
|
11
|
+
return () => anchors.delete(layer)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ownsFloatingLayer(
|
|
15
|
+
container: Element,
|
|
16
|
+
target: Element | null
|
|
17
|
+
): boolean {
|
|
18
|
+
let layer = target?.closest(`[${FLOATING_LAYER}]`)
|
|
19
|
+
const visited = new Set<Element>()
|
|
20
|
+
while (layer && !visited.has(layer)) {
|
|
21
|
+
visited.add(layer)
|
|
22
|
+
const anchor = anchors.get(layer)
|
|
23
|
+
if (!anchor) return false
|
|
24
|
+
if (container.contains(anchor)) return true
|
|
25
|
+
layer = anchor.closest(`[${FLOATING_LAYER}]`)
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The feedback kinds the systems share, from a selection tick to an error. */
|
|
2
|
+
export type HapticKind =
|
|
3
|
+
| "selection"
|
|
4
|
+
| "light"
|
|
5
|
+
| "medium"
|
|
6
|
+
| "heavy"
|
|
7
|
+
| "success"
|
|
8
|
+
| "warning"
|
|
9
|
+
| "error"
|
|
10
|
+
|
|
11
|
+
let player: (kind: HapticKind) => void = () => {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Components only ask for feedback; the app decides how it plays on this
|
|
15
|
+
* device (system haptics, a vibration, or nothing) and whether it's on.
|
|
16
|
+
*/
|
|
17
|
+
export function setHapticPlayer(
|
|
18
|
+
next: (kind: HapticKind) => void
|
|
19
|
+
): void {
|
|
20
|
+
player = next
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function haptic(kind: HapticKind): void {
|
|
24
|
+
player(kind)
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A language's name in itself, capitalized: "Français", "中文". */
|
|
2
|
+
export function getNativeLanguageName(tag: string) {
|
|
3
|
+
const name = new Intl.DisplayNames([tag], {
|
|
4
|
+
type: "language",
|
|
5
|
+
}).of(tag)
|
|
6
|
+
return name
|
|
7
|
+
? name.charAt(0).toUpperCase() + name.slice(1)
|
|
8
|
+
: tag
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A language's name as read in `displayLocale`: "French" in English. */
|
|
12
|
+
export function getLocalizedLanguageName(
|
|
13
|
+
tag: string,
|
|
14
|
+
displayLocale: string
|
|
15
|
+
) {
|
|
16
|
+
return (
|
|
17
|
+
new Intl.DisplayNames([displayLocale], {
|
|
18
|
+
type: "language",
|
|
19
|
+
}).of(tag) ?? tag
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface ScreenEdges {
|
|
2
|
+
top: number
|
|
3
|
+
right: number
|
|
4
|
+
bottom: number
|
|
5
|
+
left: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let probe: HTMLDivElement | undefined
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How far menus and popovers keep from each side of the screen: clear of
|
|
12
|
+
* the status bar, gesture bar and notch, and on touch screens a little
|
|
13
|
+
* further from the glass, as the systems' own menus are.
|
|
14
|
+
*/
|
|
15
|
+
export function screenEdges(): ScreenEdges {
|
|
16
|
+
const margin =
|
|
17
|
+
typeof window !== "undefined" &&
|
|
18
|
+
window.matchMedia("(pointer: coarse)").matches
|
|
19
|
+
? 12
|
|
20
|
+
: 8
|
|
21
|
+
if (typeof document === "undefined") {
|
|
22
|
+
return {
|
|
23
|
+
top: margin,
|
|
24
|
+
right: margin,
|
|
25
|
+
bottom: margin,
|
|
26
|
+
left: margin,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!probe) {
|
|
30
|
+
probe = document.createElement("div")
|
|
31
|
+
probe.setAttribute("aria-hidden", "true")
|
|
32
|
+
probe.style.cssText =
|
|
33
|
+
"position:fixed;inset:0;visibility:hidden;pointer-events:none;" +
|
|
34
|
+
"padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)"
|
|
35
|
+
}
|
|
36
|
+
if (!probe.isConnected) document.body.appendChild(probe)
|
|
37
|
+
const style = getComputedStyle(probe)
|
|
38
|
+
return {
|
|
39
|
+
top: Number.parseFloat(style.paddingTop) + margin,
|
|
40
|
+
right: Number.parseFloat(style.paddingRight) + margin,
|
|
41
|
+
bottom:
|
|
42
|
+
Number.parseFloat(style.paddingBottom) + margin,
|
|
43
|
+
left: Number.parseFloat(style.paddingLeft) + margin,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Material 3 expressive outlines: a circle and the scalloped "cookie", "clover", and "sunny" shapes. */
|
|
2
|
+
export const shapes = [
|
|
3
|
+
"circle",
|
|
4
|
+
"cookie4",
|
|
5
|
+
"cookie6",
|
|
6
|
+
"cookie9",
|
|
7
|
+
"clover",
|
|
8
|
+
"sunny",
|
|
9
|
+
/** Reserved for the Pherus mark itself, so it never matches a project's icon. */
|
|
10
|
+
"cookie12",
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
export type Shape = (typeof shapes)[number]
|
|
14
|
+
|
|
15
|
+
/** Lobe count and wave depth (as a fraction of the radius) for each wavy shape. */
|
|
16
|
+
const waves: Record<
|
|
17
|
+
Exclude<Shape, "circle">,
|
|
18
|
+
{ lobes: number; depth: number }
|
|
19
|
+
> = {
|
|
20
|
+
cookie4: { lobes: 4, depth: 0.08 },
|
|
21
|
+
cookie6: { lobes: 6, depth: 0.07 },
|
|
22
|
+
cookie9: { lobes: 9, depth: 0.055 },
|
|
23
|
+
clover: { lobes: 4, depth: 0.16 },
|
|
24
|
+
sunny: { lobes: 8, depth: 0.045 },
|
|
25
|
+
cookie12: { lobes: 12, depth: 0.04 },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STEPS = 144
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An SVG path for a Material 3 expressive shape centred on (cx, cy): a circle
|
|
32
|
+
* whose radius rides a cosine wave, which gives the soft scalloped "cookie",
|
|
33
|
+
* "clover", and "sunny" outlines. The wave is normalised so every shape fills
|
|
34
|
+
* the same `r`, keeping icons optically the same size.
|
|
35
|
+
*/
|
|
36
|
+
export function shapePath(
|
|
37
|
+
shape: Shape,
|
|
38
|
+
cx: number,
|
|
39
|
+
cy: number,
|
|
40
|
+
r: number
|
|
41
|
+
) {
|
|
42
|
+
if (shape === "circle") {
|
|
43
|
+
return `M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${r * 2} 0 a ${r} ${r} 0 1 0 ${-r * 2} 0 Z`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { lobes, depth } = waves[shape]
|
|
47
|
+
const base = r / (1 + depth)
|
|
48
|
+
const points: string[] = []
|
|
49
|
+
for (let i = 0; i < STEPS; i++) {
|
|
50
|
+
const theta = (i / STEPS) * Math.PI * 2 - Math.PI / 2
|
|
51
|
+
const radius =
|
|
52
|
+
base *
|
|
53
|
+
(1 +
|
|
54
|
+
depth *
|
|
55
|
+
Math.cos(lobes * (theta + Math.PI / 2)))
|
|
56
|
+
const x = cx + radius * Math.cos(theta)
|
|
57
|
+
const y = cy + radius * Math.sin(theta)
|
|
58
|
+
points.push(
|
|
59
|
+
`${i === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return `${points.join(" ")} Z`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The app wide `dualTone` rule dims every svg path for two tone icons; a
|
|
67
|
+
* drawn shape or illustration opts out with this so its colors stay true.
|
|
68
|
+
*/
|
|
69
|
+
export const plainSvg =
|
|
70
|
+
"cursor-[inherit] [&_path]:opacity-100 [&_path]:transform-none"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { springs } from "@passui/tokens/motion"
|
|
2
|
+
import type { Transition } from "motion/react"
|
|
3
|
+
|
|
4
|
+
/** Design-token springs as motion/react transitions. */
|
|
5
|
+
export const spring = {
|
|
6
|
+
smooth: { type: "spring", ...springs.smooth },
|
|
7
|
+
snappy: { type: "spring", ...springs.snappy },
|
|
8
|
+
bouncy: { type: "spring", ...springs.bouncy },
|
|
9
|
+
} as const satisfies Record<keyof typeof springs, Transition>
|
|
10
|
+
|
|
11
|
+
/** Menus, popovers and floating toolbars: grow from their anchor. */
|
|
12
|
+
export const popoverMotion = {
|
|
13
|
+
initial: { opacity: 0, scale: 0.94, y: -6 },
|
|
14
|
+
animate: { opacity: 1, scale: 1, y: 0 },
|
|
15
|
+
exit: {
|
|
16
|
+
opacity: 0,
|
|
17
|
+
scale: 0.97,
|
|
18
|
+
y: -4,
|
|
19
|
+
transition: { duration: 0.12 },
|
|
20
|
+
},
|
|
21
|
+
transition: spring.snappy,
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
/** Floating bars that rise from the bottom edge. */
|
|
25
|
+
export const riseMotion = {
|
|
26
|
+
initial: { opacity: 0, y: 32 },
|
|
27
|
+
animate: { opacity: 1, y: 0 },
|
|
28
|
+
exit: { opacity: 0, y: 24 },
|
|
29
|
+
transition: spring.smooth,
|
|
30
|
+
} as const
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@passui/tokens/base.css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@passui/tokens/theme.css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@passui/tokens/typeset.css";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@passui/tokens/utilities.css";
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@passui/tokens"
|