@xk2800/nextjs-template 0.7.1-beta.2 → 0.7.1-beta.3

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.
@@ -4,6 +4,8 @@ import * as React from "react"
4
4
  import { Camera, ImagePlus, Upload, X } from "lucide-react"
5
5
 
6
6
  import { Button } from "./button"
7
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./dialog"
8
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet"
7
9
  import { cn } from "../../lib/utils"
8
10
 
9
11
  type FileUploadProps = {
@@ -13,6 +15,13 @@ type FileUploadProps = {
13
15
  gallery?: boolean
14
16
  /** Drop files onto the component. */
15
17
  dragDrop?: boolean
18
+ /**
19
+ * Pressing the drop zone opens the picker instead of showing separate buttons. With both
20
+ * `camera` and `gallery` the user is asked which one; with one, it opens directly. Needs `dragDrop`.
21
+ */
22
+ clickable?: boolean
23
+ /** How a touch-screen user is asked "camera or gallery" with `clickable`: a bottom sheet or a centered modal. */
24
+ chooser?: "sheet" | "modal"
16
25
  /** Same format as `<input accept>`. Also enforced on dropped files. */
17
26
  accept?: string
18
27
  multiple?: boolean
@@ -49,6 +58,8 @@ function FileUpload({
49
58
  camera = false,
50
59
  gallery = false,
51
60
  dragDrop = false,
61
+ clickable = false,
62
+ chooser = "sheet",
52
63
  accept = "image/*",
53
64
  multiple = true,
54
65
  maxFiles,
@@ -68,6 +79,8 @@ function FileUpload({
68
79
  const [dragging, setDragging] = React.useState(false)
69
80
  const cameraRef = React.useRef<HTMLInputElement>(null)
70
81
  const galleryRef = React.useRef<HTMLInputElement>(null)
82
+ const [askOpen, setAskOpen] = React.useState(false)
83
+ const pointerType = React.useRef("")
71
84
 
72
85
  // Created and revoked in the same effect: with useMemo, Strict Mode's
73
86
  // simulated unmount revoked the URLs and the remount reused the dead ones.
@@ -112,6 +125,37 @@ function FileUpload({
112
125
 
113
126
  if (!camera && !gallery && !dragDrop) return null
114
127
 
128
+ // Pressing the drop zone only does something if there's a picker to open.
129
+ const pickable = clickable && !disabled && (camera || gallery)
130
+ // The camera only means something on touch devices, so mouse and keyboard skip the
131
+ // question and open the gallery (or the only source there is).
132
+ const openDefault = () => (gallery ? galleryRef : cameraRef).current?.click()
133
+ const units = multiple ? "files" : "a file"
134
+
135
+ const askTitle = `Add ${multiple ? "photos" : "a photo"}`
136
+ const askDescription = "Take a new photo or pick from your gallery."
137
+ const askButtons = (
138
+ <div className="grid gap-2">
139
+ {[
140
+ { ref: cameraRef, Icon: Camera, text: "Take photo" },
141
+ { ref: galleryRef, Icon: ImagePlus, text: "Choose from gallery" },
142
+ ].map(({ ref, Icon, text }) => (
143
+ <Button
144
+ key={text}
145
+ type="button"
146
+ variant="outline"
147
+ className="h-12 justify-start"
148
+ onClick={() => {
149
+ setAskOpen(false)
150
+ ref.current?.click()
151
+ }}
152
+ >
153
+ <Icon /> {text}
154
+ </Button>
155
+ ))}
156
+ </div>
157
+ )
158
+
115
159
  const buttons = (
116
160
  <div className="flex flex-wrap justify-center gap-2">
117
161
  {camera && (
@@ -138,6 +182,28 @@ function FileUpload({
138
182
 
139
183
  {dragDrop ? (
140
184
  <div
185
+ role={pickable ? "button" : undefined}
186
+ tabIndex={pickable ? 0 : undefined}
187
+ onPointerDown={pickable ? (e) => (pointerType.current = e.pointerType) : undefined}
188
+ onClick={
189
+ pickable
190
+ ? () => {
191
+ if (camera && gallery && pointerType.current !== "mouse") setAskOpen(true)
192
+ else openDefault()
193
+ pointerType.current = ""
194
+ }
195
+ : undefined
196
+ }
197
+ onKeyDown={
198
+ pickable
199
+ ? (e) => {
200
+ if (e.key === "Enter" || e.key === " ") {
201
+ e.preventDefault()
202
+ openDefault()
203
+ }
204
+ }
205
+ : undefined
206
+ }
141
207
  onDragOver={(e) => {
142
208
  e.preventDefault()
143
209
  if (!disabled) setDragging(true)
@@ -151,6 +217,7 @@ function FileUpload({
151
217
  aria-disabled={disabled}
152
218
  className={cn(
153
219
  "flex flex-col items-center gap-3 rounded-lg border-2 border-dashed p-6 text-center text-sm text-muted-foreground transition-colors",
220
+ pickable && "cursor-pointer outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring",
154
221
  // Before the state classes so a custom border can't hide the drag highlight.
155
222
  dropzoneClassName,
156
223
  dragging && "border-primary bg-primary/5",
@@ -159,15 +226,49 @@ function FileUpload({
159
226
  >
160
227
  {icon}
161
228
  <div className="space-y-1">
162
- <p className={labelClassName}>{label ?? `Drag and drop ${multiple ? "files" : "a file"} here`}</p>
229
+ <p className={labelClassName}>
230
+ {label ??
231
+ (pickable ? (
232
+ <>
233
+ <span className="pointer-coarse:hidden">Drag and drop {units} here, or click to browse</span>
234
+ <span className="hidden pointer-coarse:inline">Tap to add {units}</span>
235
+ </>
236
+ ) : (
237
+ `Drag and drop ${units} here`
238
+ ))}
239
+ </p>
163
240
  {description && <p className={cn("text-xs", descriptionClassName)}>{description}</p>}
164
241
  </div>
165
- {buttons}
242
+ {!pickable && buttons}
166
243
  </div>
167
244
  ) : (
168
245
  buttons
169
246
  )}
170
247
 
248
+ {pickable && camera && gallery && (
249
+ chooser === "modal" ? (
250
+ <Dialog open={askOpen} onOpenChange={setAskOpen}>
251
+ <DialogContent className="sm:max-w-sm">
252
+ <DialogHeader>
253
+ <DialogTitle>{askTitle}</DialogTitle>
254
+ <DialogDescription>{askDescription}</DialogDescription>
255
+ </DialogHeader>
256
+ {askButtons}
257
+ </DialogContent>
258
+ </Dialog>
259
+ ) : (
260
+ <Sheet open={askOpen} onOpenChange={setAskOpen}>
261
+ <SheetContent side="bottom" className="mx-auto max-w-lg rounded-t-xl pb-[max(1rem,env(safe-area-inset-bottom))]">
262
+ <SheetHeader className="pb-0">
263
+ <SheetTitle>{askTitle}</SheetTitle>
264
+ <SheetDescription>{askDescription}</SheetDescription>
265
+ </SheetHeader>
266
+ <div className="px-4">{askButtons}</div>
267
+ </SheetContent>
268
+ </Sheet>
269
+ )
270
+ )}
271
+
171
272
  {error && (
172
273
  <p role="alert" className="text-sm text-destructive">
173
274
  {error}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xk2800/nextjs-template",
3
- "version": "0.7.1-beta.2",
3
+ "version": "0.7.1-beta.3",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",