@adula/ui 0.2.0-alpha.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.
- package/LICENSE +21 -0
- package/LICENSE.shadcn.md +21 -0
- package/README.md +35 -0
- package/UPSTREAM.json +152 -0
- package/build/alert.json +18 -0
- package/build/badge.json +19 -0
- package/build/button.json +19 -0
- package/build/calendar-date.json +15 -0
- package/build/calendar.json +22 -0
- package/build/can.json +15 -0
- package/build/card.json +17 -0
- package/build/checkbox.json +18 -0
- package/build/command.json +21 -0
- package/build/confirm-action.json +18 -0
- package/build/data-table.json +29 -0
- package/build/dialog.json +21 -0
- package/build/dropdown-menu.json +18 -0
- package/build/form.json +25 -0
- package/build/hijri-calendar.json +19 -0
- package/build/input.json +17 -0
- package/build/label.json +18 -0
- package/build/manifest.json +214 -0
- package/build/pagination.json +20 -0
- package/build/popover.json +18 -0
- package/build/radio-group.json +18 -0
- package/build/resource-actions.json +22 -0
- package/build/resource-field.json +33 -0
- package/build/resource-form.json +26 -0
- package/build/resource-page.json +23 -0
- package/build/resource-show.json +25 -0
- package/build/resource-surface.json +18 -0
- package/build/resource-value.json +21 -0
- package/build/select.json +18 -0
- package/build/separator.json +18 -0
- package/build/sheet.json +18 -0
- package/build/skeleton.json +17 -0
- package/build/sonner.json +17 -0
- package/build/switch.json +18 -0
- package/build/table.json +17 -0
- package/build/tabs.json +19 -0
- package/build/textarea.json +17 -0
- package/build/theme.css +101 -0
- package/build/tooltip.json +19 -0
- package/build/ui-preferences.json +15 -0
- package/package.json +47 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dialog",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
{
|
|
10
|
+
"path": "registry/ui/dialog.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from 'cn'\nimport { XIcon } from 'lucide-react'\nimport { Dialog as DialogPrimitive } from 'radix-ui'\n\nimport { Button } from '~/components/ui/button'\nimport { useUiPreferences } from '~/components/ui/ui-preferences'\n\nconst DialogContentContext = React.createContext<React.RefObject<HTMLDivElement | null> | null>(\n null\n)\n\nfunction Dialog({\n mode,\n onOpenChange,\n children,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root> & { mode?: 'view' | 'edit' | 'confirm' }) {\n const content = React.useRef<HTMLDivElement>(null)\n const [confirming, setConfirming] = React.useState(false)\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(props.defaultOpen ?? false)\n const preferences = useUiPreferences()\n const open = props.open ?? uncontrolledOpen\n const commit = (next: boolean) => {\n setUncontrolledOpen(next)\n onOpenChange?.(next)\n }\n const request = (next: boolean) => {\n const editing = mode === 'edit' || (!mode && Boolean(content.current?.querySelector('form')))\n if (!next && editing && preferences.confirmDialogClose) setConfirming(true)\n else commit(next)\n }\n return (\n <DialogContentContext.Provider value={content}>\n <DialogPrimitive.Root data-slot=\"dialog\" {...props} open={open} onOpenChange={request}>\n {children}\n </DialogPrimitive.Root>\n {confirming && open && (\n <Dialog mode=\"confirm\" open onOpenChange={(next) => !next && setConfirming(false)}>\n <DialogContent showCloseButton={false} className=\"z-[70] sm:max-w-md\" dir=\"rtl\">\n <DialogHeader>\n <DialogTitle>إغلاق النموذج؟</DialogTitle>\n <DialogDescription>\n قد تفقد التغييرات التي لم تحفظها. يمكنك متابعة التحرير أو إغلاق النموذج.\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setConfirming(false)}>\n متابعة التحرير\n </Button>\n <Button\n type=\"button\"\n variant=\"destructive\"\n onClick={() => {\n setConfirming(false)\n commit(false)\n }}\n >\n إغلاق دون حفظ\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )}\n </DialogContentContext.Provider>\n )\n}\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n 'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DialogContent({\n ref,\n className,\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean\n}) {\n const content = React.useContext(DialogContentContext)\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={(node) => {\n if (content) content.current = node\n if (typeof ref === 'function') return ref(node)\n if (ref) ref.current = node\n }}\n data-slot=\"dialog-content\"\n className={cn(\n 'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',\n className\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"absolute top-4 end-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n >\n <XIcon />\n <span className=\"sr-only\">إغلاق</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn('flex flex-col gap-2 text-center sm:text-start', className)}\n {...props}\n />\n )\n}\n\nfunction DialogFooter({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.ComponentProps<'div'> & {\n showCloseButton?: boolean\n}) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close asChild>\n <Button variant=\"outline\">إغلاق</Button>\n </DialogPrimitive.Close>\n )}\n </div>\n )\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn('text-lg leading-none font-semibold', className)}\n {...props}\n />\n )\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n )\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n}\n",
|
|
13
|
+
"target": "inertia/components/ui/dialog.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"registryDependencies": [
|
|
18
|
+
"ui-preferences"
|
|
19
|
+
],
|
|
20
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dropdown-menu",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
{
|
|
10
|
+
"path": "registry/ui/dropdown-menu.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\"\n\nfunction DropdownMenu({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return <DropdownMenuPrimitive.Root dir=\"rtl\" data-slot=\"dropdown-menu\" {...props} />\n}\n\nfunction DropdownMenuPortal({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n return (\n <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n )\n}\n\nfunction DropdownMenuTrigger({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n return (\n <DropdownMenuPrimitive.Trigger\n data-slot=\"dropdown-menu-trigger\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuContent({\n className,\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(\n \"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n )\n}\n\nfunction DropdownMenuGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n return (\n <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n )\n}\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute start-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n )\n}\n\nfunction DropdownMenuRadioGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n return (\n <DropdownMenuPrimitive.RadioGroup\n data-slot=\"dropdown-menu-radio-group\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute start-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n )\n}\n\nfunction DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n data-inset={inset}\n className={cn(\n \"px-2 py-1.5 text-sm font-medium data-[inset]:ps-8\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"ms-auto text-xs tracking-widest text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSub({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:ps-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ms-auto size-4 rtl:rotate-180\" />\n </DropdownMenuPrimitive.SubTrigger>\n )\n}\n\nfunction DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\n \"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n DropdownMenu,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubTrigger,\n DropdownMenuSubContent,\n}\n",
|
|
13
|
+
"target": "inertia/components/ui/dropdown-menu.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
package/build/form.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "form",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"@hookform/resolvers@5.9.1",
|
|
7
|
+
"zod@4.6.5",
|
|
8
|
+
"react-hook-form@7.88.0",
|
|
9
|
+
"lucide-react@1.47.0"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"button",
|
|
13
|
+
"label"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "registry/ui/form.tsx",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport type { Label as LabelPrimitive } from \"radix-ui\"\nimport { Slot } from \"radix-ui\"\nimport {\n Controller,\n FormProvider,\n useFormContext,\n useFormState,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\"\n\nimport { Label } from \"~/components/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n {} as FormFieldContextValue\n)\n\nconst FormField = <\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n ...props\n}: ControllerProps<TFieldValues, TName>) => {\n return (\n <FormFieldContext.Provider value={{ name: props.name }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n )\n}\n\nconst useFormField = () => {\n const fieldContext = React.useContext(FormFieldContext)\n const itemContext = React.useContext(FormItemContext)\n const { getFieldState } = useFormContext()\n const formState = useFormState({ name: fieldContext.name })\n const fieldState = getFieldState(fieldContext.name, formState)\n\n if (!fieldContext) {\n throw new Error(\"useFormField should be used within <FormField>\")\n }\n\n const { id } = itemContext\n\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-form-item`,\n formDescriptionId: `${id}-form-item-description`,\n formMessageId: `${id}-form-item-message`,\n ...fieldState,\n }\n}\n\ntype FormItemContextValue = {\n id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n {} as FormItemContextValue\n)\n\nfunction FormItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const id = React.useId()\n\n return (\n <FormItemContext.Provider value={{ id }}>\n <div\n data-slot=\"form-item\"\n className={cn(\"grid gap-2\", className)}\n {...props}\n />\n </FormItemContext.Provider>\n )\n}\n\nfunction FormLabel({\n className,\n ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n const { error, formItemId } = useFormField()\n\n return (\n <Label\n data-slot=\"form-label\"\n data-error={!!error}\n className={cn(\"data-[error=true]:text-destructive\", className)}\n htmlFor={formItemId}\n {...props}\n />\n )\n}\n\nfunction FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {\n const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n return (\n <Slot.Root\n data-slot=\"form-control\"\n id={formItemId}\n aria-describedby={\n !error\n ? `${formDescriptionId}`\n : `${formDescriptionId} ${formMessageId}`\n }\n aria-invalid={!!error}\n {...props}\n />\n )\n}\n\nfunction FormDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n const { formDescriptionId } = useFormField()\n\n return (\n <p\n data-slot=\"form-description\"\n id={formDescriptionId}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction FormMessage({ className, ...props }: React.ComponentProps<\"p\">) {\n const { error, formMessageId } = useFormField()\n const body = error ? String(error?.message ?? \"\") : props.children\n\n if (!body) {\n return null\n }\n\n return (\n <p\n data-slot=\"form-message\"\n id={formMessageId}\n className={cn(\"text-sm text-destructive\", className)}\n {...props}\n >\n {body}\n </p>\n )\n}\n\nexport {\n useFormField,\n Form,\n FormItem,\n FormLabel,\n FormControl,\n FormDescription,\n FormMessage,\n FormField,\n}\n",
|
|
20
|
+
"target": "inertia/components/ui/form.tsx"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"type": "registry:ui",
|
|
24
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hijri-calendar",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"select",
|
|
7
|
+
"calendar-date"
|
|
8
|
+
],
|
|
9
|
+
"dependencies": [],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "registry/ui/hijri-calendar.tsx",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"content": "import { useState } from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport { Button } from '~/components/ui/button'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/components/ui/select'\nimport { calendarInput, calendarIso, calendarDisplay } from '~/components/ui/calendar_date'\n\nconst months = [\n 'محرم',\n 'صفر',\n 'ربيع الأول',\n 'ربيع الآخر',\n 'جمادى الأولى',\n 'جمادى الآخرة',\n 'رجب',\n 'شعبان',\n 'رمضان',\n 'شوال',\n 'ذو القعدة',\n 'ذو الحجة',\n]\nconst pad = (value: number) => String(value).padStart(2, '0')\n\nexport function HijriCalendar({\n value,\n onSelect,\n}: {\n value: string\n onSelect: (iso: string) => void\n}) {\n const initial = calendarInput(value || new Date().toISOString().slice(0, 10), 'islamic-umalqura')\n const [year, setYear] = useState(Math.max(1300, Math.min(1600, Number(initial.slice(0, 4)))))\n const [month, setMonth] = useState(Number(initial.slice(5, 7)))\n const first = calendarIso(`${year}-${pad(month)}-01`, 'islamic-umalqura')\n const offset = new Date(`${first}T00:00:00Z`).getUTCDay()\n let days = 30\n try {\n calendarIso(`${year}-${pad(month)}-30`, 'islamic-umalqura')\n } catch {\n days = 29\n }\n const move = (delta: number) => {\n const next = year * 12 + month - 1 + delta\n setYear(Math.floor(next / 12))\n setMonth((next % 12) + 1)\n }\n return (\n <div dir=\"rtl\" className=\"w-80 space-y-3 p-3\" aria-label=\"تقويم أم القرى\">\n <div className=\"flex items-center gap-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"الشهر السابق\"\n disabled={year === 1300 && month === 1}\n onClick={() => move(-1)}\n >\n <ChevronRight />\n </Button>\n <Select value={String(month)} onValueChange={(value) => setMonth(Number(value))}>\n <SelectTrigger aria-label=\"الشهر الهجري\" className=\"flex-1\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {months.map((label, index) => (\n <SelectItem key={label} value={String(index + 1)}>\n {label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select value={String(year)} onValueChange={(value) => setYear(Number(value))}>\n <SelectTrigger aria-label=\"السنة الهجرية\" className=\"w-24\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {Array.from({ length: 301 }, (_, index) => (\n <SelectItem key={index} value={String(1300 + index)}>\n {1300 + index} هـ\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"الشهر التالي\"\n disabled={year === 1600 && month === 12}\n onClick={() => move(1)}\n >\n <ChevronLeft />\n </Button>\n </div>\n <div\n className=\"grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground\"\n aria-hidden=\"true\"\n >\n {['ح', 'ن', 'ث', 'ر', 'خ', 'ج', 'س'].map((day) => (\n <span key={day}>{day}</span>\n ))}\n </div>\n <div\n className=\"grid grid-cols-7 gap-1\"\n role=\"group\"\n aria-label={`${months[month - 1]} ${year} هـ`}\n >\n {Array.from({ length: offset }, (_, index) => (\n <span key={`empty-${index}`} />\n ))}\n {Array.from({ length: days }, (_, index) => {\n const iso = new Date(Date.parse(`${first}T00:00:00Z`) + index * 86400000)\n .toISOString()\n .slice(0, 10)\n return (\n <Button\n key={iso}\n type=\"button\"\n size=\"icon-sm\"\n variant={iso === value ? 'default' : 'ghost'}\n aria-pressed={iso === value}\n aria-label={calendarDisplay(iso, 'islamic-umalqura')}\n onClick={() => onSelect(iso)}\n >\n {index + 1}\n </Button>\n )\n })}\n </div>\n <p className=\"text-center text-xs text-muted-foreground\">\n تقويم أم القرى · يُحفظ التاريخ موحدًا\n </p>\n </div>\n )\n}\n",
|
|
15
|
+
"target": "inertia/components/ui/hijri-calendar.tsx"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
19
|
+
}
|
package/build/input.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "input",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "registry/ui/input.tsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n return (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n \"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Input }\n",
|
|
12
|
+
"target": "inertia/components/ui/input.tsx"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
17
|
+
}
|
package/build/label.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "label",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
{
|
|
10
|
+
"path": "registry/ui/label.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { Label as LabelPrimitive } from \"radix-ui\"\n\nfunction Label({\n className,\n ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n return (\n <LabelPrimitive.Root\n data-slot=\"label\"\n className={cn(\n \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Label }\n",
|
|
13
|
+
"target": "inertia/components/ui/label.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0-alpha.1",
|
|
3
|
+
"kitCompatibility": "0.2",
|
|
4
|
+
"items": [
|
|
5
|
+
{
|
|
6
|
+
"name": "button",
|
|
7
|
+
"version": "0.2.0-alpha.1",
|
|
8
|
+
"hash": "cf1a045e41f5029fe9a8e3f3513a142dbe6550d6ffbe560b652a0aed1bb76f94"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "input",
|
|
12
|
+
"version": "0.2.0-alpha.1",
|
|
13
|
+
"hash": "5a398efe048aff90155d858e63290dee2b7ad31c060936ac5bfce5ecf691e022"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "textarea",
|
|
17
|
+
"version": "0.2.0-alpha.1",
|
|
18
|
+
"hash": "f0f615a9112e64ccb0d580d9325558c97750c24bcce933211acd2fa01c24b008"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "select",
|
|
22
|
+
"version": "0.2.0-alpha.1",
|
|
23
|
+
"hash": "25524a9856101efd64d4fa9922cd3c3ab36da05d84d75f06aec102f9fbc3b554"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "checkbox",
|
|
27
|
+
"version": "0.2.0-alpha.1",
|
|
28
|
+
"hash": "631d1bacffcb6856a210bb2a40301f589cd0a74dfe28363e6fe3393e6c658ffa"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "switch",
|
|
32
|
+
"version": "0.2.0-alpha.1",
|
|
33
|
+
"hash": "f41dc7b8d0b3d36a5b74460b651f20046c70a825321ea5ba8e32b575e87e9fb4"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "radio-group",
|
|
37
|
+
"version": "0.2.0-alpha.1",
|
|
38
|
+
"hash": "2f6e9ad7d314703388e20cbea51d3f3bae35e0aedb239fe7e58f71dc892110e0"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "table",
|
|
42
|
+
"version": "0.2.0-alpha.1",
|
|
43
|
+
"hash": "2a76f50a6672357e167a2ebc1ea82535c9298629d94f5aa9ee57108e5c535f66"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "form",
|
|
47
|
+
"version": "0.2.0-alpha.1",
|
|
48
|
+
"hash": "f5eef1e58a67cb703e559d7a3928fc993ffdd770abdfbeaee1f93f3685ff5265"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "label",
|
|
52
|
+
"version": "0.2.0-alpha.1",
|
|
53
|
+
"hash": "adb3d62721dfe74a6e812cee5ac8b2a0f5ad8821140a0d017e4bdabd2fd7b966"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "dialog",
|
|
57
|
+
"version": "0.2.0-alpha.1",
|
|
58
|
+
"hash": "04b47b3d9de9d4998eb610a6753c115cf3aa5db88e6889caac337f2d95f708bc"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "sheet",
|
|
62
|
+
"version": "0.2.0-alpha.1",
|
|
63
|
+
"hash": "8b511eba90dbab3ea69faafa384e6b27d5aa1ba4742b47dc526238b2babcf97a"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "dropdown-menu",
|
|
67
|
+
"version": "0.2.0-alpha.1",
|
|
68
|
+
"hash": "622b2ba53ab5d9ebf28de16078a0051adb5a1035d63f5febeb3992fc1c094ff9"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "popover",
|
|
72
|
+
"version": "0.2.0-alpha.1",
|
|
73
|
+
"hash": "288f01bbff64e03473dfb7c0b680f0ca77d610f646653b29364ae245d8491e03"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "command",
|
|
77
|
+
"version": "0.2.0-alpha.1",
|
|
78
|
+
"hash": "47fc8607025e87f3a35f974a55a946d9fee2d0eebd9a681276028e01731d1845"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "calendar",
|
|
82
|
+
"version": "0.2.0-alpha.1",
|
|
83
|
+
"hash": "a536d790c69a67daf9aad8e76901dd668e02520d38677a3f27e1eadcf6a90ad7"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "badge",
|
|
87
|
+
"version": "0.2.0-alpha.1",
|
|
88
|
+
"hash": "08a668f461549c6edf7da629899d59373e7cae5686d1533fd5d451fbf7c2e33e"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "card",
|
|
92
|
+
"version": "0.2.0-alpha.1",
|
|
93
|
+
"hash": "0c7ac44916cc64285da5d8734fd34896a72cabecada2119b68f7f04ac6995b9a"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "tabs",
|
|
97
|
+
"version": "0.2.0-alpha.1",
|
|
98
|
+
"hash": "ceac90f0124b6c52d5f0268226fc36ec1689feb8540664414158aa8fdebac0f7"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "sonner",
|
|
102
|
+
"version": "0.2.0-alpha.1",
|
|
103
|
+
"hash": "433f224e2266ddfa0f42fe7f81a87a55cb4e6284dce8187b97256154409fe10d"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "skeleton",
|
|
107
|
+
"version": "0.2.0-alpha.1",
|
|
108
|
+
"hash": "8dd2b789d3a4b70fb62a99922ef48ee7a0d23a7cda09b00cc1df49c8c29fac90"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "pagination",
|
|
112
|
+
"version": "0.2.0-alpha.1",
|
|
113
|
+
"hash": "d1119e2d1e6e80c2556b510c715370c4b9e039e8851461aa28e71c3c1d9d1329"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "tooltip",
|
|
117
|
+
"version": "0.2.0-alpha.1",
|
|
118
|
+
"hash": "616d81f4386a0fb6b714e74515fd77328db0314ddb573003adfaf4693f100fe6"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "separator",
|
|
122
|
+
"version": "0.2.0-alpha.1",
|
|
123
|
+
"hash": "deb8eb9daf72092fd11d69af65891b721f154fd1bb8d3c7fabb46f33cf612c87"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "alert",
|
|
127
|
+
"version": "0.2.0-alpha.1",
|
|
128
|
+
"hash": "17940923e71122bb30d830c569e36e3886109cbfdfb405fd3954774fed1f0ead"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "can",
|
|
132
|
+
"version": "0.2.0-alpha.1",
|
|
133
|
+
"hash": "3c88f2d7fe666e9ba919776177e5aecb388ff5cb77ecb5726ebde7d07d82007b"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "resource-value",
|
|
137
|
+
"version": "0.2.0-alpha.1",
|
|
138
|
+
"hash": "a173edb91f7f8244dbe262647cf3c1aa874cdf7f61887643a846ef86a2bfa018"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "resource-field",
|
|
142
|
+
"version": "0.2.0-alpha.1",
|
|
143
|
+
"hash": "292f0b43e5f6c53c763801c9fb6f150ccb8b3943a18b4e0ae5425bfc359e32cd"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "data-table",
|
|
147
|
+
"version": "0.2.0-alpha.1",
|
|
148
|
+
"hash": "0eea3ca16eeb860882fe7bc0fbfc7afd44b6bd14ca9e2c99cd6a80792220d1af"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "resource-form",
|
|
152
|
+
"version": "0.2.0-alpha.1",
|
|
153
|
+
"hash": "24821f2e5d6904f7e0d8fb30ef3cfb5a543801643d3c0bceeeb25d8642d92ded"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "resource-show",
|
|
157
|
+
"version": "0.2.0-alpha.1",
|
|
158
|
+
"hash": "7dafc213513d76d761319d59f69a439dbd236bb6e4a27d7286612712a753981c"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "resource-page",
|
|
162
|
+
"version": "0.2.0-alpha.1",
|
|
163
|
+
"hash": "b1bb4683f142fe4c470eb0b3aa8abc32c1c058809aba020b92b4c6df8e4bf9e7"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "resource-actions",
|
|
167
|
+
"version": "0.2.0-alpha.1",
|
|
168
|
+
"hash": "4e6dba96e17091a964d375bf96ddc1f219846502efb0b96c35d4cbc7fe8b03e6"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "resource-surface",
|
|
172
|
+
"version": "0.2.0-alpha.1",
|
|
173
|
+
"hash": "c8f783890cdb918ba326dd07ce845a81931b111b4341809e54dbb042c219edef"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "ui-preferences",
|
|
177
|
+
"version": "0.2.0-alpha.1",
|
|
178
|
+
"hash": "999e93b4c4814eef4528dca532c76504060f524f6cc00de2a8f967291855ff03"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "calendar-date",
|
|
182
|
+
"version": "0.2.0-alpha.1",
|
|
183
|
+
"hash": "d74998096825b361135b75e6d90d5f3951732bf90919b33ecf15bfc015c15a58"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "hijri-calendar",
|
|
187
|
+
"version": "0.2.0-alpha.1",
|
|
188
|
+
"hash": "39ca5de9c1df32e65e434241f3cbbae629463c9f21da93205274c177fc8b85f3"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "confirm-action",
|
|
192
|
+
"version": "0.2.0-alpha.1",
|
|
193
|
+
"hash": "7710115860f595324ab94f752abb1de09b58acc9256aadf56ea0113b3dd4bb09"
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
"dependencies": {
|
|
197
|
+
"cn": "0.3.0",
|
|
198
|
+
"class-variance-authority": "0.7.1",
|
|
199
|
+
"lucide-react": "1.47.0",
|
|
200
|
+
"radix-ui": "1.6.7",
|
|
201
|
+
"react-hook-form": "7.88.0",
|
|
202
|
+
"zod": "4.6.5",
|
|
203
|
+
"cmdk": "1.1.1",
|
|
204
|
+
"@hookform/resolvers": "5.9.1",
|
|
205
|
+
"react-day-picker": "10.0.1",
|
|
206
|
+
"sonner": "2.0.8",
|
|
207
|
+
"date-fns": "4.4.0",
|
|
208
|
+
"tw-animate-css": "1.4.0",
|
|
209
|
+
"@fontsource/noto-sans-arabic": "5.3.0",
|
|
210
|
+
"@tanstack/react-table": "9.2.4",
|
|
211
|
+
"@tanstack/react-virtual": "3.14.13",
|
|
212
|
+
"axios": "1.19.0"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pagination",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"button"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "registry/ui/pagination.tsx",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"cn\"\nimport {\n ChevronLeftIcon,\n ChevronRightIcon,\n MoreHorizontalIcon,\n} from \"lucide-react\"\n\nimport { buttonVariants, type Button } from \"~/components/ui/button\"\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n return (\n <nav\n role=\"navigation\"\n aria-label=\"التنقل بين الصفحات\"\n data-slot=\"التنقل بين الصفحات\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n )\n}\n\nfunction PaginationContent({\n className,\n ...props\n}: React.ComponentProps<\"ul\">) {\n return (\n <ul\n data-slot=\"pagination-content\"\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n return <li data-slot=\"pagination-item\" {...props} />\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean\n} & Pick<React.ComponentProps<typeof Button>, \"size\"> &\n React.ComponentProps<\"a\">\n\nfunction PaginationLink({\n className,\n isActive,\n size = \"icon\",\n ...props\n}: PaginationLinkProps) {\n return (\n <a\n aria-current={isActive ? \"page\" : undefined}\n data-slot=\"pagination-link\"\n data-active={isActive}\n className={cn(\n buttonVariants({\n variant: isActive ? \"outline\" : \"ghost\",\n size,\n }),\n className\n )}\n {...props}\n />\n )\n}\n\nfunction PaginationPrevious({\n className,\n ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n return (\n <PaginationLink\n aria-label=\"الصفحة السابقة\"\n size=\"default\"\n className={cn(\"gap-1 px-2.5 sm:ps-2.5\", className)}\n {...props}\n >\n <ChevronLeftIcon className=\"rtl:rotate-180\" />\n <span className=\"hidden sm:block\">السابق</span>\n </PaginationLink>\n )\n}\n\nfunction PaginationNext({\n className,\n ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n return (\n <PaginationLink\n aria-label=\"الصفحة التالية\"\n size=\"default\"\n className={cn(\"gap-1 px-2.5 sm:pe-2.5\", className)}\n {...props}\n >\n <span className=\"hidden sm:block\">التالي</span>\n <ChevronRightIcon className=\"rtl:rotate-180\" />\n </PaginationLink>\n )\n}\n\nfunction PaginationEllipsis({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n aria-hidden\n data-slot=\"pagination-ellipsis\"\n className={cn(\"flex size-9 items-center justify-center\", className)}\n {...props}\n >\n <MoreHorizontalIcon className=\"size-4\" />\n <span className=\"sr-only\">صفحات أخرى</span>\n </span>\n )\n}\n\nexport {\n Pagination,\n PaginationContent,\n PaginationLink,\n PaginationItem,\n PaginationPrevious,\n PaginationNext,\n PaginationEllipsis,\n}\n",
|
|
15
|
+
"target": "inertia/components/ui/pagination.tsx"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "popover",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
{
|
|
10
|
+
"path": "registry/ui/popover.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { Popover as PopoverPrimitive } from \"radix-ui\"\n\nfunction Popover({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n )\n}\n\nfunction PopoverAnchor({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"popover-header\"\n className={cn(\"flex flex-col gap-1 text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n return (\n <div\n data-slot=\"popover-title\"\n className={cn(\"font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction PopoverDescription({\n className,\n ...props\n}: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"popover-description\"\n className={cn(\"text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n PopoverAnchor,\n PopoverHeader,\n PopoverTitle,\n PopoverDescription,\n}\n",
|
|
13
|
+
"target": "inertia/components/ui/popover.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "radio-group",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
{
|
|
10
|
+
"path": "registry/ui/radio-group.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { CircleIcon } from \"lucide-react\"\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\"\n\nfunction RadioGroup({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return (\n <RadioGroupPrimitive.Root dir=\"rtl\"\n data-slot=\"radio-group\"\n className={cn(\"grid gap-3\", className)}\n {...props}\n />\n )\n}\n\nfunction RadioGroupItem({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(\n \"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"relative flex items-center justify-center\"\n >\n <CircleIcon className=\"absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n}\n\nexport { RadioGroup, RadioGroupItem }\n",
|
|
13
|
+
"target": "inertia/components/ui/radio-group.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-actions",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"dialog",
|
|
7
|
+
"can"
|
|
8
|
+
],
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"axios@1.19.0",
|
|
11
|
+
"sonner@2.0.8"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/ui/resource-actions.tsx",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"content": "import { useState } from 'react'\nimport { router } from '@inertiajs/react'\nimport axios from 'axios'\nimport { toast } from 'sonner'\nimport type { RecordPermissions } from '@adula/kit'\nimport { Button } from '~/components/ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '~/components/ui/dialog'\nimport { Can } from '~/components/ui/can'\n\nexport type TransitionAction = 'delete' | 'submit' | 'cancel'\nexport const transitionLabels: Record<TransitionAction, string> = {\n delete: 'حذف السجل',\n submit: 'اعتماد المستند',\n cancel: 'إلغاء الاعتماد',\n}\nconst descriptions: Record<TransitionAction, string> = {\n delete: 'سيُخفى السجل من القوائم ولن يمكن تعديله بعد الحذف.',\n submit: 'بعد الاعتماد لا يمكن تعديل المستند أو بنوده.',\n cancel: 'سيُعاد المستند إلى حالة الإلغاء ولن يُحتسب.',\n}\n\nexport function failureMessage(error: unknown, fallback: string) {\n if (!axios.isAxiosError(error)) return 'تعذر الاتصال بالخادم. حاول مجدداً.'\n if (error.response?.status === 409 && error.response.data?.error?.code === 'E_VERSION_CONFLICT')\n return 'تغير السجل منذ فتحه. أعد تحميل الصفحة قبل المتابعة.'\n return error.response?.data?.error?.message ?? fallback\n}\n\nexport function isConflict(error: unknown) {\n return axios.isAxiosError(error) && error.response?.status === 409\n}\n\n/** Version travels with every transition so a stale page cannot delete or approve a changed record. */\nexport function ResourceActions({\n resource,\n id,\n version,\n permissions,\n label,\n size = 'default',\n variant = 'outline',\n onDone,\n}: {\n resource: string\n id: number | string\n version?: unknown\n permissions: RecordPermissions\n label?: string\n size?: 'default' | 'sm'\n variant?: 'outline' | 'ghost'\n onDone?: (action: TransitionAction) => void\n}) {\n const [pending, setPending] = useState<TransitionAction | null>(null)\n const [busy, setBusy] = useState(false)\n const [failure, setFailure] = useState('')\n const [conflict, setConflict] = useState(false)\n const base = `/resources/${resource}/${id}`\n const execute = async () => {\n if (!pending) return\n setBusy(true)\n setFailure('')\n try {\n await axios.request({\n method: pending === 'delete' ? 'delete' : 'post',\n url: pending === 'delete' ? base : `${base}/${pending}`,\n data: version === undefined ? {} : { version },\n headers: { Accept: 'application/json' },\n withXSRFToken: true,\n })\n toast.success(`تم ${transitionLabels[pending]}`)\n const done = pending\n setPending(null)\n if (onDone) onDone(done)\n else if (done === 'delete') router.visit(`/resources/${resource}`)\n else router.reload()\n } catch (error) {\n setConflict(isConflict(error))\n setFailure(failureMessage(error, 'تعذر تنفيذ الإجراء. تحقق من صلاحياتك وحاول مجدداً.'))\n } finally {\n setBusy(false)\n }\n }\n return (\n <>\n {(['submit', 'cancel', 'delete'] as const).map((action) => (\n <Can key={action} permissions={permissions} action={action}>\n <Button\n type=\"button\"\n variant={variant}\n size={size}\n className={action === 'delete' ? 'text-destructive' : undefined}\n aria-label={label ? `${transitionLabels[action]} ${label}` : undefined}\n onClick={() => {\n setPending(action)\n setFailure('')\n setConflict(false)\n }}\n >\n {transitionLabels[action]}\n </Button>\n </Can>\n ))}\n <Dialog open={pending !== null} onOpenChange={(open) => !open && !busy && setPending(null)}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{pending ? transitionLabels[pending] : ''}</DialogTitle>\n <DialogDescription>\n {pending ? descriptions[pending] : ''}\n {label ? ` (${label})` : ''}\n </DialogDescription>\n </DialogHeader>\n {failure && (\n <div role=\"alert\" className=\"flex flex-wrap items-center gap-3 text-sm text-destructive\">\n {failure}\n {conflict && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => router.reload()}>\n إعادة التحميل\n </Button>\n )}\n </div>\n )}\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" disabled={busy} onClick={() => setPending(null)}>\n تراجع\n </Button>\n <Button\n type=\"button\"\n variant={pending === 'delete' ? 'destructive' : 'default'}\n disabled={busy || conflict}\n onClick={() => void execute()}\n >\n {busy ? 'جارٍ التنفيذ...' : 'تأكيد'}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n",
|
|
18
|
+
"target": "inertia/components/ui/resource-actions.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-field",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"input",
|
|
6
|
+
"textarea",
|
|
7
|
+
"button",
|
|
8
|
+
"label",
|
|
9
|
+
"select",
|
|
10
|
+
"switch",
|
|
11
|
+
"popover",
|
|
12
|
+
"calendar",
|
|
13
|
+
"command",
|
|
14
|
+
"resource-value",
|
|
15
|
+
"hijri-calendar",
|
|
16
|
+
"calendar-date",
|
|
17
|
+
"ui-preferences"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": [
|
|
20
|
+
"axios@1.19.0",
|
|
21
|
+
"date-fns@4.4.0",
|
|
22
|
+
"lucide-react@1.47.0"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/ui/resource-field.tsx",
|
|
27
|
+
"type": "registry:ui",
|
|
28
|
+
"content": "import { useEffect, useId, useRef, useState, type ComponentProps } from 'react'\nimport { useHttp } from '@inertiajs/react'\nimport axios from 'axios'\nimport { format } from 'date-fns'\nimport {\n CalendarDays,\n Check,\n ChevronsUpDown,\n Paperclip,\n Plus,\n Trash2,\n Undo2,\n Upload,\n X,\n} from 'lucide-react'\nimport type { CalendarSystem, ResourceEditor, ResourceField as Field } from '@adula/kit'\nimport { Button } from '~/components/ui/button'\nimport { Calendar } from '~/components/ui/calendar'\nimport { HijriCalendar } from '~/components/ui/hijri-calendar'\nimport { calendarInput, calendarIso } from '~/components/ui/calendar_date'\nimport { useUiPreferences } from '~/components/ui/ui-preferences'\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '~/components/ui/command'\nimport { Input } from '~/components/ui/input'\nimport { Label } from '~/components/ui/label'\nimport { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'\nimport { Switch } from '~/components/ui/switch'\nimport { Textarea } from '~/components/ui/textarea'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '~/components/ui/select'\nimport {\n CURRENCY_LABEL,\n fileSize,\n moneyDisplay,\n moneyInput,\n parseMoney,\n toLatinDigits,\n type AttachmentValue,\n type Option,\n} from '~/components/ui/resource-value'\n\nexport type RelationSource = { url: string; searchable: boolean }\nexport type InlineDefinition = ResourceEditor['inline'][string]\nexport type InlineRow = {\n key: string\n id?: number\n version?: number\n values: Record<string, string>\n deleted?: boolean\n dirty?: boolean\n}\nexport type FieldProps = {\n field: Field\n id: string\n value: string\n onChange: (value: string) => void\n disabled?: boolean\n error?: string\n options?: Option[]\n relation?: RelationSource\n attachment?: AttachmentValue | null\n /** Resource name; attachment uploads are claimed against it. */\n resource?: string\n /** Accessible name when the visible label lives elsewhere, e.g. inline rows. */\n label?: string\n}\nexport function ResourceSelect({\n value,\n onChange,\n options,\n placeholder = 'اختر قيمة',\n required,\n disabled,\n ...trigger\n}: Pick<\n ComponentProps<typeof SelectTrigger>,\n 'id' | 'aria-label' | 'aria-invalid' | 'aria-describedby'\n> & {\n value: string\n onChange: (value: string) => void\n options: Option[]\n placeholder?: string\n required?: boolean\n disabled?: boolean\n}) {\n // Encode values so an empty selection cannot collide with a business lookup key.\n return (\n <Select\n value={`value:${value}`}\n onValueChange={(next) => onChange(next.slice(6))}\n required={required}\n disabled={disabled}\n >\n <SelectTrigger {...trigger} className=\"h-10 w-full\" aria-required={required}>\n <SelectValue placeholder={placeholder} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"value:\">{placeholder}</SelectItem>\n {options\n .filter((option) => String(option.value) !== '')\n .map((option) => (\n <SelectItem key={option.value} value={`value:${option.value}`}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nexport function ResourceField(props: FieldProps) {\n const { field, id, error } = props\n return (\n <div\n className={`min-w-0 space-y-2 ${['text', 'json'].includes(field.type) ? 'md:col-span-2' : ''}`}\n >\n {/* The marker stays outside the label so the field's accessible name is the label alone. */}\n <div className=\"flex items-center gap-1\">\n <Label htmlFor={id}>{field.label.ar}</Label>\n {field.required && (\n <span className=\"text-destructive\" aria-hidden=\"true\">\n *\n </span>\n )}\n </div>\n <FieldControl {...props} />\n {error && (\n <p role=\"alert\" id={`${id}-error`} className=\"text-xs text-destructive\">\n {error}\n </p>\n )}\n </div>\n )\n}\n\nexport function FieldControl(props: FieldProps) {\n const { field, id, value, onChange, disabled, error, label } = props\n const shared = {\n id,\n disabled,\n 'required': field.required,\n 'aria-label': label,\n 'aria-invalid': error ? true : undefined,\n 'aria-describedby': error ? `${id}-error` : undefined,\n }\n switch (field.type) {\n case 'belongsTo':\n return <RelationCombobox {...props} />\n case 'lookup':\n return (\n <ResourceSelect\n {...shared}\n value={value}\n onChange={onChange}\n options={props.options ?? []}\n />\n )\n case 'boolean':\n return (\n <div className=\"flex h-10 items-center gap-3\">\n <Switch\n {...shared}\n required={undefined}\n checked={value === 'true'}\n onCheckedChange={(checked) => onChange(checked ? 'true' : 'false')}\n />\n <span className=\"text-sm text-muted-foreground\">{value === 'true' ? 'نعم' : 'لا'}</span>\n </div>\n )\n case 'attachment':\n return <AttachmentControl {...props} />\n case 'text':\n return (\n <Textarea\n {...shared}\n rows={4}\n value={value}\n onChange={(event) => onChange(event.target.value)}\n />\n )\n case 'json':\n return <JsonControl {...props} />\n case 'money':\n return <MoneyControl {...props} />\n case 'date':\n return <DateControl {...props} />\n case 'datetime':\n return <DatetimeControl {...props} />\n case 'integer':\n return (\n <Input\n {...shared}\n type=\"number\"\n step={1}\n inputMode=\"numeric\"\n dir=\"ltr\"\n className=\"h-10\"\n value={value}\n onChange={(event) => onChange(event.target.value)}\n />\n )\n default:\n return (\n <Input\n {...shared}\n type=\"text\"\n className=\"h-10\"\n value={value}\n onChange={(event) => onChange(event.target.value)}\n />\n )\n }\n}\n\nfunction MoneyControl({ id, value, onChange, disabled, error, field, label }: FieldProps) {\n let preview = ''\n try {\n if (value) preview = moneyDisplay(parseMoney(value))\n } catch {\n preview = ''\n }\n return (\n <div className=\"space-y-1\">\n <div className=\"relative\">\n <Input\n id={id}\n type=\"text\"\n inputMode=\"decimal\"\n dir=\"ltr\"\n placeholder=\"0.00\"\n className=\"h-10 pe-16 text-end tabular-nums\"\n value={value}\n disabled={disabled}\n required={field.required}\n aria-label={label}\n aria-invalid={error ? true : undefined}\n aria-describedby={error ? `${id}-error` : undefined}\n onChange={(event) => onChange(event.target.value)}\n onBlur={() => {\n try {\n if (value) onChange(moneyInput(parseMoney(value)))\n } catch {\n // Keep the typed text so the submit error can point at it.\n }\n }}\n />\n <span className=\"pointer-events-none absolute inset-y-0 end-3 flex items-center text-xs text-muted-foreground\">\n {CURRENCY_LABEL}\n </span>\n </div>\n {preview && (\n <p className=\"text-xs text-muted-foreground\" dir=\"ltr\">\n {preview} {CURRENCY_LABEL}\n </p>\n )}\n </div>\n )\n}\n\nfunction DateControl({ id, value, onChange, disabled, error, field, label }: FieldProps) {\n const { calendar: preference } = useUiPreferences()\n const [entryCalendar, setEntryCalendar] = useState<CalendarSystem>('gregory')\n const calendar = preference === 'both' ? entryCalendar : preference\n const [open, setOpen] = useState(false)\n let normalized = toLatinDigits(value)\n try {\n if (value.startsWith('hijri:')) normalized = calendarIso(value.slice(6), 'islamic-umalqura')\n } catch {\n normalized = ''\n }\n const displayed =\n calendar === 'islamic-umalqura'\n ? value.startsWith('hijri:')\n ? value.slice(6)\n : calendarInput(value, calendar)\n : normalized\n const selected = /^\\d{4}-\\d{2}-\\d{2}$/.test(normalized)\n ? new Date(`${normalized}T00:00:00`)\n : undefined\n const valid = selected !== undefined && !Number.isNaN(selected.getTime())\n return (\n <div className=\"space-y-1\">\n {preference === 'both' && (\n <div\n className=\"flex gap-1\"\n role=\"group\"\n aria-label={`تقويم الإدخال: ${label ?? field.label.ar}`}\n >\n <Button\n type=\"button\"\n size=\"sm\"\n variant={calendar === 'gregory' ? 'secondary' : 'ghost'}\n aria-pressed={calendar === 'gregory'}\n onClick={() => setEntryCalendar('gregory')}\n >\n إدخال ميلادي\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant={calendar === 'islamic-umalqura' ? 'secondary' : 'ghost'}\n aria-pressed={calendar === 'islamic-umalqura'}\n onClick={() => setEntryCalendar('islamic-umalqura')}\n >\n إدخال هجري\n </Button>\n </div>\n )}\n <div className=\"flex gap-2\">\n <Input\n id={id}\n type=\"text\"\n inputMode=\"numeric\"\n dir=\"ltr\"\n placeholder={calendar === 'islamic-umalqura' ? '1447-09-01' : '2026-01-31'}\n className=\"h-10 tabular-nums\"\n value={displayed}\n disabled={disabled}\n required={field.required}\n aria-label={label}\n aria-invalid={error ? true : undefined}\n aria-describedby={error ? `${id}-error` : undefined}\n onChange={(event) =>\n onChange(\n calendar === 'islamic-umalqura' && event.target.value\n ? `hijri:${event.target.value}`\n : event.target.value\n )\n }\n />\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-lg\"\n disabled={disabled}\n aria-label={`التقويم: ${label ?? field.label.ar}`}\n >\n <CalendarDays />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-0\" align=\"start\">\n {calendar === 'islamic-umalqura' ? (\n <HijriCalendar\n value={valid ? normalized : ''}\n onSelect={(iso) => {\n onChange(iso)\n setOpen(false)\n }}\n />\n ) : (\n <Calendar\n mode=\"single\"\n selected={valid ? selected : undefined}\n defaultMonth={valid ? selected : undefined}\n onSelect={(date) => {\n onChange(date ? format(date, 'yyyy-MM-dd') : '')\n setOpen(false)\n }}\n />\n )}\n </PopoverContent>\n </Popover>\n </div>\n {calendar === 'islamic-umalqura' && (\n <p className=\"text-xs text-muted-foreground\">\n هجري · أم القرى {valid && <span dir=\"ltr\">({normalized} ميلادي)</span>}\n </p>\n )}\n {preference === 'both' && calendar === 'gregory' && valid && (\n <p className=\"text-xs text-muted-foreground\" aria-live=\"polite\">\n الموافق هجريًا: <span dir=\"ltr\">{calendarInput(normalized, 'islamic-umalqura')}</span> هـ\n </p>\n )}\n </div>\n )\n}\n\nfunction DatetimeControl(props: FieldProps) {\n const { calendar } = useUiPreferences()\n if (calendar === 'gregory')\n return (\n <Input\n id={props.id}\n type=\"datetime-local\"\n dir=\"ltr\"\n className=\"h-10\"\n value={props.value}\n disabled={props.disabled}\n required={props.field.required}\n aria-label={props.label}\n aria-invalid={Boolean(props.error)}\n onChange={(event) => props.onChange(event.target.value)}\n />\n )\n const [date, time = '00:00'] = props.value.split('T')\n return (\n <div className=\"space-y-2\">\n <DateControl\n {...props}\n value={date}\n onChange={(next) => props.onChange(next ? `${next}T${time}` : '')}\n />\n <Input\n type=\"time\"\n dir=\"ltr\"\n aria-label={`الوقت: ${props.label ?? props.field.label.ar}`}\n value={time}\n disabled={props.disabled}\n onChange={(event) => props.onChange(`${date}T${event.target.value}`)}\n />\n </div>\n )\n}\n\nfunction JsonControl({ id, value, onChange, disabled, error, field, label }: FieldProps) {\n const [invalid, setInvalid] = useState('')\n const check = () => {\n if (!value.trim()) return setInvalid('')\n try {\n JSON.parse(value)\n setInvalid('')\n } catch {\n setInvalid('أدخل بيانات JSON صحيحة')\n }\n }\n return (\n <div className=\"space-y-2\">\n <Textarea\n id={id}\n dir=\"ltr\"\n rows={6}\n spellCheck={false}\n className=\"font-mono text-xs\"\n value={value}\n disabled={disabled}\n required={field.required}\n aria-label={label}\n aria-invalid={error || invalid ? true : undefined}\n aria-describedby={error ? `${id}-error` : undefined}\n onChange={(event) => onChange(event.target.value)}\n onBlur={check}\n />\n <div className=\"flex items-center gap-3\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled || !value.trim()}\n onClick={() => {\n try {\n onChange(JSON.stringify(JSON.parse(value), null, 2))\n setInvalid('')\n } catch {\n setInvalid('أدخل بيانات JSON صحيحة')\n }\n }}\n >\n تنسيق JSON\n </Button>\n {invalid && !error && (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {invalid}\n </p>\n )}\n </div>\n </div>\n )\n}\n\n/** Uploads on selection so the saved record only references an already-owned attachment id. */\nfunction AttachmentControl({\n id,\n value,\n onChange,\n disabled,\n error,\n field,\n label,\n attachment,\n resource,\n}: FieldProps) {\n const [current, setCurrent] = useState<AttachmentValue | null>(attachment ?? null)\n const [progress, setProgress] = useState<number | null>(null)\n const [failure, setFailure] = useState('')\n const input = useRef<HTMLInputElement>(null)\n const upload = async (file: File) => {\n setFailure('')\n setProgress(0)\n const body = new FormData()\n body.append('file', file)\n if (resource) body.append('resource', resource)\n body.append('field', field.key)\n try {\n const response = await axios.post<{ data: AttachmentValue }>('/attachments', body, {\n headers: { Accept: 'application/json' },\n withXSRFToken: true,\n onUploadProgress: (event) =>\n setProgress(event.total ? Math.round((event.loaded / event.total) * 100) : 50),\n })\n setCurrent(response.data.data)\n onChange(String(response.data.data.id))\n } catch (caught) {\n setFailure(\n axios.isAxiosError(caught)\n ? (caught.response?.data?.error?.message ?? 'تعذر رفع الملف. حاول مجدداً.')\n : 'تعذر الاتصال بالخادم.'\n )\n } finally {\n setProgress(null)\n if (input.current) input.current.value = ''\n }\n }\n const shown = value && current && String(current.id) === value ? current : null\n return (\n <div className=\"space-y-2\">\n <input\n ref={input}\n id={id}\n type=\"file\"\n className=\"sr-only\"\n disabled={disabled || progress !== null}\n aria-label={label}\n aria-describedby={error ? `${id}-error` : undefined}\n onChange={(event) => {\n const file = event.target.files?.[0]\n if (file) void upload(file)\n }}\n />\n {shown ? (\n <div className=\"flex flex-wrap items-center gap-3 rounded-md border bg-white px-3 py-2 text-sm\">\n <Paperclip size={15} className=\"text-muted-foreground\" />\n <a\n href={shown.url}\n download={shown.name}\n className=\"text-primary underline-offset-4 hover:underline\"\n >\n {shown.name}\n </a>\n <span className=\"text-xs text-muted-foreground\">{fileSize(shown.size)}</span>\n <span className=\"ms-auto flex gap-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => input.current?.click()}\n >\n <Upload size={14} />\n استبدال\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n className=\"text-destructive\"\n onClick={() => onChange('')}\n >\n <X size={14} />\n إزالة\n </Button>\n </span>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled || progress !== null}\n onClick={() => input.current?.click()}\n >\n <Upload size={15} />\n {progress === null ? 'اختيار ملف' : `جارٍ الرفع ${progress}%`}\n </Button>\n )}\n {progress !== null && (\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={progress}\n aria-label={`رفع ${label ?? field.label.ar}`}\n className=\"h-1.5 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div className=\"h-full bg-primary transition-all\" style={{ width: `${progress}%` }} />\n </div>\n )}\n {failure && (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {failure}\n </p>\n )}\n </div>\n )\n}\n\ntype OptionPage = { data: Option[]; nextCursor: string | null }\n/** Server-side search through the authorized options route; keyset \"more\" keeps large relations usable. */\nfunction RelationCombobox({\n id,\n value,\n onChange,\n disabled,\n error,\n field,\n label,\n options = [],\n relation,\n}: FieldProps) {\n const [open, setOpen] = useState(false)\n const [search, setSearch] = useState('')\n const [loaded, setLoaded] = useState<Option[]>(options)\n const [cursor, setCursor] = useState<string | null>(null)\n const [fetched, setFetched] = useState(false)\n const [failure, setFailure] = useState('')\n const timer = useRef<ReturnType<typeof setTimeout>>(undefined)\n const http = useHttp<Record<string, never>, OptionPage>({})\n const listId = useId()\n const current =\n loaded.find((option) => option.value === value) ??\n options.find((option) => option.value === value)\n const load = async (more: boolean, term = search) => {\n if (!relation) return\n setFailure('')\n const url = new URL(relation.url, window.location.origin)\n if (term && relation.searchable) url.searchParams.set('search', term)\n if (more && cursor) url.searchParams.set('cursor', cursor)\n try {\n const page = await http.get(url.pathname + url.search)\n if (!page) return\n setFetched(true)\n setCursor(page.nextCursor)\n setLoaded((previous) => {\n const keep = more ? previous : previous.filter((option) => option.value === value)\n return [\n ...new Map([...keep, ...page.data].map((option) => [option.value, option])).values(),\n ]\n })\n } catch {\n setFailure('تعذر تحميل الخيارات')\n }\n }\n useEffect(() => () => clearTimeout(timer.current), [])\n const searchable = Boolean(relation?.searchable)\n return (\n <Popover\n open={open}\n onOpenChange={(next) => {\n setOpen(next)\n if (next && relation && !fetched) void load(false, '')\n }}\n >\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n role=\"combobox\"\n variant=\"outline\"\n aria-expanded={open}\n aria-controls={listId}\n aria-label={label}\n aria-invalid={error ? true : undefined}\n aria-describedby={error ? `${id}-error` : undefined}\n disabled={disabled}\n className={`h-10 w-full justify-between font-normal ${current ? '' : 'text-muted-foreground'}`}\n >\n <span className=\"truncate\">{current?.label ?? (value ? `#${value}` : 'اختر سجلاً')}</span>\n <ChevronsUpDown className=\"opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-(--radix-popover-trigger-width) min-w-64 p-0\" align=\"start\">\n <Command shouldFilter={!searchable}>\n <CommandInput\n placeholder={searchable ? 'ابحث في السجلات المرتبطة' : 'تصفية الخيارات'}\n aria-label={`البحث في ${label ?? field.label.ar}`}\n value={search}\n onValueChange={(term) => {\n setSearch(term)\n if (!searchable) return\n clearTimeout(timer.current)\n timer.current = setTimeout(() => void load(false, term), 300)\n }}\n />\n <CommandList id={listId}>\n <CommandEmpty>\n {http.processing ? 'جارٍ البحث...' : 'لا توجد سجلات مطابقة'}\n </CommandEmpty>\n <CommandGroup>\n {value && !field.required && (\n <CommandItem\n value=\"__none__\"\n onSelect={() => {\n onChange('')\n setOpen(false)\n }}\n >\n <X />\n بدون اختيار\n </CommandItem>\n )}\n {loaded.map((option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label]}\n onSelect={() => {\n onChange(option.value)\n setOpen(false)\n }}\n >\n <Check className={option.value === value ? 'opacity-100' : 'opacity-0'} />\n {option.label}\n </CommandItem>\n ))}\n </CommandGroup>\n {cursor && (\n <div className=\"border-t p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full\"\n disabled={http.processing}\n onClick={() => void load(true)}\n >\n {http.processing ? 'جارٍ التحميل...' : 'المزيد'}\n </Button>\n </div>\n )}\n {failure && (\n <p role=\"alert\" className=\"p-2 text-xs text-destructive\">\n {failure}\n </p>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n )\n}\n\nlet rowSequence = 0\nexport function newInlineRow(fields: Field[]): InlineRow {\n rowSequence += 1\n return {\n key: `new-${rowSequence}`,\n values: Object.fromEntries(\n fields.map((field) => [field.key, field.type === 'boolean' ? 'false' : ''])\n ),\n dirty: true,\n }\n}\n\n/** Inline hasMany rows: new rows, versioned updates and explicit `_delete` markers in one editable table. */\nexport function InlineRows({\n field,\n inline,\n rows,\n onChange,\n errors = {},\n sectionError,\n disabled,\n resource,\n}: {\n field: Field\n inline: InlineDefinition\n rows: InlineRow[]\n onChange: (rows: InlineRow[]) => void\n errors?: Record<string, Record<string, string>>\n sectionError?: string\n disabled?: boolean\n resource: string\n}) {\n const update = (key: string, patch: Partial<InlineRow>) =>\n onChange(rows.map((row) => (row.key === key ? { ...row, ...patch } : row)))\n const editable = (row: InlineRow, key: string) =>\n row.id === undefined\n ? inline.createFields.includes(key)\n : Boolean(inline.permissions[String(row.id)]?.update) &&\n (inline.updateFields[String(row.id)] ?? []).includes(key)\n const live = rows.filter((row) => !row.deleted)\n return (\n <section aria-label={field.label.ar} className=\"rounded-xl border bg-white\">\n <div className=\"flex flex-wrap items-center justify-between gap-3 border-b px-7 py-4\">\n <div>\n <h2 className=\"text-sm font-semibold\">{field.label.ar}</h2>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {live.length ? `${live.length} بند` : 'لا توجد بنود بعد'}\n {inline.hasMore && ' · تظهر أول 100 بند فقط'}\n </p>\n </div>\n {inline.canCreate && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={disabled}\n onClick={() => onChange([...rows, newInlineRow(inline.fields)])}\n >\n <Plus size={15} />\n إضافة بند\n </Button>\n )}\n </div>\n {sectionError && (\n <p role=\"alert\" className=\"border-b px-7 py-3 text-sm text-destructive\">\n {sectionError}\n </p>\n )}\n {rows.length > 0 && (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead className=\"bg-muted text-xs text-muted-foreground\">\n <tr>\n <th className=\"w-12 px-4 py-3 text-start font-medium\">#</th>\n {inline.fields.map((child) => (\n <th key={child.key} className=\"px-3 py-3 text-start font-medium\">\n {child.label.ar}\n {child.required && (\n <span className=\"text-destructive\" aria-hidden=\"true\">\n {' '}\n *\n </span>\n )}\n </th>\n ))}\n <th className=\"w-24 px-4 py-3 text-start font-medium\">\n <span className=\"sr-only\">الإجراء</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => {\n const number = index + 1\n const rowErrors = errors[row.key] ?? {}\n return (\n <tr\n key={row.key}\n aria-label={`بند ${number}`}\n className={`border-b align-top last:border-0 ${row.deleted ? 'bg-destructive/5 opacity-60' : ''}`}\n >\n <td className=\"px-4 py-3 tabular-nums text-muted-foreground\">{number}</td>\n {inline.fields.map((child) => (\n <td key={child.key} className=\"min-w-40 px-3 py-2\">\n <FieldControl\n field={child}\n id={`${field.key}-${row.key}-${child.key}`}\n label={`${child.label.ar} للبند ${number}`}\n value={row.values[child.key] ?? ''}\n disabled={disabled || row.deleted || !editable(row, child.key)}\n error={rowErrors[child.key]}\n resource={resource}\n relation={\n child.type === 'belongsTo'\n ? {\n url: `/resources/${(field as { resource?: string }).resource}/options/${child.key}`,\n searchable: false,\n }\n : undefined\n }\n onChange={(value) =>\n update(row.key, {\n values: { ...row.values, [child.key]: value },\n dirty: true,\n })\n }\n />\n {rowErrors[child.key] && (\n <p role=\"alert\" className=\"mt-1 text-xs text-destructive\">\n {rowErrors[child.key]}\n </p>\n )}\n </td>\n ))}\n <td className=\"px-4 py-2\">\n {row.id === undefined ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={disabled}\n aria-label={`إزالة البند ${number}`}\n onClick={() => onChange(rows.filter((entry) => entry.key !== row.key))}\n >\n <Trash2 className=\"text-destructive\" />\n </Button>\n ) : inline.permissions[String(row.id)]?.delete ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n disabled={disabled}\n aria-label={\n row.deleted ? `التراجع عن حذف البند ${number}` : `حذف البند ${number}`\n }\n onClick={() => update(row.key, { deleted: !row.deleted })}\n >\n {row.deleted ? <Undo2 /> : <Trash2 className=\"text-destructive\" />}\n </Button>\n ) : null}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )}\n </section>\n )\n}\n",
|
|
29
|
+
"target": "inertia/components/ui/resource-field.tsx"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
33
|
+
}
|