@karni/ui 0.0.0-stage → 0.4.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.
Files changed (142) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +928 -2
  3. package/dist/index.cjs +9 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.js +8699 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/manifest.json +102 -0
  8. package/dist/mockServiceWorker.js +361 -0
  9. package/dist/script-BVJ9BbPm.cjs +2 -0
  10. package/dist/script-BVJ9BbPm.cjs.map +1 -0
  11. package/dist/script-D1fn4Dm4.js +6 -0
  12. package/dist/script-D1fn4Dm4.js.map +1 -0
  13. package/dist/src/auth/AuthGuard.d.ts +77 -0
  14. package/dist/src/auth/LoginForm.d.ts +63 -0
  15. package/dist/src/auth/engine.d.ts +101 -0
  16. package/dist/src/auth/provider.d.ts +43 -0
  17. package/dist/src/auth/storage.d.ts +26 -0
  18. package/dist/src/auth/types.d.ts +32 -0
  19. package/dist/src/auth/usePermission.d.ts +7 -0
  20. package/dist/src/components/brand/karni-mark.d.ts +21 -0
  21. package/dist/src/components/brand/mark-paths.d.ts +8 -0
  22. package/dist/src/components/chart/data-chart.d.ts +129 -0
  23. package/dist/src/components/empty-state/empty-state.d.ts +40 -0
  24. package/dist/src/components/file-upload/FileUpload.d.ts +108 -0
  25. package/dist/src/components/layout/layout.d.ts +150 -0
  26. package/dist/src/components/layout/route-focus.d.ts +43 -0
  27. package/dist/src/components/modal/modal.d.ts +86 -0
  28. package/dist/src/components/page/page.d.ts +60 -0
  29. package/dist/src/components/primitives/button.d.ts +45 -0
  30. package/dist/src/components/primitives/checkbox.d.ts +8 -0
  31. package/dist/src/components/primitives/dropdown-menu.d.ts +24 -0
  32. package/dist/src/components/primitives/icons.d.ts +19 -0
  33. package/dist/src/components/primitives/input.d.ts +7 -0
  34. package/dist/src/components/primitives/label.d.ts +8 -0
  35. package/dist/src/components/primitives/select.d.ts +94 -0
  36. package/dist/src/components/primitives/skeleton.d.ts +7 -0
  37. package/dist/src/components/primitives/switch.d.ts +8 -0
  38. package/dist/src/components/primitives/tabs.d.ts +10 -0
  39. package/dist/src/components/primitives/textarea.d.ts +7 -0
  40. package/dist/src/components/resource/ResourceDetail.d.ts +7 -0
  41. package/dist/src/components/resource/ResourceForm.d.ts +8 -0
  42. package/dist/src/components/resource/ResourceSelect.d.ts +9 -0
  43. package/dist/src/components/resource/ResourceTable.d.ts +17 -0
  44. package/dist/src/components/resource/audit-fields.d.ts +17 -0
  45. package/dist/src/components/resource/detail/ResourceDetail.d.ts +128 -0
  46. package/dist/src/components/resource/displayLabel.d.ts +12 -0
  47. package/dist/src/components/resource/empty-value.d.ts +17 -0
  48. package/dist/src/components/resource/form/ResourceForm.d.ts +109 -0
  49. package/dist/src/components/resource/form/field-inputs.d.ts +34 -0
  50. package/dist/src/components/resource/form/fixtures.d.ts +24 -0
  51. package/dist/src/components/resource/form/form-states.d.ts +87 -0
  52. package/dist/src/components/resource/form/prepare.d.ts +54 -0
  53. package/dist/src/components/resource/form/server-errors.d.ts +14 -0
  54. package/dist/src/components/resource/history/ChangeHistory.d.ts +60 -0
  55. package/dist/src/components/resource/import/ResourceImport.d.ts +84 -0
  56. package/dist/src/components/resource/nouns.d.ts +12 -0
  57. package/dist/src/components/resource/query-state.d.ts +20 -0
  58. package/dist/src/components/resource/resource-names.d.ts +23 -0
  59. package/dist/src/components/resource/select/ResourceSelect.d.ts +67 -0
  60. package/dist/src/components/resource/select/select-states.d.ts +68 -0
  61. package/dist/src/components/resource/select/use-debounced-value.d.ts +2 -0
  62. package/dist/src/components/resource/select/use-option-pages.d.ts +37 -0
  63. package/dist/src/components/resource/select/venue-backend.d.ts +144 -0
  64. package/dist/src/components/resource/table/BulkActionsBar.d.ts +77 -0
  65. package/dist/src/components/resource/table/CardList.d.ts +31 -0
  66. package/dist/src/components/resource/table/ConfirmDialog.d.ts +28 -0
  67. package/dist/src/components/resource/table/ExportButtons.d.ts +17 -0
  68. package/dist/src/components/resource/table/FilterBar.d.ts +29 -0
  69. package/dist/src/components/resource/table/PaginationControls.d.ts +31 -0
  70. package/dist/src/components/resource/table/ResourceTable.d.ts +137 -0
  71. package/dist/src/components/resource/table/RowCard.d.ts +31 -0
  72. package/dist/src/components/resource/table/TableView.d.ts +29 -0
  73. package/dist/src/components/resource/table/ViewOptions.d.ts +19 -0
  74. package/dist/src/components/resource/table/access.d.ts +12 -0
  75. package/dist/src/components/resource/table/demo-fixtures.d.ts +25 -0
  76. package/dist/src/components/resource/table/derive-columns.d.ts +33 -0
  77. package/dist/src/components/resource/table/grid-navigation.d.ts +23 -0
  78. package/dist/src/components/resource/table/layout.d.ts +37 -0
  79. package/dist/src/components/resource/table/preferences.d.ts +12 -0
  80. package/dist/src/components/resource/table/slots.d.ts +41 -0
  81. package/dist/src/components/resource/table/states.d.ts +91 -0
  82. package/dist/src/components/resource/table/story-fixtures.d.ts +25 -0
  83. package/dist/src/components/resource/table/structural-columns.d.ts +25 -0
  84. package/dist/src/components/resource/table/table-icons.d.ts +16 -0
  85. package/dist/src/components/resource/table/table-utils.d.ts +48 -0
  86. package/dist/src/components/resource/table/url-state.d.ts +67 -0
  87. package/dist/src/components/resource/table/use-container-width.d.ts +10 -0
  88. package/dist/src/components/resource/table/use-debounced-value.d.ts +1 -0
  89. package/dist/src/components/resource/testapp-fixtures.d.ts +78 -0
  90. package/dist/src/components/stat/stat.d.ts +54 -0
  91. package/dist/src/components/toast/toast-store.d.ts +36 -0
  92. package/dist/src/components/toast/toaster.d.ts +66 -0
  93. package/dist/src/components/toast/use-toast.d.ts +24 -0
  94. package/dist/src/hooks/keys.d.ts +24 -0
  95. package/dist/src/hooks/multipart.d.ts +44 -0
  96. package/dist/src/hooks/params.d.ts +25 -0
  97. package/dist/src/hooks/types.d.ts +41 -0
  98. package/dist/src/hooks/useChangeHistory.d.ts +30 -0
  99. package/dist/src/hooks/useResourceImport.d.ts +107 -0
  100. package/dist/src/hooks/useResourceItem.d.ts +4 -0
  101. package/dist/src/hooks/useResourceList.d.ts +4 -0
  102. package/dist/src/hooks/useResourceMutations.d.ts +13 -0
  103. package/dist/src/hooks/useUnsavedChangesGuard.d.ts +31 -0
  104. package/dist/src/index.d.ts +113 -0
  105. package/dist/src/lib/cn.d.ts +8 -0
  106. package/dist/src/lib/roles.d.ts +45 -0
  107. package/dist/src/lib/variants.d.ts +33 -0
  108. package/dist/src/primitives/registry.d.ts +81 -0
  109. package/dist/src/schema/client.d.ts +90 -0
  110. package/dist/src/schema/parse.d.ts +101 -0
  111. package/dist/src/schema/provider.d.ts +20 -0
  112. package/dist/src/schema/types.d.ts +93 -0
  113. package/dist/src/schema/useSchema.d.ts +21 -0
  114. package/dist/src/schema/zod.d.ts +4 -0
  115. package/dist/src/states/ErrorState.d.ts +43 -0
  116. package/dist/src/states/PendingButton.d.ts +16 -0
  117. package/dist/src/states/describe-error.d.ts +25 -0
  118. package/dist/src/states/hooks.d.ts +10 -0
  119. package/dist/src/theme/script.d.ts +12 -0
  120. package/dist/src/theme/theme.d.ts +45 -0
  121. package/dist/styles.css +248 -0
  122. package/dist/tailwind-preset.cjs +2 -0
  123. package/dist/tailwind-preset.cjs.map +1 -0
  124. package/dist/tailwind-preset.d.ts +140 -0
  125. package/dist/tailwind-preset.js +183 -0
  126. package/dist/tailwind-preset.js.map +1 -0
  127. package/dist/tailwind-v4.css +31 -0
  128. package/dist/vite-plugin.cjs +2 -0
  129. package/dist/vite-plugin.cjs.map +1 -0
  130. package/dist/vite-plugin.d.ts +19 -0
  131. package/dist/vite-plugin.js +16 -0
  132. package/dist/vite-plugin.js.map +1 -0
  133. package/eslint-plugin/index.js +76 -0
  134. package/eslint-plugin/lib/docs.js +11 -0
  135. package/eslint-plugin/lib/glob.js +51 -0
  136. package/eslint-plugin/rules/max-page-lines.js +65 -0
  137. package/eslint-plugin/rules/no-deep-imports.js +61 -0
  138. package/eslint-plugin/rules/no-direct-http.js +128 -0
  139. package/eslint-plugin/rules/no-raw-elements.js +65 -0
  140. package/eslint-plugin/rules/no-style-literals.js +222 -0
  141. package/eslint-plugin/rules.json +32 -0
  142. package/package.json +156 -4
@@ -0,0 +1,67 @@
1
+ import { FilterValue } from '../../../hooks/types';
2
+ import { FieldDef } from '../../../schema/types';
3
+ /**
4
+ * Where a table keeps its view state. The table only touches its own
5
+ * (namespaced) keys.
6
+ *
7
+ * An adapter for a router reads the router's current search params in
8
+ * `read`, navigates in `write`, and either calls the `subscribe` listener
9
+ * when the params change or simply passes a new store object on each change
10
+ * (the table re-reads whenever the store changes).
11
+ */
12
+ export interface ResourceTableStateStore {
13
+ /** Every current query value by key. */
14
+ read(): Readonly<Record<string, string>>;
15
+ /**
16
+ * Merge `changes` into the URL (`null` removes a key). `push` adds a
17
+ * history entry (page, sort, filter changes). Otherwise the current entry
18
+ * is replaced (a search refined while typing, a page clamped).
19
+ */
20
+ write(changes: Readonly<Record<string, string | null>>, options: {
21
+ push: boolean;
22
+ }): void;
23
+ /** Call `onChange` when the values change outside the table. Returns an unsubscribe. */
24
+ subscribe(onChange: () => void): () => void;
25
+ }
26
+ /**
27
+ * The default store: the browser's URL through `window.history`
28
+ * (`pushState` / `replaceState`, `popstate` for Back and Forward). Other
29
+ * query parameters and the hash are kept.
30
+ */
31
+ export declare function createHistoryStateStore(): ResourceTableStateStore;
32
+ /** The view a table shows: what the URL holds for it. */
33
+ export interface TableView {
34
+ page: number;
35
+ pageSize: number;
36
+ /** DRF ordering ("-starts_at,title"), where "" is "no ordering". */
37
+ ordering: string;
38
+ search: string;
39
+ filters: Readonly<Record<string, FilterValue>>;
40
+ }
41
+ export interface TableViewDefaults {
42
+ pageSize: number;
43
+ ordering: string;
44
+ /** Page sizes a URL may ask for. Anything else falls back to the default. */
45
+ pageSizes: readonly number[];
46
+ }
47
+ /** Encodes one filter value for the URL (arrays comma-joined). */
48
+ export declare function encodeFilter(value: FilterValue): string | null;
49
+ /** Decodes a URL value back to the type the filter control and the API expect. */
50
+ export declare function decodeFilter(raw: string, field: FieldDef): FilterValue;
51
+ /** Drops cleared filters so "no filter" has one representation. */
52
+ export declare function compactFilters(filters: Readonly<Record<string, FilterValue>>): Record<string, FilterValue>;
53
+ export type SetTableView = (patch: Partial<TableView>, options: {
54
+ push: boolean;
55
+ }) => void;
56
+ export interface UseTableViewOptions {
57
+ /** False keeps the view in component state only (the URL is untouched). */
58
+ syncUrl: boolean;
59
+ /** Where the URL state lives. Default: `window.history`. */
60
+ store?: ResourceTableStateStore;
61
+ namespace: string;
62
+ defaults: TableViewDefaults;
63
+ /** The fields the filter bar offers: only these are read from the URL. */
64
+ filterFields: readonly FieldDef[];
65
+ }
66
+ /** The table's view state, from the URL (or component state) with a setter. */
67
+ export declare function useTableView(options: UseTableViewOptions): [TableView, SetTableView];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The width of an element, kept current with a ResizeObserver. ResourceTable
3
+ * switches to cards from its own container's width, not the viewport's, so a
4
+ * table in a narrow sidebar or a split view lays out for the room it has.
5
+ * `undefined` until measured (and where there is no layout, as in jsdom), so
6
+ * the table keeps its default layout rather than guessing.
7
+ * Internal to @karni/ui.
8
+ */
9
+ import * as React from "react";
10
+ export declare function useContainerWidth(ref: React.RefObject<HTMLElement | null>): number | undefined;
@@ -0,0 +1 @@
1
+ export declare function useDebouncedValue<T>(value: T, delayMs: number): T;
@@ -0,0 +1,78 @@
1
+ import { RequestHandler } from 'msw';
2
+ import { ChangeHistoryPage } from '../../hooks/useChangeHistory';
3
+ import { ImportDescription, ImportJob } from '../../hooks/useResourceImport';
4
+ /** The OpenAPI document `/api/schema/` serves for the testapp (a superuser's view). */
5
+ export declare const testappOpenApi: Record<string, unknown>;
6
+ /**
7
+ * The per-role document of a role `Karni.field_roles` hides `fields` of the
8
+ * Event from: the fields are absent from it, as they are from the payload.
9
+ */
10
+ export declare function openApiHidingEventFields(fields: readonly string[]): Record<string, unknown>;
11
+ export declare const DEMO_EVENT_ID = "3f6f75d8-f305-48a8-9587-32884b211936";
12
+ export declare const demoVenue: {
13
+ id: string;
14
+ name: string;
15
+ city: string;
16
+ };
17
+ /** A testapp Event as `GET /api/events/<id>/?expand=venue` returns it. */
18
+ export declare const demoEvent: Record<string, unknown>;
19
+ /** The same Event as a role that may not read `fields` receives it: the keys are absent, not null. */
20
+ export declare function eventHiding(fields: readonly string[]): Record<string, unknown>;
21
+ /**
22
+ * How one endpoint answers: "ok", "pending" (never answers: the loading
23
+ * state), "network" (the request fails, as offline), a status with an
24
+ * optional DRF body, or a delay before answering "ok".
25
+ */
26
+ export type Outcome = "ok" | "pending" | "network" | {
27
+ status: number;
28
+ body?: unknown;
29
+ } | {
30
+ delayMs: number;
31
+ };
32
+ /** The 404 Karni answers for a row outside the read scope: the scope is named, existence is not. */
33
+ export declare const SCOPED_NOT_FOUND: {
34
+ readonly status: 404;
35
+ readonly body: {
36
+ readonly detail: "No Event matches the given query. Rows are limited by EventViewSet.access read=Granted().";
37
+ };
38
+ };
39
+ export interface DetailBackend {
40
+ schema?: Outcome;
41
+ /** The document `/api/schema/` serves (default: the testapp snapshot). */
42
+ openApi?: Record<string, unknown>;
43
+ event?: Outcome;
44
+ /** The payload `GET /api/events/<id>/` answers (default: demoEvent). */
45
+ record?: Record<string, unknown>;
46
+ remove?: Outcome;
47
+ history?: Outcome;
48
+ historyPage?: ChangeHistoryPage;
49
+ }
50
+ /** msw handlers for the schema, one Event, its delete and its history. */
51
+ export declare function detailBackend(options?: DetailBackend): RequestHandler[];
52
+ export declare const demoHistory: ChangeHistoryPage;
53
+ /** What `GET /api/events/import/` answers. */
54
+ export declare const demoImportDescription: ImportDescription;
55
+ export declare const DEMO_JOB_ID = "6b1f0e9a-3c2d-4e5f-8a7b-9c0d1e2f3a4b";
56
+ /** An import job in any state (default: just uploaded). */
57
+ export declare function demoJob(partial?: Partial<ImportJob>): ImportJob;
58
+ /** A preview: most rows import, two are refused with the reason. */
59
+ export declare const previewedJob: ImportJob;
60
+ export declare const doneJob: ImportJob;
61
+ export interface ImportBackend {
62
+ describe?: Outcome;
63
+ upload?: Outcome;
64
+ /** The job uploads create (default: demoJob()). */
65
+ uploaded?: ImportJob;
66
+ /** Answers of `GET .../import/<job>/` after a preview or commit started, in turn: polling. */
67
+ polls?: readonly ImportJob[];
68
+ preview?: Outcome;
69
+ previewed?: ImportJob;
70
+ commit?: Outcome;
71
+ committed?: ImportJob;
72
+ }
73
+ /**
74
+ * msw handlers for `/api/events/import/…`: describe, upload, poll, preview,
75
+ * commit. `GET .../import/<job>/` answers the job as the last step left it
76
+ * (after `polls`, while those last), as the backend does.
77
+ */
78
+ export declare function importBackend(options?: ImportBackend): RequestHandler[];
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ /** What the `Label`, `Value` and `Icon` slots receive. */
3
+ export interface StatSlotProps {
4
+ children: React.ReactNode;
5
+ }
6
+ /** What the `Delta` slot receives. */
7
+ export interface StatDeltaProps {
8
+ /** The `delta` prop: a number, or any node. */
9
+ delta: number | React.ReactNode;
10
+ /** The `formatDelta` prop (or the default signed format). */
11
+ formatDelta: (delta: number) => string;
12
+ }
13
+ /** The parts of the tile a project can replace (`slots={{ Delta: GoodWhenDown }}`). */
14
+ export interface StatSlots {
15
+ /** What the number measures. */
16
+ Label: React.ComponentType<StatSlotProps>;
17
+ /** The headline value. */
18
+ Value: React.ComponentType<StatSlotProps>;
19
+ /** The change indicator. The default treats a rise as neutral and a fall as destructive. */
20
+ Delta: React.ComponentType<StatDeltaProps>;
21
+ /** Wraps `icon`. Decorative, hidden from assistive tech. */
22
+ Icon: React.ComponentType<StatSlotProps>;
23
+ }
24
+ export interface StatProps extends React.HTMLAttributes<HTMLDivElement> {
25
+ /** What the number measures ("Active users"). */
26
+ label: React.ReactNode;
27
+ /** The headline value: a preformatted string or any node. */
28
+ value: React.ReactNode;
29
+ /**
30
+ * Change indicator. A number renders a signed figure with an up, down or
31
+ * flat marker (negative numbers use the destructive token) plus
32
+ * screen-reader context. Any other node renders verbatim in the delta slot.
33
+ */
34
+ delta?: number | React.ReactNode;
35
+ /** Formats a numeric `delta` (default: signed, locale formatted). */
36
+ formatDelta?: (delta: number) => string;
37
+ /** Optional icon slot, top-right. */
38
+ icon?: React.ReactNode;
39
+ /** True while the value loads: a placeholder replaces the value and the delta. */
40
+ loading?: boolean;
41
+ /** Replaces the Label, Value, Delta or Icon. See StatSlots. */
42
+ slots?: Partial<StatSlots>;
43
+ }
44
+ /** The default pieces of the tile, for wrapping one. */
45
+ export declare const defaultStatSlots: StatSlots;
46
+ /**
47
+ * KPI tile: label, value, optional delta and icon.
48
+ *
49
+ * Escape hatches: `slots` replaces the Label, Value, Delta or Icon (a Delta
50
+ * for metrics where down is good, say), `value`/`delta`/`icon` accept
51
+ * arbitrary nodes, `formatDelta` overrides numeric formatting, and `className`
52
+ * merges via cn().
53
+ */
54
+ export declare const Stat: React.ForwardRefExoticComponent<StatProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,36 @@
1
+ import type * as React from "react";
2
+ /**
3
+ * Module-level toast store.
4
+ *
5
+ * `useToast()` can be called anywhere (no provider requirement beyond a
6
+ * mounted <Toaster/>): toasts are pushed into this store and every mounted
7
+ * Toaster/useToast subscriber re-renders via useSyncExternalStore.
8
+ */
9
+ export type ToastVariant = "default" | "success" | "error" | "info";
10
+ export interface ToastOptions {
11
+ /** Stable id (defaults to an auto-generated one). Re-using an id replaces. */
12
+ id?: string;
13
+ title?: React.ReactNode;
14
+ description?: React.ReactNode;
15
+ variant?: ToastVariant;
16
+ /** Per-toast auto-dismiss override in ms (Toaster default otherwise). */
17
+ duration?: number;
18
+ /** Action slot (e.g. an "Undo" button), rendered inside the toast. */
19
+ action?: React.ReactNode;
20
+ }
21
+ export interface ToastInstance extends ToastOptions {
22
+ id: string;
23
+ variant: ToastVariant;
24
+ }
25
+ export interface ToastHandle {
26
+ id: string;
27
+ dismiss: () => void;
28
+ }
29
+ type Listener = () => void;
30
+ export declare function subscribeToToasts(listener: Listener): () => void;
31
+ export declare function getToasts(): readonly ToastInstance[];
32
+ export declare function dismissToast(id?: string): void;
33
+ export declare function addToast(options: ToastOptions): ToastHandle;
34
+ /** Test helper: clears all toasts and listeners' state (not in the barrel). */
35
+ export declare function resetToastStore(): void;
36
+ export {};
@@ -0,0 +1,66 @@
1
+ import { VariantProps } from '../../lib/variants';
2
+ import { ToastVariant } from './toast-store';
3
+ import * as React from "react";
4
+ /**
5
+ * Per-variant toast styling, with colors that resolve through --karni-* tokens
6
+ * only (success leans on primary, error on destructive, info on muted).
7
+ */
8
+ export declare const toastVariants: import('../..').VariantFn<{
9
+ variant: {
10
+ default: string;
11
+ success: string;
12
+ error: string;
13
+ info: string;
14
+ };
15
+ }>;
16
+ export type ToastVariantProps = VariantProps<typeof toastVariants>;
17
+ /** What the `Title` and `Description` slots receive. */
18
+ export interface ToasterSlotProps {
19
+ children: React.ReactNode;
20
+ variant: ToastVariant;
21
+ }
22
+ /** What the `CloseButton` slot receives. */
23
+ export interface ToasterCloseButtonProps {
24
+ /** Its accessible name ("Dismiss"). */
25
+ label: string;
26
+ variant: ToastVariant;
27
+ /** Removes the toast. */
28
+ dismiss: () => void;
29
+ }
30
+ /** The parts of each toast a project can replace (`slots={{ CloseButton: MyClose }}`). */
31
+ export interface ToasterSlots {
32
+ Title: React.ComponentType<ToasterSlotProps>;
33
+ Description: React.ComponentType<ToasterSlotProps>;
34
+ /** The top-right dismiss button. */
35
+ CloseButton: React.ComponentType<ToasterCloseButtonProps>;
36
+ }
37
+ /** The default pieces of a toast, for wrapping one. */
38
+ export declare const defaultToasterSlots: ToasterSlots;
39
+ export interface ToasterProps {
40
+ /** Auto-dismiss duration in ms for toasts without their own (default 5000). */
41
+ duration?: number;
42
+ /** Swipe-to-dismiss direction (default "right"). */
43
+ swipeDirection?: "right" | "left" | "up" | "down";
44
+ /** Accessible label prefix announced with each toast (Radix Provider). */
45
+ label?: string;
46
+ /** Extra classes for the viewport, the list the toasts render in. */
47
+ className?: string;
48
+ /** Same as `className` (kept for compatibility). */
49
+ viewportClassName?: string;
50
+ /** Extra classes for every toast root. */
51
+ toastClassName?: string;
52
+ /** Replaces the Title, Description or CloseButton of every toast (see ToasterSlots). */
53
+ slots?: Partial<ToasterSlots>;
54
+ }
55
+ /**
56
+ * Renders the toasts pushed through useToast(). Mount exactly one near the
57
+ * app root. Position defaults to the bottom-right corner. Override it with
58
+ * `className` (it merges via cn(), so e.g. "top-0 bottom-auto" wins).
59
+ *
60
+ * Keyboard and screen readers: an error is announced at once (assertive),
61
+ * other toasts politely. F8 moves focus to the toasts. Tab reaches each one
62
+ * and its buttons, and Escape dismisses the focused toast. A toast's timer
63
+ * pauses while the pointer is over the toasts or focus is inside them, and
64
+ * while the window is in the background.
65
+ */
66
+ export declare function Toaster({ duration, swipeDirection, label, className, viewportClassName, toastClassName, slots, }: ToasterProps): React.ReactElement;
@@ -0,0 +1,24 @@
1
+ import { ToastHandle, ToastInstance, ToastOptions } from './toast-store';
2
+ import * as React from "react";
3
+ export interface UseToastResult {
4
+ /** Currently visible toasts (rendered by <Toaster/>). */
5
+ toasts: readonly ToastInstance[];
6
+ /** Full-control entry point: show any ToastOptions. */
7
+ toast: (options: ToastOptions) => ToastHandle;
8
+ /** Shorthand: success variant. */
9
+ success: (title: React.ReactNode, options?: Omit<ToastOptions, "variant">) => ToastHandle;
10
+ /** Shorthand: error variant. */
11
+ error: (title: React.ReactNode, options?: Omit<ToastOptions, "variant">) => ToastHandle;
12
+ /** Shorthand: info variant. */
13
+ info: (title: React.ReactNode, options?: Omit<ToastOptions, "variant">) => ToastHandle;
14
+ /** Dismiss one toast by id, or all when called without arguments. */
15
+ dismiss: (id?: string) => void;
16
+ }
17
+ /**
18
+ * Imperative toast API + the live toast list.
19
+ *
20
+ * Requires a mounted <Toaster/> somewhere in the tree to actually render the
21
+ * toasts. The functions themselves are stable module-level references, safe
22
+ * to use in effects and callbacks without dependency churn.
23
+ */
24
+ export declare function useToast(): UseToastResult;
@@ -0,0 +1,24 @@
1
+ import { ResourceItemParams, ResourceListParams } from './types';
2
+ /** TanStack Query keys for the schema, a resource, its lists and its items (invalidate `karniKeys.resource(name)` to refresh every view of a resource). */
3
+ export declare const karniKeys: {
4
+ /** Cache key of the parsed OpenAPI schema document. */
5
+ schema(): readonly unknown[];
6
+ /**
7
+ * The invalidation root for one resource. Every list and detail key of
8
+ * `resource` lives under this prefix.
9
+ */
10
+ resource(resource: string): readonly unknown[];
11
+ /**
12
+ * Key for one list page. Params are normalized (D4): undefined/null
13
+ * dropped, keys sorted recursively, values stringified as they hit the
14
+ * query string, so `{search: "x", page: 2}` and `{page: 2, search: "x"}`
15
+ * share one entry.
16
+ */
17
+ list(resource: string, params?: ResourceListParams): readonly unknown[];
18
+ /**
19
+ * Key for one item. The id is always stringified, so `detail("events", 5)`
20
+ * and `detail("events", "5")` share one entry. Params (expand) append a
21
+ * normalized fifth element (see the module docs).
22
+ */
23
+ detail(resource: string, id: string | number, params?: ResourceItemParams): readonly unknown[];
24
+ };
@@ -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 from a PATCH payload (D2). Strings (or arrays
41
+ * of strings) on file-kind fields are the backend's display URLs, not new
42
+ * uploads.
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`). An 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,30 @@
1
+ import { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
2
+ /** `transition`: a StateField moved along a declared transition (`diff` holds `[from, to]`). */
3
+ export type ChangeAction = "create" | "update" | "delete" | "restore" | "transition";
4
+ export interface ChangeLogEntry {
5
+ id: number;
6
+ timestamp: string;
7
+ actor: {
8
+ id: string | number;
9
+ email?: string;
10
+ } | null;
11
+ action: ChangeAction;
12
+ /** `{ field: [old, new] }`. A redacted field carries `"[redacted]"` on both sides. */
13
+ diff: Record<string, [unknown, unknown]>;
14
+ fields_changed: string[];
15
+ }
16
+ export interface ChangeHistoryPage {
17
+ count: number;
18
+ next: string | null;
19
+ previous: string | null;
20
+ results: ChangeLogEntry[];
21
+ }
22
+ export interface ChangeHistoryParams {
23
+ /** Entries per page (the backend caps it). Default: the API's page size. */
24
+ pageSize?: number;
25
+ page?: number;
26
+ }
27
+ /** The query key of one row's change history. */
28
+ export declare function changeHistoryKey(resource: string, id: string | number, params?: ChangeHistoryParams): readonly unknown[];
29
+ /** Fetches one row's field-level change history (`GET /<resource>/<id>/history/`) for models with `audit = True`. */
30
+ 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 that refresh the resource's cached lists and items on each success. */
13
+ export declare function useResourceMutations<T = Record<string, unknown>>(resource: string, options?: ResourceMutationsOptions): ResourceMutations<T>;