@karni/ui 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +288 -2
  3. package/dist/index.cjs +9 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.js +4764 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/manifest.json +85 -0
  8. package/dist/mockServiceWorker.js +361 -0
  9. package/dist/src/auth/AuthGuard.d.ts +12 -0
  10. package/dist/src/auth/LoginForm.d.ts +29 -0
  11. package/dist/src/auth/engine.d.ts +99 -0
  12. package/dist/src/auth/provider.d.ts +43 -0
  13. package/dist/src/auth/storage.d.ts +26 -0
  14. package/dist/src/auth/types.d.ts +32 -0
  15. package/dist/src/auth/usePermission.d.ts +7 -0
  16. package/dist/src/components/brand/karni-mark.d.ts +21 -0
  17. package/dist/src/components/brand/mark-paths.d.ts +8 -0
  18. package/dist/src/components/chart/data-chart.d.ts +74 -0
  19. package/dist/src/components/empty-state/empty-state.d.ts +20 -0
  20. package/dist/src/components/file-upload/FileUpload.d.ts +44 -0
  21. package/dist/src/components/layout/layout.d.ts +55 -0
  22. package/dist/src/components/modal/modal.d.ts +45 -0
  23. package/dist/src/components/page/page.d.ts +29 -0
  24. package/dist/src/components/primitives/button.d.ts +44 -0
  25. package/dist/src/components/primitives/checkbox.d.ts +5 -0
  26. package/dist/src/components/primitives/dropdown-menu.d.ts +24 -0
  27. package/dist/src/components/primitives/icons.d.ts +11 -0
  28. package/dist/src/components/primitives/input.d.ts +4 -0
  29. package/dist/src/components/primitives/label.d.ts +5 -0
  30. package/dist/src/components/primitives/select.d.ts +63 -0
  31. package/dist/src/components/primitives/skeleton.d.ts +4 -0
  32. package/dist/src/components/primitives/switch.d.ts +5 -0
  33. package/dist/src/components/primitives/tabs.d.ts +10 -0
  34. package/dist/src/components/primitives/textarea.d.ts +4 -0
  35. package/dist/src/components/resource/ResourceDetail.d.ts +7 -0
  36. package/dist/src/components/resource/ResourceForm.d.ts +7 -0
  37. package/dist/src/components/resource/ResourceSelect.d.ts +7 -0
  38. package/dist/src/components/resource/ResourceTable.d.ts +11 -0
  39. package/dist/src/components/resource/audit-fields.d.ts +17 -0
  40. package/dist/src/components/resource/detail/ResourceDetail.d.ts +30 -0
  41. package/dist/src/components/resource/displayLabel.d.ts +12 -0
  42. package/dist/src/components/resource/form/ResourceForm.d.ts +50 -0
  43. package/dist/src/components/resource/form/field-inputs.d.ts +20 -0
  44. package/dist/src/components/resource/form/fixtures.d.ts +24 -0
  45. package/dist/src/components/resource/form/prepare.d.ts +54 -0
  46. package/dist/src/components/resource/history/ChangeHistory.d.ts +18 -0
  47. package/dist/src/components/resource/import/ResourceImport.d.ts +18 -0
  48. package/dist/src/components/resource/select/ResourceSelect.d.ts +36 -0
  49. package/dist/src/components/resource/select/use-debounced-value.d.ts +2 -0
  50. package/dist/src/components/resource/table/BulkActionsBar.d.ts +20 -0
  51. package/dist/src/components/resource/table/ExportButtons.d.ts +17 -0
  52. package/dist/src/components/resource/table/FilterBar.d.ts +29 -0
  53. package/dist/src/components/resource/table/PaginationControls.d.ts +19 -0
  54. package/dist/src/components/resource/table/ResourceTable.d.ts +72 -0
  55. package/dist/src/components/resource/table/demo-fixtures.d.ts +25 -0
  56. package/dist/src/components/resource/table/derive-columns.d.ts +33 -0
  57. package/dist/src/components/resource/table/table-utils.d.ts +24 -0
  58. package/dist/src/components/resource/table/use-debounced-value.d.ts +1 -0
  59. package/dist/src/components/stat/stat.d.ts +24 -0
  60. package/dist/src/components/toast/toast-store.d.ts +36 -0
  61. package/dist/src/components/toast/toaster.d.ts +33 -0
  62. package/dist/src/components/toast/use-toast.d.ts +24 -0
  63. package/dist/src/hooks/keys.d.ts +24 -0
  64. package/dist/src/hooks/multipart.d.ts +44 -0
  65. package/dist/src/hooks/params.d.ts +25 -0
  66. package/dist/src/hooks/types.d.ts +41 -0
  67. package/dist/src/hooks/useChangeHistory.d.ts +29 -0
  68. package/dist/src/hooks/useResourceImport.d.ts +107 -0
  69. package/dist/src/hooks/useResourceItem.d.ts +4 -0
  70. package/dist/src/hooks/useResourceList.d.ts +4 -0
  71. package/dist/src/hooks/useResourceMutations.d.ts +13 -0
  72. package/dist/src/index.d.ts +95 -0
  73. package/dist/src/lib/cn.d.ts +8 -0
  74. package/dist/src/lib/roles.d.ts +45 -0
  75. package/dist/src/lib/variants.d.ts +33 -0
  76. package/dist/src/schema/client.d.ts +90 -0
  77. package/dist/src/schema/parse.d.ts +101 -0
  78. package/dist/src/schema/provider.d.ts +13 -0
  79. package/dist/src/schema/types.d.ts +84 -0
  80. package/dist/src/schema/useSchema.d.ts +17 -0
  81. package/dist/src/schema/zod.d.ts +4 -0
  82. package/dist/styles.css +87 -0
  83. package/dist/tailwind-preset.cjs +2 -0
  84. package/dist/tailwind-preset.cjs.map +1 -0
  85. package/dist/tailwind-preset.d.ts +58 -0
  86. package/dist/tailwind-preset.js +64 -0
  87. package/dist/tailwind-preset.js.map +1 -0
  88. package/eslint-plugin/index.js +72 -0
  89. package/eslint-plugin/lib/docs.js +11 -0
  90. package/eslint-plugin/lib/glob.js +51 -0
  91. package/eslint-plugin/rules/max-page-lines.js +65 -0
  92. package/eslint-plugin/rules/no-deep-imports.js +61 -0
  93. package/eslint-plugin/rules/no-direct-http.js +128 -0
  94. package/eslint-plugin/rules/no-raw-elements.js +65 -0
  95. package/eslint-plugin/rules.json +26 -0
  96. package/package.json +153 -4
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Mutation body encoding (contract D2) + PATCH file-URL omission.
3
+ *
4
+ * D2: after the form's `transformSubmit` has run, a payload containing any
5
+ * top-level `File` (or an array containing one) is encoded as `FormData`;
6
+ * everything else is sent as JSON. FormData rules:
7
+ * - arrays append as repeated keys,
8
+ * - plain objects/arrays-of-objects (json fields) are JSON-stringified,
9
+ * - numbers/booleans are stringified,
10
+ * - `null`/`undefined` entries are OMITTED — multipart cannot express null
11
+ * (DRF would parse the string "null"); to null a field, send a JSON PATCH
12
+ * without the file field instead,
13
+ * - Content-Type is never set manually: fetch derives the multipart boundary.
14
+ *
15
+ * PATCH omission (D2): a string value on a file-kind field is the unchanged
16
+ * display URL the backend returned (D1: response-side `format: uri`); sending
17
+ * it back would 400 ("not a file"), so it is dropped from PATCH payloads.
18
+ * Field kinds come from the cached parsed schema when present; without it the
19
+ * payload passes through untouched (graceful degradation).
20
+ *
21
+ * Internal module: not re-exported from the barrel.
22
+ */
23
+ /** True when any top-level value is a File, or an array containing one (D2). */
24
+ export declare function containsFile(values: Record<string, unknown>): boolean;
25
+ /** Encodes a payload as FormData per the D2 rules above. */
26
+ export declare function toFormData(values: Record<string, unknown>): FormData;
27
+ /**
28
+ * D2 dispatch: FormData when a top-level File is present, the payload itself
29
+ * (JSON-serialized by the client) otherwise. Applies to create AND PATCH.
30
+ */
31
+ export declare function encodeBody(values: Record<string, unknown>): Record<string, unknown> | FormData;
32
+ /**
33
+ * Extracts the file-kind field names of `resource` from the cached parsed
34
+ * schema (`karniKeys.schema()`'s cache entry). Every access is guarded: an
35
+ * absent cache entry or an unexpected shape yields an empty set, never a
36
+ * throw.
37
+ */
38
+ export declare function fileFieldNames(cachedSchema: unknown, resource: string): ReadonlySet<string>;
39
+ /**
40
+ * Drops unchanged file values — strings (or arrays of strings) on file-kind
41
+ * fields are the backend's display URLs, not new uploads — from a PATCH
42
+ * payload (D2).
43
+ */
44
+ export declare function omitUnchangedFileUrls(values: Record<string, unknown>, fileFields: ReadonlySet<string>): Record<string, unknown>;
@@ -0,0 +1,25 @@
1
+ import { ResourceItemParams, ResourceListParams } from './types';
2
+ /** Canonical param value shape stored inside query keys (D4). */
3
+ export type NormalizedParamValue = string | readonly string[] | NormalizedParams;
4
+ export type NormalizedParams = {
5
+ readonly [key: string]: NormalizedParamValue;
6
+ };
7
+ /**
8
+ * Flattens ResourceListParams into the query record sent to the server.
9
+ * Explicit params win over a `filters` entry of the same name. Empty
10
+ * `search`/`ordering` strings and empty `expand` arrays are omitted (they
11
+ * describe "no constraint", and sending them would fragment the cache).
12
+ */
13
+ export declare function buildListQuery(params?: ResourceListParams): Record<string, string | readonly string[]>;
14
+ /** Detail requests only carry `expand` (api contract B9). */
15
+ export declare function buildItemQuery(params?: ResourceItemParams): Record<string, string>;
16
+ /**
17
+ * D4 normalization: drop undefined/null, sort keys recursively, stringify
18
+ * values as they hit the query string. The result is what `karniKeys` embeds
19
+ * in list keys.
20
+ */
21
+ export declare function normalizeParams(value: Record<string, unknown>): NormalizedParams;
22
+ /** `/events/` for resource "events" (the DRF router prefix, api B13). */
23
+ export declare function listPath(resource: string): string;
24
+ /** `/events/5/` — ids are encoded so UUIDs and slugs are safe. */
25
+ export declare function detailPath(resource: string, id: string | number): string;
@@ -0,0 +1,41 @@
1
+ import { QueryKey } from '@tanstack/react-query';
2
+ /** DRF PageNumberPagination page shape returned by every list endpoint. */
3
+ export interface Paginated<T> {
4
+ count: number;
5
+ next: string | null;
6
+ previous: string | null;
7
+ results: T[];
8
+ }
9
+ /**
10
+ * A single filter value as accepted by the backend's FilterSet (api B5/B6).
11
+ * Arrays become repeated query keys; `null`/`undefined` entries are dropped
12
+ * (they mean "no constraint", not "filter by null").
13
+ */
14
+ export type FilterValue = string | number | boolean | ReadonlyArray<string | number | boolean> | null | undefined;
15
+ /** Maps to page / page_size / ordering / search / expand + filter passthrough. */
16
+ export interface ResourceListParams {
17
+ /** 1-based page number (`page`). */
18
+ page?: number;
19
+ /** Page size (`page_size`). */
20
+ pageSize?: number;
21
+ /** DRF ordering expression, e.g. `"-created_at"` (`ordering`). */
22
+ ordering?: string;
23
+ /** Full-text search term (`search`); empty string is treated as unset. */
24
+ search?: string;
25
+ /** Extra filter params, spread flat into the query string. Explicit params above win on a name clash. */
26
+ filters?: Record<string, FilterValue>;
27
+ /** Relation paths to expand, comma-joined into `expand` (api B9). */
28
+ expand?: readonly string[];
29
+ }
30
+ export interface ResourceItemParams {
31
+ /** Relation paths to expand, comma-joined into `expand` (api B9). */
32
+ expand?: readonly string[];
33
+ }
34
+ export interface ResourceMutationsOptions {
35
+ /**
36
+ * Extra query keys invalidated after any successful mutation, on top of
37
+ * ["karni", resource]. Never pass the bare ["karni"] root (D4) — it would
38
+ * also invalidate the cached schema document.
39
+ */
40
+ invalidateKeys?: readonly QueryKey[];
41
+ }
@@ -0,0 +1,29 @@
1
+ import { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
2
+ export type ChangeAction = "create" | "update" | "delete" | "restore";
3
+ export interface ChangeLogEntry {
4
+ id: number;
5
+ timestamp: string;
6
+ actor: {
7
+ id: string | number;
8
+ email?: string;
9
+ } | null;
10
+ action: ChangeAction;
11
+ /** `{ field: [old, new] }`; a redacted field carries `"[redacted]"` on both sides. */
12
+ diff: Record<string, [unknown, unknown]>;
13
+ fields_changed: string[];
14
+ }
15
+ export interface ChangeHistoryPage {
16
+ count: number;
17
+ next: string | null;
18
+ previous: string | null;
19
+ results: ChangeLogEntry[];
20
+ }
21
+ export interface ChangeHistoryParams {
22
+ /** Entries per page (the backend caps it). Default: the API's page size. */
23
+ pageSize?: number;
24
+ page?: number;
25
+ }
26
+ /** The query key of one row's change history. */
27
+ export declare function changeHistoryKey(resource: string, id: string | number, params?: ChangeHistoryParams): readonly unknown[];
28
+ /** Fetches one row's field-level change history (`GET /<resource>/<id>/history/`) for models with `audit = True`. */
29
+ export declare function useChangeHistory(resource: string, id: string | number, params?: ChangeHistoryParams, options?: Omit<UseQueryOptions<ChangeHistoryPage>, "queryKey" | "queryFn">): UseQueryResult<ChangeHistoryPage>;
@@ -0,0 +1,107 @@
1
+ import { UseMutationResult, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
2
+ import { KarniClient } from '../schema/client';
3
+ import { ResourceListParams } from './types';
4
+ export type ExportFormat = "csv" | "xlsx";
5
+ export type ImportColumnKind = "text" | "integer" | "decimal" | "boolean" | "date" | "datetime" | "choice" | "relation" | "relations" | "json" | "file" | "other";
6
+ export interface ImportColumn {
7
+ name: string;
8
+ label: string;
9
+ kind: ImportColumnKind;
10
+ required: boolean;
11
+ readable: boolean;
12
+ writable: boolean;
13
+ choices: ReadonlyArray<{
14
+ value: string;
15
+ label: string;
16
+ }>;
17
+ /** `app_label.model` of a relation's target. */
18
+ target: string | null;
19
+ /** The target field a relation column carries (its natural key), else the id. */
20
+ lookup: string | null;
21
+ help: string;
22
+ /** The column an import matches existing rows on. */
23
+ key: boolean;
24
+ }
25
+ export interface ImportDescription {
26
+ fields: ImportColumn[];
27
+ /** The mapping the user last confirmed for this resource (header → field). */
28
+ mapping: Record<string, string>;
29
+ /** Files with more rows than this run in Celery; poll the job. */
30
+ inline_rows: number;
31
+ max_rows: number;
32
+ key: string | null;
33
+ }
34
+ export type ImportStatus = "uploaded" | "previewing" | "previewed" | "committing" | "done" | "failed";
35
+ export interface ImportResult {
36
+ rows_total: number;
37
+ created: number;
38
+ updated: number;
39
+ unchanged: number;
40
+ errors: number;
41
+ skipped: number;
42
+ }
43
+ export interface ImportProblem {
44
+ /** The line in the file (the header is line 1). */
45
+ row: number;
46
+ field: string;
47
+ message: string;
48
+ }
49
+ export interface ImportJob {
50
+ id: string;
51
+ model: string | null;
52
+ status: ImportStatus;
53
+ file_name: string;
54
+ format: string;
55
+ columns: string[];
56
+ mapping: Record<string, string>;
57
+ rows_total: number;
58
+ rows_done: number;
59
+ async: boolean;
60
+ result: ImportResult | null;
61
+ problems: ImportProblem[];
62
+ error: string | null;
63
+ batch: number | null;
64
+ created_at: string | null;
65
+ fields?: ImportColumn[];
66
+ }
67
+ /** The query key of a resource's import description. */
68
+ export declare function importDescribeKey(resource: string): readonly unknown[];
69
+ /** The query key of one import job. */
70
+ export declare function importJobKey(resource: string, jobId: string): readonly unknown[];
71
+ /** True while an import job is previewing or committing (the states worth polling). */
72
+ export declare function isImportInFlight(job: ImportJob | undefined): boolean;
73
+ /**
74
+ * Fetches `GET /<resource>/export/?format=…` with the list's query and saves
75
+ * it through a temporary object URL. Resolves with the file name. Throws the
76
+ * `KarniApiError` the backend answered (a 400 with code E803 when the export
77
+ * is larger than `KARNI_EXPORT_MAX_ROWS`).
78
+ */
79
+ export declare function downloadResourceExport(client: KarniClient, resource: string, params: ResourceListParams | undefined, format?: ExportFormat): Promise<string>;
80
+ /** Hands a blob to the browser as a download (a no-op outside a DOM, e.g. SSR). */
81
+ export declare function saveBlob(blob: Blob, filename: string): void;
82
+ /** Fetches `GET /<resource>/import/template/?format=…` and saves it (the headers, an example, allowed values). */
83
+ export declare function downloadImportTemplate(client: KarniClient, resource: string, format?: ExportFormat): Promise<string>;
84
+ /** Fetches what a resource's import accepts: its columns, required fields and allowed values. */
85
+ export declare function useImportDescribe(resource: string, options?: Omit<UseQueryOptions<ImportDescription>, "queryKey" | "queryFn">): UseQueryResult<ImportDescription>;
86
+ export interface UseImportJobOptions {
87
+ /** Polling interval while the job is previewing/committing. Default 1500 ms. */
88
+ pollMs?: number;
89
+ }
90
+ /** Fetches one import job and polls it while it runs. */
91
+ export declare function useImportJob(resource: string, jobId: string | undefined, options?: UseImportJobOptions): UseQueryResult<ImportJob>;
92
+ export interface UploadImportInput {
93
+ file: File;
94
+ /** An explicit mapping; otherwise the backend suggests or remembers one. */
95
+ mapping?: Record<string, string>;
96
+ }
97
+ export interface PreviewImportInput {
98
+ jobId: string;
99
+ mapping: Record<string, string>;
100
+ }
101
+ export interface ImportActions {
102
+ upload: UseMutationResult<ImportJob, unknown, UploadImportInput>;
103
+ preview: UseMutationResult<ImportJob, unknown, PreviewImportInput>;
104
+ commit: UseMutationResult<ImportJob, unknown, string>;
105
+ }
106
+ /** Upload, preview and commit mutations for a resource's import jobs. */
107
+ export declare function useImportActions(resource: string): ImportActions;
@@ -0,0 +1,4 @@
1
+ import { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
2
+ import { ResourceItemParams } from './types';
3
+ /** Fetches one row of a resource (`GET /<resource>/<id>/`), optionally expanded. */
4
+ export declare function useResourceItem<T = Record<string, unknown>>(resource: string, id: string | number, params?: ResourceItemParams, options?: Omit<UseQueryOptions<T>, "queryKey" | "queryFn">): UseQueryResult<T>;
@@ -0,0 +1,4 @@
1
+ import { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
2
+ import { Paginated, ResourceListParams } from './types';
3
+ /** Fetches one page of a resource's list (`GET /<resource>/`) with filters, search, ordering and `?expand=`. */
4
+ export declare function useResourceList<T = Record<string, unknown>>(resource: string, params?: ResourceListParams, options?: Omit<UseQueryOptions<Paginated<T>>, "queryKey" | "queryFn">): UseQueryResult<Paginated<T>>;
@@ -0,0 +1,13 @@
1
+ import { UseMutationResult } from '@tanstack/react-query';
2
+ import { ResourceMutationsOptions } from './types';
3
+ /** Result bundle — the three mutations, each a full TanStack mutation object. */
4
+ export interface ResourceMutations<T> {
5
+ create: UseMutationResult<T, unknown, Record<string, unknown>>;
6
+ update: UseMutationResult<T, unknown, {
7
+ id: string | number;
8
+ values: Record<string, unknown>;
9
+ }>;
10
+ remove: UseMutationResult<void, unknown, string | number>;
11
+ }
12
+ /** Create, update and remove mutations for a resource; each success refreshes the resource's cached lists and items. */
13
+ export declare function useResourceMutations<T = Record<string, unknown>>(resource: string, options?: ResourceMutationsOptions): ResourceMutations<T>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @karni/ui public barrel.
3
+ *
4
+ * Every public name of the package is re-exported here, explicitly, so that
5
+ * scripts/generate-manifest.mjs can parse this file and emit dist/manifest.json
6
+ * (contract D7). Keep exports in the explicit `export { ... } from` /
7
+ * `export type { ... } from` form — no `export *`.
8
+ */
9
+ export { cn } from './lib/cn';
10
+ export { registerComponentRole, getRoleRegistry } from './lib/roles';
11
+ export type { RegisterComponentRoleOptions } from './lib/roles';
12
+ export { variants } from './lib/variants';
13
+ export type { VariantShape, VariantSelection, VariantsConfig, VariantFn, VariantProps, } from './lib/variants';
14
+ export { Button, buttonVariants } from './components/primitives/button';
15
+ export type { ButtonProps, ButtonVariantProps } from './components/primitives/button';
16
+ export { Input } from './components/primitives/input';
17
+ export type { InputProps } from './components/primitives/input';
18
+ export { Textarea } from './components/primitives/textarea';
19
+ export type { TextareaProps } from './components/primitives/textarea';
20
+ export { Select, SelectRoot, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, KARNI_SELECT_EMPTY, } from './components/primitives/select';
21
+ export type { SelectOption, SelectProps } from './components/primitives/select';
22
+ export { Checkbox } from './components/primitives/checkbox';
23
+ export type { CheckboxProps } from './components/primitives/checkbox';
24
+ export { Switch } from './components/primitives/switch';
25
+ export type { SwitchProps } from './components/primitives/switch';
26
+ export { Label } from './components/primitives/label';
27
+ export type { LabelProps } from './components/primitives/label';
28
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './components/primitives/tabs';
29
+ export { DropdownMenu, DropdownMenuTrigger, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator, } from './components/primitives/dropdown-menu';
30
+ export { Skeleton } from './components/primitives/skeleton';
31
+ export type { SkeletonProps } from './components/primitives/skeleton';
32
+ export { ResourceTable } from './components/resource/ResourceTable';
33
+ export type { ResourceTableProps, ResourceColumn, BulkAction, } from './components/resource/ResourceTable';
34
+ export { ResourceForm } from './components/resource/ResourceForm';
35
+ export type { ResourceFormProps, FieldOverride, KarniFieldProps, } from './components/resource/ResourceForm';
36
+ export { ResourceDetail } from './components/resource/ResourceDetail';
37
+ export type { ResourceDetailProps, ResourceDetailFieldOverride, } from './components/resource/ResourceDetail';
38
+ export { ResourceSelect } from './components/resource/ResourceSelect';
39
+ export type { ResourceSelectProps } from './components/resource/ResourceSelect';
40
+ export { ChangeHistory } from './components/resource/history/ChangeHistory';
41
+ export type { ChangeHistoryProps } from './components/resource/history/ChangeHistory';
42
+ export { useChangeHistory, changeHistoryKey } from './hooks/useChangeHistory';
43
+ export type { ChangeAction, ChangeLogEntry, ChangeHistoryPage, ChangeHistoryParams, } from './hooks/useChangeHistory';
44
+ export { ResourceImport } from './components/resource/import/ResourceImport';
45
+ export type { ResourceImportProps } from './components/resource/import/ResourceImport';
46
+ export { useImportDescribe, useImportJob, useImportActions, downloadResourceExport, downloadImportTemplate, importDescribeKey, importJobKey, isImportInFlight, } from './hooks/useResourceImport';
47
+ export type { ExportFormat, ImportActions, ImportColumn, ImportColumnKind, ImportDescription, ImportJob, ImportProblem, ImportResult, ImportStatus, PreviewImportInput, UploadImportInput, UseImportJobOptions, } from './hooks/useResourceImport';
48
+ export { displayLabel } from './components/resource/displayLabel';
49
+ export { AUDIT_FIELDS } from './components/resource/audit-fields';
50
+ export { karniKeys } from './hooks/keys';
51
+ export { useResourceList } from './hooks/useResourceList';
52
+ export { useResourceItem } from './hooks/useResourceItem';
53
+ export { useResourceMutations } from './hooks/useResourceMutations';
54
+ export type { ResourceMutations } from './hooks/useResourceMutations';
55
+ export type { Paginated, FilterValue, ResourceListParams, ResourceItemParams, ResourceMutationsOptions, } from './hooks/types';
56
+ export { createKarniClient, KarniApiError } from './schema/client';
57
+ export type { KarniClient, KarniClientOptions, KarniQueryValue, KarniRequestOptions, } from './schema/client';
58
+ export { KarniProvider, useKarniClient } from './schema/provider';
59
+ export type { KarniProviderProps } from './schema/provider';
60
+ export { useSchema } from './schema/useSchema';
61
+ export type { UseSchemaResult } from './schema/useSchema';
62
+ export { zodFor } from './schema/zod';
63
+ export type { FieldKind, RelationRef, FieldDef, OperationDef, ResourceSchema, } from './schema/types';
64
+ export { AuthProvider, useAuth } from './auth/provider';
65
+ export type { AuthProviderProps, AuthContextValue } from './auth/provider';
66
+ export { AuthGuard } from './auth/AuthGuard';
67
+ export type { AuthGuardProps } from './auth/AuthGuard';
68
+ export { LoginForm } from './auth/LoginForm';
69
+ export type { LoginFormProps } from './auth/LoginForm';
70
+ export { usePermission } from './auth/usePermission';
71
+ export type { PermissionHelpers } from './auth/usePermission';
72
+ export { createLocalStorageTokenStorage, createMemoryTokenStorage } from './auth/storage';
73
+ export type { TokenStorage } from './auth/storage';
74
+ export type { KarniUser, AuthStatus, LoginResult } from './auth/types';
75
+ export { Page } from './components/page/page';
76
+ export type { PageProps } from './components/page/page';
77
+ export { Layout } from './components/layout/layout';
78
+ export type { LayoutProps, LayoutNavItem } from './components/layout/layout';
79
+ export { KarniMark } from './components/brand/karni-mark';
80
+ export type { KarniMarkProps } from './components/brand/karni-mark';
81
+ export { Modal, ModalClose } from './components/modal/modal';
82
+ export type { ModalProps } from './components/modal/modal';
83
+ export { Toaster, toastVariants } from './components/toast/toaster';
84
+ export type { ToasterProps, ToastVariantProps } from './components/toast/toaster';
85
+ export { useToast } from './components/toast/use-toast';
86
+ export type { UseToastResult } from './components/toast/use-toast';
87
+ export type { ToastHandle, ToastInstance, ToastOptions, ToastVariant, } from './components/toast/toast-store';
88
+ export { EmptyState } from './components/empty-state/empty-state';
89
+ export type { EmptyStateProps } from './components/empty-state/empty-state';
90
+ export { FileUpload } from './components/file-upload/FileUpload';
91
+ export type { FileUploadProps, FileUploadRejectReason } from './components/file-upload/FileUpload';
92
+ export { Stat } from './components/stat/stat';
93
+ export type { StatProps } from './components/stat/stat';
94
+ export { DataChart } from './components/chart/data-chart';
95
+ export type { DataChartProps, DataChartKind, DataChartPoint, DataChartSeries, DataChartTooltipContext, } from './components/chart/data-chart';
@@ -0,0 +1,8 @@
1
+ import { ClassValue } from 'clsx';
2
+ /**
3
+ * Merges class names: clsx semantics for conditionals plus tailwind-merge
4
+ * conflict resolution, so consumer-supplied `className` always wins over a
5
+ * component's own Tailwind classes. Every @karni/ui component funnels its
6
+ * `className` prop through this.
7
+ */
8
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Component role registry (enforcement contract §4 + F11).
3
+ *
4
+ * Every top-level component claims a semantic role ("data-table",
5
+ * "resource-form", …) at module scope. Two components claiming the same role
6
+ * is a conflict: it throws at import time (so vitest and the dev server both
7
+ * surface it) and `scripts/generate-manifest.mjs` fails the build on the same
8
+ * duplicate via a static scan of these call sites.
9
+ *
10
+ * Conventions:
11
+ * - The `role` argument MUST be a string literal — the build-time scan rejects
12
+ * any non-literal first argument.
13
+ * - Same role + same component name is idempotent (modules re-evaluate under
14
+ * HMR and across test files).
15
+ * - Replacing on purpose is explicit and must carry a reason:
16
+ * `{ replace: true, reason: "<why>" }` — an empty reason throws.
17
+ * - Under Vite HMR (`import.meta.hot`) an accidental conflict logs a
18
+ * console.error and replaces instead of throwing, so a rename mid-edit does
19
+ * not kill the dev server.
20
+ * - The registry reflects imported modules only (accepted v0.1 trade-off).
21
+ */
22
+ export interface RegisterComponentRoleOptions {
23
+ /** Replace an existing registration on purpose. Requires `reason`. */
24
+ replace?: boolean;
25
+ /** Why the replacement is intentional — required with `replace: true`. */
26
+ reason?: string;
27
+ }
28
+ /**
29
+ * Test-only override for HMR detection; pass `null` to restore the default.
30
+ * Not exported from the package barrel.
31
+ * @internal
32
+ */
33
+ export declare function __setHotDetectorForTests(detector: (() => boolean) | null): void;
34
+ /**
35
+ * Test-only registry reset. Not exported from the package barrel.
36
+ * @internal
37
+ */
38
+ export declare function __resetRoleRegistryForTests(): void;
39
+ /**
40
+ * Claims a UI role (such as `data-table`) for a component, so a second component with the same role fails the build. The first argument must be a string literal
41
+ * (build-scan convention, F11).
42
+ */
43
+ export declare function registerComponentRole(role: string, component: string, options?: RegisterComponentRoleOptions): void;
44
+ /** Snapshot of the current registry ({ role: component }) for tooling. */
45
+ export declare function getRoleRegistry(): Readonly<Record<string, string>>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Hand-rolled cva-style variant utility (no extra dependency).
3
+ *
4
+ * Every class string appears whole in source (contract D7): a variant map
5
+ * value is a complete Tailwind class list, selected — never concatenated —
6
+ * at call time and merged through cn().
7
+ */
8
+ /** A map of variant group name → allowed value → whole Tailwind class string. */
9
+ export type VariantShape = Record<string, Record<string, string>>;
10
+ /** The props accepted for a given variant shape (each group optional). */
11
+ export type VariantSelection<V extends VariantShape> = {
12
+ [K in keyof V]?: Extract<keyof V[K], string> | null;
13
+ };
14
+ export interface VariantsConfig<V extends VariantShape> {
15
+ /** Classes always applied, before any variant classes. */
16
+ base?: string;
17
+ variants: V;
18
+ defaultVariants?: VariantSelection<V>;
19
+ /** Extra classes applied when every listed variant value is selected. */
20
+ compoundVariants?: ReadonlyArray<VariantSelection<V> & {
21
+ className: string;
22
+ }>;
23
+ }
24
+ export type VariantFn<V extends VariantShape> = (props?: VariantSelection<V> & {
25
+ className?: string;
26
+ }) => string;
27
+ /**
28
+ * Extracts the variant props of a variant function, for typing component
29
+ * props: `type ButtonVariantProps = VariantProps<typeof buttonVariants>`.
30
+ */
31
+ export type VariantProps<F> = F extends VariantFn<infer V> ? VariantSelection<V> : never;
32
+ /** Builds a typed class-name function from variant definitions, the way every themed component declares its looks. */
33
+ export declare function variants<V extends VariantShape>(config: VariantsConfig<V>): VariantFn<V>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * createKarniClient — the typed fetch wrapper every @karni/ui layer sits on
3
+ * (contract "Schema client" + D6).
4
+ *
5
+ * Responsibilities:
6
+ * - URL joining + query-string serialization (arrays become repeated keys,
7
+ * null/undefined are dropped),
8
+ * - JSON request/response handling; a FormData body is passed through
9
+ * untouched with NO manual Content-Type (the D2 multipart rule),
10
+ * - DRF error passthrough: non-2xx responses throw KarniApiError carrying the
11
+ * parsed body (field errors / detail / non_field_errors) verbatim,
12
+ * - auth hook points per D6's client-owns-tokens design: the auth layer (not
13
+ * React) registers token injection, extra headers (session CSRF),
14
+ * credentials mode and a 401 handler via `setAuthHooks`. The client calls
15
+ * `onUnauthorized` at most ONCE per request and retries at most ONCE — a
16
+ * request that 401s again after a successful refresh is surfaced, never
17
+ * re-triggers refresh (the single-flight refresh promise itself lives in
18
+ * the auth layer behind this hook).
19
+ */
20
+ export interface KarniClientOptions {
21
+ /**
22
+ * Origin (plus optional path prefix) requests resolve against, e.g.
23
+ * "https://api.example.com" or "" for same-origin. Paths that already start
24
+ * with the prefix are not double-prefixed, so operation paths taken from
25
+ * the OpenAPI document ("/api/events/") work with any baseUrl.
26
+ */
27
+ baseUrl: string;
28
+ /** Path of the OpenAPI document. Default "/api/schema/". */
29
+ schemaUrl?: string;
30
+ /** Initial access-token getter; `setAuthHooks` can replace it later (D6). */
31
+ getAccessToken?: () => string | null;
32
+ /** Custom fetch implementation (tests, SSR). Resolved per request. */
33
+ fetch?: typeof globalThis.fetch;
34
+ }
35
+ export type KarniQueryValue = string | number | boolean | ReadonlyArray<string | number | boolean> | null | undefined;
36
+ export interface KarniRequestOptions {
37
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
38
+ query?: Record<string, KarniQueryValue>;
39
+ /** JSON-serialized unless it is FormData (multipart rule D2). */
40
+ body?: unknown;
41
+ headers?: Record<string, string>;
42
+ signal?: AbortSignal;
43
+ /**
44
+ * Skip the 401 → onUnauthorized → retry cycle for this request. The auth
45
+ * layer sets it on its own login/refresh/logout calls so a failing refresh
46
+ * can never recurse into another refresh (D6).
47
+ */
48
+ skipAuthRefresh?: boolean;
49
+ }
50
+ /**
51
+ * Hook points the auth layer registers on the client (D6). Keys present in
52
+ * the object passed to `setAuthHooks` override the current value (an explicit
53
+ * `undefined` clears one); absent keys are kept.
54
+ */
55
+ export interface KarniAuthHooks {
56
+ /** Called before every request; a non-null return is sent as Bearer. */
57
+ getAccessToken?: () => string | null;
58
+ /**
59
+ * Called once when a request gets a 401 (unless `skipAuthRefresh`). Return
60
+ * true to retry the request once with freshly-injected credentials, false
61
+ * to surface the error. Single-flight refresh lives behind this hook.
62
+ */
63
+ onUnauthorized?: (error: KarniApiError) => Promise<boolean>;
64
+ /** Extra headers merged into every request (session-mode X-CSRFToken). */
65
+ getHeaders?: () => Record<string, string>;
66
+ /** fetch credentials mode; session mode sets "include". */
67
+ credentials?: RequestCredentials;
68
+ }
69
+ /** Thrown for non-2xx responses; `data` carries the parsed DRF error body. */
70
+ export declare class KarniApiError extends Error {
71
+ readonly status: number;
72
+ /** Parsed body when JSON (field errors, detail, non_field_errors), else text. */
73
+ readonly data: unknown;
74
+ constructor(status: number, data: unknown, message?: string);
75
+ }
76
+ export interface KarniClient {
77
+ readonly baseUrl: string;
78
+ readonly schemaUrl: string;
79
+ request<T>(path: string, options?: KarniRequestOptions): Promise<T>;
80
+ /**
81
+ * The same request (auth, query, 401 retry) returning the raw `Response`
82
+ * instead of a parsed body — for file downloads (`GET /<resource>/export/`).
83
+ * Non-2xx responses still throw `KarniApiError` with the parsed error body.
84
+ */
85
+ requestRaw(path: string, options?: KarniRequestOptions): Promise<Response>;
86
+ /** D6 hook point: the auth layer wires tokens/CSRF/401 handling here. */
87
+ setAuthHooks(hooks: KarniAuthHooks): void;
88
+ }
89
+ /** Creates the fetch client every hook in this package uses: base URL, JSON and multipart bodies, DRF errors as `KarniApiError`, and auth hooks. */
90
+ export declare function createKarniClient(options: KarniClientOptions): KarniClient;
@@ -0,0 +1,101 @@
1
+ import { ParsedSchema } from './types';
2
+ export interface KarniRelationExtension {
3
+ resource?: string;
4
+ many?: boolean;
5
+ }
6
+ export interface SchemaObject {
7
+ default?: unknown;
8
+ $ref?: string;
9
+ type?: string | readonly string[];
10
+ format?: string;
11
+ title?: string;
12
+ description?: string;
13
+ enum?: ReadonlyArray<string | number | null>;
14
+ nullable?: boolean;
15
+ readOnly?: boolean;
16
+ writeOnly?: boolean;
17
+ required?: readonly string[];
18
+ properties?: Record<string, SchemaObject>;
19
+ items?: SchemaObject;
20
+ allOf?: readonly SchemaObject[];
21
+ oneOf?: readonly SchemaObject[];
22
+ anyOf?: readonly SchemaObject[];
23
+ maxLength?: number;
24
+ minLength?: number;
25
+ minimum?: number;
26
+ maximum?: number;
27
+ "x-karni-relation"?: KarniRelationExtension;
28
+ [extension: string]: unknown;
29
+ }
30
+ export interface ParameterObject {
31
+ name?: string;
32
+ in?: string;
33
+ required?: boolean;
34
+ schema?: SchemaObject;
35
+ [extension: string]: unknown;
36
+ }
37
+ export interface MediaTypeObject {
38
+ schema?: SchemaObject;
39
+ [extension: string]: unknown;
40
+ }
41
+ export interface RequestBodyObject {
42
+ content?: Record<string, MediaTypeObject>;
43
+ required?: boolean;
44
+ [extension: string]: unknown;
45
+ }
46
+ export interface ResponseObject {
47
+ description?: string;
48
+ content?: Record<string, MediaTypeObject>;
49
+ [extension: string]: unknown;
50
+ }
51
+ export interface OperationObject {
52
+ operationId?: string;
53
+ parameters?: readonly ParameterObject[];
54
+ requestBody?: RequestBodyObject;
55
+ responses?: Record<string, ResponseObject>;
56
+ "x-karni-resource"?: string;
57
+ [extension: string]: unknown;
58
+ }
59
+ export interface PathItemObject {
60
+ get?: OperationObject;
61
+ post?: OperationObject;
62
+ put?: OperationObject;
63
+ patch?: OperationObject;
64
+ delete?: OperationObject;
65
+ [extension: string]: unknown;
66
+ }
67
+ export interface OpenApiDocument {
68
+ openapi?: string;
69
+ paths?: Record<string, PathItemObject>;
70
+ components?: {
71
+ schemas?: Record<string, SchemaObject>;
72
+ [extension: string]: unknown;
73
+ };
74
+ "x-karni-auth-mode"?: string;
75
+ [extension: string]: unknown;
76
+ }
77
+ /** "starts_at" → "Starts at"; "is_public" → "Is public". */
78
+ export declare function humanize(name: string): string;
79
+ /**
80
+ * Resolves $ref chains and unwraps single-member allOf, merging siblings over
81
+ * the resolved target (so `{allOf: [$ref StatusEnum], readOnly: true}` yields
82
+ * the enum schema plus readOnly). Cycle-guarded; a cycle resolves to {}.
83
+ */
84
+ export declare function resolveSchema(doc: OpenApiDocument, schema: SchemaObject | undefined, seen?: readonly string[]): SchemaObject | undefined;
85
+ /**
86
+ * Splits drf-spectacular's generated choice list ("* `draft` - Draft" per
87
+ * line, appended to a ChoiceField's description) into labels by value and
88
+ * the human help text that remains (undefined when nothing remains). The
89
+ * raw list is markdown a form would otherwise show verbatim as help text.
90
+ */
91
+ export declare function choiceLabels(description: string | undefined): {
92
+ labels?: Record<string, string>;
93
+ rest?: string;
94
+ };
95
+ /**
96
+ * Parses an OpenAPI document (fetched from the backend's schemaUrl) into the
97
+ * normalized registry. Tolerates anything it does not understand: unknown
98
+ * constructs become kind "unknown" fields, paths without x-karni-resource are
99
+ * ignored, and a malformed document yields an empty registry.
100
+ */
101
+ export declare function parseOpenApi(input: unknown): ParsedSchema;