@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.
- package/LICENSE +201 -0
- package/README.md +288 -2
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +4764 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +85 -0
- package/dist/mockServiceWorker.js +361 -0
- package/dist/src/auth/AuthGuard.d.ts +12 -0
- package/dist/src/auth/LoginForm.d.ts +29 -0
- package/dist/src/auth/engine.d.ts +99 -0
- package/dist/src/auth/provider.d.ts +43 -0
- package/dist/src/auth/storage.d.ts +26 -0
- package/dist/src/auth/types.d.ts +32 -0
- package/dist/src/auth/usePermission.d.ts +7 -0
- package/dist/src/components/brand/karni-mark.d.ts +21 -0
- package/dist/src/components/brand/mark-paths.d.ts +8 -0
- package/dist/src/components/chart/data-chart.d.ts +74 -0
- package/dist/src/components/empty-state/empty-state.d.ts +20 -0
- package/dist/src/components/file-upload/FileUpload.d.ts +44 -0
- package/dist/src/components/layout/layout.d.ts +55 -0
- package/dist/src/components/modal/modal.d.ts +45 -0
- package/dist/src/components/page/page.d.ts +29 -0
- package/dist/src/components/primitives/button.d.ts +44 -0
- package/dist/src/components/primitives/checkbox.d.ts +5 -0
- package/dist/src/components/primitives/dropdown-menu.d.ts +24 -0
- package/dist/src/components/primitives/icons.d.ts +11 -0
- package/dist/src/components/primitives/input.d.ts +4 -0
- package/dist/src/components/primitives/label.d.ts +5 -0
- package/dist/src/components/primitives/select.d.ts +63 -0
- package/dist/src/components/primitives/skeleton.d.ts +4 -0
- package/dist/src/components/primitives/switch.d.ts +5 -0
- package/dist/src/components/primitives/tabs.d.ts +10 -0
- package/dist/src/components/primitives/textarea.d.ts +4 -0
- package/dist/src/components/resource/ResourceDetail.d.ts +7 -0
- package/dist/src/components/resource/ResourceForm.d.ts +7 -0
- package/dist/src/components/resource/ResourceSelect.d.ts +7 -0
- package/dist/src/components/resource/ResourceTable.d.ts +11 -0
- package/dist/src/components/resource/audit-fields.d.ts +17 -0
- package/dist/src/components/resource/detail/ResourceDetail.d.ts +30 -0
- package/dist/src/components/resource/displayLabel.d.ts +12 -0
- package/dist/src/components/resource/form/ResourceForm.d.ts +50 -0
- package/dist/src/components/resource/form/field-inputs.d.ts +20 -0
- package/dist/src/components/resource/form/fixtures.d.ts +24 -0
- package/dist/src/components/resource/form/prepare.d.ts +54 -0
- package/dist/src/components/resource/history/ChangeHistory.d.ts +18 -0
- package/dist/src/components/resource/import/ResourceImport.d.ts +18 -0
- package/dist/src/components/resource/select/ResourceSelect.d.ts +36 -0
- package/dist/src/components/resource/select/use-debounced-value.d.ts +2 -0
- package/dist/src/components/resource/table/BulkActionsBar.d.ts +20 -0
- package/dist/src/components/resource/table/ExportButtons.d.ts +17 -0
- package/dist/src/components/resource/table/FilterBar.d.ts +29 -0
- package/dist/src/components/resource/table/PaginationControls.d.ts +19 -0
- package/dist/src/components/resource/table/ResourceTable.d.ts +72 -0
- package/dist/src/components/resource/table/demo-fixtures.d.ts +25 -0
- package/dist/src/components/resource/table/derive-columns.d.ts +33 -0
- package/dist/src/components/resource/table/table-utils.d.ts +24 -0
- package/dist/src/components/resource/table/use-debounced-value.d.ts +1 -0
- package/dist/src/components/stat/stat.d.ts +24 -0
- package/dist/src/components/toast/toast-store.d.ts +36 -0
- package/dist/src/components/toast/toaster.d.ts +33 -0
- package/dist/src/components/toast/use-toast.d.ts +24 -0
- package/dist/src/hooks/keys.d.ts +24 -0
- package/dist/src/hooks/multipart.d.ts +44 -0
- package/dist/src/hooks/params.d.ts +25 -0
- package/dist/src/hooks/types.d.ts +41 -0
- package/dist/src/hooks/useChangeHistory.d.ts +29 -0
- package/dist/src/hooks/useResourceImport.d.ts +107 -0
- package/dist/src/hooks/useResourceItem.d.ts +4 -0
- package/dist/src/hooks/useResourceList.d.ts +4 -0
- package/dist/src/hooks/useResourceMutations.d.ts +13 -0
- package/dist/src/index.d.ts +95 -0
- package/dist/src/lib/cn.d.ts +8 -0
- package/dist/src/lib/roles.d.ts +45 -0
- package/dist/src/lib/variants.d.ts +33 -0
- package/dist/src/schema/client.d.ts +90 -0
- package/dist/src/schema/parse.d.ts +101 -0
- package/dist/src/schema/provider.d.ts +13 -0
- package/dist/src/schema/types.d.ts +84 -0
- package/dist/src/schema/useSchema.d.ts +17 -0
- package/dist/src/schema/zod.d.ts +4 -0
- package/dist/styles.css +87 -0
- package/dist/tailwind-preset.cjs +2 -0
- package/dist/tailwind-preset.cjs.map +1 -0
- package/dist/tailwind-preset.d.ts +58 -0
- package/dist/tailwind-preset.js +64 -0
- package/dist/tailwind-preset.js.map +1 -0
- package/eslint-plugin/index.js +72 -0
- package/eslint-plugin/lib/docs.js +11 -0
- package/eslint-plugin/lib/glob.js +51 -0
- package/eslint-plugin/rules/max-page-lines.js +65 -0
- package/eslint-plugin/rules/no-deep-imports.js +61 -0
- package/eslint-plugin/rules/no-direct-http.js +128 -0
- package/eslint-plugin/rules/no-raw-elements.js +65 -0
- package/eslint-plugin/rules.json +26 -0
- package/package.json +153 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Control, FieldValues } from 'react-hook-form';
|
|
2
|
+
import { FieldDef } from '../../../schema/types';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/** Props passed to a custom field component (the `fields` escape hatch). */
|
|
5
|
+
export interface KarniFieldProps {
|
|
6
|
+
field: FieldDef;
|
|
7
|
+
name: string;
|
|
8
|
+
label: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
control: Control<FieldValues>;
|
|
11
|
+
error?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** Pass-through from FieldOverride.props. */
|
|
14
|
+
props?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface FieldOverride {
|
|
17
|
+
component?: React.ComponentType<KarniFieldProps>;
|
|
18
|
+
label?: string;
|
|
19
|
+
hidden?: boolean;
|
|
20
|
+
props?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
export interface ResourceFormProps {
|
|
23
|
+
resource: string;
|
|
24
|
+
/** Present → edit mode: loads the UNEXPANDED item, PATCHes dirty fields only (D10). */
|
|
25
|
+
id?: string | number;
|
|
26
|
+
defaultValues?: Record<string, unknown>;
|
|
27
|
+
onSuccess?: (result: Record<string, unknown>) => void;
|
|
28
|
+
onError?: (error: unknown) => void;
|
|
29
|
+
fields?: Record<string, FieldOverride>;
|
|
30
|
+
/** Custom RHF-registered nodes appended after the derived fields. */
|
|
31
|
+
extraFields?: React.ReactNode;
|
|
32
|
+
transformSubmit?: (values: Record<string, unknown>) => Record<string, unknown>;
|
|
33
|
+
/** Replaces the submit row. */
|
|
34
|
+
actions?: React.ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Schema-driven create and edit form for one resource: inputs, validation and
|
|
39
|
+
* server errors derived from the OpenAPI schema.
|
|
40
|
+
*
|
|
41
|
+
* Contract "ResourceForm" + D2/D3/D10.
|
|
42
|
+
*
|
|
43
|
+
* Create mode POSTs every prepared value; edit mode loads the UNEXPANDED
|
|
44
|
+
* detail record and PATCHes only dirty fields at top-level granularity, with
|
|
45
|
+
* emptied nullable fields sent as null (D3). DRF 400 bodies map onto field
|
|
46
|
+
* errors via setError (non_field_errors → root). Escape hatches: per-field
|
|
47
|
+
* overrides (`fields`), `extraFields`, `transformSubmit` and the `actions`
|
|
48
|
+
* slot.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ResourceForm(props: ResourceFormProps): React.ReactElement;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Control, FieldValues } from 'react-hook-form';
|
|
2
|
+
import { FieldDef } from '../../../schema/types';
|
|
3
|
+
/**
|
|
4
|
+
* Default per-kind inputs for <ResourceForm> (contract: string → Input,
|
|
5
|
+
* text → Textarea, number/integer → Input[type=number], boolean → Switch,
|
|
6
|
+
* date/datetime → native inputs, enum → Select, relation → ResourceSelect
|
|
7
|
+
* (multiple for many), file → FileUpload, json → Textarea with validation).
|
|
8
|
+
*/
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
export interface DefaultFieldControlProps {
|
|
11
|
+
field: FieldDef;
|
|
12
|
+
control: Control<FieldValues>;
|
|
13
|
+
inputId: string;
|
|
14
|
+
describedBy?: string;
|
|
15
|
+
invalid: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** FieldOverride.props pass-through, spread onto the rendered input. */
|
|
18
|
+
inputProps?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export declare function DefaultFieldControl({ field, control, inputId, describedBy, invalid, disabled, inputProps, }: DefaultFieldControlProps): React.ReactElement;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FieldDef, ResourceSchema } from '../../../schema/types';
|
|
2
|
+
export declare const eventFields: readonly FieldDef[];
|
|
3
|
+
export declare const eventSchema: ResourceSchema;
|
|
4
|
+
export declare const venueFields: readonly FieldDef[];
|
|
5
|
+
export declare const venueSchema: ResourceSchema;
|
|
6
|
+
export interface VenueRecord extends Record<string, unknown> {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
city: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const venues: readonly VenueRecord[];
|
|
12
|
+
export declare const venueById: Readonly<Record<string, VenueRecord>>;
|
|
13
|
+
export declare const tags: ReadonlyArray<Record<string, unknown>>;
|
|
14
|
+
/** UNEXPANDED detail record, as edit mode receives it (D10). */
|
|
15
|
+
export declare const eventRecord: Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Minimal drf-spectacular-shaped document (OAS 3.0.3,
|
|
18
|
+
* COMPONENT_SPLIT_REQUEST=True): split request/response components,
|
|
19
|
+
* x-karni-resource on list ops, x-karni-relation on pk fields and
|
|
20
|
+
* x-karni-auth-mode at the root (api contract B13/B14).
|
|
21
|
+
*/
|
|
22
|
+
export declare const demoOpenApiDocument: Record<string, unknown>;
|
|
23
|
+
/** msw handlers for stories: schema document + events/venues/tags endpoints. */
|
|
24
|
+
export declare const demoHandlers: import('msw').HttpHandler[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { FieldDef } from '../../../schema/types';
|
|
2
|
+
export type FormValues = Record<string, unknown>;
|
|
3
|
+
/** "published_on" / "draft-mode" → "Published on" / "Draft mode". */
|
|
4
|
+
export declare function humanizeValue(value: string): string;
|
|
5
|
+
/** ISO-8601 (any offset) → the local "YYYY-MM-DDTHH:mm" a datetime-local input needs. */
|
|
6
|
+
export declare function toDatetimeLocal(value: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* datetime-local value ("YYYY-MM-DDTHH:mm[:ss]", implicitly local) →
|
|
9
|
+
* ISO-8601 with an explicit UTC offset, per the submit rule in the contract.
|
|
10
|
+
* Values that already carry a zone pass through untouched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toIsoWithOffset(value: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* D3 dirtiness at TOP-LEVEL field granularity: a react-hook-form dirtyFields
|
|
15
|
+
* entry counts as dirty when anything in its subtree is truthy, and the whole
|
|
16
|
+
* field value is then sent (DRF PATCH replaces whole field values).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isDirtyEntry(entry: unknown): boolean;
|
|
19
|
+
export interface BuildFormValuesInput {
|
|
20
|
+
fields: readonly FieldDef[];
|
|
21
|
+
/** Edit mode: the UNEXPANDED detail record (D10). Wins over defaultValues. */
|
|
22
|
+
item?: Record<string, unknown>;
|
|
23
|
+
defaultValues?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds the react-hook-form value map: every form field gets a defined,
|
|
27
|
+
* controlled-input-safe initial value. Unmatched `defaultValues` keys are kept
|
|
28
|
+
* verbatim (they seed `extraFields`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildFormValues({ fields, item, defaultValues }: BuildFormValuesInput): FormValues;
|
|
31
|
+
export interface JsonFieldError {
|
|
32
|
+
name: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PrepareSubmitInput {
|
|
36
|
+
/** The writable schema fields rendered by the form. */
|
|
37
|
+
fields: readonly FieldDef[];
|
|
38
|
+
/** Resolver output from handleSubmit. */
|
|
39
|
+
raw: FormValues;
|
|
40
|
+
mode: "create" | "update";
|
|
41
|
+
/** react-hook-form dirtyFields; only consulted in "update" mode (D3). */
|
|
42
|
+
dirtyFields?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
export interface PreparedSubmit {
|
|
45
|
+
values: Record<string, unknown>;
|
|
46
|
+
/** Per-field validation failures (invalid JSON) that must block the submit. */
|
|
47
|
+
jsonErrors: JsonFieldError[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The D3 submit pipeline. Create mode sends every defined value; update mode
|
|
51
|
+
* sends only fields whose dirtyFields subtree is truthy — always the WHOLE
|
|
52
|
+
* field value. Keys without a schema field (extraFields) pass through as-is.
|
|
53
|
+
*/
|
|
54
|
+
export declare function prepareSubmit({ fields, raw, mode, dirtyFields, }: PrepareSubmitInput): PreparedSubmit;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ChangeHistoryProps {
|
|
3
|
+
resource: string;
|
|
4
|
+
id: string | number;
|
|
5
|
+
/** Entries to show (the backend paginates; default: the API's page size). */
|
|
6
|
+
pageSize?: number;
|
|
7
|
+
title?: React.ReactNode;
|
|
8
|
+
/** Shown when the row has no recorded changes. */
|
|
9
|
+
empty?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The field-level change history of one row: when, by whom, what changed
|
|
14
|
+
* from what to what. Reads `GET /<resource>/<id>/history/` (models that opt
|
|
15
|
+
* in with `class Karni: audit = True`); fields the role may not see are
|
|
16
|
+
* already absent from each diff, redacted ones say so.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ChangeHistory({ resource, id, pageSize, title, empty, className, }: ChangeHistoryProps): React.ReactElement;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ImportJob } from '../../../hooks/useResourceImport';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface ResourceImportProps {
|
|
4
|
+
resource: string;
|
|
5
|
+
title?: React.ReactNode;
|
|
6
|
+
/** Called when a commit finishes (the job is `done`). */
|
|
7
|
+
onDone?: (job: ImportJob) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Import a CSV or XLSX file into a resource: upload, map columns to fields,
|
|
12
|
+
* preview (the mandatory dry run — N created / updated / skipped with every
|
|
13
|
+
* row's errors), then commit. Drives `/<resource>/import/…`; the backend
|
|
14
|
+
* validates every row through the same serializer the API uses, keeps rows
|
|
15
|
+
* inside the user's scope and refuses fields the role may not write. Large
|
|
16
|
+
* files run in the background with progress.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ResourceImport({ resource, title, onDone, className, }: ResourceImportProps): React.ReactElement;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ResourceSelectProps {
|
|
3
|
+
resource: string;
|
|
4
|
+
value: string | number | ReadonlyArray<string | number> | null;
|
|
5
|
+
onChange: (value: string | number | Array<string | number> | null) => void;
|
|
6
|
+
multiple?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** Defaults to displayLabel(). */
|
|
10
|
+
getOptionLabel?: (item: Record<string, unknown>) => React.ReactNode;
|
|
11
|
+
/** Extra filter params sent with the search request. */
|
|
12
|
+
filter?: Record<string, string | number | boolean>;
|
|
13
|
+
id?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Page size for the option request (only the first page is fetched, D8). */
|
|
16
|
+
pageSize?: number;
|
|
17
|
+
/** Set false to hide the clear affordance (which emits null). Default true. */
|
|
18
|
+
clearable?: boolean;
|
|
19
|
+
/** Debounce for the ?search= request in ms. Default 300 (D8). */
|
|
20
|
+
searchDebounceMs?: number;
|
|
21
|
+
"aria-label"?: string;
|
|
22
|
+
"aria-labelledby"?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Searchable picker for a related row, querying the related resource as you type.
|
|
26
|
+
*
|
|
27
|
+
* Contract D8.
|
|
28
|
+
*
|
|
29
|
+
* A text input drives a debounced `?search=` list request; options render in
|
|
30
|
+
* a NON-MODAL Radix Popover whose auto-focus is prevented so keyboard focus
|
|
31
|
+
* never leaves the input. The input implements the ARIA 1.2 combobox pattern
|
|
32
|
+
* (combobox → listbox with aria-activedescendant). Pre-selected ids that were
|
|
33
|
+
* never seen in a result page are resolved through detail fetches, and the
|
|
34
|
+
* clear affordance emits `null`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ResourceSelect(props: ResourceSelectProps): React.ReactElement;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selected-count toolbar for ResourceTable's bulk select (D5): shows how many
|
|
3
|
+
* rows are selected (selection persists across pages), a clear affordance and
|
|
4
|
+
* one button per configured bulk action.
|
|
5
|
+
* Internal to @karni/ui — the public surface is ResourceTable's props.
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
export interface BulkAction {
|
|
9
|
+
label: React.ReactNode;
|
|
10
|
+
onClick: (selectedIds: string[]) => void;
|
|
11
|
+
destructive?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface BulkActionsBarProps {
|
|
14
|
+
/** Row ids (String(row.id), D5) currently selected, across all pages. */
|
|
15
|
+
selectedIds: readonly string[];
|
|
16
|
+
bulkActions: ReadonlyArray<BulkAction>;
|
|
17
|
+
onClear: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function BulkActionsBar(props: BulkActionsBarProps): React.ReactElement;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ResourceListParams } from '../../../hooks/types';
|
|
2
|
+
import { ExportFormat } from '../../../hooks/useResourceImport';
|
|
3
|
+
/**
|
|
4
|
+
* ResourceTable's export buttons (karni.transfer, ADR 0002 P6): one per
|
|
5
|
+
* format, each downloading `GET /<resource>/export/` with the table's current
|
|
6
|
+
* filters, search and ordering — the backend applies the row scope and the
|
|
7
|
+
* role's field visibility, so the file is exactly what the table may show.
|
|
8
|
+
*
|
|
9
|
+
* Internal to @karni/ui — the public surface is ResourceTable's `exportFormats`.
|
|
10
|
+
*/
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
export interface ExportButtonsProps {
|
|
13
|
+
resource: string;
|
|
14
|
+
params: ResourceListParams;
|
|
15
|
+
formats: readonly ExportFormat[];
|
|
16
|
+
}
|
|
17
|
+
export declare function ExportButtons({ resource, params, formats, }: ExportButtonsProps): React.ReactElement;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FilterValue } from '../../../hooks/types';
|
|
2
|
+
import { FieldDef } from '../../../schema/types';
|
|
3
|
+
/**
|
|
4
|
+
* ResourceTable's search box + declared-params-derived filter bar (D5).
|
|
5
|
+
*
|
|
6
|
+
* The parent decides WHICH fields filter (enum/boolean/relation fields ∩ the
|
|
7
|
+
* list operation's declared query params, cap 4, `filters` prop overrides);
|
|
8
|
+
* this component only renders the controls:
|
|
9
|
+
* - enum → clearable <Select> ("All" clears),
|
|
10
|
+
* - boolean → clearable Yes/No <Select>,
|
|
11
|
+
* - relation → <ResourceSelect> (multiple for many-relations).
|
|
12
|
+
*
|
|
13
|
+
* Internal to @karni/ui — the public surface is ResourceTable's props.
|
|
14
|
+
*/
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
export interface FilterBarProps {
|
|
17
|
+
/** Whether the list operation declares a `search` query param. */
|
|
18
|
+
searchEnabled: boolean;
|
|
19
|
+
searchValue: string;
|
|
20
|
+
onSearchChange: (value: string) => void;
|
|
21
|
+
searchPlaceholder?: string;
|
|
22
|
+
/** Already-derived filter fields, in render order. */
|
|
23
|
+
filterFields: readonly FieldDef[];
|
|
24
|
+
values: Readonly<Record<string, FilterValue>>;
|
|
25
|
+
onFilterChange: (field: FieldDef, value: FilterValue) => void;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Search box + up to four filter controls in one wrapping row. */
|
|
29
|
+
export declare function FilterBar(props: FilterBarProps): React.ReactElement;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceTable's server-mode pagination row (DRF PageNumberPagination:
|
|
3
|
+
* `{count, next, previous, results}` with `page` / `page_size` params).
|
|
4
|
+
* Internal to @karni/ui — the public surface is ResourceTable's props.
|
|
5
|
+
*/
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
export interface PaginationControlsProps {
|
|
8
|
+
/** 1-based current page. */
|
|
9
|
+
page: number;
|
|
10
|
+
pageCount: number;
|
|
11
|
+
/** Total item count across all pages (DRF `count`). */
|
|
12
|
+
totalCount: number;
|
|
13
|
+
/** From DRF `previous` / `next` — the server is the authority. */
|
|
14
|
+
hasPrevious: boolean;
|
|
15
|
+
hasNext: boolean;
|
|
16
|
+
onPageChange: (page: number) => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function PaginationControls(props: PaginationControlsProps): React.ReactElement;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
|
2
|
+
import { KarniQueryValue } from '../../../schema/client';
|
|
3
|
+
import { ExportFormat } from '../../../hooks/useResourceImport';
|
|
4
|
+
import { BulkAction } from './BulkActionsBar';
|
|
5
|
+
import { ResourceColumn } from './derive-columns';
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
export type { BulkAction, ResourceColumn };
|
|
8
|
+
export declare const DEFAULT_PAGE_SIZE = 25;
|
|
9
|
+
export interface ResourceTableProps<T = Record<string, unknown>> {
|
|
10
|
+
resource: string;
|
|
11
|
+
/**
|
|
12
|
+
* Per-field overrides. Naming a default-hidden field (an audit field such
|
|
13
|
+
* as `created_at`, or a json/file field) shows it; `hidden: true` hides one.
|
|
14
|
+
*/
|
|
15
|
+
columns?: ReadonlyArray<ResourceColumn<T>>;
|
|
16
|
+
/**
|
|
17
|
+
* Show the audit columns (AUDIT_FIELDS: id, created_at, updated_at,
|
|
18
|
+
* created_by, updated_by) after the domain columns, and let audit fields
|
|
19
|
+
* into the derived filter bar. Default false: they are hidden.
|
|
20
|
+
*/
|
|
21
|
+
showAuditFields?: boolean;
|
|
22
|
+
/** TanStack ColumnDef pass-through, appended after derived columns. */
|
|
23
|
+
extraColumns?: ReadonlyArray<ColumnDef<T>>;
|
|
24
|
+
/** Full escape hatch, applied last over the derived + merged columns. */
|
|
25
|
+
columnsOverride?: (derived: Array<ColumnDef<T>>) => Array<ColumnDef<T>>;
|
|
26
|
+
rowActions?: (row: T) => React.ReactNode;
|
|
27
|
+
/** Passing this enables the default edit row action. */
|
|
28
|
+
onEdit?: (row: T) => void;
|
|
29
|
+
/** Enables the default delete row action (uses useResourceMutations). */
|
|
30
|
+
enableDelete?: boolean;
|
|
31
|
+
bulkActions?: ReadonlyArray<BulkAction>;
|
|
32
|
+
onRowClick?: (row: T) => void;
|
|
33
|
+
/**
|
|
34
|
+
* `expand` query values. Default: single-relation field names ∩ the list
|
|
35
|
+
* operation's expand enum — never guessed beyond the schema (D5).
|
|
36
|
+
*/
|
|
37
|
+
expand?: readonly string[];
|
|
38
|
+
initialSorting?: SortingState;
|
|
39
|
+
/** Sent as `page_size`. Default 25. */
|
|
40
|
+
pageSize?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Field names to build the filter bar from; `false` disables it. Default:
|
|
43
|
+
* (enum/boolean/relation fields) ∩ declared list query params, domain
|
|
44
|
+
* fields first, audit fields only with `showAuditFields`, cap 4 (D5).
|
|
45
|
+
* Only enum, boolean and relation fields get controls.
|
|
46
|
+
*/
|
|
47
|
+
filters?: readonly string[] | false;
|
|
48
|
+
/**
|
|
49
|
+
* Fixed filters sent with every request and hidden from the filter bar —
|
|
50
|
+
* e.g. `{ event: eventId }` for "the bookings of this event". A user filter
|
|
51
|
+
* on the same field never overrides them.
|
|
52
|
+
*/
|
|
53
|
+
params?: Readonly<Record<string, KarniQueryValue>>;
|
|
54
|
+
/**
|
|
55
|
+
* Export buttons (one per format) when the resource serves
|
|
56
|
+
* `GET /<resource>/export/` — every Karni BaseViewSet does. The file carries
|
|
57
|
+
* the table's current filters, search and ordering; the backend applies the
|
|
58
|
+
* row scope and field visibility. Default `["csv", "xlsx"]`; `false` hides them.
|
|
59
|
+
*/
|
|
60
|
+
exportFormats?: readonly ExportFormat[] | false;
|
|
61
|
+
/** Extra toolbar content, rendered to the right of the search/filter row. */
|
|
62
|
+
toolbar?: React.ReactNode;
|
|
63
|
+
/** Replaces the default EmptyState when the list has no results. */
|
|
64
|
+
empty?: React.ReactNode;
|
|
65
|
+
/** Rendered under the pagination row. */
|
|
66
|
+
footer?: React.ReactNode;
|
|
67
|
+
/** Overrides displayLabel() for relation cells (and the delete confirm). */
|
|
68
|
+
getRowLabel?: (row: Record<string, unknown>) => string;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Schema-driven data table for one resource: columns, sorting, filters, search, pagination, row actions and bulk select, all derived from the OpenAPI schema. */
|
|
72
|
+
export declare function ResourceTable<T = Record<string, unknown>>(props: ResourceTableProps<T>): React.ReactElement;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RequestHandler } from 'msw';
|
|
2
|
+
export interface DemoEvent {
|
|
3
|
+
id: number;
|
|
4
|
+
created_at: string;
|
|
5
|
+
updated_at: string;
|
|
6
|
+
created_by: number | null;
|
|
7
|
+
updated_by: number | null;
|
|
8
|
+
title: string;
|
|
9
|
+
starts_at: string;
|
|
10
|
+
capacity: number | null;
|
|
11
|
+
status: "DRAFT" | "PUBLISHED";
|
|
12
|
+
is_public: boolean;
|
|
13
|
+
organizer: number | null;
|
|
14
|
+
venue: number | null;
|
|
15
|
+
tags: number[];
|
|
16
|
+
}
|
|
17
|
+
export declare const DEMO_EVENT_COUNT = 57;
|
|
18
|
+
/** Titles run opposite to ids so title-ascending differs from the default order. */
|
|
19
|
+
export declare function makeDemoEvents(count?: number): DemoEvent[];
|
|
20
|
+
export declare const eventsOpenApi: Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* msw handlers for the demo backend. Register per test with
|
|
23
|
+
* `server.use(...demoHandlers())` or per story via `parameters.msw.handlers`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function demoHandlers(events?: DemoEvent[]): RequestHandler[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ColumnDef } from '@tanstack/react-table';
|
|
2
|
+
import { FieldDef } from '../../../schema/types';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/** Per-field override merged over the schema-derived column. */
|
|
5
|
+
export interface ResourceColumn<T = Record<string, unknown>> {
|
|
6
|
+
field: string;
|
|
7
|
+
header?: React.ReactNode;
|
|
8
|
+
cell?: (row: T) => React.ReactNode;
|
|
9
|
+
hidden?: boolean;
|
|
10
|
+
sortable?: boolean;
|
|
11
|
+
}
|
|
12
|
+
type RowLabeler = (row: Record<string, unknown>) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Default cell renderer for a schema field's value. Exported for reuse by
|
|
15
|
+
* custom cells that want the stock formatting for other fields.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderFieldCell(field: FieldDef, value: unknown, getRowLabel?: RowLabeler): React.ReactNode;
|
|
18
|
+
export interface DeriveColumnsArgs<T> {
|
|
19
|
+
fields: readonly FieldDef[];
|
|
20
|
+
columns?: ReadonlyArray<ResourceColumn<T>>;
|
|
21
|
+
getRowLabel?: RowLabeler;
|
|
22
|
+
/** Show every audit field (AUDIT_FIELDS) without naming each in `columns`. */
|
|
23
|
+
showAuditFields?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds the derived column set: domain columns (schema order), then any
|
|
27
|
+
* shown audit columns (AUDIT_FIELDS order), then `columns` entries naming
|
|
28
|
+
* fields the schema does not declare — plain accessor columns (escape hatch
|
|
29
|
+
* for computed or server-extra keys). `columns` overrides merge onto their
|
|
30
|
+
* fields; naming a default-skipped field (json, audit, ...) shows it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function deriveColumns<T>(args: DeriveColumnsArgs<T>): Array<ColumnDef<T>>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SortingState } from '@tanstack/react-table';
|
|
2
|
+
/**
|
|
3
|
+
* Maps TanStack sorting state to the DRF `ordering` query parameter.
|
|
4
|
+
* Column ids equal schema field names verbatim — that IS the ordering
|
|
5
|
+
* contract (D5) — so `[{id: "title", desc: true}]` becomes `-title`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sortingToOrdering(sorting: SortingState): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Duck-typed 404 check (works with KarniApiError or any error exposing a
|
|
10
|
+
* numeric `status`), used for the D5 page-clamp: a list 404 with page > 1
|
|
11
|
+
* decrements the page and refetches as loading, never as an error.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isNotFoundError(error: unknown): boolean;
|
|
14
|
+
export declare function pageCountFor(count: number | undefined, pageSize: number): number;
|
|
15
|
+
/** Safe record view of an unknown row (rows are plain JSON objects). */
|
|
16
|
+
export declare function asRecord(value: unknown): Record<string, unknown>;
|
|
17
|
+
/** Best-effort human message from a KarniApiError-shaped or plain Error. */
|
|
18
|
+
export declare function errorMessage(error: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Display label for an enum member: ALL_CAPS and lower_snake constants are
|
|
21
|
+
* prettified ("DRAFT" → "Draft", "in_progress" → "In progress"); mixed-case
|
|
22
|
+
* values ("iOS") pass through untouched.
|
|
23
|
+
*/
|
|
24
|
+
export declare function enumLabel(value: string | number, labels?: Readonly<Record<string, string>>): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useDebouncedValue<T>(value: T, delayMs: number): T;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface StatProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** What the number measures ("Active users"). */
|
|
4
|
+
label: React.ReactNode;
|
|
5
|
+
/** The headline value — preformatted string or any node. */
|
|
6
|
+
value: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Change indicator. A number renders a signed figure with an up/down arrow
|
|
9
|
+
* (negative numbers use the destructive token) plus screen-reader context;
|
|
10
|
+
* any other node renders verbatim in the delta slot.
|
|
11
|
+
*/
|
|
12
|
+
delta?: number | React.ReactNode;
|
|
13
|
+
/** Formats a numeric `delta` (default: signed, locale formatted). */
|
|
14
|
+
formatDelta?: (delta: number) => string;
|
|
15
|
+
/** Optional icon slot, top-right. */
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* KPI tile: label, value, optional delta and icon.
|
|
20
|
+
*
|
|
21
|
+
* Escape hatches: `value`/`delta`/`icon` accept arbitrary nodes,
|
|
22
|
+
* `formatDelta` overrides numeric formatting, `className` merges via cn().
|
|
23
|
+
*/
|
|
24
|
+
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,33 @@
|
|
|
1
|
+
import { VariantProps } from '../../lib/variants';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Per-variant toast styling — colors resolve through --karni-* tokens only
|
|
5
|
+
* (success leans on primary, error on destructive, info on muted).
|
|
6
|
+
*/
|
|
7
|
+
export declare const toastVariants: import('../..').VariantFn<{
|
|
8
|
+
variant: {
|
|
9
|
+
default: string;
|
|
10
|
+
success: string;
|
|
11
|
+
error: string;
|
|
12
|
+
info: string;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
export type ToastVariantProps = VariantProps<typeof toastVariants>;
|
|
16
|
+
export interface ToasterProps {
|
|
17
|
+
/** Auto-dismiss duration in ms for toasts without their own (default 5000). */
|
|
18
|
+
duration?: number;
|
|
19
|
+
/** Swipe-to-dismiss direction (default "right"). */
|
|
20
|
+
swipeDirection?: "right" | "left" | "up" | "down";
|
|
21
|
+
/** Accessible label prefix announced with each toast (Radix Provider). */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Extra classes for the viewport (position/stacking escape hatch). */
|
|
24
|
+
viewportClassName?: string;
|
|
25
|
+
/** Extra classes for every toast root. */
|
|
26
|
+
toastClassName?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Renders the toasts pushed through useToast(). Mount exactly one near the
|
|
30
|
+
* app root. Position defaults to the bottom-right corner; override with
|
|
31
|
+
* `viewportClassName` (it merges via cn(), so e.g. "top-0 bottom-auto" wins).
|
|
32
|
+
*/
|
|
33
|
+
export declare function Toaster({ duration, swipeDirection, label, viewportClassName, toastClassName, }: 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 — `{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
|
+
};
|