panelui-native 0.30.0 → 0.32.0
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/README.md +4 -2
- package/lib/module/components/calendar/index.js +107 -24
- package/lib/module/components/calendar/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +9 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +50 -4
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/plan/index.js +307 -21
- package/lib/module/components/plan/index.js.map +1 -1
- package/lib/module/components/post/index.js +992 -0
- package/lib/module/components/post/index.js.map +1 -0
- package/lib/module/components/response/index.js +296 -0
- package/lib/module/components/response/index.js.map +1 -0
- package/lib/module/components/response/markdown.js +463 -0
- package/lib/module/components/response/markdown.js.map +1 -0
- package/lib/module/components/select/index.js +97 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/icons/index.js +200 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/date.js +129 -22
- package/lib/module/utils/date.js.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +12 -3
- package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -1
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plan/index.d.ts +121 -10
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
- package/lib/typescript/src/components/post/index.d.ts +358 -0
- package/lib/typescript/src/components/post/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/index.d.ts +91 -0
- package/lib/typescript/src/components/response/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/markdown.d.ts +88 -0
- package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +27 -2
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +40 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/date.d.ts +49 -5
- package/lib/typescript/src/utils/date.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/calendar/index.tsx +136 -23
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/plan/index.tsx +344 -21
- package/src/components/post/index.tsx +1116 -0
- package/src/components/response/index.tsx +390 -0
- package/src/components/response/markdown.ts +489 -0
- package/src/components/select/index.tsx +132 -12
- package/src/components/table/index.tsx +747 -0
- package/src/icons/index.tsx +162 -1
- package/src/index.ts +60 -0
- package/src/utils/date.ts +146 -22
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post — a card carrying something somebody said, and what everyone did about it.
|
|
5
|
+
*
|
|
6
|
+
* Four shapes, because a feed is not one thing. `feed` is the full card: author,
|
|
7
|
+
* body, media and a row of counts. `vote` puts a score pill beside a headline
|
|
8
|
+
* and a thumbnail, the way a ranked community reads. `compact` drops the media
|
|
9
|
+
* and puts the name and handle on one line, for a dense timeline. `media` gives
|
|
10
|
+
* the image the whole card and lays the author over it.
|
|
11
|
+
*
|
|
12
|
+
* What they share is the anatomy — the same `Post.Header`, `Post.Body`,
|
|
13
|
+
* `Post.Footer` in every one — so moving between them is a prop rather than a
|
|
14
|
+
* rewrite. The variant decides padding, media shape and where the author sits;
|
|
15
|
+
* it does not decide which parts exist.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <Post variant="feed">
|
|
19
|
+
* <Post.Header>
|
|
20
|
+
* <Post.Author name="Dwayne F. White" verified timestamp="3m ago" avatar={face} />
|
|
21
|
+
* <Post.Action><EllipsisIcon /></Post.Action>
|
|
22
|
+
* </Post.Header>
|
|
23
|
+
* <Post.Body>I've been paying off my credit card #FinancialFreedom</Post.Body>
|
|
24
|
+
* <Post.Media source={photo} />
|
|
25
|
+
* <Post.Footer>
|
|
26
|
+
* <Post.Stat icon={EyeIcon} value="5,874" />
|
|
27
|
+
* <Post.Stat icon={HeartIcon} value="215" tone="like" />
|
|
28
|
+
* <Post.Stat icon={MessageCircleIcon} value="11" />
|
|
29
|
+
* <Post.Stat icon={BookmarkIcon} value="Save" align="end" />
|
|
30
|
+
* </Post.Footer>
|
|
31
|
+
* </Post>
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* ## Counts change under the finger
|
|
35
|
+
*
|
|
36
|
+
* Every control here is a toggle over a number, and the number is the point: a
|
|
37
|
+
* like that lights up but leaves `215` sitting there has not told you it
|
|
38
|
+
* counted. So a stat that changes animates the old value out and the new one in
|
|
39
|
+
* along the direction of the change, and `Post.Votes` does the same with its
|
|
40
|
+
* arrows. The alternative — repainting the digits in place — is indisting-
|
|
41
|
+
* uishable from a re-render, which is exactly the doubt the animation exists to
|
|
42
|
+
* remove.
|
|
43
|
+
*/
|
|
44
|
+
import { createContext, forwardRef, useContext, useEffect, useMemo, useRef } from 'react';
|
|
45
|
+
import { Image, Pressable, View } from 'react-native';
|
|
46
|
+
import Animated, { FadeIn, FadeInDown, FadeInUp, FadeOut, useAnimatedStyle, useReducedMotion, useSharedValue, withSequence, withSpring, withTiming } from 'react-native-reanimated';
|
|
47
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
48
|
+
import { tv } from 'tailwind-variants';
|
|
49
|
+
import { useCSSVariable } from 'uniwind';
|
|
50
|
+
import { ArrowDownIcon, ArrowUpIcon, BadgeCheckIcon } from "../../icons/index.js";
|
|
51
|
+
import { usePrevious } from "../../hooks/use-previous.js";
|
|
52
|
+
import { AnimatedPressable } from "../../primitives/animated-pressable.js";
|
|
53
|
+
import { Text, textChildren } from "../../primitives/text.js";
|
|
54
|
+
import { cn } from "../../utils/cn.js";
|
|
55
|
+
import { selectionTick } from "../../utils/haptics.js";
|
|
56
|
+
import { Avatar } from "../avatar/index.js";
|
|
57
|
+
|
|
58
|
+
/** Long enough to see the value move, short enough to keep up with a fast tap. */
|
|
59
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
60
|
+
const COUNT_DURATION = 160;
|
|
61
|
+
|
|
62
|
+
/** The pop on a toggle: past 1 and back, so the press has a shape. */
|
|
63
|
+
const POP = {
|
|
64
|
+
mass: 0.4,
|
|
65
|
+
damping: 12,
|
|
66
|
+
stiffness: 320
|
|
67
|
+
};
|
|
68
|
+
const postVariants = tv({
|
|
69
|
+
slots: {
|
|
70
|
+
root: 'w-full overflow-hidden rounded-2xl border border-border bg-card',
|
|
71
|
+
header: 'flex-row items-start gap-3',
|
|
72
|
+
author: 'min-w-0 flex-1 gap-0.5',
|
|
73
|
+
authorLine: 'flex-row items-center gap-1.5',
|
|
74
|
+
name: 'font-semibold text-card-foreground',
|
|
75
|
+
handle: 'text-muted-foreground',
|
|
76
|
+
meta: 'text-sm text-muted-foreground',
|
|
77
|
+
// As tall as the name's own line box, so at `items-start` the menu lands on
|
|
78
|
+
// the username's line rather than somewhere between the two lines of the
|
|
79
|
+
// author block — and lands there identically in all four variants.
|
|
80
|
+
action: 'h-6 shrink-0 items-center justify-center',
|
|
81
|
+
// `flex-1` for the same reason `Post.Author` has it: it is the header's
|
|
82
|
+
// flexible side, and without it the row is only as wide as the group's
|
|
83
|
+
// name — so `Post.Action` sits against the name instead of at the edge.
|
|
84
|
+
community: 'min-w-0 h-6 flex-1 flex-row items-center gap-2',
|
|
85
|
+
communityName: 'text-xs font-semibold text-card-foreground',
|
|
86
|
+
communityMeta: 'text-xs text-muted-foreground',
|
|
87
|
+
title: 'text-base font-semibold leading-snug text-card-foreground',
|
|
88
|
+
body: 'text-card-foreground',
|
|
89
|
+
media: 'overflow-hidden bg-muted',
|
|
90
|
+
footer: 'flex-row items-center',
|
|
91
|
+
stat: 'flex-row items-center gap-1.5 rounded-full',
|
|
92
|
+
statLabel: 'text-sm text-muted-foreground',
|
|
93
|
+
votes: 'flex-row items-center gap-0.5 rounded-full bg-muted',
|
|
94
|
+
voteButton: 'h-8 w-8 items-center justify-center rounded-full',
|
|
95
|
+
voteScore: 'text-sm font-semibold tabular-nums'
|
|
96
|
+
},
|
|
97
|
+
variants: {
|
|
98
|
+
variant: {
|
|
99
|
+
/** The full card — author, body, media, counts. */
|
|
100
|
+
feed: {
|
|
101
|
+
header: 'p-4 pb-3',
|
|
102
|
+
body: 'px-4 pb-3 text-base leading-relaxed',
|
|
103
|
+
media: 'mx-4 rounded-xl',
|
|
104
|
+
footer: 'gap-5 p-4'
|
|
105
|
+
},
|
|
106
|
+
/** A headline with a score beside it, the way a ranked community reads. */
|
|
107
|
+
vote: {
|
|
108
|
+
header: 'px-4 pb-2 pt-3.5',
|
|
109
|
+
body: 'px-4 pb-3 text-sm leading-relaxed',
|
|
110
|
+
media: 'mx-4 rounded-xl',
|
|
111
|
+
footer: 'gap-2 px-4 pb-3.5'
|
|
112
|
+
},
|
|
113
|
+
/** No media, name and handle on one line — for a dense timeline. */
|
|
114
|
+
compact: {
|
|
115
|
+
root: 'rounded-xl',
|
|
116
|
+
header: 'p-3.5 pb-2',
|
|
117
|
+
body: 'px-3.5 pb-2.5 text-sm leading-relaxed',
|
|
118
|
+
media: 'mx-3.5 rounded-lg',
|
|
119
|
+
footer: 'gap-6 px-3.5 pb-3'
|
|
120
|
+
},
|
|
121
|
+
/** The image is the card; the author is laid over it. */
|
|
122
|
+
media: {
|
|
123
|
+
header: 'absolute inset-x-0 top-0 z-10 p-3',
|
|
124
|
+
body: 'px-4 pb-3 pt-3 text-sm leading-relaxed',
|
|
125
|
+
media: 'rounded-none',
|
|
126
|
+
footer: 'gap-5 px-4 pb-4'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
size: {
|
|
130
|
+
default: {},
|
|
131
|
+
sm: {
|
|
132
|
+
name: 'text-sm',
|
|
133
|
+
meta: 'text-xs',
|
|
134
|
+
statLabel: 'text-xs'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
defaultVariants: {
|
|
139
|
+
variant: 'feed',
|
|
140
|
+
size: 'default'
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const PostContext = /*#__PURE__*/createContext({
|
|
144
|
+
variant: 'feed',
|
|
145
|
+
size: 'default'
|
|
146
|
+
});
|
|
147
|
+
function usePost() {
|
|
148
|
+
return useContext(PostContext);
|
|
149
|
+
}
|
|
150
|
+
const PostRoot = /*#__PURE__*/forwardRef(({
|
|
151
|
+
className,
|
|
152
|
+
variant = 'feed',
|
|
153
|
+
size = 'default',
|
|
154
|
+
onPress,
|
|
155
|
+
children,
|
|
156
|
+
...props
|
|
157
|
+
}, ref) => {
|
|
158
|
+
const {
|
|
159
|
+
root
|
|
160
|
+
} = postVariants({
|
|
161
|
+
variant,
|
|
162
|
+
size
|
|
163
|
+
});
|
|
164
|
+
const context = useMemo(() => ({
|
|
165
|
+
variant,
|
|
166
|
+
size
|
|
167
|
+
}), [variant, size]);
|
|
168
|
+
const body = /*#__PURE__*/_jsx(PostContext.Provider, {
|
|
169
|
+
value: context,
|
|
170
|
+
children: children
|
|
171
|
+
});
|
|
172
|
+
if (!onPress) {
|
|
173
|
+
return /*#__PURE__*/_jsx(View, {
|
|
174
|
+
ref: ref,
|
|
175
|
+
className: root({
|
|
176
|
+
className
|
|
177
|
+
}),
|
|
178
|
+
...props,
|
|
179
|
+
children: body
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return (
|
|
183
|
+
/*#__PURE__*/
|
|
184
|
+
// `article` rather than `button`: the card opens, but the row of controls
|
|
185
|
+
// inside it is the reason anyone is here, and announcing the whole thing
|
|
186
|
+
// as one button buries them.
|
|
187
|
+
_jsx(AnimatedPressable, {
|
|
188
|
+
ref: ref,
|
|
189
|
+
accessibilityRole: "link",
|
|
190
|
+
onPress: onPress,
|
|
191
|
+
className: root({
|
|
192
|
+
className
|
|
193
|
+
}),
|
|
194
|
+
...props,
|
|
195
|
+
children: body
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
PostRoot.displayName = 'Post';
|
|
200
|
+
|
|
201
|
+
/* -------------------------------------------------------------------------- */
|
|
202
|
+
/* Header */
|
|
203
|
+
/* -------------------------------------------------------------------------- */
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The author row. `Post.Action` is pulled to the trailing edge, so the overflow
|
|
207
|
+
* menu stays in the corner as a long display name wraps rather than riding down
|
|
208
|
+
* with it.
|
|
209
|
+
*/
|
|
210
|
+
const PostHeader = /*#__PURE__*/forwardRef(({
|
|
211
|
+
className,
|
|
212
|
+
children,
|
|
213
|
+
...props
|
|
214
|
+
}, ref) => {
|
|
215
|
+
const {
|
|
216
|
+
variant,
|
|
217
|
+
size
|
|
218
|
+
} = usePost();
|
|
219
|
+
const {
|
|
220
|
+
header
|
|
221
|
+
} = postVariants({
|
|
222
|
+
variant,
|
|
223
|
+
size
|
|
224
|
+
});
|
|
225
|
+
const content = [];
|
|
226
|
+
const actions = [];
|
|
227
|
+
for (const child of Array.isArray(children) ? children : [children]) {
|
|
228
|
+
if (isType(child, PostAction)) actions.push(child);else content.push(child);
|
|
229
|
+
}
|
|
230
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
231
|
+
ref: ref,
|
|
232
|
+
className: header({
|
|
233
|
+
className
|
|
234
|
+
}),
|
|
235
|
+
...props,
|
|
236
|
+
children: [content, actions]
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
PostHeader.displayName = 'Post.Header';
|
|
240
|
+
function isType(child, type) {
|
|
241
|
+
return typeof child === 'object' && child !== null && 'type' in child && child.type === type;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Who posted it.
|
|
245
|
+
*
|
|
246
|
+
* The handle sits beside the name in `compact` and beneath it everywhere else,
|
|
247
|
+
* which is the difference between a timeline row and a card: a dense row cannot
|
|
248
|
+
* afford a second line, and a card looks starved without one.
|
|
249
|
+
*/
|
|
250
|
+
const PostAuthor = /*#__PURE__*/forwardRef(({
|
|
251
|
+
className,
|
|
252
|
+
name,
|
|
253
|
+
handle,
|
|
254
|
+
avatar,
|
|
255
|
+
fallback,
|
|
256
|
+
verified,
|
|
257
|
+
timestamp,
|
|
258
|
+
children,
|
|
259
|
+
...props
|
|
260
|
+
}, ref) => {
|
|
261
|
+
const {
|
|
262
|
+
variant,
|
|
263
|
+
size
|
|
264
|
+
} = usePost();
|
|
265
|
+
const {
|
|
266
|
+
author,
|
|
267
|
+
authorLine,
|
|
268
|
+
name: nameSlot,
|
|
269
|
+
handle: handleSlot,
|
|
270
|
+
meta
|
|
271
|
+
} = postVariants({
|
|
272
|
+
variant,
|
|
273
|
+
size
|
|
274
|
+
});
|
|
275
|
+
const verifiedColor = useCSSVariable('--color-primary');
|
|
276
|
+
const onVerified = useCSSVariable('--color-primary-foreground');
|
|
277
|
+
const inline = variant === 'compact';
|
|
278
|
+
const overlaid = variant === 'media';
|
|
279
|
+
return (
|
|
280
|
+
/*#__PURE__*/
|
|
281
|
+
// Top-aligned, not centred. The name is the first line of the block, and
|
|
282
|
+
// aligning to the top is what puts it — and therefore the menu opposite
|
|
283
|
+
// it — on the same rule in every variant, whether the block below the
|
|
284
|
+
// name is one line, two, or none.
|
|
285
|
+
_jsxs(View, {
|
|
286
|
+
ref: ref,
|
|
287
|
+
className: "min-w-0 flex-1 flex-row items-start gap-2.5",
|
|
288
|
+
...props,
|
|
289
|
+
children: [/*#__PURE__*/_jsx(Avatar, {
|
|
290
|
+
size: variant === 'compact' ? 'sm' : 'md',
|
|
291
|
+
source: avatar,
|
|
292
|
+
fallback: fallback ?? initials(name),
|
|
293
|
+
className: overlaid ? 'border-white/30' : undefined
|
|
294
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
295
|
+
className: author({
|
|
296
|
+
className
|
|
297
|
+
}),
|
|
298
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
299
|
+
className: authorLine({
|
|
300
|
+
className: 'h-6'
|
|
301
|
+
}),
|
|
302
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
303
|
+
numberOfLines: 1,
|
|
304
|
+
className: nameSlot({
|
|
305
|
+
className: cn('shrink', overlaid && 'text-white')
|
|
306
|
+
}),
|
|
307
|
+
children: name
|
|
308
|
+
}), verified ? /*#__PURE__*/_jsx(BadgeCheckIcon, {
|
|
309
|
+
size: 15,
|
|
310
|
+
color: typeof verifiedColor === 'string' ? verifiedColor : '#2563eb',
|
|
311
|
+
checkColor: typeof onVerified === 'string' ? onVerified : '#ffffff'
|
|
312
|
+
}) : null, inline && handle ? /*#__PURE__*/_jsx(Text, {
|
|
313
|
+
size: "sm",
|
|
314
|
+
numberOfLines: 1,
|
|
315
|
+
className: handleSlot({
|
|
316
|
+
className: 'shrink'
|
|
317
|
+
}),
|
|
318
|
+
children: handle
|
|
319
|
+
}) : null, inline && timestamp !== undefined ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
320
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
321
|
+
size: "sm",
|
|
322
|
+
className: handleSlot(),
|
|
323
|
+
children: "\xB7"
|
|
324
|
+
}), textChildren(timestamp, text => /*#__PURE__*/_jsx(Text, {
|
|
325
|
+
size: "sm",
|
|
326
|
+
className: handleSlot(),
|
|
327
|
+
children: text
|
|
328
|
+
}))]
|
|
329
|
+
}) : null]
|
|
330
|
+
}), !inline && (handle || timestamp !== undefined) ? /*#__PURE__*/_jsx(Text, {
|
|
331
|
+
numberOfLines: 1,
|
|
332
|
+
className: meta({
|
|
333
|
+
className: overlaid ? 'text-white/80' : undefined
|
|
334
|
+
}),
|
|
335
|
+
children: [handle, typeof timestamp === 'string' ? timestamp : undefined].filter(Boolean).join(' · ')
|
|
336
|
+
}) : null, !inline && timestamp !== undefined && typeof timestamp !== 'string' ? timestamp : null, children]
|
|
337
|
+
})]
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
PostAuthor.displayName = 'Post.Author';
|
|
342
|
+
|
|
343
|
+
/** First letters of the first two words — enough to tell two people apart. */
|
|
344
|
+
function initials(name) {
|
|
345
|
+
return name.split(/\s+/).slice(0, 2).map(word => word[0] ?? '').join('').toUpperCase();
|
|
346
|
+
}
|
|
347
|
+
/** The header's trailing slot — an overflow menu, a follow button, a badge. */
|
|
348
|
+
const PostAction = /*#__PURE__*/forwardRef(({
|
|
349
|
+
className,
|
|
350
|
+
children,
|
|
351
|
+
...props
|
|
352
|
+
}, ref) => {
|
|
353
|
+
const {
|
|
354
|
+
variant,
|
|
355
|
+
size
|
|
356
|
+
} = usePost();
|
|
357
|
+
const {
|
|
358
|
+
action
|
|
359
|
+
} = postVariants({
|
|
360
|
+
variant,
|
|
361
|
+
size
|
|
362
|
+
});
|
|
363
|
+
return /*#__PURE__*/_jsx(View, {
|
|
364
|
+
ref: ref,
|
|
365
|
+
className: action({
|
|
366
|
+
className
|
|
367
|
+
}),
|
|
368
|
+
...props,
|
|
369
|
+
children: children
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
PostAction.displayName = 'Post.Action';
|
|
373
|
+
/**
|
|
374
|
+
* The group a post was made in, above its headline.
|
|
375
|
+
*
|
|
376
|
+
* Smaller and quieter than an author row, because in a ranked community the
|
|
377
|
+
* post's subject is the headline — the group is where it came from, not who
|
|
378
|
+
* said it.
|
|
379
|
+
*/
|
|
380
|
+
const PostCommunity = /*#__PURE__*/forwardRef(({
|
|
381
|
+
className,
|
|
382
|
+
name,
|
|
383
|
+
avatar,
|
|
384
|
+
meta,
|
|
385
|
+
children,
|
|
386
|
+
...props
|
|
387
|
+
}, ref) => {
|
|
388
|
+
const {
|
|
389
|
+
variant,
|
|
390
|
+
size
|
|
391
|
+
} = usePost();
|
|
392
|
+
const {
|
|
393
|
+
community,
|
|
394
|
+
communityName,
|
|
395
|
+
communityMeta
|
|
396
|
+
} = postVariants({
|
|
397
|
+
variant,
|
|
398
|
+
size
|
|
399
|
+
});
|
|
400
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
401
|
+
ref: ref,
|
|
402
|
+
className: community({
|
|
403
|
+
className
|
|
404
|
+
}),
|
|
405
|
+
...props,
|
|
406
|
+
children: [/*#__PURE__*/_jsx(Avatar, {
|
|
407
|
+
size: "sm",
|
|
408
|
+
source: avatar,
|
|
409
|
+
fallback: name.replace(/^[^a-z0-9]*/i, '').slice(0, 1).toUpperCase(),
|
|
410
|
+
className: "h-6 w-6"
|
|
411
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
412
|
+
numberOfLines: 1,
|
|
413
|
+
className: communityName({
|
|
414
|
+
className: 'shrink'
|
|
415
|
+
}),
|
|
416
|
+
children: name
|
|
417
|
+
}), meta !== undefined ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
418
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
419
|
+
className: communityMeta(),
|
|
420
|
+
children: "\xB7"
|
|
421
|
+
}), textChildren(meta, text => /*#__PURE__*/_jsx(Text, {
|
|
422
|
+
className: communityMeta(),
|
|
423
|
+
children: text
|
|
424
|
+
}))]
|
|
425
|
+
}) : null, children]
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
PostCommunity.displayName = 'Post.Community';
|
|
429
|
+
|
|
430
|
+
/* -------------------------------------------------------------------------- */
|
|
431
|
+
/* Body */
|
|
432
|
+
/* -------------------------------------------------------------------------- */
|
|
433
|
+
|
|
434
|
+
/** The headline of a post whose subject is a headline rather than a person. */
|
|
435
|
+
const PostTitle = /*#__PURE__*/forwardRef(({
|
|
436
|
+
className,
|
|
437
|
+
children,
|
|
438
|
+
...props
|
|
439
|
+
}, ref) => {
|
|
440
|
+
const {
|
|
441
|
+
variant,
|
|
442
|
+
size
|
|
443
|
+
} = usePost();
|
|
444
|
+
const {
|
|
445
|
+
title
|
|
446
|
+
} = postVariants({
|
|
447
|
+
variant,
|
|
448
|
+
size
|
|
449
|
+
});
|
|
450
|
+
return /*#__PURE__*/_jsx(View, {
|
|
451
|
+
ref: ref,
|
|
452
|
+
className: cn('px-4 pb-2', className),
|
|
453
|
+
...props,
|
|
454
|
+
children: textChildren(children, text => /*#__PURE__*/_jsx(Text, {
|
|
455
|
+
className: title(),
|
|
456
|
+
children: text
|
|
457
|
+
}))
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
PostTitle.displayName = 'Post.Title';
|
|
461
|
+
/**
|
|
462
|
+
* What was written.
|
|
463
|
+
*
|
|
464
|
+
* Hashtags and mentions inside a string child are picked out and coloured. They
|
|
465
|
+
* are found in the text rather than declared, because a post body arrives as
|
|
466
|
+
* one string from wherever it was typed — asking the caller to pre-split it
|
|
467
|
+
* would mean every caller writing the same tokeniser.
|
|
468
|
+
*
|
|
469
|
+
* A non-string child is left entirely alone: it has already been laid out by
|
|
470
|
+
* someone who knew more than we do.
|
|
471
|
+
*/
|
|
472
|
+
const PostBody = /*#__PURE__*/forwardRef(({
|
|
473
|
+
className,
|
|
474
|
+
numberOfLines,
|
|
475
|
+
onTagPress,
|
|
476
|
+
onMentionPress,
|
|
477
|
+
children,
|
|
478
|
+
...props
|
|
479
|
+
}, ref) => {
|
|
480
|
+
const {
|
|
481
|
+
variant,
|
|
482
|
+
size
|
|
483
|
+
} = usePost();
|
|
484
|
+
const {
|
|
485
|
+
body
|
|
486
|
+
} = postVariants({
|
|
487
|
+
variant,
|
|
488
|
+
size
|
|
489
|
+
});
|
|
490
|
+
if (typeof children !== 'string') {
|
|
491
|
+
return /*#__PURE__*/_jsx(View, {
|
|
492
|
+
ref: ref,
|
|
493
|
+
className: cn(body(), className),
|
|
494
|
+
...props,
|
|
495
|
+
children: children
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return /*#__PURE__*/_jsx(View, {
|
|
499
|
+
ref: ref,
|
|
500
|
+
className: cn(body({
|
|
501
|
+
className
|
|
502
|
+
})),
|
|
503
|
+
...props,
|
|
504
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
505
|
+
numberOfLines: numberOfLines,
|
|
506
|
+
className: body({
|
|
507
|
+
className: 'p-0'
|
|
508
|
+
}),
|
|
509
|
+
children: tokenize(children).map((token, index) => token.kind === 'text' ? token.value : /*#__PURE__*/_jsx(Text, {
|
|
510
|
+
className: "text-primary",
|
|
511
|
+
onPress: token.kind === 'tag' ? onTagPress && (() => onTagPress(token.value.slice(1))) : onMentionPress && (() => onMentionPress(token.value.slice(1))),
|
|
512
|
+
children: token.value
|
|
513
|
+
}, index))
|
|
514
|
+
})
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
PostBody.displayName = 'Post.Body';
|
|
518
|
+
/**
|
|
519
|
+
* Splits a body into plain runs, hashtags and mentions.
|
|
520
|
+
*
|
|
521
|
+
* The tag pattern stops at whitespace and at punctuation that ends a sentence,
|
|
522
|
+
* so "#DebtSnowball." highlights the tag and leaves the full stop black — the
|
|
523
|
+
* naive `\S+` swallows it and the sentence loses its ending.
|
|
524
|
+
*/
|
|
525
|
+
function tokenize(text) {
|
|
526
|
+
const pattern = /[#@][\w-]+/g;
|
|
527
|
+
const tokens = [];
|
|
528
|
+
let last = 0;
|
|
529
|
+
for (const match of text.matchAll(pattern)) {
|
|
530
|
+
const start = match.index ?? 0;
|
|
531
|
+
if (start > last) tokens.push({
|
|
532
|
+
kind: 'text',
|
|
533
|
+
value: text.slice(last, start)
|
|
534
|
+
});
|
|
535
|
+
tokens.push({
|
|
536
|
+
kind: match[0].startsWith('#') ? 'tag' : 'mention',
|
|
537
|
+
value: match[0]
|
|
538
|
+
});
|
|
539
|
+
last = start + match[0].length;
|
|
540
|
+
}
|
|
541
|
+
if (last < text.length) tokens.push({
|
|
542
|
+
kind: 'text',
|
|
543
|
+
value: text.slice(last)
|
|
544
|
+
});
|
|
545
|
+
return tokens;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Black, fading to nothing.
|
|
549
|
+
*
|
|
550
|
+
* Three stops rather than two: a straight linear ramp from 55% to nothing has a
|
|
551
|
+
* visible shoulder where it meets the picture, and the eye reads that shoulder
|
|
552
|
+
* as an edge — which is the whole thing a scrim exists to avoid.
|
|
553
|
+
*/
|
|
554
|
+
const SCRIM_COLOURS = ['rgba(0,0,0,0.55)', 'rgba(0,0,0,0.28)', 'rgba(0,0,0,0)'];
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The picture.
|
|
558
|
+
*
|
|
559
|
+
* A fixed aspect ratio rather than the image's own, because a feed of cards
|
|
560
|
+
* whose heights are decided by whatever was uploaded scrolls like a broken
|
|
561
|
+
* staircase. The image covers the box and is cropped by it.
|
|
562
|
+
*/
|
|
563
|
+
const PostMedia = /*#__PURE__*/forwardRef(({
|
|
564
|
+
className,
|
|
565
|
+
source,
|
|
566
|
+
aspectRatio = 16 / 10,
|
|
567
|
+
scrim,
|
|
568
|
+
overlay,
|
|
569
|
+
alt,
|
|
570
|
+
onPress,
|
|
571
|
+
children,
|
|
572
|
+
...props
|
|
573
|
+
}, ref) => {
|
|
574
|
+
const {
|
|
575
|
+
variant,
|
|
576
|
+
size
|
|
577
|
+
} = usePost();
|
|
578
|
+
const {
|
|
579
|
+
media
|
|
580
|
+
} = postVariants({
|
|
581
|
+
variant,
|
|
582
|
+
size
|
|
583
|
+
});
|
|
584
|
+
// The media variant lays the author over the image, so it needs shading by
|
|
585
|
+
// default — the caller should not have to remember what makes their own
|
|
586
|
+
// layout readable.
|
|
587
|
+
const shade = scrim ?? (variant === 'media' ? 'top' : 'none');
|
|
588
|
+
const picture = /*#__PURE__*/_jsxs(_Fragment, {
|
|
589
|
+
children: [/*#__PURE__*/_jsx(Image, {
|
|
590
|
+
source: source,
|
|
591
|
+
resizeMode: "cover",
|
|
592
|
+
accessible: !!alt,
|
|
593
|
+
accessibilityLabel: alt,
|
|
594
|
+
accessibilityRole: alt ? 'image' : undefined,
|
|
595
|
+
className: "h-full w-full"
|
|
596
|
+
}), shade === 'top' || shade === 'both' ? /*#__PURE__*/_jsx(LinearGradient, {
|
|
597
|
+
pointerEvents: "none",
|
|
598
|
+
colors: SCRIM_COLOURS
|
|
599
|
+
// A fixed height, not a fraction. The scrim is there to carry one
|
|
600
|
+
// row of type, and a fraction of a 4:5 portrait shades half the
|
|
601
|
+
// photograph to do a job that needs the top 112 points of it.
|
|
602
|
+
,
|
|
603
|
+
className: "absolute inset-x-0 top-0 h-28"
|
|
604
|
+
}) : null, shade === 'bottom' || shade === 'both' ? /*#__PURE__*/_jsx(LinearGradient, {
|
|
605
|
+
pointerEvents: "none",
|
|
606
|
+
colors: [SCRIM_COLOURS[2], SCRIM_COLOURS[1], SCRIM_COLOURS[0]],
|
|
607
|
+
className: "absolute inset-x-0 bottom-0 h-32"
|
|
608
|
+
}) : null, overlay ? /*#__PURE__*/_jsx(View, {
|
|
609
|
+
pointerEvents: "box-none",
|
|
610
|
+
className: "absolute inset-0",
|
|
611
|
+
children: overlay
|
|
612
|
+
}) : null, children]
|
|
613
|
+
});
|
|
614
|
+
if (!onPress) {
|
|
615
|
+
return /*#__PURE__*/_jsx(View, {
|
|
616
|
+
ref: ref,
|
|
617
|
+
className: media({
|
|
618
|
+
className
|
|
619
|
+
}),
|
|
620
|
+
style: {
|
|
621
|
+
aspectRatio
|
|
622
|
+
},
|
|
623
|
+
...props,
|
|
624
|
+
children: picture
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
628
|
+
ref: ref,
|
|
629
|
+
accessibilityRole: "imagebutton",
|
|
630
|
+
accessibilityLabel: alt,
|
|
631
|
+
onPress: onPress,
|
|
632
|
+
className: media({
|
|
633
|
+
className
|
|
634
|
+
}),
|
|
635
|
+
style: {
|
|
636
|
+
aspectRatio
|
|
637
|
+
},
|
|
638
|
+
...props,
|
|
639
|
+
children: picture
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
PostMedia.displayName = 'Post.Media';
|
|
643
|
+
|
|
644
|
+
/* -------------------------------------------------------------------------- */
|
|
645
|
+
/* Footer */
|
|
646
|
+
/* -------------------------------------------------------------------------- */
|
|
647
|
+
|
|
648
|
+
/** The row of counts and controls under the post. */
|
|
649
|
+
const PostFooter = /*#__PURE__*/forwardRef(({
|
|
650
|
+
className,
|
|
651
|
+
children,
|
|
652
|
+
...props
|
|
653
|
+
}, ref) => {
|
|
654
|
+
const {
|
|
655
|
+
variant,
|
|
656
|
+
size
|
|
657
|
+
} = usePost();
|
|
658
|
+
const {
|
|
659
|
+
footer
|
|
660
|
+
} = postVariants({
|
|
661
|
+
variant,
|
|
662
|
+
size
|
|
663
|
+
});
|
|
664
|
+
return /*#__PURE__*/_jsx(View, {
|
|
665
|
+
ref: ref,
|
|
666
|
+
className: footer({
|
|
667
|
+
className
|
|
668
|
+
}),
|
|
669
|
+
...props,
|
|
670
|
+
children: children
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
PostFooter.displayName = 'Post.Footer';
|
|
674
|
+
|
|
675
|
+
/** What lighting up means. A like is red, a save is the accent, a repost green. */
|
|
676
|
+
const statTone = {
|
|
677
|
+
default: '--color-primary',
|
|
678
|
+
like: '--color-destructive',
|
|
679
|
+
save: '--color-primary',
|
|
680
|
+
repost: '--color-success'
|
|
681
|
+
};
|
|
682
|
+
/**
|
|
683
|
+
* One count in the footer, and the control that changes it.
|
|
684
|
+
*
|
|
685
|
+
* Pressing it pops the icon past its own size and settles back, fills it, and
|
|
686
|
+
* animates the value: the old number leaves upwards and the new one arrives
|
|
687
|
+
* from below when the count goes up, and the other way when it comes down.
|
|
688
|
+
* Repainting the digits in place is indistinguishable from a re-render, which
|
|
689
|
+
* is the doubt this exists to remove.
|
|
690
|
+
*/
|
|
691
|
+
const PostStat = /*#__PURE__*/forwardRef(({
|
|
692
|
+
className,
|
|
693
|
+
icon: Icon,
|
|
694
|
+
value,
|
|
695
|
+
active = false,
|
|
696
|
+
tone = 'default',
|
|
697
|
+
align = 'start',
|
|
698
|
+
onPress,
|
|
699
|
+
children,
|
|
700
|
+
...props
|
|
701
|
+
}, ref) => {
|
|
702
|
+
const {
|
|
703
|
+
variant,
|
|
704
|
+
size
|
|
705
|
+
} = usePost();
|
|
706
|
+
const {
|
|
707
|
+
stat,
|
|
708
|
+
statLabel
|
|
709
|
+
} = postVariants({
|
|
710
|
+
variant,
|
|
711
|
+
size
|
|
712
|
+
});
|
|
713
|
+
const litColor = useCSSVariable(statTone[tone]);
|
|
714
|
+
const restColor = useCSSVariable('--color-muted-foreground');
|
|
715
|
+
const reducedMotion = useReducedMotion();
|
|
716
|
+
const pop = useSharedValue(1);
|
|
717
|
+
const first = useRef(true);
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
if (first.current) {
|
|
720
|
+
first.current = false;
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (reducedMotion) return;
|
|
724
|
+
// Past 1 and back. A toggle that only grows reads as a state that got
|
|
725
|
+
// bigger; one that overshoots reads as a press.
|
|
726
|
+
pop.value = withSequence(withTiming(active ? 1.25 : 0.85, {
|
|
727
|
+
duration: 90
|
|
728
|
+
}), withSpring(1, POP));
|
|
729
|
+
}, [active, reducedMotion, pop]);
|
|
730
|
+
const iconStyle = useAnimatedStyle(() => ({
|
|
731
|
+
transform: [{
|
|
732
|
+
scale: pop.value
|
|
733
|
+
}]
|
|
734
|
+
}));
|
|
735
|
+
const colour = active ? typeof litColor === 'string' ? litColor : '#e11d48' : typeof restColor === 'string' ? restColor : '#737373';
|
|
736
|
+
const inner = /*#__PURE__*/_jsxs(_Fragment, {
|
|
737
|
+
children: [Icon ? /*#__PURE__*/_jsx(Animated.View, {
|
|
738
|
+
style: iconStyle,
|
|
739
|
+
children: /*#__PURE__*/_jsx(Icon, {
|
|
740
|
+
size: variant === 'compact' ? 15 : 17,
|
|
741
|
+
color: colour,
|
|
742
|
+
filled: active
|
|
743
|
+
})
|
|
744
|
+
}) : null, value !== undefined ? /*#__PURE__*/_jsx(PostCount, {
|
|
745
|
+
value: value,
|
|
746
|
+
className: statLabel({
|
|
747
|
+
className: active ? '' : undefined
|
|
748
|
+
}),
|
|
749
|
+
color: active ? colour : undefined
|
|
750
|
+
}) : null, children]
|
|
751
|
+
});
|
|
752
|
+
const classes = stat({
|
|
753
|
+
className: cn(align === 'end' && 'ms-auto', className)
|
|
754
|
+
});
|
|
755
|
+
if (!onPress) {
|
|
756
|
+
return /*#__PURE__*/_jsx(View, {
|
|
757
|
+
ref: ref,
|
|
758
|
+
className: classes,
|
|
759
|
+
children: inner
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
763
|
+
ref: ref,
|
|
764
|
+
accessibilityRole: "button",
|
|
765
|
+
accessibilityState: {
|
|
766
|
+
selected: active
|
|
767
|
+
}
|
|
768
|
+
// The row is dense, so the targets overlap their own boxes rather than
|
|
769
|
+
// being spaced far enough apart to be hit — spacing them would be a
|
|
770
|
+
// footer twice as wide as the numbers in it.
|
|
771
|
+
,
|
|
772
|
+
hitSlop: 10,
|
|
773
|
+
onPress: event => {
|
|
774
|
+
selectionTick();
|
|
775
|
+
onPress(event);
|
|
776
|
+
},
|
|
777
|
+
className: classes,
|
|
778
|
+
...props,
|
|
779
|
+
children: inner
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
PostStat.displayName = 'Post.Stat';
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* A number that moves when it changes.
|
|
786
|
+
*
|
|
787
|
+
* The new value arrives from the direction the count travelled — rising into
|
|
788
|
+
* place when it went up, dropping in when it came down — while the old one
|
|
789
|
+
* fades. `215 → 216` then reads as a count going up rather than as text being
|
|
790
|
+
* replaced, which is the doubt worth removing: repainting digits in place is
|
|
791
|
+
* indistinguishable from a re-render.
|
|
792
|
+
*
|
|
793
|
+
* Only the entering half carries the direction. The element leaving was
|
|
794
|
+
* rendered before anyone knew which way the number would go, so asking it to
|
|
795
|
+
* animate knowingly would mean knowing the future.
|
|
796
|
+
*
|
|
797
|
+
* A value that is not a number — "Save" — cross-fades, since there is no
|
|
798
|
+
* direction for a word to go.
|
|
799
|
+
*/
|
|
800
|
+
function PostCount({
|
|
801
|
+
value,
|
|
802
|
+
className,
|
|
803
|
+
color
|
|
804
|
+
}) {
|
|
805
|
+
const reducedMotion = useReducedMotion();
|
|
806
|
+
// Read during the render the value changes on, not after it: an effect would
|
|
807
|
+
// settle the direction one frame after the animation had already started.
|
|
808
|
+
const before = numeric(usePrevious(value));
|
|
809
|
+
const after = numeric(value);
|
|
810
|
+
const direction = before === null || after === null || before === after ? 0 : after > before ? 1 : -1;
|
|
811
|
+
const label = /*#__PURE__*/_jsx(Text, {
|
|
812
|
+
className: className,
|
|
813
|
+
style: color ? {
|
|
814
|
+
color
|
|
815
|
+
} : undefined,
|
|
816
|
+
children: value
|
|
817
|
+
});
|
|
818
|
+
if (reducedMotion) return label;
|
|
819
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
820
|
+
entering: direction === 1 ? FadeInUp.duration(COUNT_DURATION) : direction === -1 ? FadeInDown.duration(COUNT_DURATION) : FadeIn.duration(COUNT_DURATION),
|
|
821
|
+
exiting: FadeOut.duration(COUNT_DURATION),
|
|
822
|
+
children: label
|
|
823
|
+
}, String(value));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** The value as a number, where it is one. `"5,874"` counts; `"Save"` does not. */
|
|
827
|
+
function numeric(value) {
|
|
828
|
+
if (typeof value === 'number') return value;
|
|
829
|
+
if (typeof value !== 'string') return null;
|
|
830
|
+
const parsed = Number(value.replace(/[,\s]/g, ''));
|
|
831
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/* -------------------------------------------------------------------------- */
|
|
835
|
+
/* Votes */
|
|
836
|
+
/* -------------------------------------------------------------------------- */
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* The score, and the two arrows that move it.
|
|
840
|
+
*
|
|
841
|
+
* Both arrows fill and take a colour when cast, and the whole pill tints with
|
|
842
|
+
* them — the arrow alone is 16pt of a card, which is not enough to notice from
|
|
843
|
+
* arm's length. The score rolls in the direction of the change.
|
|
844
|
+
*
|
|
845
|
+
* Pressing the arrow already cast clears the vote. This is not a nicety: a
|
|
846
|
+
* score is a thing people change their minds about, and a vote that cannot be
|
|
847
|
+
* withdrawn is one they think twice before casting.
|
|
848
|
+
*/
|
|
849
|
+
const PostVotes = /*#__PURE__*/forwardRef(({
|
|
850
|
+
className,
|
|
851
|
+
score,
|
|
852
|
+
vote = null,
|
|
853
|
+
onVote,
|
|
854
|
+
orientation = 'horizontal',
|
|
855
|
+
disabled = false,
|
|
856
|
+
...props
|
|
857
|
+
}, ref) => {
|
|
858
|
+
const {
|
|
859
|
+
variant,
|
|
860
|
+
size
|
|
861
|
+
} = usePost();
|
|
862
|
+
const {
|
|
863
|
+
votes,
|
|
864
|
+
voteButton,
|
|
865
|
+
voteScore
|
|
866
|
+
} = postVariants({
|
|
867
|
+
variant,
|
|
868
|
+
size
|
|
869
|
+
});
|
|
870
|
+
const upColor = useCSSVariable('--color-success');
|
|
871
|
+
const downColor = useCSSVariable('--color-destructive');
|
|
872
|
+
const restColor = useCSSVariable('--color-muted-foreground');
|
|
873
|
+
const up = typeof upColor === 'string' ? upColor : '#16a34a';
|
|
874
|
+
const down = typeof downColor === 'string' ? downColor : '#dc2626';
|
|
875
|
+
const rest = typeof restColor === 'string' ? restColor : '#737373';
|
|
876
|
+
const cast = next => {
|
|
877
|
+
selectionTick();
|
|
878
|
+
onVote?.(vote === next ? null : next);
|
|
879
|
+
};
|
|
880
|
+
const scoreColour = vote === 'up' ? up : vote === 'down' ? down : undefined;
|
|
881
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
882
|
+
ref: ref,
|
|
883
|
+
accessibilityRole: "adjustable",
|
|
884
|
+
accessibilityLabel: "Score",
|
|
885
|
+
accessibilityValue: {
|
|
886
|
+
text: String(score)
|
|
887
|
+
},
|
|
888
|
+
className: votes({
|
|
889
|
+
className: cn(orientation === 'vertical' && 'flex-col gap-0', vote === 'up' && 'bg-success/12', vote === 'down' && 'bg-destructive/12', className)
|
|
890
|
+
}),
|
|
891
|
+
...props,
|
|
892
|
+
children: [/*#__PURE__*/_jsx(VoteArrow, {
|
|
893
|
+
direction: "up",
|
|
894
|
+
active: vote === 'up',
|
|
895
|
+
color: vote === 'up' ? up : rest,
|
|
896
|
+
disabled: disabled,
|
|
897
|
+
className: voteButton(),
|
|
898
|
+
onPress: () => cast('up')
|
|
899
|
+
}), /*#__PURE__*/_jsx(PostCount, {
|
|
900
|
+
value: score,
|
|
901
|
+
className: voteScore({
|
|
902
|
+
className: 'min-w-6 text-center'
|
|
903
|
+
}),
|
|
904
|
+
color: scoreColour
|
|
905
|
+
}), /*#__PURE__*/_jsx(VoteArrow, {
|
|
906
|
+
direction: "down",
|
|
907
|
+
active: vote === 'down',
|
|
908
|
+
color: vote === 'down' ? down : rest,
|
|
909
|
+
disabled: disabled,
|
|
910
|
+
className: voteButton(),
|
|
911
|
+
onPress: () => cast('down')
|
|
912
|
+
})]
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
PostVotes.displayName = 'Post.Votes';
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* One arrow.
|
|
919
|
+
*
|
|
920
|
+
* It travels along its own axis as it is cast — up for the up arrow, down for
|
|
921
|
+
* the down one — and springs back. The direction of the throw is the whole
|
|
922
|
+
* point: an arrow that merely scales says *something happened*, one that moves
|
|
923
|
+
* the way it points says *which way*.
|
|
924
|
+
*/
|
|
925
|
+
function VoteArrow({
|
|
926
|
+
direction,
|
|
927
|
+
active,
|
|
928
|
+
color,
|
|
929
|
+
disabled,
|
|
930
|
+
className,
|
|
931
|
+
onPress
|
|
932
|
+
}) {
|
|
933
|
+
const reducedMotion = useReducedMotion();
|
|
934
|
+
const shift = useSharedValue(0);
|
|
935
|
+
const scale = useSharedValue(1);
|
|
936
|
+
const first = useRef(true);
|
|
937
|
+
useEffect(() => {
|
|
938
|
+
if (first.current) {
|
|
939
|
+
first.current = false;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (reducedMotion) return;
|
|
943
|
+
const travel = direction === 'up' ? -5 : 5;
|
|
944
|
+
shift.value = withSequence(withTiming(active ? travel : -travel * 0.4, {
|
|
945
|
+
duration: 100
|
|
946
|
+
}), withSpring(0, POP));
|
|
947
|
+
scale.value = withSequence(withTiming(active ? 1.2 : 0.9, {
|
|
948
|
+
duration: 100
|
|
949
|
+
}), withSpring(1, POP));
|
|
950
|
+
}, [active, direction, reducedMotion, shift, scale]);
|
|
951
|
+
const style = useAnimatedStyle(() => ({
|
|
952
|
+
transform: [{
|
|
953
|
+
translateY: shift.value
|
|
954
|
+
}, {
|
|
955
|
+
scale: scale.value
|
|
956
|
+
}]
|
|
957
|
+
}));
|
|
958
|
+
const Arrow = direction === 'up' ? ArrowUpIcon : ArrowDownIcon;
|
|
959
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
960
|
+
accessibilityRole: "button",
|
|
961
|
+
accessibilityLabel: direction === 'up' ? 'Upvote' : 'Downvote',
|
|
962
|
+
accessibilityState: {
|
|
963
|
+
selected: active,
|
|
964
|
+
disabled
|
|
965
|
+
},
|
|
966
|
+
disabled: disabled,
|
|
967
|
+
hitSlop: 6,
|
|
968
|
+
onPress: onPress,
|
|
969
|
+
className: cn(className, disabled && 'opacity-[0.64]'),
|
|
970
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
971
|
+
style: style,
|
|
972
|
+
children: /*#__PURE__*/_jsx(Arrow, {
|
|
973
|
+
size: 17,
|
|
974
|
+
color: color,
|
|
975
|
+
filled: active
|
|
976
|
+
})
|
|
977
|
+
})
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
export const Post = Object.assign(PostRoot, {
|
|
981
|
+
Header: PostHeader,
|
|
982
|
+
Author: PostAuthor,
|
|
983
|
+
Action: PostAction,
|
|
984
|
+
Community: PostCommunity,
|
|
985
|
+
Title: PostTitle,
|
|
986
|
+
Body: PostBody,
|
|
987
|
+
Media: PostMedia,
|
|
988
|
+
Footer: PostFooter,
|
|
989
|
+
Stat: PostStat,
|
|
990
|
+
Votes: PostVotes
|
|
991
|
+
});
|
|
992
|
+
//# sourceMappingURL=index.js.map
|