@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.
@@ -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
+ }
@@ -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 SIZE-ONLY UI ramp (micro · caption · label · compact · callout ·
10
- * body · subtitle · title · headline · display). Like the tokens, these set
11
- * font-size only; pair with `leading-*` via `className` where a fixed leading
12
- * matters. `weight` + `tone` cover the common cases.
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