@adula/ui 0.2.0-alpha.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/build/calendar-date.json +1 -1
- package/build/data-table.json +5 -2
- package/build/manifest.json +69 -49
- package/build/record-assignments.json +28 -0
- package/build/record-collaboration.json +28 -0
- package/build/record-workflows.json +26 -0
- package/build/resource-actions.json +1 -1
- package/build/resource-field.json +1 -1
- package/build/resource-form.json +1 -1
- package/build/resource-import.json +24 -0
- package/build/resource-page.json +3 -2
- package/build/resource-show.json +5 -2
- package/build/resource-surface.json +1 -1
- package/build/resource-value.json +1 -1
- package/build/theme.css +8 -4
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"path": "registry/ui/resource-value.tsx",
|
|
15
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
|
|
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/**\n * A related record is labelled by the title the server computed from the resource's\n * `title` fields (`_title`, #47). Older servers send none; then the first readable string\n * of the record is used.\n */\nexport function relationLabel(row: SerializedRecord | undefined, fallback: unknown) {\n if (row && '_title' in row)\n return typeof row._title === 'string' && row._title\n ? row._title\n : fallback === null || fallback === undefined\n ? '—'\n : `#${fallback}`\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 'user':\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 case 'user':\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 (field.type === 'integer')\n return (\n <span className=\"tabular-nums\" dir=\"ltr\">\n {displayValue(field, row, related, lookups, calendar)}\n </span>\n )\n // Arabic dates carry Arabic month names and time markers; isolate them instead of forcing LTR.\n if (field.type === 'date' || field.type === 'datetime')\n return <bdi className=\"tabular-nums\">{displayValue(field, row, related, lookups, calendar)}</bdi>\n return <>{displayValue(field, row, related, lookups, calendar)}</>\n}\n",
|
|
17
17
|
"target": "inertia/components/ui/resource-value.tsx"
|
|
18
18
|
}
|
|
19
19
|
],
|
package/build/theme.css
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
@source "../";
|
|
8
8
|
|
|
9
9
|
@theme inline {
|
|
10
|
-
--font-sans: 'Noto Sans Arabic', sans-serif;
|
|
10
|
+
--font-sans: var(--brand-font, 'Noto Sans Arabic'), sans-serif;
|
|
11
11
|
--color-background: var(--background);
|
|
12
12
|
--color-foreground: var(--foreground);
|
|
13
13
|
--color-card: var(--card);
|
|
@@ -15,28 +15,32 @@
|
|
|
15
15
|
--color-popover: var(--card);
|
|
16
16
|
--color-popover-foreground: var(--foreground);
|
|
17
17
|
--color-primary: var(--primary);
|
|
18
|
-
--color-primary-foreground:
|
|
18
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
19
19
|
--color-secondary: var(--secondary);
|
|
20
20
|
--color-secondary-foreground: var(--foreground);
|
|
21
21
|
--color-muted: var(--secondary);
|
|
22
22
|
--color-muted-foreground: var(--muted-foreground);
|
|
23
23
|
--color-accent: var(--accent);
|
|
24
24
|
--color-accent-foreground: var(--primary);
|
|
25
|
-
--color-destructive:
|
|
25
|
+
--color-destructive: var(--destructive);
|
|
26
26
|
--color-border: var(--border);
|
|
27
27
|
--color-input: var(--border);
|
|
28
|
-
--color-ring:
|
|
28
|
+
--color-ring: var(--ring);
|
|
29
29
|
--radius-sm: calc(var(--radius) - 4px);
|
|
30
30
|
--radius-md: calc(var(--radius) - 2px);
|
|
31
31
|
--radius-lg: var(--radius);
|
|
32
32
|
--radius-xl: calc(var(--radius) + 4px);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* Identity tokens: override any of these (and --brand-font) in the project-owned brand.css. */
|
|
35
36
|
:root {
|
|
36
37
|
--background: #f4f6f2;
|
|
37
38
|
--foreground: #1c3028;
|
|
38
39
|
--card: #ffffff;
|
|
39
40
|
--primary: #185640;
|
|
41
|
+
--primary-foreground: #ffffff;
|
|
42
|
+
--destructive: #b64239;
|
|
43
|
+
--ring: #659981;
|
|
40
44
|
--secondary: #edf1eb;
|
|
41
45
|
--muted-foreground: #65766b;
|
|
42
46
|
--accent: #e5eee5;
|