@podoba/react 0.0.30 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +45 -6
- package/src/components/brand-page-header.tsx +244 -44
- package/src/components/button.tsx +3 -3
- package/src/components/cta-pill.tsx +19 -4
- package/src/components/dialog.tsx +4 -0
- package/src/components/input.tsx +13 -7
- package/src/components/rich-text-editor.tsx +9 -9
- package/src/components/select.tsx +13 -11
- package/src/components/side-panel.examples.tsx +80 -0
- package/src/components/side-panel.tsx +140 -0
- package/src/components/text.tsx +6 -4
- package/src/editor/block-editor.tsx +522 -0
- package/src/editor/index.ts +3 -0
- package/src/index.ts +1 -0
- package/src/utils/safe-link-url.ts +13 -0
- package/src/utils/uic.ts +5 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Button } from './button'
|
|
4
|
+
import { SidePanel } from './side-panel'
|
|
5
|
+
|
|
6
|
+
function DefaultExample(): React.ReactNode {
|
|
7
|
+
const [open, setOpen] = useState(false)
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<Button onPress={() => setOpen(true)}>Open panel</Button>
|
|
11
|
+
<SidePanel
|
|
12
|
+
isOpen={open}
|
|
13
|
+
onOpenChange={setOpen}
|
|
14
|
+
title="Assistant"
|
|
15
|
+
description="Acme Robotics"
|
|
16
|
+
closeLabel="Close assistant"
|
|
17
|
+
footer={<Button className="w-full">Send</Button>}
|
|
18
|
+
>
|
|
19
|
+
<p className="text-small text-fg-muted">Ask a question about the current Brand.</p>
|
|
20
|
+
</SidePanel>
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function SizeExample({ size }: { size: 'sm' | 'md' | 'lg' }): React.ReactNode {
|
|
26
|
+
const [open, setOpen] = useState(false)
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Button variant="secondary" onPress={() => setOpen(true)}>
|
|
30
|
+
{size.toUpperCase()}
|
|
31
|
+
</Button>
|
|
32
|
+
<SidePanel
|
|
33
|
+
isOpen={open}
|
|
34
|
+
onOpenChange={setOpen}
|
|
35
|
+
title={`${size.toUpperCase()} panel`}
|
|
36
|
+
closeLabel="Close panel"
|
|
37
|
+
size={size}
|
|
38
|
+
>
|
|
39
|
+
<p className="text-small text-fg">Responsive owned-scroll content.</p>
|
|
40
|
+
</SidePanel>
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function PendingExample(): React.ReactNode {
|
|
46
|
+
const [open, setOpen] = useState(false)
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Button variant="secondary" onPress={() => setOpen(true)}>Pending state</Button>
|
|
50
|
+
<SidePanel
|
|
51
|
+
isOpen={open}
|
|
52
|
+
onOpenChange={setOpen}
|
|
53
|
+
title="Saving"
|
|
54
|
+
description="Dismissal is temporarily disabled."
|
|
55
|
+
closeLabel="Close panel"
|
|
56
|
+
isDismissable={false}
|
|
57
|
+
>
|
|
58
|
+
<div role="status" className="text-small text-fg-muted">Working…</div>
|
|
59
|
+
</SidePanel>
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const examples = {
|
|
65
|
+
default: () => <DefaultExample />,
|
|
66
|
+
sizes: () => (
|
|
67
|
+
<div className="flex gap-2">
|
|
68
|
+
<SizeExample size="sm" />
|
|
69
|
+
<SizeExample size="md" />
|
|
70
|
+
<SizeExample size="lg" />
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
states: () => <PendingExample />,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const meta = {
|
|
77
|
+
category: 'Layout',
|
|
78
|
+
description: 'Accessible right-side modal panel with responsive full-screen mobile behavior.',
|
|
79
|
+
}
|
|
80
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button as RACButton,
|
|
4
|
+
Dialog as RACDialog,
|
|
5
|
+
Heading,
|
|
6
|
+
Modal as RACModal,
|
|
7
|
+
ModalOverlay as RACModalOverlay,
|
|
8
|
+
} from 'react-aria-components'
|
|
9
|
+
|
|
10
|
+
import { uic } from '../utils/uic'
|
|
11
|
+
|
|
12
|
+
export type SidePanelSize = 'sm' | 'md' | 'lg'
|
|
13
|
+
|
|
14
|
+
export interface SidePanelProps {
|
|
15
|
+
/** Controlled open state. */
|
|
16
|
+
isOpen: boolean
|
|
17
|
+
/** Called for Escape, backdrop dismissal and the close control. */
|
|
18
|
+
onOpenChange: (isOpen: boolean) => void
|
|
19
|
+
/** Accessible panel title. */
|
|
20
|
+
title: ReactNode
|
|
21
|
+
/** Optional supporting copy below the title. */
|
|
22
|
+
description?: ReactNode
|
|
23
|
+
/** App-supplied localized label for the close control. */
|
|
24
|
+
closeLabel: string
|
|
25
|
+
/** Panel width from the shared responsive scale. */
|
|
26
|
+
size?: SidePanelSize
|
|
27
|
+
/** Disable Escape/backdrop dismissal while a critical operation is pending. */
|
|
28
|
+
isDismissable?: boolean
|
|
29
|
+
/** Owned-scroll panel content. */
|
|
30
|
+
children: ReactNode | ((options: { close: () => void }) => ReactNode)
|
|
31
|
+
/** Optional pinned action area below the scroll region. */
|
|
32
|
+
footer?: ReactNode | ((options: { close: () => void }) => ReactNode)
|
|
33
|
+
/** Optional test id forwarded to the dialog element. */
|
|
34
|
+
'data-testid'?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SidePanelOverlay = uic(RACModalOverlay, {
|
|
38
|
+
displayName: 'SidePanel.Overlay',
|
|
39
|
+
baseClass:
|
|
40
|
+
'fixed inset-0 z-50 flex justify-end bg-modal-backdrop backdrop-blur-modal-backdrop ' +
|
|
41
|
+
'data-[entering]:animate-modal-overlay-in data-[exiting]:animate-modal-overlay-out ' +
|
|
42
|
+
'motion-reduce:data-[entering]:animate-none motion-reduce:data-[exiting]:animate-none',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const SidePanelSurface = uic(RACModal, {
|
|
46
|
+
displayName: 'SidePanel.Surface',
|
|
47
|
+
baseClass:
|
|
48
|
+
'ml-auto flex h-dvh w-full flex-col overflow-hidden bg-surface outline-none shadow-modal-surface ' +
|
|
49
|
+
'sm:rounded-l-lg data-[entering]:animate-side-panel-in data-[exiting]:animate-side-panel-out ' +
|
|
50
|
+
'motion-reduce:data-[entering]:animate-none motion-reduce:data-[exiting]:animate-none',
|
|
51
|
+
variants: {
|
|
52
|
+
size: {
|
|
53
|
+
sm: 'sm:max-w-sm',
|
|
54
|
+
md: 'sm:max-w-md',
|
|
55
|
+
lg: 'sm:max-w-lg',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultVariants: { size: 'md' },
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const SidePanelClose = uic(RACButton, {
|
|
62
|
+
displayName: 'SidePanel.Close',
|
|
63
|
+
baseClass:
|
|
64
|
+
'inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full text-fg-subtle outline-none ' +
|
|
65
|
+
'transition-colors hover:bg-surface-muted hover:text-fg ' +
|
|
66
|
+
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Responsive modal side panel.
|
|
71
|
+
*
|
|
72
|
+
* React Aria owns focus containment, Escape/backdrop dismissal, scroll locking
|
|
73
|
+
* and trigger focus return. The surface is a full-screen owned-scroll sheet on
|
|
74
|
+
* mobile and a right-aligned panel on larger viewports. All copy and product
|
|
75
|
+
* content are supplied by the consuming application.
|
|
76
|
+
*/
|
|
77
|
+
export function SidePanel({
|
|
78
|
+
isOpen,
|
|
79
|
+
onOpenChange,
|
|
80
|
+
title,
|
|
81
|
+
description,
|
|
82
|
+
closeLabel,
|
|
83
|
+
size = 'md',
|
|
84
|
+
isDismissable = true,
|
|
85
|
+
children,
|
|
86
|
+
footer,
|
|
87
|
+
'data-testid': testId,
|
|
88
|
+
}: SidePanelProps): React.ReactNode {
|
|
89
|
+
return (
|
|
90
|
+
<SidePanelOverlay
|
|
91
|
+
isOpen={isOpen}
|
|
92
|
+
onOpenChange={onOpenChange}
|
|
93
|
+
isDismissable={isDismissable}
|
|
94
|
+
>
|
|
95
|
+
<SidePanelSurface size={size}>
|
|
96
|
+
<RACDialog
|
|
97
|
+
data-testid={testId}
|
|
98
|
+
className="flex min-h-0 flex-1 flex-col outline-none"
|
|
99
|
+
>
|
|
100
|
+
{renderProps => (
|
|
101
|
+
<>
|
|
102
|
+
<header className="flex shrink-0 items-start justify-between gap-4 border-b border-border px-5 py-4">
|
|
103
|
+
<div className="min-w-0 pt-1">
|
|
104
|
+
<Heading slot="title" className="text-heading1 font-medium tracking-tight text-fg">
|
|
105
|
+
{title}
|
|
106
|
+
</Heading>
|
|
107
|
+
{description ? (
|
|
108
|
+
<p className="mt-1 text-small text-fg-muted">{description}</p>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
<SidePanelClose aria-label={closeLabel} onClick={() => onOpenChange(false)}>
|
|
112
|
+
<svg
|
|
113
|
+
width="20"
|
|
114
|
+
height="20"
|
|
115
|
+
viewBox="0 0 24 24"
|
|
116
|
+
fill="none"
|
|
117
|
+
stroke="currentColor"
|
|
118
|
+
strokeWidth="2"
|
|
119
|
+
strokeLinecap="round"
|
|
120
|
+
aria-hidden="true"
|
|
121
|
+
>
|
|
122
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
123
|
+
</svg>
|
|
124
|
+
</SidePanelClose>
|
|
125
|
+
</header>
|
|
126
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-5">
|
|
127
|
+
{typeof children === 'function' ? children(renderProps) : children}
|
|
128
|
+
</div>
|
|
129
|
+
{footer ? (
|
|
130
|
+
<footer className="shrink-0 border-t border-border px-5 pt-4 pb-mobile-cta-bottom sm:pb-4">
|
|
131
|
+
{typeof footer === 'function' ? footer(renderProps) : footer}
|
|
132
|
+
</footer>
|
|
133
|
+
) : null}
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
</RACDialog>
|
|
137
|
+
</SidePanelSurface>
|
|
138
|
+
</SidePanelOverlay>
|
|
139
|
+
)
|
|
140
|
+
}
|
package/src/components/text.tsx
CHANGED
|
@@ -6,10 +6,12 @@ import { uic } from '../utils/uic'
|
|
|
6
6
|
* They express the `@app/tokens` type ramp as variant props so screens stop
|
|
7
7
|
* hand-writing `text-[NNpx]` literals (the scale, not arbitrary pixels). Two ramps:
|
|
8
8
|
*
|
|
9
|
-
* • <Text> — the
|
|
10
|
-
* body · subtitle · title · headline · display).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* • <Text> — the UI ramp (micro · caption · label · compact · small · callout ·
|
|
10
|
+
* body · subtitle · title · headline · display). Four of these carry the gs
|
|
11
|
+
* source leading with them (compact 13/16, small 14/18, body 16/22,
|
|
12
|
+
* display 28/30); the rest are size-only — pair those with `leading-*` via
|
|
13
|
+
* `className` where a fixed leading matters. `weight` + `tone` cover the
|
|
14
|
+
* common cases.
|
|
13
15
|
* • <Heading> — the semantic heading ramp (`level` 1–5 → text-heading1…5), which
|
|
14
16
|
* DO carry a matching line-height. Renders an <h2> by default; pass `asChild`
|
|
15
17
|
* to project the styles onto the correct heading element for the document
|