@mostrom/app-shell 0.1.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/.claude/ralph-loop.local.md +9 -0
- package/README.md +172 -0
- package/bin/init.js +269 -0
- package/bun.lock +401 -0
- package/components.json +28 -0
- package/package.json +74 -0
- package/scripts/publish-npm.sh +202 -0
- package/src/AppShell.tsx +847 -0
- package/src/components/PageHeader.tsx +160 -0
- package/src/components/data-table/README.md +447 -0
- package/src/components/data-table/data-table-preferences.tsx +184 -0
- package/src/components/data-table/data-table-toolbar.tsx +118 -0
- package/src/components/data-table/data-table.tsx +37 -0
- package/src/components/data-table/index.ts +32 -0
- package/src/components/global-header/AllServicesButton.tsx +127 -0
- package/src/components/global-header/CategoriesButton.tsx +120 -0
- package/src/components/global-header/GlobalHeader.tsx +59 -0
- package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
- package/src/components/global-header/HeaderUtilities.tsx +243 -0
- package/src/components/global-header/ServicesMenu.tsx +246 -0
- package/src/components/layout/AppBreadcrumb.tsx +70 -0
- package/src/components/layout/AppFlashbar.tsx +95 -0
- package/src/components/layout/AppLayout.tsx +271 -0
- package/src/components/layout/AppNavigation.tsx +313 -0
- package/src/components/layout/AppSidebar.tsx +229 -0
- package/src/components/patterns/index.ts +14 -0
- package/src/components/patterns/p-alert-5.tsx +19 -0
- package/src/components/patterns/p-autocomplete-5.tsx +89 -0
- package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
- package/src/components/patterns/p-button-42.tsx +37 -0
- package/src/components/patterns/p-button-51.tsx +14 -0
- package/src/components/patterns/p-button-6.tsx +5 -0
- package/src/components/patterns/p-calendar-1.tsx +18 -0
- package/src/components/patterns/p-card-1.tsx +33 -0
- package/src/components/patterns/p-card-2.tsx +26 -0
- package/src/components/patterns/p-card-5.tsx +31 -0
- package/src/components/patterns/p-collapsible-7.tsx +121 -0
- package/src/components/patterns/p-command-6.tsx +113 -0
- package/src/components/patterns/p-dialog-1.tsx +56 -0
- package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
- package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
- package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
- package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
- package/src/components/patterns/p-empty-2.tsx +34 -0
- package/src/components/patterns/p-file-upload-1.tsx +72 -0
- package/src/components/patterns/p-filters-1.tsx +666 -0
- package/src/components/patterns/p-frame-2.tsx +26 -0
- package/src/components/patterns/p-tabs-2.tsx +129 -0
- package/src/components/reui/alert.tsx +92 -0
- package/src/components/reui/autocomplete.tsx +343 -0
- package/src/components/reui/badge.tsx +87 -0
- package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
- package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
- package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
- package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
- package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
- package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
- package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
- package/src/components/reui/data-grid/data-grid.tsx +209 -0
- package/src/components/reui/date-selector.tsx +1330 -0
- package/src/components/reui/filters.tsx +1869 -0
- package/src/components/reui/frame.tsx +134 -0
- package/src/components/reui/index.ts +17 -0
- package/src/components/reui/timeline.tsx +219 -0
- package/src/components/search/Autocomplete.tsx +183 -0
- package/src/components/search/AutocompleteClient.tsx +293 -0
- package/src/components/search/GlobalSearch.tsx +187 -0
- package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
- package/src/components/section-drawer/index.ts +19 -0
- package/src/components/section-drawer/section-drawer.css +665 -0
- package/src/components/section-drawer/section-drawer.tsx +467 -0
- package/src/components/sectioned-list-board/README.md +78 -0
- package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
- package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
- package/src/components/sectioned-list-board/index.ts +19 -0
- package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
- package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
- package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
- package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
- package/src/components/sectioned-list-board/types.ts +216 -0
- package/src/components/sectioned-list-table/README.md +80 -0
- package/src/components/sectioned-list-table/index.ts +14 -0
- package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
- package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
- package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
- package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
- package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
- package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
- package/src/components/sectioned-list-table/types.ts +120 -0
- package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
- package/src/components/ui/actions-dropdown.tsx +109 -0
- package/src/components/ui/assignee-selector.tsx +209 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/chart.tsx +376 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +182 -0
- package/src/components/ui/context-menu.tsx +250 -0
- package/src/components/ui/create-button-group.tsx +128 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/drawer.tsx +133 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/index.ts +37 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/page-header.tsx +80 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +38 -0
- package/src/components/ui/switch.tsx +33 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-copy-to-clipboard.ts +37 -0
- package/src/hooks/use-file-upload.ts +415 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +95 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +1859 -0
- package/src/urls.ts +83 -0
- package/src/vite.d.ts +22 -0
- package/src/vite.js +241 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import type React from "react"
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ChangeEvent,
|
|
7
|
+
type DragEvent,
|
|
8
|
+
type InputHTMLAttributes,
|
|
9
|
+
} from "react"
|
|
10
|
+
|
|
11
|
+
export type FileMetadata = {
|
|
12
|
+
name: string
|
|
13
|
+
size: number
|
|
14
|
+
type: string
|
|
15
|
+
url: string
|
|
16
|
+
id: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FileWithPreview = {
|
|
20
|
+
file: File | FileMetadata
|
|
21
|
+
id: string
|
|
22
|
+
preview?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FileUploadOptions = {
|
|
26
|
+
maxFiles?: number // Only used when multiple is true, defaults to Infinity
|
|
27
|
+
maxSize?: number // in bytes
|
|
28
|
+
accept?: string
|
|
29
|
+
multiple?: boolean // Defaults to false
|
|
30
|
+
initialFiles?: FileMetadata[]
|
|
31
|
+
onFilesChange?: (files: FileWithPreview[]) => void // Callback when files change
|
|
32
|
+
onFilesAdded?: (addedFiles: FileWithPreview[]) => void // Callback when new files are added
|
|
33
|
+
onError?: (errors: string[]) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type FileUploadState = {
|
|
37
|
+
files: FileWithPreview[]
|
|
38
|
+
isDragging: boolean
|
|
39
|
+
errors: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FileUploadActions = {
|
|
43
|
+
addFiles: (files: FileList | File[]) => void
|
|
44
|
+
removeFile: (id: string) => void
|
|
45
|
+
clearFiles: () => void
|
|
46
|
+
clearErrors: () => void
|
|
47
|
+
handleDragEnter: (e: DragEvent<HTMLElement>) => void
|
|
48
|
+
handleDragLeave: (e: DragEvent<HTMLElement>) => void
|
|
49
|
+
handleDragOver: (e: DragEvent<HTMLElement>) => void
|
|
50
|
+
handleDrop: (e: DragEvent<HTMLElement>) => void
|
|
51
|
+
handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void
|
|
52
|
+
openFileDialog: () => void
|
|
53
|
+
getInputProps: (
|
|
54
|
+
props?: InputHTMLAttributes<HTMLInputElement>
|
|
55
|
+
) => InputHTMLAttributes<HTMLInputElement> & {
|
|
56
|
+
ref: React.Ref<HTMLInputElement>
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const useFileUpload = (
|
|
61
|
+
options: FileUploadOptions = {}
|
|
62
|
+
): [FileUploadState, FileUploadActions] => {
|
|
63
|
+
const {
|
|
64
|
+
maxFiles = Number.POSITIVE_INFINITY,
|
|
65
|
+
maxSize = Number.POSITIVE_INFINITY,
|
|
66
|
+
accept = "*",
|
|
67
|
+
multiple = false,
|
|
68
|
+
initialFiles = [],
|
|
69
|
+
onFilesChange,
|
|
70
|
+
onFilesAdded,
|
|
71
|
+
onError,
|
|
72
|
+
} = options
|
|
73
|
+
|
|
74
|
+
const [state, setState] = useState<FileUploadState>({
|
|
75
|
+
files: initialFiles.map((file) => ({
|
|
76
|
+
file,
|
|
77
|
+
id: file.id,
|
|
78
|
+
preview: file.url,
|
|
79
|
+
})),
|
|
80
|
+
isDragging: false,
|
|
81
|
+
errors: [],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
85
|
+
|
|
86
|
+
const validateFile = useCallback(
|
|
87
|
+
(file: File | FileMetadata): string | null => {
|
|
88
|
+
if (file instanceof File) {
|
|
89
|
+
if (file.size > maxSize) {
|
|
90
|
+
return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
if (file.size > maxSize) {
|
|
94
|
+
return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (accept !== "*") {
|
|
99
|
+
const acceptedTypes = accept.split(",").map((type) => type.trim())
|
|
100
|
+
const fileType = file instanceof File ? file.type || "" : file.type
|
|
101
|
+
const fileExtension = `.${file instanceof File ? file.name.split(".").pop() : file.name.split(".").pop()}`
|
|
102
|
+
|
|
103
|
+
const isAccepted = acceptedTypes.some((type) => {
|
|
104
|
+
if (type.startsWith(".")) {
|
|
105
|
+
return fileExtension.toLowerCase() === type.toLowerCase()
|
|
106
|
+
}
|
|
107
|
+
if (type.endsWith("/*")) {
|
|
108
|
+
const baseType = type.split("/")[0]
|
|
109
|
+
return fileType.startsWith(`${baseType}/`)
|
|
110
|
+
}
|
|
111
|
+
return fileType === type
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (!isAccepted) {
|
|
115
|
+
return `File "${file instanceof File ? file.name : file.name}" is not an accepted file type.`
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null
|
|
120
|
+
},
|
|
121
|
+
[accept, maxSize]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const createPreview = useCallback(
|
|
125
|
+
(file: File | FileMetadata): string | undefined => {
|
|
126
|
+
if (file instanceof File) {
|
|
127
|
+
return URL.createObjectURL(file)
|
|
128
|
+
}
|
|
129
|
+
return file.url
|
|
130
|
+
},
|
|
131
|
+
[]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const generateUniqueId = useCallback((file: File | FileMetadata): string => {
|
|
135
|
+
if (file instanceof File) {
|
|
136
|
+
return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
|
137
|
+
}
|
|
138
|
+
return file.id
|
|
139
|
+
}, [])
|
|
140
|
+
|
|
141
|
+
const clearFiles = useCallback(() => {
|
|
142
|
+
setState((prev) => {
|
|
143
|
+
// Clean up object URLs
|
|
144
|
+
for (const file of prev.files) {
|
|
145
|
+
if (
|
|
146
|
+
file.preview &&
|
|
147
|
+
file.file instanceof File &&
|
|
148
|
+
file.file.type.startsWith("image/")
|
|
149
|
+
) {
|
|
150
|
+
URL.revokeObjectURL(file.preview)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (inputRef.current) {
|
|
155
|
+
inputRef.current.value = ""
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const newState = {
|
|
159
|
+
...prev,
|
|
160
|
+
files: [],
|
|
161
|
+
errors: [],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onFilesChange?.(newState.files)
|
|
165
|
+
return newState
|
|
166
|
+
})
|
|
167
|
+
}, [onFilesChange])
|
|
168
|
+
|
|
169
|
+
const addFiles = useCallback(
|
|
170
|
+
(newFiles: FileList | File[]) => {
|
|
171
|
+
if (!newFiles || newFiles.length === 0) return
|
|
172
|
+
|
|
173
|
+
const newFilesArray = Array.from(newFiles)
|
|
174
|
+
const errors: string[] = []
|
|
175
|
+
|
|
176
|
+
// Clear existing errors when new files are uploaded
|
|
177
|
+
setState((prev) => ({ ...prev, errors: [] }))
|
|
178
|
+
|
|
179
|
+
// In single file mode, clear existing files first
|
|
180
|
+
if (!multiple) {
|
|
181
|
+
clearFiles()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if adding these files would exceed maxFiles (only in multiple mode)
|
|
185
|
+
if (
|
|
186
|
+
multiple &&
|
|
187
|
+
maxFiles !== Number.POSITIVE_INFINITY &&
|
|
188
|
+
state.files.length + newFilesArray.length > maxFiles
|
|
189
|
+
) {
|
|
190
|
+
errors.push(`You can only upload a maximum of ${maxFiles} files.`)
|
|
191
|
+
onError?.(errors)
|
|
192
|
+
setState((prev) => ({ ...prev, errors }))
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const validFiles: FileWithPreview[] = []
|
|
197
|
+
|
|
198
|
+
for (const file of newFilesArray) {
|
|
199
|
+
// Only check for duplicates if multiple files are allowed
|
|
200
|
+
if (multiple) {
|
|
201
|
+
const isDuplicate = state.files.some(
|
|
202
|
+
(existingFile) =>
|
|
203
|
+
existingFile.file.name === file.name &&
|
|
204
|
+
existingFile.file.size === file.size
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Skip duplicate files silently
|
|
208
|
+
if (isDuplicate) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check file size
|
|
214
|
+
if (file.size > maxSize) {
|
|
215
|
+
errors.push(
|
|
216
|
+
multiple
|
|
217
|
+
? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`
|
|
218
|
+
: `File exceeds the maximum size of ${formatBytes(maxSize)}.`
|
|
219
|
+
)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const error = validateFile(file)
|
|
224
|
+
if (error) {
|
|
225
|
+
errors.push(error)
|
|
226
|
+
} else {
|
|
227
|
+
validFiles.push({
|
|
228
|
+
file,
|
|
229
|
+
id: generateUniqueId(file),
|
|
230
|
+
preview: createPreview(file),
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Only update state if we have valid files to add
|
|
236
|
+
if (validFiles.length > 0) {
|
|
237
|
+
// Call the onFilesAdded callback with the newly added valid files
|
|
238
|
+
onFilesAdded?.(validFiles)
|
|
239
|
+
|
|
240
|
+
setState((prev) => {
|
|
241
|
+
const newFiles = !multiple
|
|
242
|
+
? validFiles
|
|
243
|
+
: [...prev.files, ...validFiles]
|
|
244
|
+
onFilesChange?.(newFiles)
|
|
245
|
+
return {
|
|
246
|
+
...prev,
|
|
247
|
+
files: newFiles,
|
|
248
|
+
errors,
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
} else if (errors.length > 0) {
|
|
252
|
+
onError?.(errors)
|
|
253
|
+
setState((prev) => ({
|
|
254
|
+
...prev,
|
|
255
|
+
errors,
|
|
256
|
+
}))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Reset input value after handling files
|
|
260
|
+
if (inputRef.current) {
|
|
261
|
+
inputRef.current.value = ""
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
[
|
|
265
|
+
state.files,
|
|
266
|
+
maxFiles,
|
|
267
|
+
multiple,
|
|
268
|
+
maxSize,
|
|
269
|
+
validateFile,
|
|
270
|
+
createPreview,
|
|
271
|
+
generateUniqueId,
|
|
272
|
+
clearFiles,
|
|
273
|
+
onFilesChange,
|
|
274
|
+
onFilesAdded,
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const removeFile = useCallback(
|
|
279
|
+
(id: string) => {
|
|
280
|
+
setState((prev) => {
|
|
281
|
+
const fileToRemove = prev.files.find((file) => file.id === id)
|
|
282
|
+
if (
|
|
283
|
+
fileToRemove &&
|
|
284
|
+
fileToRemove.preview &&
|
|
285
|
+
fileToRemove.file instanceof File &&
|
|
286
|
+
fileToRemove.file.type.startsWith("image/")
|
|
287
|
+
) {
|
|
288
|
+
URL.revokeObjectURL(fileToRemove.preview)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const newFiles = prev.files.filter((file) => file.id !== id)
|
|
292
|
+
onFilesChange?.(newFiles)
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...prev,
|
|
296
|
+
files: newFiles,
|
|
297
|
+
errors: [],
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
},
|
|
301
|
+
[onFilesChange]
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
const clearErrors = useCallback(() => {
|
|
305
|
+
setState((prev) => ({
|
|
306
|
+
...prev,
|
|
307
|
+
errors: [],
|
|
308
|
+
}))
|
|
309
|
+
}, [])
|
|
310
|
+
|
|
311
|
+
const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {
|
|
312
|
+
e.preventDefault()
|
|
313
|
+
e.stopPropagation()
|
|
314
|
+
setState((prev) => ({ ...prev, isDragging: true }))
|
|
315
|
+
}, [])
|
|
316
|
+
|
|
317
|
+
const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {
|
|
318
|
+
e.preventDefault()
|
|
319
|
+
e.stopPropagation()
|
|
320
|
+
|
|
321
|
+
if (e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setState((prev) => ({ ...prev, isDragging: false }))
|
|
326
|
+
}, [])
|
|
327
|
+
|
|
328
|
+
const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {
|
|
329
|
+
e.preventDefault()
|
|
330
|
+
e.stopPropagation()
|
|
331
|
+
}, [])
|
|
332
|
+
|
|
333
|
+
const handleDrop = useCallback(
|
|
334
|
+
(e: DragEvent<HTMLElement>) => {
|
|
335
|
+
e.preventDefault()
|
|
336
|
+
e.stopPropagation()
|
|
337
|
+
setState((prev) => ({ ...prev, isDragging: false }))
|
|
338
|
+
|
|
339
|
+
// Don't process files if the input is disabled
|
|
340
|
+
if (inputRef.current?.disabled) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
345
|
+
// In single file mode, only use the first file
|
|
346
|
+
if (!multiple) {
|
|
347
|
+
const file = e.dataTransfer.files[0]
|
|
348
|
+
addFiles([file])
|
|
349
|
+
} else {
|
|
350
|
+
addFiles(e.dataTransfer.files)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
[addFiles, multiple]
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
const handleFileChange = useCallback(
|
|
358
|
+
(e: ChangeEvent<HTMLInputElement>) => {
|
|
359
|
+
if (e.target.files && e.target.files.length > 0) {
|
|
360
|
+
addFiles(e.target.files)
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
[addFiles]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
const openFileDialog = useCallback(() => {
|
|
367
|
+
if (inputRef.current) {
|
|
368
|
+
inputRef.current.click()
|
|
369
|
+
}
|
|
370
|
+
}, [])
|
|
371
|
+
|
|
372
|
+
const getInputProps = useCallback(
|
|
373
|
+
(props: InputHTMLAttributes<HTMLInputElement> = {}) => {
|
|
374
|
+
return {
|
|
375
|
+
...props,
|
|
376
|
+
type: "file" as const,
|
|
377
|
+
onChange: handleFileChange,
|
|
378
|
+
accept: props.accept || accept,
|
|
379
|
+
multiple: props.multiple !== undefined ? props.multiple : multiple,
|
|
380
|
+
ref: inputRef,
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
[accept, multiple, handleFileChange]
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return [
|
|
387
|
+
state,
|
|
388
|
+
{
|
|
389
|
+
addFiles,
|
|
390
|
+
removeFile,
|
|
391
|
+
clearFiles,
|
|
392
|
+
clearErrors,
|
|
393
|
+
handleDragEnter,
|
|
394
|
+
handleDragLeave,
|
|
395
|
+
handleDragOver,
|
|
396
|
+
handleDrop,
|
|
397
|
+
handleFileChange,
|
|
398
|
+
openFileDialog,
|
|
399
|
+
getInputProps,
|
|
400
|
+
},
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Helper function to format bytes to human-readable format
|
|
405
|
+
export const formatBytes = (bytes: number, decimals = 2): string => {
|
|
406
|
+
if (bytes === 0) return "0 Bytes"
|
|
407
|
+
|
|
408
|
+
const k = 1024
|
|
409
|
+
const dm = decimals < 0 ? 0 : decimals
|
|
410
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
411
|
+
|
|
412
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
413
|
+
|
|
414
|
+
return Number.parseFloat((bytes / k ** i).toFixed(dm)) + sizes[i]
|
|
415
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export { AppShell } from "./AppShell";
|
|
2
|
+
export type {
|
|
3
|
+
AppShellProps,
|
|
4
|
+
CompatibleNavigationFollowEvent,
|
|
5
|
+
CompatibleNavigationItem,
|
|
6
|
+
} from "./AppShell";
|
|
7
|
+
|
|
8
|
+
// Menu types (for settings/user dropdown menus)
|
|
9
|
+
export type {
|
|
10
|
+
MenuDropdownItem,
|
|
11
|
+
MenuDropdownItemGroup,
|
|
12
|
+
MenuDropdownItemOrGroup,
|
|
13
|
+
MenuDropdownItems,
|
|
14
|
+
} from "./components/global-header/ServicesMenu";
|
|
15
|
+
export { basePath, apiUrl, serviceUrl } from "./urls";
|
|
16
|
+
|
|
17
|
+
// Breadcrumb components
|
|
18
|
+
export { AppBreadcrumb } from "./components/layout/AppBreadcrumb";
|
|
19
|
+
export type { AppBreadcrumbProps, BreadcrumbItem } from "./components/layout/AppBreadcrumb";
|
|
20
|
+
|
|
21
|
+
// Page Header components
|
|
22
|
+
export {
|
|
23
|
+
PageHeader,
|
|
24
|
+
PageHeaderTitle,
|
|
25
|
+
PageHeaderDescription,
|
|
26
|
+
PageHeaderActions,
|
|
27
|
+
} from "./components/PageHeader";
|
|
28
|
+
export type { PageHeaderProps } from "./components/PageHeader";
|
|
29
|
+
|
|
30
|
+
// Navigation components
|
|
31
|
+
export { AppNavigation } from "./components/layout/AppNavigation";
|
|
32
|
+
export type {
|
|
33
|
+
AppNavigationProps,
|
|
34
|
+
NavigationFollowEvent,
|
|
35
|
+
NavigationFollowDetail,
|
|
36
|
+
} from "./components/layout/AppNavigation";
|
|
37
|
+
|
|
38
|
+
// Layout components
|
|
39
|
+
export { AppLayout } from "./components/layout/AppLayout";
|
|
40
|
+
export type { AppLayoutProps } from "./components/layout/AppLayout";
|
|
41
|
+
|
|
42
|
+
// Flashbar components (replaces Cloudscape Flashbar)
|
|
43
|
+
export { AppFlashbar } from "./components/layout/AppFlashbar";
|
|
44
|
+
export type { AppFlashbarProps, FlashbarMessage } from "./components/layout/AppFlashbar";
|
|
45
|
+
|
|
46
|
+
// Autocomplete components (based on Base UI)
|
|
47
|
+
export {
|
|
48
|
+
Autocomplete,
|
|
49
|
+
AutocompleteValue,
|
|
50
|
+
AutocompleteInput,
|
|
51
|
+
AutocompletePortal,
|
|
52
|
+
AutocompleteBackdrop,
|
|
53
|
+
AutocompletePositioner,
|
|
54
|
+
AutocompleteContent,
|
|
55
|
+
AutocompleteList,
|
|
56
|
+
AutocompleteItem,
|
|
57
|
+
AutocompleteGroup,
|
|
58
|
+
AutocompleteGroupLabel,
|
|
59
|
+
AutocompleteEmpty,
|
|
60
|
+
AutocompleteClear,
|
|
61
|
+
AutocompleteTrigger,
|
|
62
|
+
} from "./components/search/Autocomplete";
|
|
63
|
+
export type { AutocompleteContentProps } from "./components/search/Autocomplete";
|
|
64
|
+
|
|
65
|
+
// Re-exported from merged design-system package for backwards compatibility
|
|
66
|
+
export const DESIGN_SYSTEM_VERSION = "0.0.0";
|
|
67
|
+
|
|
68
|
+
// UI Components (shadcn/ui + reui)
|
|
69
|
+
export * from "./components/ui";
|
|
70
|
+
|
|
71
|
+
// Pattern Components (reui.io)
|
|
72
|
+
export * from "./components/patterns";
|
|
73
|
+
|
|
74
|
+
// ReUI Components
|
|
75
|
+
export * from "./components/reui";
|
|
76
|
+
|
|
77
|
+
// Hooks
|
|
78
|
+
export * from "./hooks/use-copy-to-clipboard";
|
|
79
|
+
export * from "./hooks/use-file-upload";
|
|
80
|
+
export * from "./hooks/use-mobile";
|
|
81
|
+
|
|
82
|
+
// Section Drawer
|
|
83
|
+
export {
|
|
84
|
+
SectionDrawer,
|
|
85
|
+
DealDrawerContent,
|
|
86
|
+
type SectionDrawerProps,
|
|
87
|
+
type SectionDrawerUser,
|
|
88
|
+
type SectionDrawerActivity,
|
|
89
|
+
type DealDrawerContentProps,
|
|
90
|
+
type DealClient,
|
|
91
|
+
type DealOwner,
|
|
92
|
+
} from "./components/section-drawer";
|
|
93
|
+
|
|
94
|
+
// Utilities
|
|
95
|
+
export { cn } from "./lib/utils";
|