@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,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-form",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"alert",
|
|
7
|
+
"label",
|
|
8
|
+
"resource-actions",
|
|
9
|
+
"resource-field",
|
|
10
|
+
"resource-value"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"axios@1.19.0",
|
|
14
|
+
"sonner@2.0.8",
|
|
15
|
+
"lucide-react@1.47.0"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/ui/resource-form.tsx",
|
|
20
|
+
"type": "registry:ui",
|
|
21
|
+
"content": "import { useState, type FormEvent } from 'react'\nimport { router } from '@inertiajs/react'\nimport { Link } from '@adonisjs/inertia/react'\nimport axios from 'axios'\nimport { toast } from 'sonner'\nimport { RefreshCw, Save } from 'lucide-react'\nimport type { RecordPermissions, ResourceEditor, ResourceField as Field } from '@adula/kit'\nimport { Alert, AlertDescription, AlertTitle } from '~/components/ui/alert'\nimport { Button } from '~/components/ui/button'\nimport { ResourceActions, failureMessage, isConflict } from '~/components/ui/resource-actions'\nimport {\n InlineRows,\n ResourceField,\n ResourceSelect,\n type InlineRow,\n} from '~/components/ui/resource-field'\nimport { Label } from '~/components/ui/label'\nimport { inputValue, isAttachment, parseValue } from '~/components/ui/resource-value'\n\ntype Failure = { message: string; conflict: boolean }\ntype RowErrors = Record<string, Record<string, Record<string, string>>>\ntype ServerError = { field?: string; message: string }\n\nfunction initialValue(field: Field, value: unknown) {\n if (field.type === 'boolean' && (value === null || value === undefined)) return 'false'\n return inputValue(field, value)\n}\n\nexport function ResourceForm({\n editor,\n permissions = {},\n}: {\n editor: ResourceEditor\n permissions?: RecordPermissions\n}) {\n const base = `/resources/${editor.name}`\n const scalar = editor.fields.filter((field) => field.type !== 'hasMany')\n const inlineFields = editor.fields.filter(\n (field) => field.type === 'hasMany' && field.inline && editor.inline[field.key]\n )\n const [values, setValues] = useState<Record<string, string>>(() =>\n Object.fromEntries(\n scalar.map((field) => [field.key, initialValue(field, editor.record?.[field.key])])\n )\n )\n const [rows, setRows] = useState<Record<string, InlineRow[]>>(() =>\n Object.fromEntries(\n inlineFields.map((field) => {\n const definition = editor.inline[field.key]\n return [\n field.key,\n definition.rows.map((row) => ({\n key: `row-${row.id}`,\n id: Number(row.id),\n version:\n row.version === undefined || row.version === null ? undefined : Number(row.version),\n values: Object.fromEntries(\n definition.fields.map((child) => [child.key, initialValue(child, row[child.key])])\n ),\n })),\n ]\n })\n )\n )\n const [unit, setUnit] = useState(\n String(editor.record?.orgUnitId ?? editor.orgUnits[0]?.value ?? '')\n )\n const [failure, setFailure] = useState<Failure | null>(null)\n const [errors, setErrors] = useState<Record<string, string>>({})\n const [rowErrors, setRowErrors] = useState<RowErrors>({})\n const [sectionErrors, setSectionErrors] = useState<Record<string, string>>({})\n const [busy, setBusy] = useState(false)\n\n const inlinePayload = (field: Field, collected: RowErrors) => {\n const definition = editor.inline[field.key]\n const list = rows[field.key] ?? []\n const changed = list.filter((row) =>\n row.id === undefined ? !row.deleted : Boolean(row.dirty || row.deleted)\n )\n const payload = changed.map((row) => {\n if (row.deleted) return { id: row.id, version: row.version, _delete: true }\n const allowed =\n row.id === undefined\n ? definition.createFields\n : (definition.updateFields[String(row.id)] ?? [])\n const entry: Record<string, unknown> =\n row.id === undefined ? {} : { id: row.id, version: row.version }\n for (const child of definition.fields) {\n if (!allowed.includes(child.key)) continue\n const raw = row.values[child.key] ?? ''\n if (raw === '' && !child.required && row.id === undefined) continue\n try {\n const parsed = parseValue(child, raw)\n if (parsed === null && child.required) throw new Error('هذا الحقل مطلوب')\n entry[child.key] = parsed\n } catch (error) {\n collected[field.key] ??= {}\n collected[field.key][row.key] ??= {}\n collected[field.key][row.key][child.key] = (error as Error).message\n }\n }\n return entry\n })\n return changed.length ? payload : undefined\n }\n\n const mapServerErrors = (entries: ServerError[]) => {\n const fieldErrors: Record<string, string> = {}\n const sections: Record<string, string> = {}\n const unknown: string[] = []\n for (const entry of entries) {\n const key = entry.field ?? ''\n if (scalar.some((field) => field.key === key)) {\n fieldErrors[key] = entry.message\n continue\n }\n const section = inlineFields.find((field) =>\n editor.inline[field.key].fields.some(\n (child) => child.key === key || key.endsWith(`.${child.key}`)\n )\n )\n if (section) {\n const child = editor.inline[section.key].fields.find(\n (candidate) => candidate.key === key || key.endsWith(`.${candidate.key}`)\n )\n // Nested validators report the child field only, never which row; surface it on the section.\n sections[section.key] = `${child?.label.ar ?? key}: ${entry.message}`\n continue\n }\n unknown.push(key ? `${key}: ${entry.message}` : entry.message)\n }\n setErrors(fieldErrors)\n setSectionErrors(sections)\n return unknown\n }\n\n const submit = async (event: FormEvent) => {\n event.preventDefault()\n setFailure(null)\n setErrors({})\n setRowErrors({})\n setSectionErrors({})\n const payload: Record<string, unknown> = {}\n const fieldErrors: Record<string, string> = {}\n const collected: RowErrors = {}\n for (const field of editor.fields) {\n if (field.type === 'hasMany') {\n if (!editor.inline[field.key]) continue\n const inline = inlinePayload(field, collected)\n if (inline) payload[field.key] = inline\n continue\n }\n const raw = values[field.key] ?? ''\n // Updates submit the whole form: the resource validator checks the submitted\n // document, and `version` — not a partial payload — protects concurrent edits.\n if (editor.mode === 'create' && raw === '' && !field.required) continue\n try {\n const parsed = parseValue(field, raw)\n if (parsed === null && field.required) throw new Error('هذا الحقل مطلوب')\n payload[field.key] = parsed\n } catch (error) {\n fieldErrors[field.key] = (error as Error).message\n }\n }\n if (Object.keys(fieldErrors).length || Object.keys(collected).length) {\n setErrors(fieldErrors)\n setRowErrors(collected)\n setFailure({ message: 'راجع الحقول المحددة ثم أعد المحاولة.', conflict: false })\n return\n }\n if (editor.scoped) payload.orgUnitId = Number(unit)\n if (editor.record?.version !== undefined && editor.record.version !== null)\n payload.version = editor.record.version\n setBusy(true)\n try {\n const response = await axios.request<{ data: { id: number } }>({\n method: editor.mode === 'create' ? 'post' : 'patch',\n url: base + (editor.record ? `/${editor.record.id}` : ''),\n data: payload,\n headers: { Accept: 'application/json' },\n withXSRFToken: true,\n })\n toast.success(editor.mode === 'create' ? 'تمت إضافة السجل' : 'تم حفظ التعديلات')\n router.visit(`${base}/${response.data.data.id}`)\n } catch (error) {\n const unknown =\n axios.isAxiosError(error) && Array.isArray(error.response?.data?.errors)\n ? mapServerErrors(error.response.data.errors as ServerError[])\n : []\n const conflict = isConflict(error)\n setFailure({\n conflict,\n message: conflict\n ? 'تغير السجل منذ فتحه. احتفظ بتعديلاتك ثم أعد تحميل الصفحة.'\n : unknown.length\n ? unknown.join(' · ')\n : failureMessage(error, 'تعذر الحفظ. راجع البيانات وحاول مجدداً.'),\n })\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <form onSubmit={submit} noValidate className=\"max-w-4xl space-y-6\" aria-busy={busy}>\n {failure && (\n <Alert variant=\"destructive\" role=\"alert\">\n <AlertTitle>{failure.conflict ? 'تعارض في الإصدار' : 'تعذر الحفظ'}</AlertTitle>\n <AlertDescription className=\"flex flex-wrap items-center gap-3\">\n {failure.message}\n {failure.conflict && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => router.reload()}>\n <RefreshCw size={14} />\n إعادة التحميل\n </Button>\n )}\n </AlertDescription>\n </Alert>\n )}\n <section className=\"rounded-xl border bg-white\">\n <div className=\"border-b px-7 py-5\">\n <h2 className=\"text-sm font-semibold\">تفاصيل السجل</h2>\n <p className=\"mt-2 text-xs text-muted-foreground\">\n الحقول المتاحة بحسب صلاحياتك · الحقول المعلّمة بـ{' '}\n <span className=\"text-destructive\">*</span> مطلوبة\n </p>\n </div>\n <div className=\"grid gap-6 p-7 md:grid-cols-2\">\n {editor.scoped && (\n <div className=\"min-w-0 space-y-2\">\n <Label htmlFor=\"orgUnitId\" className=\"flex items-center gap-2 text-sm font-medium\">\n الوحدة التنظيمية\n <span className=\"text-destructive\" aria-hidden=\"true\">\n *\n </span>\n </Label>\n <ResourceSelect\n id=\"orgUnitId\"\n required\n value={unit}\n disabled={busy}\n onChange={setUnit}\n options={editor.orgUnits}\n />\n </div>\n )}\n {scalar.map((field) => {\n const stored = editor.record?.[field.key]\n return (\n <ResourceField\n key={field.key}\n field={field}\n id={field.key}\n value={values[field.key] ?? ''}\n options={editor.options[field.key]}\n error={errors[field.key]}\n disabled={busy}\n resource={editor.name}\n attachment={field.type === 'attachment' && isAttachment(stored) ? stored : null}\n onChange={(value) => setValues((current) => ({ ...current, [field.key]: value }))}\n relation={\n field.type === 'belongsTo'\n ? {\n url: `${base}/options/${field.key}${editor.record ? `?id=${editor.record.id}` : ''}`,\n searchable: editor.relationSearch[field.key] ?? false,\n }\n : undefined\n }\n />\n )\n })}\n </div>\n </section>\n {inlineFields.map((field) => (\n <InlineRows\n key={field.key}\n field={field}\n inline={editor.inline[field.key]}\n rows={rows[field.key] ?? []}\n errors={rowErrors[field.key]}\n sectionError={sectionErrors[field.key]}\n disabled={busy}\n resource={editor.name}\n onChange={(next) => setRows((current) => ({ ...current, [field.key]: next }))}\n />\n ))}\n {editor.record?.version !== undefined && editor.record.version !== null && (\n <input type=\"hidden\" name=\"version\" value={String(editor.record.version)} />\n )}\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex flex-wrap gap-2\">\n {editor.record && (\n <ResourceActions\n resource={editor.name}\n id={editor.record.id as number}\n version={editor.record.version ?? undefined}\n permissions={permissions}\n onDone={(action) =>\n router.visit(action === 'delete' ? base : `${base}/${editor.record?.id}`)\n }\n />\n )}\n </div>\n <div className=\"flex gap-3\">\n <Button type=\"button\" variant=\"outline\" asChild>\n <Link href={editor.record ? `${base}/${editor.record.id}` : base}>إلغاء</Link>\n </Button>\n <Button type=\"submit\" disabled={busy}>\n <Save size={16} />\n {busy ? 'جارٍ الحفظ...' : 'حفظ السجل'}\n </Button>\n </div>\n </div>\n </form>\n )\n}\n",
|
|
22
|
+
"target": "inertia/components/ui/resource-form.tsx"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-page",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"data-table",
|
|
7
|
+
"resource-form",
|
|
8
|
+
"resource-show",
|
|
9
|
+
"resource-surface"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [
|
|
12
|
+
"lucide-react@1.47.0"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "registry/ui/resource-page.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"content": "import { Head } from '@inertiajs/react'\nimport { Link } from '@adonisjs/inertia/react'\nimport { Plus } from 'lucide-react'\nimport type {\n RecordPermissions,\n ResourceActivity,\n ResourceChildren,\n ResourceDescription,\n ResourceEditor,\n ResourceList,\n ResourceLookups,\n SavedView,\n ResourceShow as Show,\n} from '@adula/kit'\nimport { Button } from '~/components/ui/button'\nimport { DataTable } from '~/components/ui/data-table'\nimport { ResourceForm } from '~/components/ui/resource-form'\nimport { ResourceShow } from '~/components/ui/resource-show'\nimport { ResourceSurface, type ResourcePresentation } from '~/components/ui/resource-surface'\n\n/** The discriminated view stays nested: Inertia's page typing flattens top-level unions. */\nexport type ResourceView =\n | {\n mode: 'index'\n resource: ResourceDescription\n lookups: ResourceLookups\n savedViews: SavedView[]\n }\n | { mode: 'form'; editor: ResourceEditor; permissions: RecordPermissions }\n | {\n mode: 'show'\n resource: ResourceDescription\n result: Show\n lookups: ResourceLookups\n childResources: Record<string, ResourceDescription>\n }\nexport type ResourcePageProps = {\n view: ResourceView\n /** Index pages receive the list as a top-level `inertia.scroll` prop so pages merge into `result.data`. */\n result?: ResourceList\n childrenData?: ResourceChildren\n activity?: ResourceActivity\n /** Set to page only when the user requests a non-modal form/detail override. */\n presentation?: ResourcePresentation\n}\n\nexport function ResourcePage({\n view,\n result,\n childrenData,\n activity,\n presentation,\n}: ResourcePageProps) {\n const name = view.mode === 'form' ? view.editor.name : view.resource.name\n const label = view.mode === 'form' ? view.editor.label : view.resource.label\n const title =\n view.mode === 'index'\n ? label\n : view.mode === 'form'\n ? `${view.editor.mode === 'create' ? 'إضافة' : 'تعديل'} · ${label}`\n : `تفاصيل · ${label}`\n return (\n <>\n <Head title={title} />\n {view.mode === 'index' && (\n <div className=\"mb-8 flex flex-wrap items-start justify-between gap-5\">\n <div>\n <h1 className=\"text-3xl font-semibold tracking-tight\">{title}</h1>\n <p className=\"mt-3 text-sm text-muted-foreground\">\n سجلاتك ومتابعتك اليومية في مكان واحد.\n </p>\n </div>\n {view.mode === 'index' && view.resource.canCreate && (\n <Button asChild>\n <Link href={`/resources/${name}/create`}>\n <Plus size={16} />\n إضافة سجل\n </Link>\n </Button>\n )}\n </div>\n )}\n {view.mode === 'index' ? (\n result ? (\n <DataTable\n key={name}\n resource={view.resource}\n result={result}\n lookups={view.lookups}\n savedViews={view.savedViews}\n />\n ) : null\n ) : (\n <ResourceSurface\n key={`${name}:${view.mode}:${view.mode === 'form' ? (view.editor.record?.id ?? 'new') : view.result.data.id}`}\n mode={view.mode === 'form' ? 'edit' : 'view'}\n title={title}\n description={\n view.mode === 'form'\n ? 'أكمل الحقول المطلوبة ثم احفظ السجل.'\n : 'راجع التفاصيل المتاحة لك وأكمل عملك.'\n }\n backHref={`/resources/${name}`}\n presentation={presentation}\n >\n {view.mode === 'form' ? (\n <ResourceForm\n key={`${name}:${view.editor.record?.id ?? 'new'}`}\n editor={view.editor}\n permissions={view.permissions}\n />\n ) : (\n <ResourceShow\n key={`${name}:${view.result.data.id}`}\n resource={view.resource}\n result={view.result}\n lookups={view.lookups}\n childResources={view.childResources}\n childrenData={childrenData}\n activity={activity}\n />\n )}\n </ResourceSurface>\n )}\n </>\n )\n}\n",
|
|
19
|
+
"target": "inertia/components/ui/resource-page.tsx"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-show",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"badge",
|
|
7
|
+
"skeleton",
|
|
8
|
+
"can",
|
|
9
|
+
"resource-actions",
|
|
10
|
+
"resource-value",
|
|
11
|
+
"ui-preferences"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": [
|
|
14
|
+
"lucide-react@1.47.0"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "registry/ui/resource-show.tsx",
|
|
19
|
+
"type": "registry:ui",
|
|
20
|
+
"content": "import { Deferred, router } from '@inertiajs/react'\nimport { Link } from '@adonisjs/inertia/react'\nimport { History, Pencil } from 'lucide-react'\nimport type {\n ResourceActivity,\n ResourceChildren as Children,\n ResourceDescription,\n ResourceShow as Show,\n SerializedRecord,\n} from '@adula/kit'\nimport { Badge } from '~/components/ui/badge'\nimport { Button } from '~/components/ui/button'\nimport { useUiPreferences } from '~/components/ui/ui-preferences'\nimport { Can } from '~/components/ui/can'\nimport { Skeleton } from '~/components/ui/skeleton'\nimport { ResourceActions } from '~/components/ui/resource-actions'\nimport { ResourceValue, formatDatetime, type LookupOptions } from '~/components/ui/resource-value'\n\nexport type ResourceChildren = Children\nconst meta = new Set(['id', 'version', 'docStatus', 'orgUnitId'])\nconst activityLabels: Record<string, string> = {\n create: 'إنشاء السجل',\n update: 'تعديل السجل',\n delete: 'حذف السجل',\n submit: 'اعتماد المستند',\n cancel: 'إلغاء الاعتماد',\n amend: 'تعديل بعد الاعتماد',\n}\n\nexport function documentStatus(status: unknown) {\n return status === 1 ? 'معتمد' : status === 2 ? 'ملغي' : 'مسودة'\n}\n\nexport function ResourceShow({\n resource,\n result,\n lookups = {},\n childResources = {},\n childrenData,\n activity,\n}: {\n resource: ResourceDescription\n result: Show\n lookups?: LookupOptions\n childResources?: Record<string, ResourceDescription>\n childrenData?: ResourceChildren\n activity?: ResourceActivity\n}) {\n const { calendar } = useUiPreferences()\n const base = `/resources/${resource.name}/${result.data.id}`\n const fields = resource.fields.filter(\n (field) =>\n resource.show.includes(field.key) && field.type !== 'hasMany' && field.key in result.data\n )\n const children = resource.fields.filter(\n (field) => field.type === 'hasMany' && resource.show.includes(field.key)\n )\n const fieldLabel = (key: string) =>\n resource.fields.find((field) => field.key === key)?.label.ar ?? key\n return (\n <div className=\"max-w-5xl space-y-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <Can permissions={result.permissions} action=\"update\">\n <Button asChild>\n <Link href={`${base}/edit`}>\n <Pencil size={15} />\n تعديل السجل\n </Link>\n </Button>\n </Can>\n <ResourceActions\n resource={resource.name}\n id={result.data.id as number}\n version={result.data.version ?? undefined}\n permissions={result.permissions}\n onDone={(action) =>\n action === 'delete' ? router.visit(`/resources/${resource.name}`) : router.reload()\n }\n />\n {resource.submittable && (\n <Badge\n variant={result.data.docStatus === 1 ? 'default' : 'secondary'}\n className={result.data.docStatus === 2 ? 'bg-red-50 text-red-800' : ''}\n >\n {documentStatus(result.data.docStatus)}\n </Badge>\n )}\n </div>\n <dl className=\"grid gap-7 rounded-xl border bg-white p-7 md:grid-cols-2\">\n {fields.map((field) => (\n <div\n key={field.key}\n className={['text', 'json'].includes(field.type) ? 'md:col-span-2' : ''}\n >\n <dt className=\"mb-2 text-xs text-muted-foreground\">{field.label.ar}</dt>\n <dd className=\"break-words text-sm whitespace-pre-wrap\">\n <ResourceValue\n field={field}\n row={result.data}\n related={result.related}\n lookups={lookups}\n />\n </dd>\n </div>\n ))}\n {resource.scoped && result.data.orgUnitId !== undefined && (\n <div>\n <dt className=\"mb-2 text-xs text-muted-foreground\">الوحدة التنظيمية</dt>\n <dd className=\"text-sm\">#{String(result.data.orgUnitId)}</dd>\n </div>\n )}\n </dl>\n {children.length > 0 && (\n <Deferred data=\"childrenData\" fallback={<SectionSkeleton title=\"البنود\" rows={3} />}>\n <div className=\"space-y-5\">\n {children.map((field) => {\n const group = childrenData?.[field.key]\n if (!group) return null\n const child = childResources[field.key]\n const columns = child\n ? child.fields.filter(\n (entry) => child.list.includes(entry.key) && entry.type !== 'hasMany'\n )\n : []\n const keys = columns.length\n ? columns.map((entry) => entry.key)\n : [...new Set(group.rows.flatMap((row) => Object.keys(row)))].filter(\n (key) => !meta.has(key)\n )\n return (\n <section\n key={field.key}\n aria-label={field.label.ar}\n className=\"rounded-xl border bg-white\"\n >\n <div className=\"flex items-center justify-between border-b px-7 py-4\">\n <h2 className=\"text-sm font-semibold\">{field.label.ar}</h2>\n <span className=\"text-xs text-muted-foreground\">\n {group.rows.length} بند{group.hasMore ? ' · تظهر أول 100 بند متاح' : ''}\n </span>\n </div>\n {group.rows.length ? (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[480px] text-sm\">\n <thead className=\"bg-muted text-xs text-muted-foreground\">\n <tr>\n {keys.map((key) => (\n <th\n key={key}\n scope=\"col\"\n className=\"px-5 py-3 text-start font-medium\"\n >\n {columns.find((entry) => entry.key === key)?.label.ar ?? key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {group.rows.map((row) => (\n <tr key={String(row.id)} className=\"border-b last:border-0\">\n {keys.map((key) => {\n const column = columns.find((entry) => entry.key === key)\n return (\n <td key={key} className=\"px-5 py-3\">\n {column ? (\n <ResourceValue field={column} row={row} lookups={lookups} />\n ) : (\n plain(row, key)\n )}\n </td>\n )\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n ) : (\n <p className=\"px-7 py-6 text-xs text-muted-foreground\">لا توجد بنود متاحة.</p>\n )}\n </section>\n )\n })}\n </div>\n </Deferred>\n )}\n <Deferred data=\"activity\" fallback={<SectionSkeleton title=\"سجل النشاط\" rows={4} />}>\n <section aria-label=\"سجل النشاط\" className=\"rounded-xl border bg-white\">\n <div className=\"flex items-center gap-2 border-b px-7 py-4\">\n <History size={16} className=\"text-muted-foreground\" />\n <h2 className=\"text-sm font-semibold\">سجل النشاط</h2>\n <span className=\"ms-auto text-xs text-muted-foreground\">\n آخر {activity?.length ?? 0} إجراء\n </span>\n </div>\n {activity?.length ? (\n <ol className=\"divide-y\">\n {activity.map((entry) => (\n <li\n key={entry.id}\n className=\"flex flex-wrap items-baseline gap-x-4 gap-y-1 px-7 py-3 text-sm\"\n >\n <span className=\"font-medium\">\n {activityLabels[entry.action] ?? entry.action}\n </span>\n <span className=\"text-xs text-muted-foreground\" dir=\"ltr\">\n {entry.actorEmail ?? `#${entry.actorId}`}\n </span>\n <time\n dateTime={entry.createdAt}\n className=\"text-xs text-muted-foreground tabular-nums\"\n >\n {formatDatetime(entry.createdAt, calendar)}\n </time>\n {entry.fields.length > 0 && (\n <span className=\"basis-full text-xs text-muted-foreground\">\n الحقول: {entry.fields.map(fieldLabel).join('، ')}\n </span>\n )}\n </li>\n ))}\n </ol>\n ) : (\n <p className=\"px-7 py-6 text-xs text-muted-foreground\">لا يوجد نشاط مسجل بعد.</p>\n )}\n </section>\n </Deferred>\n </div>\n )\n}\n\nfunction plain(row: SerializedRecord, key: string) {\n const value = row[key]\n if (value === null || value === undefined) return '—'\n return typeof value === 'object' ? JSON.stringify(value) : String(value)\n}\n\nfunction SectionSkeleton({ title, rows }: { title: string; rows: number }) {\n return (\n <section\n role=\"status\"\n aria-label={`جارٍ تحميل ${title}`}\n className=\"rounded-xl border bg-white\"\n >\n <div className=\"border-b px-7 py-4\">\n <Skeleton className=\"h-4 w-32\" />\n </div>\n <div className=\"space-y-3 px-7 py-5\">\n {Array.from({ length: rows }, (_, index) => (\n <Skeleton key={index} className=\"h-4 w-full\" />\n ))}\n </div>\n </section>\n )\n}\n",
|
|
21
|
+
"target": "inertia/components/ui/resource-show.tsx"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-surface",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"dialog"
|
|
7
|
+
],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "registry/ui/resource-surface.tsx",
|
|
12
|
+
"type": "registry:ui",
|
|
13
|
+
"content": "import { useEffect, useRef, useState, type ReactNode } from 'react'\nimport { router } from '@inertiajs/react'\nimport { Link } from '@adonisjs/inertia/react'\nimport { Button } from '~/components/ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '~/components/ui/dialog'\n\nexport type ResourcePresentation = 'dialog' | 'page'\n\n/** Forms and record details are modal unless the user requests a page override. */\nexport function ResourceSurface({\n title,\n description,\n backHref,\n presentation = 'dialog',\n mode,\n children,\n}: {\n title: string\n description: string\n backHref: string\n presentation?: ResourcePresentation\n mode?: 'view' | 'edit'\n children: ReactNode\n}) {\n const [open, setOpen] = useState(true)\n const closeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n useEffect(() => () => clearTimeout(closeTimer.current), [])\n if (presentation === 'page')\n return (\n <section dir=\"rtl\" className=\"space-y-6\">\n <Button variant=\"outline\" asChild>\n <Link href={backHref}>العودة إلى القائمة</Link>\n </Button>\n <header>\n <h1 className=\"text-3xl font-semibold\">{title}</h1>\n <p className=\"mt-3 text-sm text-muted-foreground\">{description}</p>\n </header>\n {children}\n </section>\n )\n\n return (\n <Dialog\n mode={mode}\n open={open}\n onOpenChange={(open) => {\n if (!open) {\n setOpen(false)\n closeTimer.current = setTimeout(\n () => {\n router.visit(backHref, {\n onSuccess: () => {\n // A direct route has no mounted DialogTrigger to restore focus to.\n const heading = document.querySelector<HTMLElement>('main h1')\n heading?.setAttribute('tabindex', '-1')\n heading?.focus()\n },\n })\n },\n window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 160\n )\n }\n }}\n >\n <DialogContent\n dir=\"rtl\"\n className=\"flex max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-5xl\"\n >\n <DialogHeader className=\"shrink-0 border-b px-6 py-5 pe-12 text-start\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <div className=\"min-h-0 overflow-y-auto overscroll-contain p-4 sm:p-6\">{children}</div>\n </DialogContent>\n </Dialog>\n )\n}\n",
|
|
14
|
+
"target": "inertia/components/ui/resource-surface.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resource-value",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"badge",
|
|
6
|
+
"calendar-date",
|
|
7
|
+
"ui-preferences"
|
|
8
|
+
],
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"lucide-react@1.47.0"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/ui/resource-value.tsx",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"content": "import type { ReactNode } from 'react'\nimport { Paperclip } from 'lucide-react'\nimport type { CalendarPreference, ResourceField, SerializedRecord } from '@adula/kit'\nimport { calendarDisplay, calendarIso } from '~/components/ui/calendar_date'\nimport { useUiPreferences } from '~/components/ui/ui-preferences'\nimport { Badge } from '~/components/ui/badge'\n\nexport type Option = { value: string; label: string }\nexport type LookupOptions = Record<string, Option[]>\nexport type AttachmentValue = {\n id: number\n name: string\n size: number\n mimeType: string\n url: string\n}\n\n/** Project-owned copy: change the label once here for another currency. */\nexport const CURRENCY_LABEL = 'ر.س'\nconst locale = 'ar-u-nu-latn'\nconst integerFormat = new Intl.NumberFormat(locale)\nconst sizeFormat = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 })\nconst meta = new Set(['id', 'version', 'docStatus', 'orgUnitId'])\n\nexport function toLatinDigits(raw: string) {\n return raw\n .replace(/[٠-٩]/g, (digit) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(digit)))\n .replace(/[۰-۹]/g, (digit) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(digit)))\n .replaceAll('٫', '.')\n .replaceAll('٬', '')\n .replaceAll(',', '')\n .trim()\n}\n\nfunction minorUnits(value: unknown) {\n if (value === null || value === undefined || value === '') return null\n try {\n return BigInt(String(value))\n } catch {\n return null\n }\n}\n\nexport function moneyInput(value: unknown) {\n const minor = minorUnits(value)\n if (minor === null) return ''\n const abs = minor < 0n ? -minor : minor\n return `${minor < 0n ? '-' : ''}${abs / 100n}.${String(abs % 100n).padStart(2, '0')}`\n}\n\nexport function moneyDisplay(value: unknown) {\n const minor = minorUnits(value)\n if (minor === null) return ''\n const abs = minor < 0n ? -minor : minor\n return `${minor < 0n ? '-' : ''}${integerFormat.format(abs / 100n)}.${String(abs % 100n).padStart(2, '0')}`\n}\n\nexport function parseMoney(raw: string) {\n const value = toLatinDigits(raw)\n if (!/^-?\\d+(\\.\\d{1,2})?$/.test(value)) throw new Error('أدخل مبلغاً بخانتين عشريتين كحد أقصى')\n const [whole, cents = ''] = value.replace('-', '').split('.')\n return String(\n (BigInt(whole) * 100n + BigInt(cents.padEnd(2, '0'))) * (value.startsWith('-') ? -1n : 1n)\n )\n}\n\nexport function isAttachment(value: unknown): value is AttachmentValue {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'id' in value &&\n 'name' in value &&\n typeof (value as { url?: unknown }).url === 'string'\n )\n}\n\nexport function fileSize(bytes: number) {\n if (bytes < 1024) return `${integerFormat.format(bytes)} بايت`\n if (bytes < 1024 * 1024) return `${sizeFormat.format(bytes / 1024)} ك.ب`\n return `${sizeFormat.format(bytes / (1024 * 1024))} م.ب`\n}\n\nexport function formatDate(value: unknown, calendar: CalendarPreference = 'gregory') {\n return calendarDisplay(value, calendar)\n}\n\nexport function formatDatetime(value: unknown, calendar: CalendarPreference = 'gregory') {\n return calendarDisplay(value, calendar, true)\n}\n\n/** The first readable string of a related record labels it; the schema keeps labels out of payloads. */\nexport function relationLabel(row: SerializedRecord | undefined, fallback: unknown) {\n const label =\n row &&\n Object.entries(row).find(([key, entry]) => !meta.has(key) && typeof entry === 'string' && entry)\n return label\n ? String(label[1])\n : fallback === null || fallback === undefined\n ? '—'\n : `#${fallback}`\n}\n\nexport function inputValue(field: ResourceField, value: unknown) {\n if (value === null || value === undefined) return ''\n if (field.type === 'money') return moneyInput(value)\n if (field.type === 'json') return JSON.stringify(value, null, 2)\n if (field.type === 'attachment') return isAttachment(value) ? String(value.id) : String(value)\n if (field.type === 'datetime') {\n const date = new Date(String(value))\n if (Number.isNaN(date.getTime())) return ''\n return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16)\n }\n if (field.type === 'date') return String(value).slice(0, 10)\n return String(value)\n}\n\n/** Converts a control's string state into the JSON the resource route validates. */\nexport function parseValue(field: ResourceField, raw: string): unknown {\n if (raw === '') return null\n switch (field.type) {\n case 'money':\n return parseMoney(raw)\n case 'boolean':\n return raw === 'true'\n case 'integer': {\n const value = Number(toLatinDigits(raw))\n if (!Number.isInteger(value)) throw new Error('أدخل عدداً صحيحاً')\n return value\n }\n case 'belongsTo':\n case 'attachment':\n return Number(raw)\n case 'json':\n try {\n return JSON.parse(raw)\n } catch {\n throw new Error('أدخل بيانات JSON صحيحة')\n }\n case 'date': {\n return raw.startsWith('hijri:')\n ? calendarIso(raw.slice(6), 'islamic-umalqura')\n : calendarIso(raw, 'gregory')\n }\n case 'datetime': {\n const [day, time] = raw.split('T')\n const isoDay = day.startsWith('hijri:')\n ? calendarIso(day.slice(6), 'islamic-umalqura')\n : calendarIso(day, 'gregory')\n const date = new Date(`${isoDay}T${time}`)\n if (Number.isNaN(date.getTime())) throw new Error('أدخل تاريخاً ووقتاً صحيحين')\n return date.toISOString()\n }\n default:\n return raw\n }\n}\n\nexport function displayValue(\n field: ResourceField,\n row: SerializedRecord,\n related: Record<string, SerializedRecord[]> = {},\n lookups: LookupOptions = {},\n calendar: CalendarPreference = 'gregory'\n) {\n const value = row[field.key]\n if (value === null || value === undefined || value === '') return '—'\n switch (field.type) {\n case 'money':\n return `${moneyDisplay(value)} ${CURRENCY_LABEL}`\n case 'boolean':\n return value ? 'نعم' : 'لا'\n case 'integer':\n return typeof value === 'number' ? integerFormat.format(value) : String(value)\n case 'belongsTo':\n return relationLabel(\n related[field.key]?.find((item) => String(item.id) === String(value)),\n value\n )\n case 'lookup':\n return (\n lookups[field.key]?.find((option) => option.value === String(value))?.label ?? String(value)\n )\n case 'json':\n return JSON.stringify(value)\n case 'attachment':\n return isAttachment(value) ? value.name : 'مرفق'\n case 'date':\n return formatDate(value, calendar)\n case 'datetime':\n return formatDatetime(value, calendar)\n default:\n return String(value)\n }\n}\n\n/** Prefix formula-like and leading-control values before quoting CSV cells. */\nexport function csvCell(value: string) {\n const safe = /^[\\s\u0000-\u001f]*[=+@-]|^[\\t\\r\\n]/.test(value) ? `'${value}` : value\n return `\"${safe.replaceAll('\"', '\"\"')}\"`\n}\n\nexport function ResourceValue({\n field,\n row,\n related,\n lookups,\n}: {\n field: ResourceField\n row: SerializedRecord\n related?: Record<string, SerializedRecord[]>\n lookups?: LookupOptions\n}): ReactNode {\n const { calendar } = useUiPreferences()\n const value = row[field.key]\n if (value === null || value === undefined || value === '')\n return <span className=\"text-muted-foreground\">—</span>\n if (field.type === 'attachment' && isAttachment(value))\n return (\n <a\n href={value.url}\n download={value.name}\n className=\"inline-flex items-center gap-2 text-primary underline-offset-4 hover:underline\"\n >\n <Paperclip size={14} />\n {value.name}\n <span className=\"text-xs text-muted-foreground\">({fileSize(value.size)})</span>\n </a>\n )\n if (field.type === 'boolean')\n return <Badge variant={value ? 'default' : 'secondary'}>{value ? 'نعم' : 'لا'}</Badge>\n if (field.type === 'money')\n return (\n <span className=\"tabular-nums\" dir=\"ltr\">\n {moneyDisplay(value)}{' '}\n <span className=\"text-xs text-muted-foreground\">{CURRENCY_LABEL}</span>\n </span>\n )\n if (field.type === 'json')\n return (\n <pre dir=\"ltr\" className=\"max-h-64 overflow-auto rounded-md bg-muted p-3 text-xs\">\n {JSON.stringify(value, null, 2)}\n </pre>\n )\n if (['integer', 'date', 'datetime'].includes(field.type))\n return (\n <span className=\"tabular-nums\" dir=\"ltr\">\n {displayValue(field, row, related, lookups, calendar)}\n </span>\n )\n return <>{displayValue(field, row, related, lookups, calendar)}</>\n}\n",
|
|
17
|
+
"target": "inertia/components/ui/resource-value.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "select",
|
|
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/select.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\nimport { Select as SelectPrimitive } from \"radix-ui\"\n\nfunction Select({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n return <SelectPrimitive.Root dir=\"rtl\" data-slot=\"select\" {...props} />\n}\n\nfunction SelectGroup({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />\n}\n\nfunction SelectValue({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />\n}\n\nfunction SelectTrigger({\n className,\n size = \"default\",\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n size?: \"sm\" | \"default\"\n}) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n className={cn(\n \"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap 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 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_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 <SelectPrimitive.Icon asChild>\n <ChevronDownIcon className=\"size-4 opacity-50\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n}\n\nfunction SelectContent({\n className,\n children,\n position = \"item-aligned\",\n align = \"center\",\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(\n \"relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover 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 position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n align={align}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n}\n\nfunction SelectLabel({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(\"px-2 py-1.5 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction SelectItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n \"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pe-8 ps-2 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 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n className\n )}\n {...props}\n >\n <span\n data-slot=\"select-item-indicator\"\n className=\"absolute end-2 flex size-3.5 items-center justify-center\"\n >\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n}\n\nfunction SelectSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\"pointer-events-none -mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n )\n}\n\nfunction SelectScrollUpButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronUpIcon className=\"size-4\" />\n </SelectPrimitive.ScrollUpButton>\n )\n}\n\nfunction SelectScrollDownButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronDownIcon className=\"size-4\" />\n </SelectPrimitive.ScrollDownButton>\n )\n}\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n}\n",
|
|
13
|
+
"target": "inertia/components/ui/select.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": "separator",
|
|
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/separator.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { Separator as SeparatorPrimitive } from \"radix-ui\"\n\nfunction Separator({\n className,\n orientation = \"horizontal\",\n decorative = true,\n ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n return (\n <SeparatorPrimitive.Root\n data-slot=\"separator\"\n decorative={decorative}\n orientation={orientation}\n className={cn(\n \"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Separator }\n",
|
|
13
|
+
"target": "inertia/components/ui/separator.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
package/build/sheet.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sheet",
|
|
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/sheet.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 SheetPrimitive } from \"radix-ui\"\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />\n}\n\nfunction SheetTrigger({\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\n\nfunction SheetClose({\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\n\nfunction SheetPortal({\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\n\nfunction SheetOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n return (\n <SheetPrimitive.Overlay\n data-slot=\"sheet-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 SheetContent({\n className,\n children,\n side = \"right\",\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n showCloseButton?: boolean\n}) {\n return (\n <SheetPortal>\n <SheetOverlay />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n \"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500\",\n side === \"right\" &&\n \"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm\",\n side === \"left\" &&\n \"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm\",\n side === \"top\" &&\n \"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top\",\n side === \"bottom\" &&\n \"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom\",\n className\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <SheetPrimitive.Close 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-secondary\">\n <XIcon className=\"size-4\" />\n <span className=\"sr-only\">إغلاق</span>\n </SheetPrimitive.Close>\n )}\n </SheetPrimitive.Content>\n </SheetPortal>\n )\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n {...props}\n />\n )\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n {...props}\n />\n )\n}\n\nfunction SheetTitle({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn(\"font-semibold text-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction SheetDescription({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Sheet,\n SheetTrigger,\n SheetClose,\n SheetContent,\n SheetHeader,\n SheetFooter,\n SheetTitle,\n SheetDescription,\n}\n",
|
|
13
|
+
"target": "inertia/components/ui/sheet.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skeleton",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "registry/ui/skeleton.tsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"content": "import { cn } from \"cn\"\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"skeleton\"\n className={cn(\"animate-pulse rounded-md bg-accent\", className)}\n {...props}\n />\n )\n}\n\nexport { Skeleton }\n",
|
|
12
|
+
"target": "inertia/components/ui/skeleton.tsx"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sonner",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"sonner@2.0.8",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "registry/ui/sonner.tsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"content": "\"use client\"\n\nimport {\n CircleCheckIcon,\n InfoIcon,\n Loader2Icon,\n OctagonXIcon,\n TriangleAlertIcon,\n} from \"lucide-react\"\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n\n return (\n <Sonner\n theme=\"light\"\n dir=\"rtl\"\n className=\"toaster group\"\n icons={{\n success: <CircleCheckIcon className=\"size-4\" />,\n info: <InfoIcon className=\"size-4\" />,\n warning: <TriangleAlertIcon className=\"size-4\" />,\n error: <OctagonXIcon className=\"size-4\" />,\n loading: <Loader2Icon className=\"size-4 animate-spin\" />,\n }}\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n \"--border-radius\": \"var(--radius)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
|
|
12
|
+
"target": "inertia/components/ui/sonner.tsx"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "switch",
|
|
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/switch.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { Switch as SwitchPrimitive } from \"radix-ui\"\n\nfunction Switch({\n className,\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root> & {\n size?: \"sm\" | \"default\"\n}) {\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n data-size={size}\n className={cn(\n \"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80\",\n className\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] rtl:data-[state=checked]:-translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground\"\n )}\n />\n </SwitchPrimitive.Root>\n )\n}\n\nexport { Switch }\n",
|
|
13
|
+
"target": "inertia/components/ui/switch.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
package/build/table.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "table",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "registry/ui/table.tsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n return (\n <div\n data-slot=\"table-container\"\n className=\"relative w-full overflow-x-auto\"\n >\n <table\n data-slot=\"table\"\n className={cn(\"w-full caption-bottom text-sm\", className)}\n {...props}\n />\n </div>\n )\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n return (\n <thead\n data-slot=\"table-header\"\n className={cn(\"[&_tr]:border-b\", className)}\n {...props}\n />\n )\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n return (\n <tbody\n data-slot=\"table-body\"\n className={cn(\"[&_tr:last-child]:border-0\", className)}\n {...props}\n />\n )\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n return (\n <tfoot\n data-slot=\"table-footer\"\n className={cn(\n \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n return (\n <th\n data-slot=\"table-head\"\n className={cn(\n \"h-10 px-2 text-start align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n return (\n <td\n data-slot=\"table-cell\"\n className={cn(\n \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0 [&>[role=checkbox]]:translate-y-[2px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableCaption({\n className,\n ...props\n}: React.ComponentProps<\"caption\">) {\n return (\n <caption\n data-slot=\"table-caption\"\n className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Table,\n TableHeader,\n TableBody,\n TableFooter,\n TableHead,\n TableRow,\n TableCell,\n TableCaption,\n}\n",
|
|
12
|
+
"target": "inertia/components/ui/table.tsx"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
17
|
+
}
|
package/build/tabs.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tabs",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"radix-ui@1.6.7",
|
|
6
|
+
"class-variance-authority@0.7.1",
|
|
7
|
+
"lucide-react@1.47.0"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "registry/ui/tabs.tsx",
|
|
12
|
+
"type": "registry:ui",
|
|
13
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root dir=\"rtl\"\n data-slot=\"tabs\"\n data-orientation={orientation}\n orientation={orientation}\n className={cn(\n \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction TabsList({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n VariantProps<typeof tabsListVariants>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n \"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground\",\n \"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-end-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }\n",
|
|
14
|
+
"target": "inertia/components/ui/tabs.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "textarea",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "registry/ui/textarea.tsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground 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 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Textarea }\n",
|
|
12
|
+
"target": "inertia/components/ui/textarea.tsx"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
17
|
+
}
|
package/build/theme.css
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
@import 'tailwindcss' source(none);
|
|
2
|
+
@import 'tw-animate-css';
|
|
3
|
+
@import '@fontsource/noto-sans-arabic/400.css';
|
|
4
|
+
@import '@fontsource/noto-sans-arabic/500.css';
|
|
5
|
+
@import '@fontsource/noto-sans-arabic/600.css';
|
|
6
|
+
@import '@fontsource/noto-sans-arabic/700.css';
|
|
7
|
+
@source "../";
|
|
8
|
+
|
|
9
|
+
@theme inline {
|
|
10
|
+
--font-sans: 'Noto Sans Arabic', sans-serif;
|
|
11
|
+
--color-background: var(--background);
|
|
12
|
+
--color-foreground: var(--foreground);
|
|
13
|
+
--color-card: var(--card);
|
|
14
|
+
--color-card-foreground: var(--foreground);
|
|
15
|
+
--color-popover: var(--card);
|
|
16
|
+
--color-popover-foreground: var(--foreground);
|
|
17
|
+
--color-primary: var(--primary);
|
|
18
|
+
--color-primary-foreground: #ffffff;
|
|
19
|
+
--color-secondary: var(--secondary);
|
|
20
|
+
--color-secondary-foreground: var(--foreground);
|
|
21
|
+
--color-muted: var(--secondary);
|
|
22
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
23
|
+
--color-accent: var(--accent);
|
|
24
|
+
--color-accent-foreground: var(--primary);
|
|
25
|
+
--color-destructive: #b64239;
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-input: var(--border);
|
|
28
|
+
--color-ring: #659981;
|
|
29
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
30
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
31
|
+
--radius-lg: var(--radius);
|
|
32
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:root {
|
|
36
|
+
--background: #f4f6f2;
|
|
37
|
+
--foreground: #1c3028;
|
|
38
|
+
--card: #ffffff;
|
|
39
|
+
--primary: #185640;
|
|
40
|
+
--secondary: #edf1eb;
|
|
41
|
+
--muted-foreground: #65766b;
|
|
42
|
+
--accent: #e5eee5;
|
|
43
|
+
--border: #dbe3d9;
|
|
44
|
+
--radius: 0.625rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@layer base {
|
|
48
|
+
*,
|
|
49
|
+
::before,
|
|
50
|
+
::after {
|
|
51
|
+
border-color: var(--border);
|
|
52
|
+
}
|
|
53
|
+
button,
|
|
54
|
+
input,
|
|
55
|
+
textarea,
|
|
56
|
+
select {
|
|
57
|
+
font: inherit;
|
|
58
|
+
}
|
|
59
|
+
button,
|
|
60
|
+
[role='button'] {
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
:focus-visible {
|
|
64
|
+
outline-offset: 3px;
|
|
65
|
+
}
|
|
66
|
+
[dir='rtl'] {
|
|
67
|
+
text-align: start;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
body {
|
|
71
|
+
font-family: var(--font-sans);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@media (prefers-reduced-motion: reduce) {
|
|
75
|
+
*,
|
|
76
|
+
*::before,
|
|
77
|
+
*::after {
|
|
78
|
+
animation-duration: 0.01ms !important;
|
|
79
|
+
transition-duration: 0.01ms !important;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Keep the persistent shell fixed; animate only incoming page content. */
|
|
84
|
+
@keyframes adula-page-enter {
|
|
85
|
+
from {
|
|
86
|
+
opacity: 0.35;
|
|
87
|
+
transform: translateY(4px);
|
|
88
|
+
}
|
|
89
|
+
to {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
transform: translateY(0);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
.adula-page-enter {
|
|
95
|
+
animation: adula-page-enter 180ms ease-out;
|
|
96
|
+
}
|
|
97
|
+
@media (prefers-reduced-motion: reduce) {
|
|
98
|
+
.adula-page-enter {
|
|
99
|
+
animation: none;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tooltip",
|
|
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/tooltip.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\"\n\nfunction TooltipProvider({\n delayDuration = 0,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n return (\n <TooltipPrimitive.Provider\n data-slot=\"tooltip-provider\"\n delayDuration={delayDuration}\n {...props}\n />\n )\n}\n\nfunction Tooltip({\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n return <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n}\n\nfunction TooltipTrigger({\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n className,\n sideOffset = 0,\n children,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n data-slot=\"tooltip-content\"\n sideOffset={sideOffset}\n className={cn(\n \"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 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\",\n className\n )}\n {...props}\n >\n {children}\n <TooltipPrimitive.Arrow className=\"z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground\" />\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n",
|
|
13
|
+
"target": "inertia/components/ui/tooltip.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"docs": "The `tooltip` component has been added. Remember to wrap your app with the `TooltipProvider` component.\n\n```tsx title=\"app/layout.tsx\"\nimport { TooltipProvider } from \"@/components/ui/tooltip\"\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang=\"en\">\n <body>\n <TooltipProvider>{children}</TooltipProvider>\n </body>\n </html>\n )\n}\n```\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ui-preferences",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [],
|
|
5
|
+
"dependencies": [],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "registry/ui/ui-preferences.tsx",
|
|
9
|
+
"type": "registry:ui",
|
|
10
|
+
"content": "import { usePage } from '@inertiajs/react'\nimport type { UiPreferences } from '@adula/kit'\n\n/** A consumer without shared preferences keeps the documented defaults. */\nexport function useUiPreferences(): UiPreferences {\n const page = usePage<{ uiPreferences?: UiPreferences }>()\n return (\n page.props.uiPreferences ?? {\n calendar: 'gregory',\n confirmDialogClose: true,\n pageTransitions: true,\n }\n )\n}\n",
|
|
11
|
+
"target": "inertia/components/ui/ui-preferences.tsx"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adula/ui",
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Arabic RTL shadcn registry copied into AdonisJS applications",
|
|
7
|
+
"files": [
|
|
8
|
+
"build",
|
|
9
|
+
"LICENSE.shadcn.md",
|
|
10
|
+
"UPSTREAM.json",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./registry/*": "./build/*.json"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"provenance": true
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24.0.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/adulash/adula-kit.git",
|
|
28
|
+
"directory": "packages/ui"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/adulash/adula-kit/tree/main/packages/ui#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/adulash/adula-kit/issues"
|
|
33
|
+
},
|
|
34
|
+
"author": "adula contributors",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"adonisjs",
|
|
37
|
+
"business-applications",
|
|
38
|
+
"arabic",
|
|
39
|
+
"rtl",
|
|
40
|
+
"shadcn",
|
|
41
|
+
"react"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "node --test tests/*.test.mjs",
|
|
45
|
+
"build": "node ../../scripts/build-ui-registry.mjs"
|
|
46
|
+
}
|
|
47
|
+
}
|