@carlonicora/nextjs-jsonapi 1.116.0 → 1.117.1

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.
Files changed (52) hide show
  1. package/dist/{AssistantMessageInterface-Ca0G0vZw.d.mts → AssistantMessageInterface-DH1QwtC_.d.mts} +1 -0
  2. package/dist/{AssistantMessageInterface-DEZ5AnVl.d.ts → AssistantMessageInterface-giWvsOrX.d.ts} +1 -0
  3. package/dist/{BlockNoteEditor-R6U4PCQ7.js → BlockNoteEditor-CEWZCORH.js} +9 -9
  4. package/dist/{BlockNoteEditor-R6U4PCQ7.js.map → BlockNoteEditor-CEWZCORH.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-RFRK3STN.mjs → BlockNoteEditor-K4T6QOJO.mjs} +2 -2
  6. package/dist/billing/index.js +299 -299
  7. package/dist/billing/index.mjs +1 -1
  8. package/dist/{chunk-SPRFCAQI.js → chunk-56QAXZKI.js} +470 -328
  9. package/dist/chunk-56QAXZKI.js.map +1 -0
  10. package/dist/{chunk-RKUUGCYB.mjs → chunk-6D4GJHLK.mjs} +526 -384
  11. package/dist/chunk-6D4GJHLK.mjs.map +1 -0
  12. package/dist/client/index.js +2 -2
  13. package/dist/client/index.mjs +1 -1
  14. package/dist/components/index.d.mts +69 -22
  15. package/dist/components/index.d.ts +69 -22
  16. package/dist/components/index.js +4 -2
  17. package/dist/components/index.js.map +1 -1
  18. package/dist/components/index.mjs +3 -1
  19. package/dist/contexts/index.d.mts +1 -1
  20. package/dist/contexts/index.d.ts +1 -1
  21. package/dist/contexts/index.js +2 -2
  22. package/dist/contexts/index.mjs +1 -1
  23. package/dist/core/index.d.mts +2 -2
  24. package/dist/core/index.d.ts +2 -2
  25. package/dist/features/help/index.js +30 -30
  26. package/dist/features/help/index.mjs +1 -1
  27. package/dist/index.d.mts +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/components/forms/CommonDeleter.tsx +49 -18
  31. package/src/components/forms/CommonEditorButtons.tsx +4 -2
  32. package/src/components/forms/CommonEditorHeader.tsx +20 -14
  33. package/src/components/forms/CommonEditorTrigger.tsx +18 -4
  34. package/src/components/forms/DateRangeSelector.tsx +55 -19
  35. package/src/components/forms/EditorSheet.tsx +36 -9
  36. package/src/components/forms/FileUploader.tsx +51 -21
  37. package/src/components/forms/FormDate.tsx +6 -2
  38. package/src/components/forms/FormDateTime.tsx +11 -2
  39. package/src/components/forms/FormFieldWrapper.tsx +3 -1
  40. package/src/components/forms/FormInput.tsx +3 -1
  41. package/src/components/forms/FormPassword.tsx +10 -1
  42. package/src/components/forms/FormPlaceAutocomplete.tsx +8 -3
  43. package/src/components/forms/FormSelect.tsx +3 -0
  44. package/src/components/forms/FormSlider.tsx +3 -1
  45. package/src/components/forms/FormTextarea.tsx +15 -1
  46. package/src/components/navigations/Breadcrumb.tsx +31 -5
  47. package/src/hooks/useSocket.ts +12 -1
  48. package/src/interfaces/breadcrumb.item.data.interface.ts +1 -0
  49. package/src/shadcnui/ui/badge.tsx +15 -0
  50. package/dist/chunk-RKUUGCYB.mjs.map +0 -1
  51. package/dist/chunk-SPRFCAQI.js.map +0 -1
  52. /package/dist/{BlockNoteEditor-RFRK3STN.mjs.map → BlockNoteEditor-K4T6QOJO.mjs.map} +0 -0
@@ -62,6 +62,17 @@ export type EditorSheetProps<T extends FieldValues> = {
62
62
  dialogOpen?: boolean;
63
63
  onDialogOpenChange?: (open: boolean) => void;
64
64
 
65
+ /** Render a fully custom footer instead of the default CommonEditorButtons.
66
+ * `setOpen` is the dirty-checked open handler (shows the discard dialog);
67
+ * `closeWithoutConfirm` is the raw setter that bypasses the discard dialog
68
+ * (use after a successful submit when no confirmation is needed). */
69
+ renderFooter?: (props: {
70
+ form: UseFormReturn<T>;
71
+ isEdit: boolean;
72
+ setOpen: (open: boolean) => void;
73
+ closeWithoutConfirm: (open: boolean) => void;
74
+ }) => ReactNode;
75
+
65
76
  children: ReactNode;
66
77
  };
67
78
 
@@ -90,6 +101,7 @@ export function EditorSheet<T extends FieldValues>({
90
101
  onClose,
91
102
  dialogOpen,
92
103
  onDialogOpenChange,
104
+ renderFooter,
93
105
  children,
94
106
  }: EditorSheetProps<T>) {
95
107
  const t = useTranslations();
@@ -147,6 +159,7 @@ export function EditorSheet<T extends FieldValues>({
147
159
  <>
148
160
  <Sheet open={open} onOpenChange={handleOpenChange}>
149
161
  {dialogOpen === undefined &&
162
+ forceShow === undefined &&
150
163
  (trigger ? (
151
164
  <SheetTrigger>{trigger}</SheetTrigger>
152
165
  ) : (
@@ -184,17 +197,31 @@ export function EditorSheet<T extends FieldValues>({
184
197
  </SheetDescription>
185
198
  </SheetHeader>
186
199
  <Form {...form}>
187
- <form onSubmit={form.handleSubmit(wrappedOnSubmit)} className="flex min-h-0 flex-1 flex-col">
200
+ <form
201
+ onSubmit={(e) => {
202
+ // The Sheet content is portaled, but React synthetic events still
203
+ // bubble up the React tree — so without stopPropagation an inner
204
+ // EditorSheet's submit also triggers the outer form's submit
205
+ // (e.g. a create dialog opened from within another editor).
206
+ e.stopPropagation();
207
+ return form.handleSubmit(wrappedOnSubmit)(e);
208
+ }}
209
+ className="flex min-h-0 flex-1 flex-col"
210
+ >
188
211
  <div className="flex-1 overflow-y-auto px-6 py-4">{children}</div>
189
212
  <SheetFooter className="shrink-0 border-t px-6 py-4">
190
- <CommonEditorButtons
191
- form={form}
192
- setOpen={handleOpenChange}
193
- isEdit={isEdit}
194
- disabled={disabled}
195
- hideSubmit={hideSubmit}
196
- centerButtons={centerButtons}
197
- />
213
+ {renderFooter ? (
214
+ renderFooter({ form, isEdit, setOpen: handleOpenChange, closeWithoutConfirm: setOpen })
215
+ ) : (
216
+ <CommonEditorButtons
217
+ form={form}
218
+ setOpen={handleOpenChange}
219
+ isEdit={isEdit}
220
+ disabled={disabled}
221
+ hideSubmit={hideSubmit}
222
+ centerButtons={centerButtons}
223
+ />
224
+ )}
198
225
  </SheetFooter>
199
226
  </form>
200
227
  </Form>
@@ -10,12 +10,13 @@ import {
10
10
  useCallback,
11
11
  useContext,
12
12
  useEffect,
13
+ useMemo,
13
14
  useRef,
14
15
  useState,
15
16
  } from "react";
16
- import { DropzoneOptions, DropzoneState, FileRejection, useDropzone } from "react-dropzone";
17
+ import { Accept, DropzoneOptions, DropzoneState, FileRejection, useDropzone } from "react-dropzone";
17
18
  import { showError } from "../../utils/toast";
18
- import { buttonVariants, Input } from "../../shadcnui";
19
+ import { buttonVariants, Input, Tooltip, TooltipContent, TooltipTrigger } from "../../shadcnui";
19
20
  import { cn } from "../../utils";
20
21
 
21
22
  export type { DropzoneOptions } from "react-dropzone";
@@ -31,6 +32,7 @@ type FileUploaderContextType = {
31
32
  setActiveIndex: Dispatch<SetStateAction<number>>;
32
33
  orientation: "horizontal" | "vertical";
33
34
  direction: DirectionOptions;
35
+ accept?: Accept;
34
36
  };
35
37
 
36
38
  const FileUploaderContext = createContext<FileUploaderContextType | null>(null);
@@ -195,6 +197,7 @@ export const FileUploader = forwardRef<HTMLDivElement, FileUploaderProps & React
195
197
  setActiveIndex,
196
198
  orientation,
197
199
  direction,
200
+ accept: dropzoneOptions.accept,
198
201
  }}
199
202
  >
200
203
  <div
@@ -283,35 +286,62 @@ FileUploaderItem.displayName = "FileUploaderItem";
283
286
 
284
287
  export const FileInput = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
285
288
  ({ className, children, ...props }, ref) => {
286
- const { dropzoneState, isFileTooBig, isLOF } = useFileUpload();
289
+ const { dropzoneState, isFileTooBig, isLOF, accept } = useFileUpload();
290
+ const t = useTranslations();
287
291
  const rootProps = isLOF ? {} : dropzoneState.getRootProps();
288
292
  // Get isDragActive from the context for FileInput as well, to ensure it can react if needed, or to simplify its own styling.
289
293
  const { isDragActive: parentIsDragActive } = dropzoneState;
290
294
 
295
+ const acceptedLabels = useMemo(() => {
296
+ if (!accept) return null;
297
+ const extensions = new Set<string>();
298
+ let hasWildcardImages = false;
299
+ for (const [mime, exts] of Object.entries(accept)) {
300
+ if (mime === "image/*") hasWildcardImages = true;
301
+ for (const ext of exts) extensions.add(ext);
302
+ }
303
+ const labels = Array.from(extensions).sort();
304
+ if (hasWildcardImages) labels.push(t("ui.labels.images"));
305
+ return labels.length > 0 ? labels : null;
306
+ }, [accept, t]);
307
+
308
+ const dropArea = (
309
+ <div
310
+ className={cn(
311
+ "w-full rounded-lg duration-300 ease-in-out",
312
+ // Simpler border logic: if parent is drag-active, it controls the border.
313
+ // Otherwise, FileInput can show its own accept/reject/default border.
314
+ {
315
+ "border-green-500": dropzoneState.isDragAccept && !parentIsDragActive,
316
+ "border-red-500": (dropzoneState.isDragReject || isFileTooBig) && !parentIsDragActive,
317
+ "border-gray-300":
318
+ !dropzoneState.isDragAccept && !dropzoneState.isDragReject && !isFileTooBig && !parentIsDragActive,
319
+ },
320
+ // className from props should be last to allow overrides if necessary
321
+ className,
322
+ )}
323
+ {...rootProps}
324
+ >
325
+ {children}
326
+ </div>
327
+ );
328
+
291
329
  return (
292
330
  <div
293
331
  ref={ref}
294
332
  {...props}
295
333
  className={cn(`relative w-full ${isLOF ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`, className)}
296
334
  >
297
- <div
298
- className={cn(
299
- "w-full rounded-lg duration-300 ease-in-out",
300
- // Simpler border logic: if parent is drag-active, it controls the border.
301
- // Otherwise, FileInput can show its own accept/reject/default border.
302
- {
303
- "border-green-500": dropzoneState.isDragAccept && !parentIsDragActive,
304
- "border-red-500": (dropzoneState.isDragReject || isFileTooBig) && !parentIsDragActive,
305
- "border-gray-300":
306
- !dropzoneState.isDragAccept && !dropzoneState.isDragReject && !isFileTooBig && !parentIsDragActive,
307
- },
308
- // className from props should be last to allow overrides if necessary
309
- className,
310
- )}
311
- {...rootProps}
312
- >
313
- {children}
314
- </div>
335
+ {acceptedLabels ? (
336
+ <Tooltip>
337
+ <TooltipTrigger render={dropArea} />
338
+ <TooltipContent>
339
+ {t("ui.labels.accepted_file_types")}: {acceptedLabels.join(", ")}
340
+ </TooltipContent>
341
+ </Tooltip>
342
+ ) : (
343
+ dropArea
344
+ )}
315
345
  <Input
316
346
  ref={dropzoneState.inputRef}
317
347
  disabled={isLOF}
@@ -26,6 +26,8 @@ export function FormDate({
26
26
  isRequired = false,
27
27
  defaultMonth,
28
28
  allowEmpty,
29
+ description,
30
+ futureYears,
29
31
  }: {
30
32
  form: any;
31
33
  id: string;
@@ -36,6 +38,8 @@ export function FormDate({
36
38
  isRequired?: boolean;
37
39
  defaultMonth?: Date;
38
40
  allowEmpty?: boolean;
41
+ description?: string;
42
+ futureYears?: number;
39
43
  }) {
40
44
  const t = useI18nTranslations();
41
45
  const locale = useI18nLocale();
@@ -117,7 +121,7 @@ export function FormDate({
117
121
 
118
122
  return (
119
123
  <div className="flex w-full flex-col">
120
- <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired}>
124
+ <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired} description={description}>
121
125
  {(field) => (
122
126
  <Popover open={open} onOpenChange={setOpen} modal={true}>
123
127
  <InputGroup>
@@ -162,7 +166,7 @@ export function FormDate({
162
166
  month={displayMonth}
163
167
  onMonthChange={setDisplayMonth}
164
168
  startMonth={new Date(1900, 0)}
165
- endMonth={new Date(new Date().getFullYear() + 10, 11)}
169
+ endMonth={new Date(new Date().getFullYear() + (futureYears ?? 10), 11)}
166
170
  />
167
171
  {allowEmpty !== false && (
168
172
  <div className="border-t p-2">
@@ -27,6 +27,9 @@ export function FormDateTime({
27
27
  onChange,
28
28
  allowEmpty,
29
29
  defaultMonth,
30
+ placeholder,
31
+ isRequired,
32
+ description,
30
33
  }: {
31
34
  form: any;
32
35
  id: string;
@@ -36,6 +39,8 @@ export function FormDateTime({
36
39
  onChange?: (date?: Date) => Promise<void>;
37
40
  allowEmpty?: boolean;
38
41
  defaultMonth?: Date;
42
+ isRequired?: boolean;
43
+ description?: string;
39
44
  }) {
40
45
  const [open, setOpen] = useState<boolean>(false);
41
46
  const t = useI18nTranslations();
@@ -100,7 +105,7 @@ export function FormDateTime({
100
105
 
101
106
  return (
102
107
  <div className="flex w-full flex-col">
103
- <FormFieldWrapper form={form} name={id} label={name}>
108
+ <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired} description={description}>
104
109
  {(field) => (
105
110
  <div className="relative flex flex-row">
106
111
  <Popover open={open} onOpenChange={setOpen} modal={true}>
@@ -113,7 +118,11 @@ export function FormDateTime({
113
118
  />
114
119
  }
115
120
  >
116
- {field.value ? formatDateTime(field.value) : <span>{t(`common.pick_date_time`)}</span>}
121
+ {field.value ? (
122
+ formatDateTime(field.value)
123
+ ) : (
124
+ <span>{placeholder ? placeholder : t(`common.pick_date_time`)}</span>
125
+ )}
117
126
  <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
118
127
  </PopoverTrigger>
119
128
  {field.value && allowEmpty !== false && (
@@ -18,6 +18,7 @@ type FormFieldWrapperProps<T extends FieldValues> = {
18
18
  description?: string;
19
19
  isRequired?: boolean;
20
20
  orientation?: "vertical" | "horizontal" | "responsive";
21
+ className?: string;
21
22
  children: (field: ControllerRenderProps<T, Path<T>>, fieldState: ControllerFieldState) => ReactNode;
22
23
  testId?: string;
23
24
  };
@@ -29,6 +30,7 @@ export function FormFieldWrapper<T extends FieldValues>({
29
30
  description,
30
31
  isRequired,
31
32
  orientation = "vertical",
33
+ className,
32
34
  children,
33
35
  testId,
34
36
  }: FormFieldWrapperProps<T>) {
@@ -37,7 +39,7 @@ export function FormFieldWrapper<T extends FieldValues>({
37
39
  control={form.control}
38
40
  name={name}
39
41
  render={({ field, fieldState }) => (
40
- <Field orientation={orientation} data-invalid={!!fieldState.error} data-testid={testId}>
42
+ <Field orientation={orientation} className={className} data-invalid={!!fieldState.error} data-testid={testId}>
41
43
  {label && (
42
44
  <FieldLabel>
43
45
  {label}
@@ -9,6 +9,7 @@ export function FormInput({
9
9
  form,
10
10
  id,
11
11
  name,
12
+ description,
12
13
  placeholder,
13
14
  type,
14
15
  onBlur,
@@ -22,6 +23,7 @@ export function FormInput({
22
23
  form: any;
23
24
  id: string;
24
25
  name?: string;
26
+ description?: string;
25
27
  placeholder?: string;
26
28
  type?: "text" | "number" | "currency" | "decimal" | "password" | "link";
27
29
  onBlur?: () => Promise<void>;
@@ -36,7 +38,7 @@ export function FormInput({
36
38
 
37
39
  return (
38
40
  <div className="flex w-full flex-col">
39
- <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired}>
41
+ <FormFieldWrapper form={form} name={id} label={name} description={description} isRequired={isRequired}>
40
42
  {(field) => {
41
43
  const handleBlur = async (e: React.FocusEvent<HTMLInputElement>) => {
42
44
  let value = e.target.value;
@@ -12,6 +12,7 @@ export function FormPassword({
12
12
  disabled,
13
13
  testId,
14
14
  isRequired,
15
+ description,
15
16
  }: {
16
17
  form: any;
17
18
  id: string;
@@ -21,10 +22,18 @@ export function FormPassword({
21
22
  disabled?: boolean;
22
23
  testId?: string;
23
24
  isRequired?: boolean;
25
+ description?: string;
24
26
  }) {
25
27
  return (
26
28
  <div className="flex w-full flex-col">
27
- <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired} testId={testId}>
29
+ <FormFieldWrapper
30
+ form={form}
31
+ name={id}
32
+ label={name}
33
+ isRequired={isRequired}
34
+ description={description}
35
+ testId={testId}
36
+ >
28
37
  {(field) => (
29
38
  <PasswordInput
30
39
  {...field}
@@ -42,6 +42,7 @@ interface PlaceAutocompleteProps {
42
42
  form: any;
43
43
  id: string;
44
44
  name?: string;
45
+ description?: string;
45
46
  placeholder?: string;
46
47
  disabled?: boolean;
47
48
  testId?: string;
@@ -56,12 +57,15 @@ interface PlaceAutocompleteProps {
56
57
  * For regions, use: ["administrative_area_level_1", "administrative_area_level_2"]
57
58
  */
58
59
  includeTypes?: string[];
60
+ /** Google Places language code for results. Defaults to "en". */
61
+ languageCode?: string;
59
62
  }
60
63
 
61
64
  export function FormPlaceAutocomplete({
62
65
  form,
63
66
  id,
64
67
  name,
68
+ description,
65
69
  placeholder,
66
70
  disabled,
67
71
  testId,
@@ -69,6 +73,7 @@ export function FormPlaceAutocomplete({
69
73
  onPlaceSelect,
70
74
  className,
71
75
  includeTypes,
76
+ languageCode = "en",
72
77
  }: PlaceAutocompleteProps) {
73
78
  const [inputValue, setInputValue] = useState("");
74
79
  const [suggestions, setSuggestions] = useState<PlaceSuggestion[]>([]);
@@ -117,7 +122,7 @@ export function FormPlaceAutocomplete({
117
122
  body: JSON.stringify({
118
123
  input: input,
119
124
  ...(includeTypes ? { includedPrimaryTypes: includeTypes } : {}),
120
- languageCode: "en",
125
+ languageCode: languageCode,
121
126
  }),
122
127
  });
123
128
 
@@ -270,7 +275,7 @@ export function FormPlaceAutocomplete({
270
275
  if (loadError) {
271
276
  return (
272
277
  <div className="flex w-full flex-col">
273
- <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired}>
278
+ <FormFieldWrapper form={form} name={id} label={name} description={description} isRequired={isRequired}>
274
279
  {(field) => (
275
280
  <Input
276
281
  {...field}
@@ -287,7 +292,7 @@ export function FormPlaceAutocomplete({
287
292
 
288
293
  return (
289
294
  <div className="flex w-full flex-col" ref={containerRef}>
290
- <FormFieldWrapper form={form} name={id} label={name} isRequired={isRequired}>
295
+ <FormFieldWrapper form={form} name={id} label={name} description={description} isRequired={isRequired}>
291
296
  {(field) => (
292
297
  <div className="relative">
293
298
  <Input
@@ -9,6 +9,7 @@ export function FormSelect({
9
9
  form,
10
10
  id,
11
11
  name,
12
+ description,
12
13
  placeholder,
13
14
  disabled,
14
15
  values,
@@ -21,6 +22,7 @@ export function FormSelect({
21
22
  form: any;
22
23
  id: string;
23
24
  name?: string;
25
+ description?: string;
24
26
  placeholder?: string;
25
27
  disabled?: boolean;
26
28
  values: { id: string; text: string }[];
@@ -36,6 +38,7 @@ export function FormSelect({
36
38
  form={form}
37
39
  name={id}
38
40
  label={name}
41
+ description={description}
39
42
  isRequired={isRequired}
40
43
  orientation={useRows ? "horizontal" : "vertical"}
41
44
  testId={testId}
@@ -10,6 +10,7 @@ export function FormSlider({
10
10
  name,
11
11
  disabled,
12
12
  showPercentage,
13
+ description,
13
14
  }: {
14
15
  form: any;
15
16
  id: string;
@@ -17,12 +18,13 @@ export function FormSlider({
17
18
  placeholder?: string;
18
19
  disabled?: boolean;
19
20
  showPercentage?: boolean;
21
+ description?: string;
20
22
  }) {
21
23
  const value = useWatch({ control: form.control, name: id });
22
24
 
23
25
  return (
24
26
  <div className="flex w-full flex-col">
25
- <FormFieldWrapper form={form} name={id} label={name}>
27
+ <FormFieldWrapper form={form} name={id} label={name} description={description}>
26
28
  {() => (
27
29
  <div>
28
30
  {showPercentage && (
@@ -11,6 +11,9 @@ export function FormTextarea({
11
11
  className,
12
12
  placeholder,
13
13
  testId,
14
+ description,
15
+ isRequired,
16
+ textareaRef,
14
17
  }: {
15
18
  form: any;
16
19
  id: string;
@@ -18,13 +21,24 @@ export function FormTextarea({
18
21
  placeholder?: string;
19
22
  className?: string;
20
23
  testId?: string;
24
+ description?: string;
25
+ isRequired?: boolean;
26
+ textareaRef?: React.RefObject<HTMLTextAreaElement>;
21
27
  }) {
22
28
  return (
23
29
  <div className="flex w-full flex-col">
24
- <FormFieldWrapper form={form} name={id} label={name} testId={testId}>
30
+ <FormFieldWrapper
31
+ form={form}
32
+ name={id}
33
+ label={name}
34
+ description={description}
35
+ isRequired={isRequired}
36
+ testId={testId}
37
+ >
25
38
  {(field) => (
26
39
  <Textarea
27
40
  {...field}
41
+ ref={textareaRef}
28
42
  className={cn("min-h-96 w-full", className)}
29
43
  disabled={form.formState.isSubmitting}
30
44
  placeholder={placeholder}
@@ -45,7 +45,13 @@ function BreadcrumbDesktop({
45
45
  {items.length > ITEMS_TO_DISPLAY ? (
46
46
  <>
47
47
  <BreadcrumbItem>
48
- {items[0].href ? <Link href={items[0].href}>{items[0].name}</Link> : <>{items[0].name}</>}
48
+ {items[0].href ? (
49
+ <Link href={items[0].href} onClick={items[0].onClick}>
50
+ {items[0].name}
51
+ </Link>
52
+ ) : (
53
+ <>{items[0].name}</>
54
+ )}
49
55
  </BreadcrumbItem>
50
56
  <BreadcrumbSeparator />
51
57
  <BreadcrumbItem>
@@ -56,7 +62,9 @@ function BreadcrumbDesktop({
56
62
  <DropdownMenuContent align="start">
57
63
  {items.slice(1, -ITEMS_TO_DISPLAY + 2).map((item, index) => (
58
64
  <DropdownMenuItem key={index}>
59
- <Link href={item.href ? item.href : "#"}>{item.name}</Link>
65
+ <Link href={item.href ? item.href : "#"} onClick={item.onClick}>
66
+ {item.name}
67
+ </Link>
60
68
  </DropdownMenuItem>
61
69
  ))}
62
70
  </DropdownMenuContent>
@@ -66,7 +74,13 @@ function BreadcrumbDesktop({
66
74
  {items.slice(-ITEMS_TO_DISPLAY + 2).map((item, index) => (
67
75
  <Fragment key={index}>
68
76
  <BreadcrumbItem>
69
- {item.href ? <Link href={item.href}>{item.name}</Link> : <>{item.name}</>}
77
+ {item.href ? (
78
+ <Link href={item.href} onClick={item.onClick}>
79
+ {item.name}
80
+ </Link>
81
+ ) : (
82
+ <>{item.name}</>
83
+ )}
70
84
  </BreadcrumbItem>
71
85
  {index < items.slice(-ITEMS_TO_DISPLAY + 2).length - 1 && <BreadcrumbSeparator />}
72
86
  </Fragment>
@@ -77,7 +91,13 @@ function BreadcrumbDesktop({
77
91
  {items.map((item, index) => (
78
92
  <Fragment key={index}>
79
93
  <BreadcrumbItem>
80
- {item.href ? <Link href={item.href}>{item.name}</Link> : <>{item.name}</>}
94
+ {item.href ? (
95
+ <Link href={item.href} onClick={item.onClick}>
96
+ {item.name}
97
+ </Link>
98
+ ) : (
99
+ <>{item.name}</>
100
+ )}
81
101
  </BreadcrumbItem>
82
102
  {index < items.length - 1 && <BreadcrumbSeparator />}
83
103
  </Fragment>
@@ -124,7 +144,13 @@ function BreadcrumbMobile({
124
144
  <DropdownMenuContent align="start">
125
145
  {allItems.map((item, index) => (
126
146
  <DropdownMenuItem key={index}>
127
- {item.href ? <Link href={item.href}>{item.name}</Link> : <>{item.name}</>}
147
+ {item.href ? (
148
+ <Link href={item.href} onClick={item.onClick}>
149
+ {item.name}
150
+ </Link>
151
+ ) : (
152
+ <>{item.name}</>
153
+ )}
128
154
  </DropdownMenuItem>
129
155
  ))}
130
156
  </DropdownMenuContent>
@@ -114,7 +114,18 @@ export function useSocket({ token }: UseSocketOptions): UseSocketReturn {
114
114
  };
115
115
 
116
116
  const handleNotification = (data: any) => {
117
- const notification = rehydrate(Modules.Notification, data) as NotificationInterface;
117
+ // The socket sends a JSON:API document { data, included }; rehydrate expects
118
+ // { jsonApi, included }. Map it (and guard against an unexpected shape so a
119
+ // malformed payload never throws an uncaught error that breaks the socket).
120
+ const resource = data?.data ?? data?.jsonApi;
121
+ if (!resource?.type) {
122
+ console.warn("[useSocket] ignoring notification with unexpected payload shape", data);
123
+ return;
124
+ }
125
+ const notification = rehydrate(Modules.Notification, {
126
+ jsonApi: resource,
127
+ included: data?.included || [],
128
+ }) as NotificationInterface;
118
129
  if (notification) {
119
130
  setSocketNotifications((prev) => {
120
131
  const newNotifications = [...prev, notification];
@@ -1,4 +1,5 @@
1
1
  export type BreadcrumbItemData = {
2
2
  name: string;
3
3
  href?: string;
4
+ onClick?: () => void;
4
5
  };
@@ -17,6 +17,18 @@ const badgeVariants = cva(
17
17
  "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30",
18
18
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
19
19
  link: "text-primary underline-offset-4 hover:underline",
20
+ warning: "bg-warning text-warning-foreground [a]:hover:bg-warning/80",
21
+ blue: "bg-sky-500 text-primary-foreground [a]:hover:bg-sky-500/80",
22
+ green: "bg-emerald-500 text-primary-foreground [a]:hover:bg-emerald-500/80",
23
+ red: "bg-red-500 text-primary-foreground [a]:hover:bg-red-500/80",
24
+ yellow: "bg-yellow-500 text-primary-foreground [a]:hover:bg-yellow-500/80",
25
+ purple: "bg-purple-500 text-primary-foreground [a]:hover:bg-purple-500/80",
26
+ pink: "bg-pink-500 text-primary-foreground [a]:hover:bg-pink-500/80",
27
+ gray: "bg-gray-500 text-primary-foreground [a]:hover:bg-gray-500/80",
28
+ orange: "bg-orange-500 text-primary-foreground [a]:hover:bg-orange-500/80",
29
+ teal: "bg-teal-500 text-primary-foreground [a]:hover:bg-teal-500/80",
30
+ lime: "bg-lime-500 text-primary-foreground [a]:hover:bg-lime-500/80",
31
+ none: "border-input bg-transparent text-muted-foreground min-w-20 min-h-5",
20
32
  },
21
33
  },
22
34
  defaultVariants: {
@@ -47,4 +59,7 @@ function Badge({
47
59
  });
48
60
  }
49
61
 
62
+ type BadgeProps = useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>;
63
+
50
64
  export { Badge, badgeVariants };
65
+ export type { BadgeProps };