@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 adula-kit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 shadcn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @adula/ui
|
|
2
|
+
|
|
3
|
+
Version 0.2.0-alpha.1, unreleased. For complete consumer setup, use the canonical [installation guide](https://github.com/adulash/adula-kit/blob/main/docs/installation.md). Full 1.0 acceptance remains pending.
|
|
4
|
+
|
|
5
|
+
Arabic RTL shadcn registry for project-owned React components. This package ships JSON assets, not a runtime React component library. Install with `node ace adula:ui add all` or `node ace adula:ui add dialog`. The command invokes the pinned shadcn CLI and records each generated file hash in `ui.lock.json`. Project modifications are preserved; use `--preview` to review upstream differences before merging an update.
|
|
6
|
+
|
|
7
|
+
The 25 primitives are based on the official shadcn new-york-v4 registry. See LICENSE.shadcn.md and UPSTREAM.json for attribution and source hashes. Adaptations include logical spacing, Arabic labels, RTL keyboard direction, Arabic calendar defaults, RTL notifications and switch movement. Visual acceptance of the application remains a separate check.
|
|
8
|
+
|
|
9
|
+
Build with `pnpm --filter @adula/ui build`. Refreshing upstream sources is an explicit maintenance task: run scripts/sync-ui-registry.mjs, review changes, run scripts/prepare-ui-registry.mjs to recheck dependency metadata and adapt sources, then build and visually test.
|
|
10
|
+
|
|
11
|
+
Required host tooling: Node 24, React 19, shadcn 4.21.0, Tailwind CSS 4.3.3 and @tailwindcss/vite 4.3.3. The kit installer supports the official AdonisJS React starter layout. The registry is unpublished until explicitly authorized.
|
|
12
|
+
|
|
13
|
+
The theme disables automatic Tailwind source discovery and explicitly scans the consumer's inertia directory. It does not scan node_modules or unrelated workspace files. Add explicit source paths for any additional application-owned frontend directories.
|
|
14
|
+
|
|
15
|
+
## Resource components
|
|
16
|
+
|
|
17
|
+
Nine kit components sit on top of the primitives and are registered by `scripts/register-resource-ui.mjs`. They consume the `ResourceDescription`, `ResourceList`, `ResourceEditor`, `ResourceShow`, `ResourceChildren`, `ResourceLookups` and `ResourceActivity` contracts exported by `@adula/kit`; the copied files are project-owned like every other registry item.
|
|
18
|
+
|
|
19
|
+
| Component | Role |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `resource-page` | Page shell for the `index`, `form` and `show` modes. The discriminated `view` prop stays nested because Inertia page typing flattens top-level unions; the index list arrives as a top-level `result` prop built with `inertia.scroll()`. |
|
|
22
|
+
| `resource-surface` | Default shadcn Dialog for forms and record details, with RTL, scrollable content, accessible title/description and route dismissal. Use `presentation="page"` only for an explicit user-requested page. Custom overrides pass title, description and backHref. |
|
|
23
|
+
| `data-table` | URL-driven sort, text search and per-kind filters (lookup select, boolean select, typed date with calendar, related-record search, text). Keyset paging uses Inertia's `InfiniteScroll` in manual mode with a scroll trigger; pages merge into `result.data` (`matchOn('id')`) while row permissions and related labels accumulate client-side. Rows above 200 render through `@tanstack/react-virtual`. CSV export writes the loaded rows with a UTF-8 BOM, Arabic headers and the same formatting as the cells. Row actions go through `Can` and `resource-actions`. |
|
|
24
|
+
| `resource-form` | Controlled state per field, JSON `POST`/`PATCH` with the hidden `version`, VineJS `422` errors mapped to fields, inline child errors surfaced on their section (nested validators do not report the row), `409` conflicts with a reload action, organizational unit select, and delete/submit/cancel buttons rendered only from the record permissions. |
|
|
25
|
+
| `resource-field` | One control per field kind: text/textarea, integer, money (minor units in and out, Arabic-locale display with two decimals and `CURRENCY_LABEL`), boolean switch, date (typed `YYYY-MM-DD` plus calendar popover), datetime, JSON (parse check and pretty printing), attachment (uploads on selection to `POST /attachments`, progress, download link, replace and clear; the saved value is the attachment id), lookup select, belongsTo combobox (server search through `/resources/:resource/options/:field` with debounce and keyset "more") and inline hasMany rows (`InlineRows`: add, edit, `_delete` markers, per-row `version`). |
|
|
26
|
+
| `resource-value` | Formatting and parsing shared by the table, the show page and CSV export, plus the `ResourceValue` renderer (download links, badges, `<pre>` JSON, LTR numerics). |
|
|
27
|
+
| `resource-show` | Field cards, status badge, actions by permission, deferred `childrenData` tables described by `childResources`, and the deferred `activity` list, each behind a skeleton. |
|
|
28
|
+
| `resource-actions` | Confirmation dialog and versioned `DELETE`/`submit`/`cancel` requests shared by the table and the show page. |
|
|
29
|
+
| `can` | Renders children only when the server-computed record permission allows the action. |
|
|
30
|
+
|
|
31
|
+
Override rule: a page file at `inertia/pages/<resource>/{index,form,show}.tsx` replaces the generic page for that resource and mode; there is no setting or hook. See docs/decisions/015-generic-resource-ui.md.
|
|
32
|
+
|
|
33
|
+
All UI work follows the installed `.agents/skills/adula-frontend-design/SKILL.md`: request missing company identity at kickoff, design for business workflows, compose the local shadcn components, and use modal forms/details by default. Record supplied branding in project-owned `docs/design-identity.md`. `ResourcePage` accepts `presentation="page"` for an explicit exception; existing customized copies require review before updating.
|
|
34
|
+
|
|
35
|
+
Server prerequisites for the generic pages: `ResourceService.lookups()` for lookup labels and filters, `ResourceService.activity()` for the deferred activity prop, and an index renderer that wraps the list in `inertia.scroll()` with the `cursor` page name. The reference `resources_controller.ts` shows the complete wiring.
|
package/UPSTREAM.json
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "button",
|
|
4
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/button.json",
|
|
5
|
+
"path": "registry/ui/button.tsx",
|
|
6
|
+
"sha256": "79dd6f75f8136394442202d6b8b922fb269eaad0a5dba579397c9d5b41f893bb"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "input",
|
|
10
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/input.json",
|
|
11
|
+
"path": "registry/ui/input.tsx",
|
|
12
|
+
"sha256": "0c9457181f6ddc80969bcf854e92c362903a55b6e889fbef3fd85343ecc4af5b"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "textarea",
|
|
16
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/textarea.json",
|
|
17
|
+
"path": "registry/ui/textarea.tsx",
|
|
18
|
+
"sha256": "5cafcfeeee29534f449387a16b585f5923d2faab1a0cdc50a874416b175f59ce"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "select",
|
|
22
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/select.json",
|
|
23
|
+
"path": "registry/ui/select.tsx",
|
|
24
|
+
"sha256": "962830436bd0b7ee50e556f0747e277d6a86869ed27d167516346b15f26309d8"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "checkbox",
|
|
28
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/checkbox.json",
|
|
29
|
+
"path": "registry/ui/checkbox.tsx",
|
|
30
|
+
"sha256": "ba0ac64064e2d8fa7299d53e9e84b9a4772fef8dd4cf6170c56d3030a1d69941"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "switch",
|
|
34
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/switch.json",
|
|
35
|
+
"path": "registry/ui/switch.tsx",
|
|
36
|
+
"sha256": "743676f33f823ffa8008bbd5eb94cb68e227599447d60e132a202291a395f8e4"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "radio-group",
|
|
40
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/radio-group.json",
|
|
41
|
+
"path": "registry/ui/radio-group.tsx",
|
|
42
|
+
"sha256": "cfc4f99ee68be2fbbd70de3ffad82e1462d5e8e0bb0aecfd190a4f5115325862"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "table",
|
|
46
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/table.json",
|
|
47
|
+
"path": "registry/ui/table.tsx",
|
|
48
|
+
"sha256": "bd1a9fc83a5a2149e504038e274bb7e5a0351af59f6896df52ac4d5b439695ac"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "form",
|
|
52
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/form.json",
|
|
53
|
+
"path": "registry/ui/form.tsx",
|
|
54
|
+
"sha256": "dfa22e6614b807df60bbe7e40dd44ef74cf894b358f37c3ed356ebbeef9b4599"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "label",
|
|
58
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/label.json",
|
|
59
|
+
"path": "registry/ui/label.tsx",
|
|
60
|
+
"sha256": "344bde38b8531615dbf8e14af3895517c24d8ef6ce56ddeda7e2e6fe4b5d0c00"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "dialog",
|
|
64
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/dialog.json",
|
|
65
|
+
"path": "registry/ui/dialog.tsx",
|
|
66
|
+
"sha256": "2ade6074e5fad0a4eb171ead44c43d3301a3c83d186d7236fb1fdbea80ee556b"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "sheet",
|
|
70
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/sheet.json",
|
|
71
|
+
"path": "registry/ui/sheet.tsx",
|
|
72
|
+
"sha256": "ea473d38a26a9b0b879e5a13139899ad45e9782e896b3fa205c7763a6c506876"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "dropdown-menu",
|
|
76
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/dropdown-menu.json",
|
|
77
|
+
"path": "registry/ui/dropdown-menu.tsx",
|
|
78
|
+
"sha256": "d954efb735f13734751207ad22cb0cddd7f6ec53c59d16675d103dae08cb61fb"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "popover",
|
|
82
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/popover.json",
|
|
83
|
+
"path": "registry/ui/popover.tsx",
|
|
84
|
+
"sha256": "3f99d3add6c9fa4f03721e81297ee10e1310785ee1d9d9d4c82a114d84ab0757"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "command",
|
|
88
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/command.json",
|
|
89
|
+
"path": "registry/ui/command.tsx",
|
|
90
|
+
"sha256": "319e9264567dd46f04d705028bc345470663ca5919f31292c4ec43ea5ceb9336"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "calendar",
|
|
94
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/calendar.json",
|
|
95
|
+
"path": "registry/ui/calendar.tsx",
|
|
96
|
+
"sha256": "78ed2cc49dafc5bb5df7b1d4a325e5fc314237386747362e5f0761a2c539a934"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "badge",
|
|
100
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/badge.json",
|
|
101
|
+
"path": "registry/ui/badge.tsx",
|
|
102
|
+
"sha256": "13a9380dfff853ee730c14a52df49f22aecd31f40f2998d7c1e7ec458b6e1650"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "card",
|
|
106
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/card.json",
|
|
107
|
+
"path": "registry/ui/card.tsx",
|
|
108
|
+
"sha256": "ef8305b12c3112dab42a4708b41b1ebcc042fa5ab4c20f039da8d45b09ed5059"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "tabs",
|
|
112
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/tabs.json",
|
|
113
|
+
"path": "registry/ui/tabs.tsx",
|
|
114
|
+
"sha256": "e1df6e2f95ece373ca909c1d54b8ba8cf4c47dd425c1784bbb9a01c38dd0936b"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "sonner",
|
|
118
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/sonner.json",
|
|
119
|
+
"path": "registry/ui/sonner.tsx",
|
|
120
|
+
"sha256": "13ccc9b0fe4248e622f83a1d3b2f0566b72b5e04162d7d0c8db8945ecc71b55f"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "skeleton",
|
|
124
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/skeleton.json",
|
|
125
|
+
"path": "registry/ui/skeleton.tsx",
|
|
126
|
+
"sha256": "f51ce0fcd31e5c31437382e80ea0af94b4fc3d64221d27091d36824546780a43"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "pagination",
|
|
130
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/pagination.json",
|
|
131
|
+
"path": "registry/ui/pagination.tsx",
|
|
132
|
+
"sha256": "224843c4f509e8b8c9848adefa43d1491846f0851138fa9b51cc6ba1319c5653"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "tooltip",
|
|
136
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/tooltip.json",
|
|
137
|
+
"path": "registry/ui/tooltip.tsx",
|
|
138
|
+
"sha256": "611e4305879b2773eafe76bf3c5caa69d8e47367fd97887a71634a27828f4433"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "separator",
|
|
142
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/separator.json",
|
|
143
|
+
"path": "registry/ui/separator.tsx",
|
|
144
|
+
"sha256": "ad7d5403fe337160e41dbf04147896e448ae3a747b8d06660d627d74d60021bc"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "alert",
|
|
148
|
+
"url": "https://ui.shadcn.com/r/styles/new-york-v4/alert.json",
|
|
149
|
+
"path": "registry/ui/alert.tsx",
|
|
150
|
+
"sha256": "d03dcae5cdb63c381c1144ca9a0c49ba0e46466967d7deb94662034042acb034"
|
|
151
|
+
}
|
|
152
|
+
]
|
package/build/alert.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alert",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"class-variance-authority@0.7.1",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"files": [
|
|
9
|
+
{
|
|
10
|
+
"path": "registry/ui/alert.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\n\nconst alertVariants = cva(\n \"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n destructive:\n \"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Alert({\n className,\n variant,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n return (\n <div\n data-slot=\"alert\"\n role=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\n \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Alert, AlertTitle, AlertDescription }\n",
|
|
13
|
+
"target": "inertia/components/ui/alert.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
package/build/badge.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "badge",
|
|
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/badge.tsx",
|
|
12
|
+
"type": "registry:ui",
|
|
13
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Slot } from \"radix-ui\"\n\nconst badgeVariants = cva(\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90\",\n outline:\n \"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n ghost: \"[a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 [a&]:hover:underline\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot.Root : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n data-variant={variant}\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n",
|
|
14
|
+
"target": "inertia/components/ui/badge.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "button",
|
|
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/button.tsx",
|
|
12
|
+
"type": "registry:ui",
|
|
13
|
+
"content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Slot } from \"radix-ui\"\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot.Root : \"button\"\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n",
|
|
14
|
+
"target": "inertia/components/ui/button.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calendar-date",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [],
|
|
5
|
+
"dependencies": [],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "registry/ui/calendar_date.ts",
|
|
9
|
+
"type": "registry:ui",
|
|
10
|
+
"content": "import type { CalendarSystem, CalendarPreference } from '@adula/kit'\n\nconst dayMs = 86400000\nconst hijri = new Intl.DateTimeFormat('en-u-nu-latn', {\n calendar: 'islamic-umalqura',\n timeZone: 'UTC',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n})\n\nexport function calendarInput(iso: string, calendar: CalendarSystem): string {\n if (!iso) return ''\n if (calendar === 'gregory') return iso.slice(0, 10)\n const date = new Date(`${iso.slice(0, 10)}T00:00:00Z`)\n if (Number.isNaN(date.getTime())) return ''\n const parts = Object.fromEntries(hijri.formatToParts(date).map((part) => [part.type, part.value]))\n return `${parts.year.padStart(4, '0')}-${parts.month.padStart(2, '0')}-${parts.day.padStart(2, '0')}`\n}\n\n/** Invert the platform's Umm al-Qura calendar; never approximate lunar months. */\nexport function calendarIso(raw: string, calendar: CalendarSystem): string {\n const value = raw\n .replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)))\n .replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)))\n .trim()\n if (!value) return ''\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) throw new Error('أدخل التاريخ بصيغة سنة-شهر-يوم')\n if (calendar === 'gregory') {\n const date = new Date(`${value}T00:00:00Z`)\n if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value)\n throw new Error('التاريخ الميلادي غير صالح')\n return value\n }\n const [year, month, day] = value.split('-').map(Number)\n if (year < 1300 || year > 1600 || month < 1 || month > 12 || day < 1 || day > 30)\n throw new Error('اختر تاريخ أم القرى بين عامي 1300 و1600 هـ')\n let low = Math.floor(Date.UTC(1882, 0, 1) / dayMs)\n let high = Math.floor(Date.UTC(2175, 11, 31) / dayMs)\n while (low <= high) {\n const middle = Math.floor((low + high) / 2)\n const iso = new Date(middle * dayMs).toISOString().slice(0, 10)\n const candidate = calendarInput(iso, calendar)\n if (candidate === value) return iso\n if (candidate < value) low = middle + 1\n else high = middle - 1\n }\n throw new Error('هذا اليوم غير موجود في تقويم أم القرى')\n}\n\nexport function calendarDisplay(\n value: unknown,\n calendar: CalendarPreference,\n withTime = false\n): string {\n if (!value) return '—'\n if (calendar === 'both')\n return `${calendarDisplay(value, 'gregory', withTime)} · ${calendarDisplay(value, 'islamic-umalqura', withTime)}`\n const date = new Date(String(value))\n if (Number.isNaN(date.getTime())) return String(value)\n return new Intl.DateTimeFormat('ar-u-nu-latn', {\n calendar,\n dateStyle: 'medium',\n ...(withTime ? { timeStyle: 'short' as const } : { timeZone: 'UTC' }),\n }).format(date)\n}\n",
|
|
11
|
+
"target": "inertia/components/ui/calendar_date.ts"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calendar",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"react-day-picker@10.0.1",
|
|
6
|
+
"date-fns@4.4.0",
|
|
7
|
+
"lucide-react@1.47.0"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"button"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/ui/calendar.tsx",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"content": "\"use client\"\nimport { ar } from 'react-day-picker/locale'\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport {\n ChevronDownIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n} from \"lucide-react\"\nimport {\n DayPicker,\n getDefaultClassNames,\n type DayButton,\n} from \"react-day-picker\"\n\nimport { Button, buttonVariants } from \"~/components/ui/button\"\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n ...props\n}: React.ComponentProps<typeof DayPicker> & {\n buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"]\n}) {\n const defaultClassNames = getDefaultClassNames()\n\n return (\n <DayPicker\n dir=\"rtl\"\n locale={ar}\n showOutsideDays={showOutsideDays}\n className={cn(\n \"group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) =>\n date.toLocaleString(\"ar\", { month: \"short\" }),\n ...formatters,\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\n \"relative flex flex-col gap-4 md:flex-row\",\n defaultClassNames.months\n ),\n month: cn(\"flex w-full flex-col gap-4\", defaultClassNames.month),\n nav: cn(\n \"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1\",\n defaultClassNames.nav\n ),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) p-0 select-none aria-disabled:opacity-50\",\n defaultClassNames.button_next\n ),\n month_caption: cn(\n \"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)\",\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n \"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium\",\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n \"relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50\",\n defaultClassNames.dropdown_root\n ),\n dropdown: cn(\n \"absolute inset-0 bg-popover opacity-0\",\n defaultClassNames.dropdown\n ),\n caption_label: cn(\n \"font-medium select-none\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"flex h-8 items-center gap-1 rounded-md pe-1 ps-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground\",\n defaultClassNames.caption_label\n ),\n month_grid: cn(\"w-full border-collapse\", defaultClassNames.month_grid),\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none\",\n defaultClassNames.weekday\n ),\n week: cn(\"mt-2 flex w-full\", defaultClassNames.week),\n week_number_header: cn(\n \"w-(--cell-size) select-none\",\n defaultClassNames.week_number_header\n ),\n week_number: cn(\n \"text-[0.8rem] text-muted-foreground select-none\",\n defaultClassNames.week_number\n ),\n day: cn(\n \"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md\",\n props.showWeekNumber\n ? \"[&:nth-child(2)[data-selected=true]_button]:rounded-l-md\"\n : \"[&:first-child[data-selected=true]_button]:rounded-l-md\",\n defaultClassNames.day\n ),\n range_start: cn(\n \"rounded-l-md bg-accent\",\n defaultClassNames.range_start\n ),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none\",\n defaultClassNames.today\n ),\n outside: cn(\n \"text-muted-foreground aria-selected:text-muted-foreground\",\n defaultClassNames.outside\n ),\n disabled: cn(\n \"text-muted-foreground opacity-50\",\n defaultClassNames.disabled\n ),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames,\n }}\n components={{\n Root: ({ className, rootRef, ...props }) => {\n return (\n <div\n data-slot=\"calendar\"\n ref={rootRef}\n className={cn(className)}\n {...props}\n />\n )\n },\n Chevron: ({ className, orientation, ...props }) => {\n if (orientation === \"left\") {\n return (\n <ChevronLeftIcon className={cn(\"size-4\", className)} {...props} />\n )\n }\n\n if (orientation === \"right\") {\n return (\n <ChevronRightIcon\n className={cn(\"size-4\", className)}\n {...props}\n />\n )\n }\n\n return (\n <ChevronDownIcon className={cn(\"size-4\", className)} {...props} />\n )\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, ...props }) => {\n return (\n <td {...props}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">\n {children}\n </div>\n </td>\n )\n },\n ...components,\n }}\n {...props}\n />\n )\n}\n\nfunction CalendarDayButton({\n className,\n day,\n modifiers,\n ...props\n}: React.ComponentProps<typeof DayButton>) {\n const defaultClassNames = getDefaultClassNames()\n\n const ref = React.useRef<HTMLButtonElement>(null)\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus()\n }, [modifiers.focused])\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected &&\n !modifiers.range_start &&\n !modifiers.range_end &&\n !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70\",\n defaultClassNames.day,\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Calendar, CalendarDayButton }\n",
|
|
17
|
+
"target": "inertia/components/ui/calendar.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"type": "registry:ui",
|
|
21
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
22
|
+
}
|
package/build/can.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "can",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [],
|
|
5
|
+
"dependencies": [],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "registry/ui/can.tsx",
|
|
9
|
+
"type": "registry:ui",
|
|
10
|
+
"content": "import type { ReactNode } from 'react'\nimport type { Action, RecordPermissions } from '@adula/kit'\n\n/** CASL is evaluated server-side before redaction; hidden condition fields stay private. */\nexport function Can({ permissions, action, children }: {\n permissions: RecordPermissions; action: Action; children: ReactNode\n}) {\n return permissions[action] ? children : null\n}\n",
|
|
11
|
+
"target": "inertia/components/ui/can.tsx"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
15
|
+
}
|
package/build/card.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "card",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"lucide-react@1.47.0"
|
|
6
|
+
],
|
|
7
|
+
"files": [
|
|
8
|
+
{
|
|
9
|
+
"path": "registry/ui/card.tsx",
|
|
10
|
+
"type": "registry:ui",
|
|
11
|
+
"content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card\"\n className={cn(\n \"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"leading-none font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\n \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-content\"\n className={cn(\"px-6\", className)}\n {...props}\n />\n )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardAction,\n CardDescription,\n CardContent,\n}\n",
|
|
12
|
+
"target": "inertia/components/ui/card.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": "checkbox",
|
|
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/checkbox.tsx",
|
|
11
|
+
"type": "registry:ui",
|
|
12
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { CheckIcon } from \"lucide-react\"\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\"\n\nfunction Checkbox({\n className,\n ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-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-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"grid place-content-center text-current transition-none\"\n >\n <CheckIcon className=\"size-3.5\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n )\n}\n\nexport { Checkbox }\n",
|
|
13
|
+
"target": "inertia/components/ui/checkbox.tsx"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "command",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"cn@0.3.0",
|
|
5
|
+
"cmdk@1.1.1",
|
|
6
|
+
"lucide-react@1.47.0"
|
|
7
|
+
],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"dialog"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/ui/command.tsx",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { cn } from \"cn\"\nimport { SearchIcon } from \"lucide-react\"\n\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"~/components/ui/dialog\"\n\nfunction Command({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive>) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandDialog({\n title = \"قائمة الأوامر\",\n description = \"ابحث عن أمر لتنفيذه\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof Dialog> & {\n title?: string\n description?: string\n className?: string\n showCloseButton?: boolean\n}) {\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent\n className={cn(\"overflow-hidden p-0\", className)}\n showCloseButton={showCloseButton}\n >\n <Command className=\"**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction CommandInput({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n return (\n <div\n data-slot=\"command-input-wrapper\"\n className=\"flex h-9 items-center gap-2 border-b px-3\"\n >\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nfunction CommandList({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.List>) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\n \"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandEmpty({\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n data-slot=\"command-empty\"\n className=\"py-6 text-center text-sm\"\n {...props}\n />\n )\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"-mx-1 h-px bg-border\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandItem({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\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 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]: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 )\n}\n\nfunction CommandShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\n \"ms-auto text-xs tracking-widest text-muted-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n}\n",
|
|
16
|
+
"target": "inertia/components/ui/command.tsx"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"type": "registry:ui",
|
|
20
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "confirm-action",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"dialog",
|
|
6
|
+
"button"
|
|
7
|
+
],
|
|
8
|
+
"dependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "registry/ui/confirm-action.tsx",
|
|
12
|
+
"type": "registry:ui",
|
|
13
|
+
"content": "import { useState } from 'react'\nimport { Button } from '~/components/ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '~/components/ui/dialog'\n\ntype Confirmation = {\n title: string\n description: string\n action: () => void\n destructive?: boolean\n label?: string\n}\n\n/** Keep confirmation separate from editor state; cancel never sends a mutation. */\nexport function useConfirmAction() {\n const [pending, confirm] = useState<Confirmation | null>(null)\n return {\n confirm,\n confirmation: (\n <Dialog\n mode=\"confirm\"\n open={Boolean(pending)}\n onOpenChange={(open) => !open && confirm(null)}\n >\n <DialogContent dir=\"rtl\" className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{pending?.title ?? 'تأكيد الإجراء'}</DialogTitle>\n <DialogDescription>{pending?.description}</DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => confirm(null)}>\n تراجع\n </Button>\n <Button\n type=\"button\"\n variant={pending?.destructive ? 'destructive' : 'default'}\n onClick={() => {\n const action = pending?.action\n confirm(null)\n action?.()\n }}\n >\n {pending?.label ?? 'تأكيد التغيير'}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n ),\n }\n}\n",
|
|
14
|
+
"target": "inertia/components/ui/confirm-action.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "data-table",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"registryDependencies": [
|
|
5
|
+
"button",
|
|
6
|
+
"input",
|
|
7
|
+
"badge",
|
|
8
|
+
"can",
|
|
9
|
+
"resource-actions",
|
|
10
|
+
"resource-field",
|
|
11
|
+
"resource-value",
|
|
12
|
+
"ui-preferences"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": [
|
|
15
|
+
"@tanstack/react-table@9.2.4",
|
|
16
|
+
"@tanstack/react-virtual@3.14.13",
|
|
17
|
+
"axios@1.19.0",
|
|
18
|
+
"lucide-react@1.47.0"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "registry/ui/data-table.tsx",
|
|
23
|
+
"type": "registry:ui",
|
|
24
|
+
"content": "import { useRef, useState, type ComponentRef, type ReactNode } from 'react'\nimport { InfiniteScroll, router, usePage } from '@inertiajs/react'\nimport { Link } from '@adonisjs/inertia/react'\nimport { tableFeatures, useTable } from '@tanstack/react-table'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport axios from 'axios'\nimport {\n ArrowDown,\n ArrowDownUp,\n ArrowUp,\n Download,\n Inbox,\n Loader2,\n Bookmark,\n BookmarkPlus,\n Search,\n SlidersHorizontal,\n X,\n} from 'lucide-react'\nimport type {\n ResourceDescription,\n ResourceField,\n ResourceList,\n SavedView,\n SavedViewQuery,\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 { Input } from '~/components/ui/input'\nimport { ResourceActions } from '~/components/ui/resource-actions'\nimport { FieldControl } from '~/components/ui/resource-field'\nimport {\n csvCell,\n displayValue,\n parseValue,\n relationLabel,\n type LookupOptions,\n type Option,\n} from '~/components/ui/resource-value'\n\n/** Rows above this count render through the virtualizer (plan section 13). */\nexport const VIRTUAL_ROWS = 200\nconst ROW_HEIGHT = 52\nconst PAGE_SIZE = 100\nconst features = tableFeatures({})\ntype Query = Record<string, string | undefined>\n\nexport function DataTable({\n resource,\n result,\n lookups = {},\n savedViews = [],\n}: {\n resource: ResourceDescription\n result: ResourceList\n lookups?: LookupOptions\n savedViews?: SavedView[]\n}) {\n const { calendar } = useUiPreferences()\n const { url } = usePage()\n const query = new URL(url, 'http://localhost').searchParams\n const [search, setSearch] = useState(query.get('search') ?? '')\n const [failure, setFailure] = useState('')\n const container = useRef<HTMLDivElement>(null)\n const scroller = useRef<ComponentRef<typeof InfiniteScroll>>(null)\n // Inertia merges `result.data` page by page; permissions and labels of earlier pages accumulate here.\n const store = useRef<{\n source: ResourceList | null\n permissions: ResourceList['permissions']\n related: ResourceList['related']\n }>({ source: null, permissions: {}, related: {} })\n if (store.current.source !== result) {\n const related = { ...store.current.related }\n for (const [key, rows] of Object.entries(result.related))\n related[key] = [\n ...new Map([...(related[key] ?? []), ...rows].map((row) => [String(row.id), row])).values(),\n ]\n store.current = {\n source: result,\n permissions: { ...store.current.permissions, ...result.permissions },\n related,\n }\n }\n const { permissions, related } = store.current\n const fields = resource.fields.filter(\n (field) => resource.list.includes(field.key) && field.type !== 'hasMany'\n )\n const filterable = fields.filter((field) => field.filterable)\n const table = useTable({\n features,\n data: result.data,\n columns: fields.map((field) => ({ accessorKey: field.key, id: field.key })),\n getRowId: (row) => String(row.id),\n })\n const rows = table.getRowModel().rows\n const virtualized = rows.length > VIRTUAL_ROWS\n const virtual = useVirtualizer({\n count: rows.length,\n getScrollElement: () => container.current,\n estimateSize: () => ROW_HEIGHT,\n overscan: 10,\n enabled: virtualized,\n getItemKey: (index) => rows[index].id,\n })\n const items = virtualized\n ? virtual.getVirtualItems()\n : rows.map((_, index) => ({ index, start: 0, end: 0 }))\n const navigate = (changes: Query) => {\n const next = new URLSearchParams(query)\n next.delete('cursor')\n for (const [key, value] of Object.entries(changes))\n value ? next.set(key, value) : next.delete(key)\n const suffix = next.toString()\n router.get(\n `/resources/${resource.name}${suffix ? `?${suffix}` : ''}`,\n {},\n { preserveScroll: true }\n )\n }\n const sort = query.get('sort')\n const direction = query.get('direction') === 'desc' ? 'desc' : 'asc'\n const activeFilters = filterable.filter((field) => query.get(`filters[${field.key}]`))\n const csv = () => {\n const content = [\n fields.map((field) => field.label.ar),\n ...result.data.map((row) =>\n fields.map((field) => displayValue(field, row, related, lookups, calendar))\n ),\n ]\n const href = URL.createObjectURL(\n new Blob(['' + content.map((row) => row.map(csvCell).join(',')).join('\\r\\n')], {\n type: 'text/csv;charset=utf-8',\n })\n )\n const link = document.createElement('a')\n link.href = href\n link.download = `${resource.label}.csv`\n link.click()\n URL.revokeObjectURL(href)\n }\n const refresh = () => router.visit(url, { preserveScroll: true })\n const currentQuery = (): SavedViewQuery => {\n const view: SavedViewQuery = {}\n const text = query.get('search')\n if (text) view.search = text\n if (sort) {\n view.sort = sort\n view.direction = direction\n }\n const filters: Record<string, string> = {}\n for (const field of filterable) {\n const value = query.get(`filters[${field.key}]`)\n if (value) filters[field.key] = value\n }\n if (Object.keys(filters).length) view.filters = filters\n return view\n }\n const applyView = (view: SavedView) => {\n const next = new URLSearchParams()\n if (view.query.search) next.set('search', view.query.search)\n if (view.query.sort) {\n next.set('sort', view.query.sort)\n next.set('direction', view.query.direction ?? 'asc')\n }\n for (const [key, value] of Object.entries(view.query.filters ?? {}))\n if (value !== null) next.set(`filters[${key}]`, String(value))\n setSearch(view.query.search ?? '')\n const suffix = next.toString()\n router.get(\n `/resources/${resource.name}${suffix ? `?${suffix}` : ''}`,\n {},\n { preserveScroll: true }\n )\n }\n return (\n <section\n aria-label={`قائمة ${resource.label}`}\n className=\"min-w-0 overflow-hidden rounded-xl border border-border bg-white shadow-[0_2px_10px_#1c302804]\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-4 border-b px-5 py-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <h2 className=\"text-sm font-semibold\">سجل {resource.label}</h2>\n <Badge variant=\"secondary\" className=\"font-normal tabular-nums\">\n {rows.length} معروض\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n {result.meta.estimatedTotal !== undefined\n ? `نحو ${result.meta.estimatedTotal} سجل ضمن نطاقك`\n : 'حسب نطاق صلاحياتك'}\n </span>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={csv} disabled={!rows.length}>\n <Download size={15} />\n تصدير المعروض\n </Button>\n </div>\n {(resource.searchable || filterable.length > 0) && (\n <SavedViewsBar\n resource={resource}\n views={savedViews}\n current={currentQuery}\n onApply={applyView}\n onChanged={refresh}\n onFailure={setFailure}\n />\n )}\n {(resource.searchable || filterable.length > 0) && (\n <div className=\"flex flex-wrap items-end gap-3 border-b px-5 py-4\">\n {resource.searchable && (\n <form\n role=\"search\"\n className=\"relative w-full max-w-sm\"\n onSubmit={(event) => {\n event.preventDefault()\n navigate({ search })\n }}\n >\n <Search\n className=\"pointer-events-none absolute start-3 top-2.5 text-muted-foreground\"\n size={16}\n />\n <Input\n aria-label={`البحث في ${resource.label}`}\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"ابحث في السجلات...\"\n className=\"h-9 bg-background/60 ps-10 text-xs\"\n />\n </form>\n )}\n {filterable.length > 0 && (\n <span className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <SlidersHorizontal size={15} />\n تصفية\n </span>\n )}\n {filterable.map((field) => (\n <Filter\n key={field.key}\n field={field}\n value={query.get(`filters[${field.key}]`) ?? ''}\n options={lookups[field.key]}\n onApply={(value) => navigate({ [`filters[${field.key}]`]: value })}\n />\n ))}\n {activeFilters.length > 0 && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() =>\n navigate(\n Object.fromEntries(\n activeFilters.map((field) => [`filters[${field.key}]`, undefined])\n )\n )\n }\n >\n <X size={14} />\n مسح التصفية\n </Button>\n )}\n </div>\n )}\n <InfiniteScroll\n ref={(instance) => {\n scroller.current = instance\n }}\n data=\"result\"\n manual\n onlyNext\n preserveUrl\n params={{\n data: { limit: PAGE_SIZE },\n onBefore: () => {\n setFailure('')\n },\n onHttpException: () => {\n setFailure('تعذر تحميل السجلات التالية. أعد المحاولة.')\n return false\n },\n onNetworkError: () => {\n setFailure('تعذر الاتصال بالخادم. تحقق من الاتصال ثم أعد المحاولة.')\n return false\n },\n }}\n next={({ fetch, loading, hasMore }) => (\n <div className=\"flex flex-wrap items-center justify-center gap-4 border-t px-5 py-3 text-xs text-muted-foreground\">\n {failure && (\n <p role=\"alert\" className=\"text-destructive\">\n {failure}\n </p>\n )}\n {loading ? (\n <span role=\"status\" className=\"inline-flex items-center gap-2\">\n <Loader2 size={14} className=\"animate-spin\" />\n جارٍ تحميل المزيد...\n </span>\n ) : hasMore ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={fetch}>\n تحميل المزيد\n </Button>\n ) : (\n <span>{rows.length ? 'نهاية السجلات' : ''}</span>\n )}\n </div>\n )}\n >\n <div\n ref={container}\n data-slot=\"table-scroll\"\n className=\"max-h-[600px] overflow-auto\"\n onScroll={(event) => {\n const element = event.currentTarget\n if (\n !failure &&\n element.scrollHeight - element.scrollTop - element.clientHeight < 200 &&\n scroller.current?.hasNext()\n )\n scroller.current.fetchNext()\n }}\n >\n <table\n className=\"w-full min-w-[720px] text-start text-sm\"\n aria-rowcount={rows.length + 1}\n aria-label={resource.label}\n >\n <thead className=\"sticky top-0 z-10 bg-muted text-xs text-muted-foreground\">\n <tr>\n {fields.map((field) => (\n <th\n key={field.key}\n scope=\"col\"\n className=\"h-11 whitespace-nowrap px-5 text-start font-medium\"\n aria-sort={\n sort === field.key\n ? direction === 'desc'\n ? 'descending'\n : 'ascending'\n : undefined\n }\n >\n {field.sortable ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-2 hover:text-foreground\"\n onClick={() =>\n navigate({\n sort: field.key,\n direction: sort === field.key && direction === 'asc' ? 'desc' : 'asc',\n })\n }\n >\n {field.label.ar}\n {sort === field.key ? (\n direction === 'desc' ? (\n <ArrowDown size={12} />\n ) : (\n <ArrowUp size={12} />\n )\n ) : (\n <ArrowDownUp size={12} className=\"opacity-60\" />\n )}\n </button>\n ) : (\n field.label.ar\n )}\n </th>\n ))}\n <th scope=\"col\" className=\"px-5 text-start font-medium\">\n الإجراءات\n </th>\n </tr>\n </thead>\n <tbody>\n {virtualized && items[0] && items[0].start > 0 && (\n <tr aria-hidden=\"true\">\n <td colSpan={fields.length + 1} style={{ height: items[0].start }} />\n </tr>\n )}\n {items.map(({ index }) => {\n const row = rows[index].original\n const id = String(row.id)\n return (\n <tr\n key={id}\n aria-rowindex={index + 2}\n className=\"border-b last:border-0 hover:bg-background\"\n style={{ height: ROW_HEIGHT }}\n >\n {fields.map((field) => (\n <Cell\n key={field.key}\n field={field}\n row={row}\n related={related}\n lookups={lookups}\n />\n ))}\n <td className=\"whitespace-nowrap px-5 text-xs\">\n <span className=\"inline-flex items-center gap-1\">\n <Button variant=\"ghost\" size=\"sm\" asChild>\n <Link\n href={`/resources/${resource.name}/${id}`}\n aria-label={`عرض السجل ${id}`}\n >\n عرض\n </Link>\n </Button>\n <Can permissions={permissions[id] ?? {}} action=\"update\">\n <Button variant=\"ghost\" size=\"sm\" asChild>\n <Link\n href={`/resources/${resource.name}/${id}/edit`}\n aria-label={`تعديل السجل ${id}`}\n >\n تعديل\n </Link>\n </Button>\n </Can>\n <ResourceActions\n resource={resource.name}\n id={id}\n version={row.version ?? undefined}\n permissions={permissions[id] ?? {}}\n label={`السجل ${id}`}\n size=\"sm\"\n variant=\"ghost\"\n onDone={refresh}\n />\n </span>\n </td>\n </tr>\n )\n })}\n {virtualized && (\n <tr aria-hidden=\"true\">\n <td\n colSpan={fields.length + 1}\n style={{\n height: Math.max(0, virtual.getTotalSize() - (items.at(-1)?.end ?? 0)),\n }}\n />\n </tr>\n )}\n {!rows.length && (\n <tr>\n <td colSpan={fields.length + 1} className=\"py-16 text-center\">\n <Inbox\n className=\"mx-auto mb-3 text-muted-foreground\"\n size={30}\n strokeWidth={1.3}\n />\n <p className=\"text-sm font-medium\">لا توجد سجلات مطابقة</p>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {query.get('search') || activeFilters.length\n ? 'غيّر البحث أو التصفية لعرض سجلات أخرى.'\n : 'لم تُضف سجلات ضمن نطاقك بعد.'}\n </p>\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n </InfiniteScroll>\n </section>\n )\n}\n\n/** Presets over the same query keys the list already accepts; the kit validates every one. */\nfunction SavedViewsBar({\n resource,\n views,\n current,\n onApply,\n onChanged,\n onFailure,\n}: {\n resource: ResourceDescription\n views: SavedView[]\n current: () => SavedViewQuery\n onApply: (view: SavedView) => void\n onChanged: () => void\n onFailure: (message: string) => void\n}) {\n const [naming, setNaming] = useState(false)\n const [name, setName] = useState('')\n const [shared, setShared] = useState(false)\n const [busy, setBusy] = useState(false)\n const base = `/resources/${resource.name}/views`\n const save = async (event: React.FormEvent) => {\n event.preventDefault()\n setBusy(true)\n onFailure('')\n try {\n await axios.post(\n base,\n { name, query: current(), shared },\n { headers: { Accept: 'application/json' }, withXSRFToken: true }\n )\n setNaming(false)\n setName('')\n setShared(false)\n onChanged()\n } catch (error) {\n onFailure(\n axios.isAxiosError(error) && error.response?.data?.error?.message\n ? String(error.response.data.error.message)\n : 'تعذر حفظ العرض.'\n )\n } finally {\n setBusy(false)\n }\n }\n const remove = async (view: SavedView) => {\n setBusy(true)\n onFailure('')\n try {\n await axios.delete(`${base}/${view.id}`, {\n headers: { Accept: 'application/json' },\n withXSRFToken: true,\n })\n onChanged()\n } catch {\n onFailure('تعذر حذف العرض.')\n } finally {\n setBusy(false)\n }\n }\n return (\n <div className=\"flex flex-wrap items-center gap-2 border-b px-5 py-3\">\n <span className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Bookmark size={15} />\n العروض المحفوظة\n </span>\n {views.length === 0 && <span className=\"text-xs text-muted-foreground\">لا عروض بعد</span>}\n {views.map((view) => (\n <span\n key={view.id}\n className=\"flex items-center gap-1 rounded-full border border-border bg-background/60 ps-1 pe-3\"\n >\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 rounded-full px-2 text-xs\"\n onClick={() => onApply(view)}\n >\n {view.name}\n {!view.own && <span className=\"text-muted-foreground\"> · مشترك</span>}\n </Button>\n {view.own && (\n <button\n type=\"button\"\n aria-label={`حذف العرض ${view.name}`}\n disabled={busy}\n onClick={() => remove(view)}\n className=\"text-muted-foreground transition-colors hover:text-destructive\"\n >\n <X size={13} />\n </button>\n )}\n </span>\n ))}\n {naming ? (\n <form onSubmit={save} className=\"flex flex-wrap items-center gap-2\">\n <Input\n aria-label=\"اسم العرض\"\n value={name}\n autoFocus\n maxLength={60}\n onChange={(event) => setName(event.target.value)}\n placeholder=\"اسم العرض\"\n className=\"h-8 w-44 text-xs\"\n />\n <label className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={shared}\n onChange={(event) => setShared(event.target.checked)}\n />\n مشترك\n </label>\n <Button type=\"submit\" size=\"sm\" className=\"h-8\" disabled={busy || !name.trim()}>\n حفظ\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8\"\n onClick={() => setNaming(false)}\n >\n إلغاء\n </Button>\n </form>\n ) : (\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 text-xs\" onClick={() => setNaming(true)}>\n <BookmarkPlus size={14} />\n حفظ العرض الحالي\n </Button>\n )}\n </div>\n )\n}\n\nfunction Cell({\n field,\n row,\n related,\n lookups,\n}: {\n field: ResourceField\n row: SerializedRecord\n related: ResourceList['related']\n lookups: LookupOptions\n}) {\n const { calendar } = useUiPreferences()\n const text = displayValue(field, row, related, lookups, calendar)\n const numeric = ['money', 'integer', 'date', 'datetime'].includes(field.type)\n return (\n <td\n className={`max-w-[280px] truncate px-5 ${numeric ? 'tabular-nums' : ''}`}\n dir={numeric ? 'ltr' : undefined}\n title={text}\n >\n {field.type === 'boolean' && row[field.key] !== null && row[field.key] !== undefined ? (\n <Badge variant={row[field.key] ? 'default' : 'secondary'}>{text}</Badge>\n ) : (\n text\n )}\n </td>\n )\n}\n\nfunction Filter({\n field,\n value,\n options,\n onApply,\n}: {\n field: ResourceField\n value: string\n options?: Option[]\n onApply: (value: string | undefined) => void\n}): ReactNode {\n const [draft, setDraft] = useState(value)\n const id = `filter-${field.key}`\n const select = (choices: Option[]) => (\n <select\n id={id}\n className=\"h-9 rounded-md border border-input bg-white px-3 text-xs\"\n value={value}\n onChange={(event) => onApply(event.target.value || undefined)}\n >\n <option value=\"\">الكل</option>\n {choices.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n )\n return (\n <label htmlFor={id} className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n {field.label.ar}\n {field.type === 'lookup' ? (\n select(options ?? [])\n ) : field.type === 'boolean' ? (\n select([\n { value: 'true', label: 'نعم' },\n { value: 'false', label: 'لا' },\n ])\n ) : field.type === 'belongsTo' ? (\n <RelationFilter field={field} value={value} onApply={onApply} />\n ) : field.type === 'date' ? (\n <div className=\"w-56\">\n <FieldControl\n field={field}\n id={id}\n value={draft}\n label={`تصفية ${field.label.ar}`}\n onChange={(next) => {\n setDraft(next)\n try {\n const parsed = parseValue(field, next)\n onApply(parsed ? String(parsed) : undefined)\n } catch {\n // Keep partial or invalid input without issuing an invalid date query.\n }\n }}\n />\n </div>\n ) : (\n <Input\n id={id}\n className=\"h-9 w-44 text-xs\"\n value={draft}\n placeholder=\"اضغط Enter للتطبيق\"\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n onApply(draft || undefined)\n }\n }}\n />\n )}\n </label>\n )\n}\n\n/** Filters by a related record through the target list route, which enforces the target's own rules. */\nfunction RelationFilter({\n field,\n value,\n onApply,\n}: {\n field: ResourceField & { type: 'belongsTo' }\n value: string\n onApply: (value: string | undefined) => void\n}) {\n const [term, setTerm] = useState('')\n const [choices, setChoices] = useState<Option[]>([])\n const [busy, setBusy] = useState(false)\n const timer = useRef<ReturnType<typeof setTimeout>>(undefined)\n const load = async (search: string) => {\n setBusy(true)\n try {\n const response = await axios.get<ResourceList>(`/resources/${field.resource}`, {\n params: { limit: 50, estimate: 'false', ...(search ? { search } : {}) },\n headers: { Accept: 'application/json' },\n })\n setChoices(\n response.data.data.map((row) => ({\n value: String(row.id),\n label: relationLabel(row, row.id),\n }))\n )\n } catch {\n setChoices([])\n } finally {\n setBusy(false)\n }\n }\n return (\n <span className=\"flex items-center gap-2\">\n <Input\n id={`filter-${field.key}`}\n className=\"h-9 w-44 text-xs\"\n value={term}\n placeholder={value ? `#${value} · ابحث للتغيير` : 'ابحث في السجلات المرتبطة'}\n onChange={(event) => {\n setTerm(event.target.value)\n clearTimeout(timer.current)\n timer.current = setTimeout(() => void load(event.target.value), 300)\n }}\n onFocus={() => {\n if (!choices.length) void load('')\n }}\n />\n <select\n aria-label={`اختيار ${field.label.ar}`}\n className=\"h-9 max-w-44 rounded-md border border-input bg-white px-2 text-xs\"\n value={value}\n disabled={busy && !choices.length}\n onChange={(event) => onApply(event.target.value || undefined)}\n >\n <option value=\"\">{busy ? 'جارٍ البحث...' : 'الكل'}</option>\n {value && !choices.some((choice) => choice.value === value) && (\n <option value={value}>#{value}</option>\n )}\n {choices.map((choice) => (\n <option key={choice.value} value={choice.value}>\n {choice.label}\n </option>\n ))}\n </select>\n </span>\n )\n}\n",
|
|
25
|
+
"target": "inertia/components/ui/data-table.tsx"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json"
|
|
29
|
+
}
|